吃豆豆Pygame源码深度解析:从环境搭建到鬼魂AI与打包
2026/9/14 9:32:54 网站建设 项目流程

简介:以 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所对应的环境里。建议的版本组合如下:

组件推荐值说明
Python3.8 ~ 3.12pygame 2.x 对这三个版本支持最稳
pygame2.0 以上低版本 sprite 和 mixer 的 API 有差异
操作系统任意差异主要在音效初始化和路径分隔符

Linux 下如果出现pygame.error: audio deviceNo module named 'pygame'但 pip 列表里明明有它,多半是系统缺 SDL 相关库,常见做法是安装libsdl2-devlibsdl2-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.pyghost.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=16speed=2,玩家走过一个格子需要 8 帧;在 60 FPS 下约 0.13 秒,这是吃豆豆比较正统的手感。调参数时这两者要联动:

TILE_SIZEspeed 建议值每格耗时(60FPS)
162 ~ 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, 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_speedmin封顶,而不是无限叠加。关卡越高,鬼魂速度越快的思路没问题,但如果速度超过玩家速度,游戏会从“技巧挑战”变成“必死局”。所以大多数吃豆豆作品给鬼魂速度设置一个天花板,难度增量则来自鬼魂数量、地图复杂度以及惊恐模式时长的缩短。

另外,当大关卡重开时还需要处理一个细节:如果玩家当前处于惊恐模式且计时器还在走,进入下一关前要把计时器清零,否则新关卡的鬼魂还可能处于半惊吓状态,看起来像鬼魂集体抽搐。

能做到这一步,游戏已经比网上一大半同主题源码耐玩了。剩下的挑战是把源码交给别人而不是留在自己电脑上,这就是最后一章要处理的事。

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 节的打包,整个源码从解压、读码、调参到交付的闭环就补齐了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询