
简介以 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 或 Anacondapip不一定指向你正在用的那个解释器。用python -m pip能保证装进python所对应的环境里。建议的版本组合如下组件推荐值说明Python3.8 ~ 3.12pygame 2.x 对这三个版本支持最稳pygame2.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.pypython main.py绝大多数源码包在等待这一步时暴露出三个高频报错按出现频率排报错一ModuleNotFoundError: No module named pygamepip 列表里有 pygame 但仍然报这个错说明你运行的 Python 和你装包的 Python 不是同一个。Windows 用where pythonLinux 和 macOS 用which python看输出路径是否和python -m pip指向的解释器一致。不一致时直接用那个解释了。报错二pygame.error: Unable to open file assets/sounds/pill.wav这类报错本质是工作目录不对。你双击运行脚本时程序当前路径是脚本所在目录在命令行运行时当前路径是终端所在目录如果两者不同相对路径assets/...就找不到。我一般会明确在项目根目录跑命令不跳进子目录这样最省事。报错三窗口弹出来是乱码或直接闪退通常出在地图文件编码上。有些下载包是 GBK 编码Python 3 默认用 UTF-8 读取文件于是注释或中文字符串炸裂。解决办法不是逐个文件改编码而是在读取地图的open()里显式指定encodingutf-8或encodinggbk视原始文件而定。提示先按 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, points10) elif char o: Pill(px, py, points50, is_powerTrue) 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_SIZE16、speed2玩家走过一个格子需要 8 帧在 60 FPS 下约 0.13 秒这是吃豆豆比较正统的手感。调参数时这两者要联动TILE_SIZEspeed 建议值每格耗时60FPS162 ~ 35 ~ 8 帧243 ~ 46 ~ 8 帧324 ~ 56.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, keylambda d: manhattan(ghost, player, d)) else: # 追逐模式下选离玩家最近的格 return min(candidates, keylambda 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 节的打包整个源码从解压、读码、调参到交付的闭环就补齐了。本文还有配套的精品资源点击获取