简介:以 Python 与 Pygame 实现的经典“吃豆豆”小游戏完整源码包,面向希望入门游戏开发或巩固 Python 编程基础的初学者,也适合作为课程设计参考。项目从零搭建游戏主循环,涵盖玩家移动、键盘事件处理、碰撞检测、分数统计、状态切换等核心逻辑,并配有精灵图、字体、音效和动态图素材,便于直接运行与二次修改。
压缩包共 12 个文件,包含 3 个 Python 主程序,以及 6 张 PNG 图片、1 个 TTF 字体、1 段 MP3 音频和 1 个 GIF 动图,整体约 11.77MB,结构简洁、依赖清晰。目前已有 469 人学习下载。通过阅读和运行代码,可以直观掌握 Pygame 中窗口、Surface、Sprite、Rect、事件队列与 mixer 模块的实际用法,理解从初始化、输入响应、碰撞判断、动画渲染、音效播放到游戏结束的完整开发流程,也能学习 pygame.time 控制帧率、精灵更新等细节。适合作为新手第一个练手项目,也可用于课程设计或兴趣开发。
1. 为什么一个吃豆豆源码zip值得你解开来看
网上搜“免费python源码大全”,pygame小游戏永远占一半,其中吃豆豆又是出现频率最高的那个。可很多人从下载站拿到这个 python吃豆豆小游戏源码下载.zip 之后,第一反应是找 exe,解压出来看到一排 .py 文件反而犹豫了:这到底能不能跑?其实这个包里装的是一份完整的 pygame 工程,地图、玩家、鬼魂AI、计分、音效、关卡切换都在里面。它和教程里的代码片段最大的区别在于:这是一个能直接运行、能改完立刻看到效果的小系统。
对刚学完 Python 基础语法、想找一个练手项目的人来说,它比重复刷题有用,因为你能读到面向对象、事件循环、资源加载这些真实工程要素是怎么拼到一起的;对写了五年以上业务代码、每天面对 CRUD 的人来说,它同样值得拆开看,因为状态机、网格坐标、碰撞检测、追逐算法这些概念在这里都以最小体量完整实现了一遍,没有框架噪音。下面按我处理这类源码包的习惯顺序来:先跑通,再读代码,再调参数,最后考虑交付给别人。
2. 先跑起来:把 python吃豆豆源码从 .zip 变成窗口
2.1 确认 Python 环境与 Pygame 依赖
运行 pygame 项目的门槛不在代码,在环境。这类下载包通常不会写清楚用什么 Python 版本,解压后第一件事不是改代码,而是确认本机解释器和依赖是否匹配。我一般先做两步检查:
python --version python -m pip show pygame第一条看 Python 版本,第二条看 pygame 是否已安装。如果第二条提示找不到包,就安装它:
python -m pip install pygame这里刻意用python -m pip而不是直接敲pip install,是因为很多机器上同时装了 Python 3、Python 2 或 Anaconda,pip不一定指向你正在用的那个解释器。用python -m pip能保证装进python所对应的环境里。建议的版本组合如下:
| 组件 | 推荐值 | 说明 |
|---|---|---|
| Python | 3.8 ~ 3.12 | pygame 2.x 对这三个版本支持最稳 |
| pygame | 2.0 以上 | 低版本 sprite 和 mixer 的 API 有差异 |
| 操作系统 | 任意 | 差异主要在音效初始化和路径分隔符 |
Linux 下如果出现pygame.error: audio device或No module named 'pygame'但 pip 列表里明明有它,多半是系统缺 SDL 相关库,常见做法是安装libsdl2-dev、libsdl2-mixer-dev后再重装 pygame。Windows 上这类问题少,最常见的是多个 Python 版本混装导致装错了环境。
2.2 读一遍目录结构再动 main.py
我处理任何下载源码包的习惯是:先看目录,再动代码。网上能找到的吃豆豆源码包命名很杂,但结构基本跳不出下面这个模子:
my_pacman/ ├── main.py # 入口:初始化窗口、启动主循环 ├── settings.py # 常量与全局配置 ├── assets/ │ ├── sounds/ # 吃豆、吃到鬼魂、关卡开始的音效 │ └── images/ # 玩家帧、鬼魂帧、豆子、大绿豆 ├── sprites/ │ ├── __init__.py │ ├── player.py # 玩家类 │ └── ghost.py # 鬼魂类 └── maps/ └── level1.txt # 关卡地图这段树形结构里,main.py负责创建窗口和游戏循环,settings.py集中存放常量,sprites目录放各个角色类,maps目录放关卡数据。一个源码包值不值得继续看,就看它的硬编码多不多:TILE_SIZE = 24写在settings.py里说明作者有意识做配置管理;如果散落在player.py、ghost.py里各写一遍,运行大概率没问题,但改起来就是四处找数字。
解析一份可行的下载包时,如果maps目录存在,说明关卡和代码已经分离,后续换地图就不用动逻辑代码,只改文本文件;如果地图也硬编码在脚本里,那第 4 章我会建议你顺手把这个欠账还上。
2.3 运行入口与启动过程的三个高频报错
环境确认和目录读完之后就启动,入口一般叫main.py:
python main.py绝大多数源码包在等待这一步时暴露出三个高频报错,按出现频率排:
报错一:ModuleNotFoundError: No module named 'pygame'
pip 列表里有 pygame 但仍然报这个错,说明你运行的 Python 和你装包的 Python 不是同一个。Windows 用where python,Linux 和 macOS 用which python,看输出路径是否和python -m pip指向的解释器一致。不一致时直接用那个解释了。
报错二:pygame.error: Unable to open file 'assets/sounds/pill.wav'
这类报错本质是工作目录不对。你双击运行脚本时,程序当前路径是脚本所在目录;在命令行运行时,当前路径是终端所在目录,如果两者不同,相对路径assets/...就找不到。我一般会明确在项目根目录跑命令,不跳进子目录,这样最省事。
报错三:窗口弹出来是乱码或直接闪退
通常出在地图文件编码上。有些下载包是 GBK 编码,Python 3 默认用 UTF-8 读取文件,于是注释或中文字符串炸裂。解决办法不是逐个文件改编码,而是在读取地图的open()里显式指定encoding='utf-8'或encoding='gbk',视原始文件而定。
提示:先按 zip 包里的原始目录层级原样解压,不要重命名外层文件夹,也不要改 assets 目录名。游戏里所有资源路径都基于这个层结构,改动后一分钟启动报错,排查成本反而更高。
跑起来之后别急着直接玩,先对准方向键走两步确认移动是正常的,然后回到代码里看它是怎么实现的,这就进到核心逻辑了。
3. 读懂核心逻辑:地图、移动与鬼魂AI的实现套路
3.1 字符串地图:图纸与运行时的同一份数据
吃豆豆这类网格游戏的关卡设计有个很朴素的传统:用字符串表示地图,一行一个字符串,一个字符一个格子。网上能找到的 python 小游戏源码里,八九成都是这套做法,因为可读性最好,改关卡就像改文本文件一样直接。
MAP = [ "####################", "#........#.........#", "#o##.###.#.###.##o#", "#.................#", "#.##.#.#####.#.##.#", "#....#...#...#....#", "####.###.#.###.####", "####.#.......#.####", "####.#.## ##.#.####", "#........P........#", "####.#.#####.#.####", "####.#.......#.####", "####.#.#####.#.####", "#....#...#...#....#", "#.##.#.#####.#.##.#", "#o................o#", "####################", ]字符和游戏元素的对应关系一般是:
| 字符 | 含义 |
|---|---|
# | 墙壁,不可穿过 |
. | 小豆子,吃一个加 10 分 |
o | 大绿豆,吃后进入惊恐模式 |
P | 玩家初始位置 |
G | 鬼魂初始位置 |
| 空格 | 鬼魂出生点或安全通道 |
加载地图的核心逻辑是两层嵌套循环,把字符换成精灵:
# 逐行逐列扫描字符串地图 for row, line in enumerate(MAP): for col, char in enumerate(line): px = col * TILE_SIZE # 列号换算成像素坐标 py = row * TILE_SIZE if char == ".": Pill(px, py, points=10) elif char == "o": Pill(px, py, points=50, is_power=True) elif char == "#": Wall(px, py)这段代码的逻辑并不复杂:外层enumerate拿到行号和字符串,内层enumerate拿到列号和字符;每个字符在TILE_SIZE的分辨率下换算成像素坐标,再交给对应的精灵类。这里最关键的是保持TILE_SIZE全局一致,地图字符在逻辑上是格子,而精灵在物理上是像素点,两者之间只靠这个换算关系活着。矩形碰撞和鬼魂移动的方向判定,后续全部依赖px % TILE_SIZE == 0这类取模判断。
很多下载包没有把地图抽到文件里,直接写死在代码里。读的时候不亏,改的时候麻烦。如果你拿到的是maps/level1.txt而不是硬编码字符串,说明作者把数据层和逻辑层分开了,这是源码质量不低的一个信号。
3.2 移动不是键盘直连,而是网格对齐加方向状态机
吃豆豆和贪吃蛇不同,方向键不能随时生效。比如玩家正向右走,你按一下上方向键,真实街机里玩家会等走到下一个格子中心才转向;如果按了原方向的反向键,则立即掉头。这个手感差别用一句话概括:方向键改的是目标方向,而不是速度向量。
# player.py 中的移动状态,简化版 def update(self): # 只有到达格子中心时才允许改方向 if self.rect.left % TILE_SIZE == 0 and self.rect.top % TILE_SIZE == 0: # 尝试切换到目标方向,撞墙则保持原方向 next_pos = self.rect.move(self.target_direction * self.speed) if not self.check_wall(next_pos): self.direction = self.target_direction # 朝当前方向移动 next_pos = self.rect.move(self.direction * self.speed) if not self.check_wall(next_pos): self.rect = next_pos这段代码里有两个地方要注意。第一,self.target_direction是按键阶段设置的方向,self.direction才是真正生效的方向;键盘按下时只是修改目标方向,真正切方向发生在格子中心。第二,移动前都会用next_pos预先检测墙,也就是先算目标矩形位置,再和墙壁精灵做碰撞检测,撞上就不移动而不是移动后再弹回,避免卡进墙壁。
参数self.speed的单位是“每帧像素”,不是“每秒像素”。如果TILE_SIZE=16、speed=2,玩家走过一个格子需要 8 帧;在 60 FPS 下约 0.13 秒,这是吃豆豆比较正统的手感。调参数时这两者要联动:
| TILE_SIZE | speed 建议值 | 每格耗时(60FPS) |
|---|---|---|
| 16 | 2 ~ 3 | 5 ~ 8 帧 |
| 24 | 3 ~ 4 | 6 ~ 8 帧 |
| 32 | 4 ~ 5 | 6.4 ~ 8 帧 |
很多初学者把 speed 调很大来“提速”,结果鬼魂和玩家同时穿墙,其实穿墙的本质不是速度过快,而是单帧位移超过了墙壁宽度,导致check_wall检测的next_pos从墙的一侧跳到了另一侧。保持单帧位移小于TILE_SIZE的一半是安全下限。
3.3 鬼魂AI:先曼哈顿距离,再谈 A* 和 BFS
吃豆豆源码包里最容易注水的就是鬼魂逻辑。有些实现让鬼魂追着玩家当前位置跑,效果是鬼魂永远跟不上,玩家觉得没挑战;有些实现是鬼魂完全随机游走,又显得太蠢。一个能玩的鬼魂需要至少两种行为切换:平时追逐玩家,吃到绿豆后转身逃离。
先明确一个事实:网格地图上两个格子的距离通常用曼哈顿距离,也就是abs(gx - px) + abs(gy - py),而不是直线距离。因为角色只能上下左右移动,对角线距离没有意义。
def choose_direction(ghost, player, walls, frightened): # 获取当前格子上下左右四个候选方向 candidates = [] for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): nx, ny = ghost.x + dx, ghost.y + dy if (nx, ny) not in walls: # 禁止掉头:排除反方向会导致鬼魂原地抖动 if (dx, dy) != (-ghost.dx, -ghost.dy): candidates.append((dx, dy)) if frightened: # 惊恐模式下选离玩家最远的格 return max(candidates, key=lambda d: manhattan(ghost, player, d)) else: # 追逐模式下选离玩家最近的格 return min(candidates, key=lambda d: manhattan(ghost, player, d))这段实现的核心逻辑是从当前位置的四个邻居里,去掉墙和反向方向,再按距离玩家远近排序。这样每个格子都局部最优,不需要全图搜索,在尺寸不大的地图上表现完全够用。frightened是惊恐模式标记,吃到绿豆后置 True,持续数秒后恢复。manhattan函数计算的是“假设选择某个方向后,鬼魂下一格到玩家的曼哈顿距离”,所以是预测一步,不是站在当前格算,这能让鬼魂在岔路口提前拐向玩家。
真正的大型吃豆豆源代码用四只鬼魂分别实现四种策略,这属于进阶玩法:
| 鬼魂 | 策略 | 常见实现 |
|---|---|---|
| Blinky(红) | 直接追玩家 | 一直用曼哈顿最近格 |
| Pinky(粉) | 包抄玩家前方 | 目标是玩家面向方向前 4 格 |
| Inky(蓝) | 镜像围堵 | 取玩家与红鬼的中点反方向 |
| Clyde(橙) | 近处随机 | 距离玩家 8 格以内才追 |
大多数下载包只实现了红鬼的追逐逻辑,自己改出粉鬼和橙鬼后,游戏性和代码复杂度都会上一个台阶,这段也是我建议你重点花时间的地方。
4. 调深玩法:把“能玩”改成“愿意玩”的五个参数
4.1 抽一个 config.py,把魔法数字集中管理
很多下载包把分数、速度、鬼魂数量直接写在游戏循环里,能跑但没法调。我的习惯是先建一个config.py,把会影响手感的所有数值集中在一起,之后调平衡基本改文件不碰游戏逻辑。
# config.py TILE_SIZE = 24 FPS = 60 # 计分 PILL_SCORE = 10 POWER_PILL_SCORE = 50 GHOST_SCORE_BASE = 200 # 惊恐模式吃第一只鬼的得分 GHOST_SCORE_MULTIPLIER = 2 # 同一轮惊恐模式连吃翻倍 # 鬼魂 BASE_GHOST_SPEED = 2 FRIGHTENED_TIME = 6 # 吃到大绿豆后的惊恐秒数 FRIGHTENED_SPEED_REDUCE = 1 # 惊恐时减速的像素值 # 关卡 GHOST_SPEED_INCREASE = 0.2 # 每关增加的鬼魂速度 PILL_CHERRY_SCORE = 100 # 清空本关豆子后的樱桃奖励参数设计成什么样,直接决定玩家体验。下面这张表是我调这类小游戏时常用的调整逻辑,部分随机下载包里的默认值不具备参考性,要自己试完再定:
| 参数 | 调高后果 | 调低后果 |
|---|---|---|
BASE_GHOST_SPEED | 追逐压迫感强,玩家反应时间缩短 | 鬼魂像散步,追不上玩家 |
FRIGHTENED_TIME | 玩家更敢主动引鬼魂来吃 | 绿豆形同虚设 |
GHOST_SCORE_MULTIPLIER | 鼓励一吃一串,但要靠站位 | 玩家只吃单个鬼,不追求连吃 |
PILL_SCORE | 单纯提高总分,对策略无影响 | 玩家更依赖吃鬼魂得分 |
调整手感的正确姿势是每次只改一个参数,跑五分钟看胜率。比如把BASE_GHOST_SPEED从 2 改到 3,玩家明显觉得节奏变快,但还没到必输的程度,这个值就可以留下。一次改五个参数然后说不清是哪个变量起了作用,是调优里最容易犯的错。
把配置抽完之后,源码包里的settings.py如果已经做了这件事就直接用;没做的话,把散落的数值搬进来是值得花半小时做的事,后面调难度、调平衡都要靠它。
4.2 关卡难度曲线与“豆子清零进下一关”
难度曲线是“愿意玩”和“能玩”之间的分水岭。只做一关的吃豆豆源码,玩家打五分钟就会腻。加第二关的常见做法是:豆子清空后,保持地图不变,重新填满豆子,同时提升鬼魂速度。
def next_level(): level += 1 # 根据关卡数动态提升鬼魂速度,但设一个上限 ghost_speed = min(BASE_GHOST_SPEED + level * GHOST_SPEED_INCREASE, 4) # 重新填充当前地图上的豆子 pills.empty() create_pills_from_map(current_map, pills) # 玩家和鬼魂回到出生点 player.reset() for ghost in ghosts: ghost.reset()这里有个值得注意的地方:ghost_speed用min封顶,而不是无限叠加。关卡越高,鬼魂速度越快的思路没问题,但如果速度超过玩家速度,游戏会从“技巧挑战”变成“必死局”。所以大多数吃豆豆作品给鬼魂速度设置一个天花板,难度增量则来自鬼魂数量、地图复杂度以及惊恐模式时长的缩短。
另外,当大关卡重开时还需要处理一个细节:如果玩家当前处于惊恐模式且计时器还在走,进入下一关前要把计时器清零,否则新关卡的鬼魂还可能处于半惊吓状态,看起来像鬼魂集体抽搐。
能做到这一步,游戏已经比网上一大半同主题源码耐玩了。剩下的挑战是把源码交给别人而不是留在自己电脑上,这就是最后一章要处理的事。
5. 打包发给别人:pyinstaller 资源合并与无头验收
5.1 用 PyInstaller 把源码和资源打成一个 exe
自己电脑上跑得好不算交付。很多吃豆豆源码包的问题在于资源文件、音频、地图散落在多个目录,发给别人时丢失一部分就启动失败。常见做法是用 PyInstaller 打包成单文件,并把资源目录一起塞进去。命令如下:
pyinstaller -F -w -i assets/pacman.ico --add-data "assets;assets" main.py参数说明:-F表示打包成单个 exe 文件,-w表示不弹出控制台窗口,-i指定图标,--add-data "assets;assets"把整个 assets 目录合并到产物里。Windows 上使用分号分隔源路径和目标路径,Linux 和 macOS 上要改成冒号。需要注意的是,--add-data把资源放进了打包后的临时目录,代码里取资源就不能再直接写"assets/sounds/pill.wav"了。
import sys from pathlib import Path def resource_path(relative): # 打包后资源在 _MEIPASS 临时目录,而非脚本目录 if getattr(sys, '_MEIPASS', None): return str(Path(sys._MEIPASS) / relative) return str(Path(__file__).parent / relative)这是从源码运行到打包运行之间最容易翻车的一行:开发时当前目录是项目根目录,"assets/..."能用;打包成单文件后,资源被解压到临时目录_MEIPASS里,路径自然失效。用这个resource_path统一替换所有资源加载的路径,打包后就能正常找到音效和图像。
验证打包是否成功,最直接的办法是在另一台没有装 Python 的机器上双击运行,听一下音效是否还在。如果音效正常播放,说明--add-data生效了;音效缺失但窗口能开,大概率是路径没有走_MEIPASS。
5.2 不加窗口的地图合法性检查
改完代码、调完参数之后,还有一个特别容易省掉的步骤;地图检查。吃豆豆的地图如果行长度不一致,或者有两个玩家出生点、没有鬼魂出生点,游戏运行到一半才会暴露问题,而这类问题往往在游戏进行到十几秒后才炸,排查效率极低。更好的做法是用一段无头脚本做静态校验,不启动窗口,直接检查地图数据:
python -m pytest test_map.py -q对应的test_map.py逻辑可以这样写:
# test_map.py from pacman_map import load_map VALID_CHARS = set("#.oPG ") def test_all_rows_same_length(): rows = load_map("maps/level1.txt") lengths = {len(row) for row in rows} assert len(lengths) == 1, f"地图行长度不一致: {lengths}" def test_all_chars_valid(): rows = load_map("maps/level1.txt") invalid = {c for row in rows for c in row if c not in VALID_CHARS} assert not invalid, f"非法字符: {invalid}" def test_single_player(): rows = load_map("maps/level1.txt") count = sum(row.count("P") for row in rows) assert count == 1, f"玩家出生点数量应为 1,实际 {count}"这段测试把地图当作数据文件来验收,三个断言分别检查行长度一致、字符合法、玩家出生点只有一个。这样每次改完地图,跑一遍测试就能确认“图纸”没问题,而不必打开窗口玩到十几秒才发现鬼魂没出生点。配合第 5.1 节的打包,整个源码从解压、读码、调参到交付的闭环就补齐了。
本文还有配套的精品资源,点击获取