Python贪吃蛇实战:完整源码与Pygame游戏开发入门 如果你已经学完了 Python 的基础语法正处在“教程都看懂了但不知道下一步该做什么”的阶段那么贪吃蛇这个项目非常值得停下来认真做一遍。它看起来像是一个怀旧小游戏但背后涉及的 Python 基础、事件处理、数据结构、碰撞检测、游戏循环和实时渲染几乎覆盖了编写任何带图形界面的小程序的完整链路。这篇文章就用一个可以直接运行的 Python 贪吃蛇源码为例带你把从环境准备、代码实现、运行验证到常见问题排查的整个过程走一遍。读完你不仅能跑出这个游戏还能理解每一段代码为什么要这样写并且知道如何把它改造成自己的课程设计或开源小作品。1. 为什么建议用贪吃蛇练手很多自学者会遇到一个尴尬期语法、列表、字典、函数都学过但要做练习时不是太简单就是太复杂。控制台里打印一个九九乘法表已经没什么新意直接做一个完整的 Web 项目又涉及太多未知的知识。贪吃蛇正好处于一个非常合适的难度区间。从技术角度看贪吃蛇是“麻雀虽小五脏俱全”的典型代表。它不像爬虫那样只依赖一个第三方库也不像自动化脚本那样只有一条直线逻辑。贪吃蛇程序至少要有这些组成部分一个持续运行的游戏循环负责控制游戏节奏键盘事件监听用来读取玩家的方向输入蛇身的增长逻辑本质上是列表头插和尾删食物生成本质上是随机坐标与冲突检测撞墙、撞自身、吃到食物三种核心判断实时画面绘制把内存中的状态翻译成屏幕上的方块。这意味着写完一个贪吃蛇你不仅仅是复习了while循环和if判断而是真正体验了一次“完整程序”的构建过程。程序不是在 main 函数里一次性跑完就结束而是不断接收输入、修改状态、刷新画面直到玩家退出。这种思维方式是未来写任何更复杂系统的基础。哪些人最适合拿这个项目练手如果你是 Python 初学者刚学完基础语法想找一个巩固练习如果你想熟悉 Pygame 这个游戏开发库如果你在准备课程设计或想写一个可以展示的小作品甚至如果你只是想在电脑上跑出一个可以玩的小游戏都可以选它。这个项目的代码量不算大完整的源码也就一两百行但信息密度很高读代码的过程本身就是一次很好的学习。2. 贪吃蛇背后的核心概念在进入代码之前先花少量时间弄清楚游戏实现依赖的几个基础概念。这是后面看源码时不困惑的关键。2.1 游戏循环游戏和普通脚本最大的区别是“一直在跑”。普通脚本按顺序执行完就结束而游戏需要不断重复“获取输入、更新状态、绘制画面”这三件事直到玩家退出。这个循环每执行一次就是一帧。控制帧节奏的频率称为 FPSFrames Per Second也就是每秒刷新多少次画面。贪吃蛇的 FPS 可以很低比如 10 到 15 帧游戏节奏已经比较合理这也是为什么代码里会用clock.tick(fps)来限制速度。2.2 坐标系与格子化Pygame 的默认坐标系以窗口左上角为原点(0, 0)向右为 x 轴正方向向下为 y 轴正方向。为了让蛇的移动规整我们可以把整个窗口按固定大小切分成格子一个格子就是蛇和食物的最小绘制单元。假设窗口是600 * 600每个格子CELL 20那么横向和纵向各有 30 个格子。蛇的坐标始终是格子的整数倍这样就不会出现在两个格子之间来回闪烁的情况。2.3 蛇的数据结构蛇身本质上是“按顺序排列的一组坐标”。这里最合适的数据结构是列表list头部在snake[0]尾部在snake[-1]。每次移动时先根据方向计算出新的头部坐标用insert(0, new_head)头插如果没吃到食物用pop()删掉尾部保持长度不变如果吃到食物就不删尾部蛇身长度加一。这种“头插 尾删”的做法是贪吃蛇最核心的编程模型。2.4 碰撞检测游戏中只有三类碰撞检测蛇头碰到窗口边界、蛇头碰到自身身体、蛇头碰到食物。前两者是游戏结束条件最后一个是加分和增长条件。实现方式并不复杂因为我们使用格子坐标所以只需要判断新的头部坐标是否越界、是否已存在于蛇身列表中、是否等于食物坐标。2.5 事件驱动Pygame 用一个事件队列保存用户操作。窗口关闭、按键按下、鼠标操作都会成为事件。pygame.event.get()会取出当前帧里发生的事件然后我们根据事件类型做出响应。贪吃蛇只需要监听窗口关闭事件和键盘方向键。方向键不能简单设置为“按一下就变”还要防止蛇倒转否则蛇头从右往左走的时候按左键游戏会立刻撞到自己。这些概念的关系可以用下面的表来理解概念作用对应代码位置游戏循环让程序持续运行并刷新画面while running:坐标系确定蛇和食物的位置new_head (x dx, y dy)蛇身列表保存每一节身体坐标snake.insert(0, new_head)碰撞检测判断结束或吃到食物if new_head in snake事件驱动响应玩家操作for event in pygame.event.get()3. 环境准备与前置条件开始写代码前需要先准备运行环境。这个项目依赖 Python 和 Pygame依赖非常少安装过程也比较简单。3.1 检查 Python 环境打开命令行工具输入以下命令检查 Python 是否已安装python --version如果提示找不到命令需要先到 Python 官网下载并安装 Python。安装完成后重新打开命令行再执行上面的检查命令。建议使用 Python 3.8 及以上版本本文的代码在 3.8 以上环境中可以正常使用。3.2 安装 PygamePygame 是 Python 生态里最常用的 2D 游戏库贪吃蛇只是它的一个入门示例。在命令行中执行pip install pygame安装过程会自动拉取对应平台的 Pygame 包。安装完成后可以通过下面的命令验证python -c import pygame; print(pygame.version.ver)如果能看到版本号输出说明 pygame 已成功安装。如果安装缓慢可以考虑配置国内 PyPI 镜像源但这一步不是必须的。这个项目只需要一个 Python 文件即可运行。为了后续拓展建议创建一个项目目录比如snake_game在目录里新建snake_game.py文件。完整的工程结构可以先这样规划snake_game/ ├── snake_game.py └── requirements.txtrequirements.txt的内容就一行写上依赖名称即可方便以后在其它机器上复现环境pygame4. 程序整体流程拆解不要急着把源码从头敲到尾。先站在更高角度理解程序的“骨架”这样写出来的代码才结构清晰。整个程序可以拆成五步初始化创建窗口、初始化 Pygame、定义常量、生成初始蛇身和食物。事件处理读取键盘输入更新希望前进的方向如果是新方向且不反向就先记录。游戏逻辑按照方向移动蛇头判断是否撞墙或撞自己决定游戏是否结束判断是否吃到食物决定蛇是否增长。绘制刷新背景、绘制蛇身、绘制食物、绘制分数和游戏结束文案。控制节奏用clock.tick(fps)限制每帧时长。如果用伪代码来描述核心主循环大概是这样的初始化游戏 while 运行中: 取出所有事件 if 点击关闭: 退出 if 按下方向键: 保存新方向 if 游戏未结束: 计算新蛇头位置 if 新蛇头撞墙: 游戏结束 elif 新蛇头撞自身: 游戏结束 else: 蛇头向前插入 if 吃到食物: 分数加一 生成新食物 else: 删除蛇尾 绘制画面 控制帧率这个循环是一个标准的“游戏循环”。看起来简单但大多数人第一次自己写时容易犯的错误是把“新方向”立即应用成“当前方向”这会导致同一帧里按下多个方向键时出现非常奇怪的行为。后面的源码实现里会专门处理这个问题。5. 完整源码实现下面给出一个可直接运行的完整代码。请把代码保存为项目目录下的snake_game.py文件。# snake_game.py import random import sys import pygame # 游戏窗口与格子参数 WIDTH, HEIGHT 600, 600 CELL 20 INIT_FPS 10 # 颜色定义RGB COLOR_BG (18, 18, 18) COLOR_BODY (46, 204, 64) COLOR_HEAD (0, 255, 0) COLOR_FOOD (231, 76, 60) COLOR_TEXT (236, 240, 241) COLOR_GAMEOVER (231, 76, 60) # 按键与方向向量 DIR_MAP { pygame.K_UP: (0, -CELL), pygame.K_DOWN: (0, CELL), pygame.K_LEFT: (-CELL, 0), pygame.K_RIGHT: (CELL, 0), } def is_reverse(direction, candidate): 判断 candidate 是否是 direction 的反方向 return direction[0] candidate[0] 0 and direction[1] candidate[1] 0 def random_food(snake): 在窗口内随机生成一个不在蛇身上的食物 while True: pos ( random.randrange(0, WIDTH, CELL), random.randrange(0, HEIGHT, CELL), ) if pos not in snake: return pos def reset_game(): 重置游戏状态 cx WIDTH // 2 // CELL * CELL cy HEIGHT // 2 // CELL * CELL snake [ (cx, cy), (cx - CELL, cy), (cx - 2 * CELL, cy), ] direction (CELL, 0) next_direction direction food random_food(snake) score 0 return snake, direction, next_direction, food, score def draw_snake(screen, snake): 先画蛇身再画蛇头蛇头在最上层 for pos in snake[1:]: pygame.draw.rect(screen, COLOR_BODY, (pos[0], pos[1], CELL, CELL)) pygame.draw.rect(screen, COLOR_HEAD, (snake[0][0], snake[0][1], CELL, CELL)) def draw_food(screen, food): pygame.draw.rect(screen, COLOR_FOOD, (food[0], food[1], CELL, CELL)) def show_text(screen, text, center, size32, colorCOLOR_TEXT): font pygame.font.Font(None, size) surface font.render(text, True, color) rect surface.get_rect(centercenter) screen.blit(surface, rect) def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(Python 贪吃蛇示例) clock pygame.time.Clock() snake, direction, next_direction, food, score reset_game() running True game_over False while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_r and game_over: snake, direction, next_direction, food, score reset_game() game_over False elif event.key in (pygame.K_q, pygame.K_ESCAPE): running False candidate DIR_MAP.get(event.key) if candidate and not is_reverse(direction, candidate): next_direction candidate if not game_over: direction next_direction head_x snake[0][0] direction[0] head_y snake[0][1] direction[1] new_head (head_x, head_y) wall_hit ( head_x 0 or head_x WIDTH or head_y 0 or head_y HEIGHT ) body_hit new_head in snake if wall_hit or body_hit: game_over True else: snake.insert(0, new_head) if new_head food: score 1 food random_food(snake) else: snake.pop() screen.fill(COLOR_BG) draw_snake(screen, snake) draw_food(screen, food) show_text(screen, fScore: {score}, (WIDTH // 2, 28)) if game_over: show_text( screen, Game Over! Press R to restart, Q to quit, (WIDTH // 2, HEIGHT // 2), 40, COLOR_GAMEOVER, ) pygame.display.flip() fps INIT_FPS min(score // 5, 30) clock.tick(fps) pygame.quit() sys.exit() if __name__ __main__: main()这份代码在功能上保留了贪吃蛇的核心体验并且做了三件看起来简单但很重要的设计。第一方向键不会直接覆盖当前方向而是先写入next_direction在下一帧移动时才真正生效这避免了在同一帧连续按多个方向键导致蛇反向移动。第二绘制蛇头时单独处理避免身体覆盖头部颜色。第三FPS 会随分数提升而逐渐加快但设置了上限防止速度失控。6. 关键代码解析完整代码已经给出这里再把几个容易出问题的片段单独拿出来解释。做对这几处游戏体验才算真正合格。6.1 方向控制与防反转方向控制是新手最容易写错的地方。最简单的写法是按下方向键后立即改方向但这样有三个问题如果蛇正在向右走玩家按了左键蛇会立刻回头并撞上自己如果玩家在同一帧连续按了“上”和“左”有可能出现方向不受控的抖动。代码里的处理方式是candidate DIR_MAP.get(event.key) if candidate and not is_reverse(direction, candidate): next_direction candidateis_reverse的原理是两个方向向量的 x 和 y 分别相加如果两个方向都等于 0说明它们互为反方向。例如(20, 0)和(-20, 0)x 相加等于 0y 相加等于 0是反向。而(20, 0)和(0, -20)x 相加不等于 0允许转向。要注意的是这里比较的是direction不是next_direction所以玩家在这一帧里第一次按下的方向会成为新的next_direction随后的反向按键会被拒绝。这是一个很实用的细节处理。6.2 食物生成与死循环风险random_food函数里有一个while True循环只要生成的食物坐标已经在蛇身上就重新生成。这在游戏前中期是最高效的做法。但当蛇身很长时可用空格变少随机生成需要不断重试。如果蛇占满了大部分窗口理论上有极低概率会多次重试。教学代码里可以接受工程化代码更推荐先把所有空闲格子收集成列表再用random.choice从列表取一个彻底避免循环重试。6.3 蛇的增长逻辑蛇增长的本质是“吃食物时只插头不删尾”。看代码snake.insert(0, new_head) if new_head food: score 1 food random_food(snake) else: snake.pop()每次移动都会把新的蛇头插入头部然后判断是否吃到食物。没吃到就删掉尾部这样蛇身长度不变吃到了就不删蛇身长度加一。因为蛇身是一个坐标列表所以画蛇的时候只需遍历列表画方块即可坐标顺序天然对应身体的连接顺序。6.4 碰撞检测的两种结果代码里将撞墙和撞自身并列判断wall_hit ( head_x 0 or head_x WIDTH or head_y 0 or head_y HEIGHT ) body_hit new_head in snake这里把越界判断写成开区间因为窗口坐标是从 0 到WIDTH - 1如果head_x WIDTH蛇头其实已经超出窗口右侧边界应该判为撞墙。判断撞自身时使用了new_head in snake这个写法依赖snake中每个坐标都是可哈希的元组所以完全没问题。还有一个细节是当蛇头撞墙时新头部的位置还没有被insert进蛇身所以不会出现“越界且也插入到了蛇身列表里”的脏数据。6.5 剩余空间不足的游戏体验随着蛇越来越长游戏的真正难度不是速度而是可移动空间越来越少。贪吃蛇还有一条隐含规则蛇不能永远增长占用全部格子必须在无路可走之前结束。这个项目没有处理“剩余空间太少时自动胜利或判定为平局”的逻辑因此蛇很长之后很容易在绕圈过程中因为空间不足撞到自己。如果想要做成完整游戏可以在分数达到一定值后提升关卡、缩小地图或减少食物刷新频率。7. 运行结果与验证代码写完后在项目目录下执行python snake_game.py正常情况下会弹出窗口窗口标题为“Python 贪吃蛇示例”游戏自动开始一条绿色小蛇从窗口中心向右移动红色食物随机出现在某处。玩家可通过方向键控制方向吃到食物后分数加一蛇身变长。撞到墙壁或自身后画面中央显示结束提示按 R 重新开始按 Q 或 ESC 退出。对于刚写完代码的人来说建议做一轮简单的功能验证验证点操作预期结果基础移动启动后不按键蛇自动向右移动方向控制依次按上、下、左、右蛇按对应方向移动防反向蛇向右时按左蛇不会掉头方向不改变食物增长控制蛇头吃到食物分数加一蛇身变长重生食物吃掉一个食物后新食物出现在非蛇身位置上撞墙结束控制蛇头冲出边界游戏结束文案显示撞自身结束让蛇绕圈撞向自己游戏结束文案显示重新开始游戏结束后按 R蛇回到初始位置分数归零退出游戏按 Q 或关闭窗口程序退出如果你在无图形界面的 Linux 服务器上运行Pygame 可能会在pygame.display.set_mode时报错因为缺少显示环境。这是正常现象不是代码问题。这个项目最好在有桌面环境的 Windows、macOS 或带图形界面的 Linux 上练习。运行成功后还可以做一个很小的验证把窗口标题和颜色参数改成自己喜欢的配色重新运行看效果。这一步能帮你确认你确实理解了这些常量在代码中的位置和作用。8. 常见问题与排查思路无论代码看起来多简单真正跑起来时仍可能遇到各种问题。下面的表总结了学习这个项目过程中最常遇到的几种情况。问题现象可能原因排查方式解决方案ModuleNotFoundError: No module named pygame未安装 pygame或安装到了不同 Python 环境执行pip show pygame查看安装位置重新执行pip install pygame窗口一闪而过直接退出代码中缺少主循环或pygame.init()失败在代码文件末尾检查是否有main()调用确认有if __name__ __main__:入口蛇无法转向按键事件没有被正确处理检查事件循环中是否处理了pygame.KEYDOWN将DIR_MAP.get(event.key)放到事件循环内超快速按方向键后蛇反向方向键直接修改当前方向在事件循环里打印direction和next_direction使用next_direction延迟到移动时生效蛇吃到食物后没有变长删除尾部的逻辑写错了位置检查代码中snake.pop()是否被无条件执行只在未吃到食物时执行pop()食物随机生成卡顿蛇身很长随机位置频繁撞到蛇身在食物生成函数里增加打印日志改用“空闲格子列表 random.choice”方式中文和文字显示为方框系统没有匹配字体或默认字体不支持中文检查系统字体使用英文文本或安装中文字体后改用SysFontFPS 增长速度过快tick参数被设置成非常大的值打印fps查看数值给速度设置上限如min(score // 5, 30)远程服务器无法打开窗口没有图形显示环境检查运行环境是否是纯命令行改为本地桌面环境或配合虚拟显示方案如果程序启动后没有任何窗口优先看命令行终端里有没有报错信息。大多数运行问题都能从报错堆栈里找到具体代码行先定位哪一行出错再回到对应代码检查。9. 从贪吃蛇到工程化最佳实践与拓展建议单文件的贪吃蛇适合学习和线上分享但如果想把它升级成一个可以继续迭代的小项目建议按照工程化的思路做一次重构。9.1 拆分布局可以把代码拆成config.py、game.py、entities.py等模块。config.py专门放常量和配置entities.py放蛇、食物相关类game.py放主循环。这样做的好处是颜色、窗口尺寸、格子大小都集中在配置文件中调整参数不用满文件翻找蛇和食物的逻辑可以单独测试不用依赖渲染。这是一个很简单但很有效的进阶练习。9.2 增加可测试的核心逻辑游戏逻辑和画面渲染混在一起不利于单元测试。更健壮的做法是把“蛇移动、食物生成、碰撞判断”抽成纯函数不依赖 Pygame 窗口也能测试。下面是一个最简单的测试思路def test_move_eat_food(): snake [(100, 100)] direction (20, 0) food (120, 100) new_head (snake[0][0] direction[0], snake[0][1] direction[1]) snake.insert(0, new_head) if new_head food: snake.append((100, 100)) # 模拟增长 assert len(snake) 2这里的测试代码只是为了展示思路实际用pytest编写会更规范。把游戏核心逻辑与界面渲染分离之后这类测试就可以稳定运行不必打开游戏窗口。9.3 值得尝试的拓展方向贪吃蛇最大的价值在于“做完之后能改”。它可以朝着很多方向扩展每个方向都能让你学到新东西。如果你想加障碍物可以在地图中固定生成一些墙体坐标碰撞检测增加墙体判断。如果你想做关卡系统可以每提高 5 分增加障碍物数量或缩小地图尺寸。如果你想保存最高分可以引入文件读写或sqlite3。如果你想做 AI 自动演示可以把每帧计算出的最短路径转化为方向键输入。甚至可以为蛇身增加平滑移动效果从一格格跳动改成连续位移这会涉及更复杂的坐标插值。这些扩展中最推荐的是“最高分持久化”因为它只需要很少代码就能完成却让游戏有了长期留存的价值。运行时读取文本文件中的最高分结束时写入新分数这种模式在以后的很多项目里都会用到。9.4 下一步怎么练习看完这篇文章之后建议不要只复制代码运行一次就结束。你可以先做这三件事第一把CELL从20改成40观察游戏画面和难度变化。这会帮你理解格子大小对可玩性的影响。第二把INIT_FPS从10改成20再对比两种速度下遇到障碍时的手感差异。第三在reset_game()里把蛇的初始长度改成 5看看更长的蛇会不会让游戏起步难度明显增加。在此基础上再尝试自己写一个简化版源码不参考文章里的代码遇到问题时先打印关键变量来排查。比起背诵名词亲手改一版并运行成功带来的收获要大得多。贪吃蛇真的是为数不多的、用一个周末就能完整做出来且能一直玩下去的项目。把它搞定后面的游戏开发和图形界面程序学习都会顺利很多。