资讯动态

Pygame贪吃蛇开发实战:从设计思路到代码实现与调试技巧

发布时间:2026/10/15 17:47:24 来源:尧图企业网站定制
1. 为什么选择用 Pygame 写贪吃蛇1.1 贪吃蛇这个项目到底适合谁贪吃蛇这个游戏几乎每个学编程的人都写过。但很多人写完之后就扔了觉得“太简单了没什么意思”。其实恰恰相反贪吃蛇是一个非常好的练手项目它麻雀虽小五脏俱全有游戏循环、有碰撞检测、有状态管理、有输入处理、有渲染刷新甚至还能加上音效、计分、关卡加速这些扩展功能。你如果能把它写透后面再去做更复杂的游戏或者图形界面程序思路会顺畅很多。我当初第一次用 Pygame 写贪吃蛇的时候踩了不少坑。比如蛇身移动时坐标怎么更新才不会出现“头尾重叠”的视觉 bug比如食物生成时怎么避免刷到蛇身上比如帧率控制不好导致蛇跑得忽快忽慢。这些问题看起来小但每一个都值得单独拿出来讲清楚。所以这篇内容我打算把整个开发过程拆开从设计思路到代码实现再到调试技巧尽量把每个环节的“为什么”都说明白。适合读这篇内容的人学过 Python 基础语法知道列表、字典、循环、函数怎么用但还没怎么接触过图形化编程或者游戏开发的。如果你已经写过 Pygame 项目也可以看看我在细节处理上的一些经验说不定能帮你省掉一些调试时间。1.2 Pygame 到底帮我们做了什么很多人一上来就写代码但其实应该先搞清楚 Pygame 这个库的定位。Pygame 本质上是一套基于 SDL 的 Python 封装它帮你处理了窗口创建、事件循环、图像绘制、声音播放这些底层操作。如果没有 Pygame你要用 Python 做一个游戏窗口得直接调操作系统的图形接口那复杂度完全不是一个量级。具体到贪吃蛇这个项目Pygame 主要帮我们解决四件事窗口和渲染创建一个固定尺寸的窗口在上面画矩形、画圆、贴图片。贪吃蛇的蛇身和食物本质上就是一堆矩形块。事件处理监听键盘按键、窗口关闭事件。贪吃蛇只需要处理方向键和退出事件。时间控制通过pygame.time.Clock控制游戏帧率让蛇的移动速度稳定可控。碰撞检测虽然 Pygame 没有直接提供“矩形碰撞”的一行代码但Rect对象自带的colliderect方法足够我们判断蛇头是否撞到食物或自己的身体。理解了这些你就知道为什么贪吃蛇用 Pygame 写特别合适——它刚好覆盖了 Pygame 最核心的几个功能又不会复杂到让人无从下手。1.3 整体设计思路先想清楚再动手我在写任何游戏之前都会先问自己三个问题游戏有哪些状态状态之间怎么切换每个状态下要处理什么逻辑贪吃蛇的状态其实很简单运行中、游戏结束、暂停可选。运行中状态下每一帧要做的事情是处理输入、更新蛇的位置、判断是否吃到食物、判断是否撞墙或撞自己、重新绘制画面。游戏结束状态下显示结束画面和分数等待玩家按键重新开始或退出。数据结构方面蛇身我用一个列表来存列表里每个元素是一个坐标元组(x, y)列表的第一个元素是蛇头。这样蛇移动的时候只需要在列表头部插入新坐标然后根据是否吃到食物决定是否删除尾部坐标。这个思路比用队列或者链表都简单而且 Python 列表的insert(0, ...)和pop()操作在这个规模下性能完全够用。食物用一个单独的坐标元组表示生成时随机选一个格子但要检查这个格子是否已经在蛇身上。方向用一个变量存当前移动方向同时要记录“上一次的方向”防止玩家在蛇移动的间隙快速按两个相反方向导致蛇直接掉头撞自己。这些设计决策看起来琐碎但每一个都直接影响后面代码写起来顺不顺。我见过不少人写到一半发现蛇身更新逻辑有问题回头改数据结构结果整个代码都要重写。所以前期花十分钟想清楚后面能省一个小时。2. 核心细节与实操要点拆解2.1 网格坐标系为什么不用像素坐标贪吃蛇的移动是“一格一格”跳的不是平滑移动。如果你直接用像素坐标蛇头每次移动 20 像素那蛇身每个块的坐标就是(x, y)、(x-20, y)、(x-40, y)这样。但这样有个问题判断两个块是否重叠时你需要比较像素值是否完全相等浮点数或者计算误差可能导致判断失败。更稳妥的做法是用网格坐标。比如窗口是 600x600每个格子 20x20那网格就是 30x30。蛇头在网格中的位置是(15, 10)表示第 15 列第 10 行。绘制的时候再乘以格子大小转换成像素坐标。这样做的好处是碰撞检测只需要比较整数是否相等食物生成只需要在 0 到 29 之间随机取整数逻辑非常干净。我一般会定义两个常量CELL_SIZE 20 GRID_WIDTH 30 GRID_HEIGHT 30 WINDOW_WIDTH CELL_SIZE * GRID_WIDTH WINDOW_HEIGHT CELL_SIZE * GRID_HEIGHT这样后面所有坐标都用网格单位只有绘制的时候才做一次乘法转换。这个习惯我强烈建议你养成后面做任何网格类游戏都会轻松很多。2.2 蛇身移动的核心逻辑蛇移动的本质是在蛇头前方新增一个格子然后判断是否吃到食物。如果吃到尾部不动如果没吃到删除尾部格子。用代码表达就是new_head (snake[0][0] direction[0], snake[0][1] direction[1]) snake.insert(0, new_head) if new_head food: # 吃到食物不删除尾部蛇变长 food generate_food() else: snake.pop()这段代码看起来简单但有几个细节容易出错。第一方向不能直接取反。比如当前方向是向右(1, 0)玩家按了左键如果你直接把方向改成(-1, 0)下一帧蛇头就会撞到自己脖子。解决办法是维护一个current_direction和一个next_direction每帧开始时把next_direction赋给current_direction而按键事件只修改next_direction并且检查新方向是否与当前方向相反。第二蛇身列表的第一个元素是蛇头这个约定要在整个代码里保持一致。我见过有人一会儿用snake[0]表示头一会儿用snake[-1]表示头最后自己都搞混了。第三吃到食物后蛇变长但变长的位置是在头部新增尾部不删除。这符合直觉蛇头往前伸了一格尾巴没动所以整体长度加一。2.3 食物生成的随机策略食物生成看起来就是random.randint一下但有个坑如果随机到的位置正好在蛇身上你不能直接把食物放在那里否则玩家一吃就出问题。常见的处理方式是循环随机直到找到一个不在蛇身上的位置def generate_food(snake): while True: x random.randint(0, GRID_WIDTH - 1) y random.randint(0, GRID_HEIGHT - 1) if (x, y) not in snake: return (x, y)这个写法在蛇很短的时候没问题但如果蛇很长比如占了 800 个格子中的 700 个那循环可能会跑很多次才找到一个空位。更优雅的做法是先收集所有空格子然后从空格子列表里随机选一个def generate_food(snake): empty_cells [(x, y) for x in range(GRID_WIDTH) for y in range(GRID_HEIGHT) if (x, y) not in snake] if not empty_cells: return None # 蛇已经占满整个屏幕游戏胜利 return random.choice(empty_cells)这个写法虽然每次都要遍历整个网格但 30x30 也就 900 个格子现代计算机跑这个毫无压力。而且它顺便处理了“蛇占满屏幕”的胜利条件一举两得。2.4 碰撞检测的三种情况贪吃蛇的碰撞检测分三种撞墙、撞自己、吃到食物。撞墙的判断很简单蛇头坐标超出网格范围就是撞墙if new_head[0] 0 or new_head[0] GRID_WIDTH or new_head[1] 0 or new_head[1] GRID_HEIGHT: game_over True撞自己的判断是看新蛇头坐标是否已经在蛇身列表里。但这里有个细节如果蛇没吃到食物尾部会在这一帧被删除所以新蛇头撞到“即将被删除的尾部”不算撞自己。严格来说应该判断new_head in snake[:-1]排除掉尾部那一格。不过在实际游戏中蛇头不可能在一帧内移动到尾部的位置因为尾部离头部至少有两格距离除非蛇长度只有 1 或 2。但为了代码严谨我还是建议用snake[:-1]。吃到食物的判断就是new_head food这个最简单。我一般会把这三个判断写成一个函数返回一个状态枚举或者字符串这样主循环里逻辑更清晰def check_collision(new_head, snake, food): if new_head[0] 0 or new_head[0] GRID_WIDTH or new_head[1] 0 or new_head[1] GRID_HEIGHT: return wall if new_head in snake[:-1]: return self if new_head food: return food return none2.5 帧率控制与游戏速度Pygame 的Clock.tick(fps)控制的是每秒循环多少次。贪吃蛇不需要 60 帧那么快一般 8 到 15 帧就够了。帧率越高蛇移动越快。我一般初始设 10 帧然后每吃到一个食物帧率加 0.5 或者 1这样游戏会越来越有挑战性。但这里有个问题如果你直接在每一帧里更新蛇的位置那蛇的移动速度就完全由帧率决定了。帧率 10 的时候蛇每秒移动 10 格看起来还行。但如果你想让蛇移动更平滑比如每 0.1 秒移动一格但画面刷新保持 60 帧那就需要把“逻辑更新”和“画面渲染”分开。Pygame 里可以用一个计时器变量来累积时间move_timer 0 move_interval 100 # 毫秒 dt clock.tick(60) move_timer dt if move_timer move_interval: move_timer 0 update_snake()这样做的好处是画面可以保持 60 帧的流畅度但蛇的移动速度由move_interval独立控制。不过对于贪吃蛇这种格子游戏其实没必要这么复杂直接用低帧率就够了。我提这个是因为如果你后面想做更复杂的游戏这种“逻辑与渲染分离”的思路非常重要。3. 完整实操流程与代码实现3.1 环境准备与项目结构首先确保你装了 Python版本 3.7 以上都行。然后安装 Pygamepip install pygame如果你用的是虚拟环境记得先激活再装。装完之后可以跑一个最简单的测试import pygame pygame.init() screen pygame.display.set_mode((400, 300)) pygame.display.set_caption(测试) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False pygame.quit()如果能看到一个空白窗口并且能正常关闭说明环境没问题。项目结构我建议就一个文件snake.py最多再加一个settings.py放常量。贪吃蛇这个规模不需要搞复杂的模块拆分一个文件写清楚反而更好维护。但如果你想让代码更整洁可以把蛇的逻辑、食物生成、渲染分别封装成类这个看个人习惯。3.2 初始化与常量定义先把所有常量定义好后面改参数只需要改这一处import pygame import random import sys # 网格设置 CELL_SIZE 20 GRID_WIDTH 30 GRID_HEIGHT 30 WINDOW_WIDTH CELL_SIZE * GRID_WIDTH WINDOW_HEIGHT CELL_SIZE * GRID_HEIGHT # 颜色定义 BLACK (0, 0, 0) WHITE (255, 255, 255) GREEN (0, 200, 0) DARK_GREEN (0, 150, 0) RED (200, 0, 0) GRAY (40, 40, 40) # 游戏设置 INITIAL_FPS 10 FPS_INCREMENT 0.5 MAX_FPS 25颜色我选了深绿色做蛇头浅一点的绿色做蛇身红色做食物黑色背景灰色画网格线。这样视觉上比较清晰不会太刺眼。3.3 蛇与食物的数据结构蛇我用一个列表存坐标元组初始长度 3放在屏幕中间偏左的位置方向向右def init_snake(): start_x GRID_WIDTH // 4 start_y GRID_HEIGHT // 2 return [(start_x, start_y), (start_x - 1, start_y), (start_x - 2, start_y)]食物生成用前面说的空格子随机法def generate_food(snake): empty_cells [(x, y) for x in range(GRID_WIDTH) for y in range(GRID_HEIGHT) if (x, y) not in snake] if not empty_cells: return None return random.choice(empty_cells)方向我用一个字典映射按键到方向向量DIRECTIONS { pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1), pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0), }注意 Pygame 的坐标系原点在左上角所以向上是 y 减 1向下是 y 加 1。这个和数学坐标系是反的刚开始写的时候容易搞混。3.4 主循环的完整实现主循环的结构是这样的def main(): pygame.init() screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(贪吃蛇) clock pygame.time.Clock() font pygame.font.SysFont(simhei, 24) snake init_snake() food generate_food(snake) direction (1, 0) next_direction (1, 0) score 0 fps INITIAL_FPS game_over False while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() if event.type pygame.KEYDOWN: if game_over: if event.key pygame.K_r: snake init_snake() food generate_food(snake) direction (1, 0) next_direction (1, 0) score 0 fps INITIAL_FPS game_over False elif event.key pygame.K_q: pygame.quit() sys.exit() else: if event.key in DIRECTIONS: new_dir DIRECTIONS[event.key] if (new_dir[0] direction[0], new_dir[1] direction[1]) ! (0, 0): next_direction new_dir if not game_over: direction next_direction new_head (snake[0][0] direction[0], snake[0][1] direction[1]) # 碰撞检测 if new_head[0] 0 or new_head[0] GRID_WIDTH or new_head[1] 0 or new_head[1] GRID_HEIGHT: game_over True elif new_head in snake[:-1]: game_over True else: snake.insert(0, new_head) if new_head food: score 1 fps min(fps FPS_INCREMENT, MAX_FPS) food generate_food(snake) if food is None: game_over True else: snake.pop() # 绘制 screen.fill(BLACK) for x in range(0, WINDOW_WIDTH, CELL_SIZE): pygame.draw.line(screen, GRAY, (x, 0), (x, WINDOW_HEIGHT)) for y in range(0, WINDOW_HEIGHT, CELL_SIZE): pygame.draw.line(screen, GRAY, (0, y), (WINDOW_WIDTH, y)) for i, (x, y) in enumerate(snake): color DARK_GREEN if i 0 else GREEN rect pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE) pygame.draw.rect(screen, color, rect) pygame.draw.rect(screen, BLACK, rect, 1) if food: fx, fy food rect pygame.Rect(fx * CELL_SIZE, fy * CELL_SIZE, CELL_SIZE, CELL_SIZE) pygame.draw.rect(screen, RED, rect) score_text font.render(f分数: {score}, True, WHITE) screen.blit(score_text, (10, 10)) if game_over: over_text font.render(游戏结束 按 R 重开 按 Q 退出, True, WHITE) text_rect over_text.get_rect(center(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2)) screen.blit(over_text, text_rect) pygame.display.flip() clock.tick(fps) if __name__ __main__: main()这段代码可以直接跑。我特意把网格线画出来了方便调试的时候看清楚每个格子的位置。实际发布的时候可以去掉网格线看起来更干净。3.5 关键参数的计算与选择帧率初始值我选 10是因为在 30x30 的网格上蛇每秒移动 10 格从屏幕左边到右边需要 3 秒这个速度对新手来说比较友好。如果设成 20蛇一秒就穿过半个屏幕反应时间太短。每吃一个食物帧率加 0.5上限 25。这样前期节奏慢后期逐渐加快但不会快到无法操作。我实测下来25 帧已经是普通人的反应极限了再快就变成拼手速而不是拼策略了。格子大小 20 像素30x30 的网格窗口就是 600x600。这个尺寸在大多数显示器上都能完整显示不会超出屏幕。如果你想要更大的游戏区域可以改成 40x40但窗口就变成 800x800 了小屏幕笔记本可能显示不全。分数显示放在左上角字体大小 24用系统自带的黑体。如果你要发布给其他人玩建议把字体文件打包进去不然在没有黑体的系统上会显示成方块。4. 常见问题与排查技巧实录4.1 蛇移动时出现“闪烁”或“拖影”这个问题我遇到过好几次表现是蛇移动的时候旧的位置没有完全擦除新位置和旧位置重叠在一起看起来像拖影。原因通常是你没有在每一帧开始时清空屏幕。screen.fill(BLACK)这一行必须放在绘制任何东西之前。如果你用的是pygame.display.update()而不是pygame.display.flip()还需要注意更新区域的问题。我建议直接用flip()它会更新整个屏幕虽然效率略低但不会出这种问题。另一个可能的原因是帧率太高蛇移动太快视觉上看起来像闪烁。把帧率降到 10 左右就能解决。4.2 按键没反应或者方向控制不灵敏Pygame 的按键事件是在事件循环里处理的如果你在事件循环里做了耗时操作比如复杂的计算或者文件读写按键事件就会堆积导致反应迟钝。贪吃蛇的逻辑很简单一般不会出现这个问题。但如果你发现按键没反应先检查事件循环是不是在每一帧都执行了。还有一个常见问题是按键重复触发。Pygame 默认会发送按键重复事件如果你按住方向键不放KEYDOWN事件会连续触发。对于贪吃蛇来说这其实没关系因为方向改变只在next_direction上生效连续触发也只是重复设置同一个方向。但如果你要做“按一次键只改变一次方向”的逻辑就需要用pygame.key.set_repeat()来控制。4.3 蛇吃到食物后没有变长这个问题通常是尾部删除逻辑写错了。检查你的代码吃到食物的时候应该跳过snake.pop()。如果你把pop()写在了if外面那不管吃没吃到食物蛇的长度都不会变。还有一种可能是食物生成的位置和蛇头位置判断有问题。比如你用像素坐标判断new_head food但食物存的是网格坐标那永远不可能相等。确保所有坐标都在同一个坐标系里。4.4 游戏结束后按 R 无法重开重开逻辑需要把所有状态变量都重置蛇、食物、方向、分数、帧率、游戏结束标志。少重置一个就会出问题。比如你重置了蛇但没重置方向那新蛇可能一开始就往错误的方向移动。我建议把重置逻辑写成一个函数这样不容易漏def reset_game(): snake init_snake() food generate_food(snake) direction (1, 0) next_direction (1, 0) score 0 fps INITIAL_FPS game_over False return snake, food, direction, next_direction, score, fps, game_over4.5 常见问题速查表问题现象可能原因解决方法蛇移动有拖影没有清空屏幕每帧开始加screen.fill(BLACK)按键无反应事件循环未执行检查pygame.event.get()是否在循环内蛇不变长尾部删除逻辑错误吃到食物时跳过snake.pop()食物刷在蛇身上生成时未检查用空格子列表随机选择游戏结束无法重开状态未完全重置用reset_game()统一重置蛇移动速度忽快忽慢帧率不稳定用clock.tick(fps)固定帧率窗口无法关闭未处理 QUIT 事件在事件循环里处理pygame.QUIT4.6 几个我踩过的坑和独家技巧第一个坑是蛇身颜色渐变。我一开始想给蛇身做一个从深绿到浅绿的渐变效果结果发现每帧都要重新计算每个块的颜色代码复杂不说性能还下降了。后来我改成蛇头深绿、蛇身统一浅绿简单又好看。如果你真的想要渐变可以在蛇身列表上按索引插值但没必要。第二个坑是食物闪烁效果。我想让食物有一个呼吸灯一样的闪烁结果用pygame.time.get_ticks()计算时间每帧改变食物的透明度。但 Pygame 的draw.rect不支持透明度需要用Surface加set_alpha。后来我放弃了直接画一个红色方块加一个白色小点看起来像苹果效果也不错。第三个技巧是加一个“暂停”功能。按空格键暂停再按恢复。实现很简单加一个paused变量在更新逻辑外面套一层if not paused。但要注意暂停时不要更新帧率计时器否则恢复后蛇会突然跳一大步。第四个技巧是记录最高分。用一个文本文件存最高分每次游戏结束时比较并更新。文件读写用with open就行注意处理文件不存在的情况。这个功能虽然简单但能大大增加游戏的可玩性。第五个技巧是给蛇加一个“死亡动画”。游戏结束时让蛇身从尾部开始逐帧消失而不是直接显示结束画面。实现方式是维护一个death_timer每帧删除蛇身列表的最后一个元素直到列表为空再显示结束文字。这个效果做出来之后整个游戏的质感会提升一个档次。4.7 性能优化与扩展方向贪吃蛇这个规模基本不需要考虑性能问题但如果你把网格调到 100x100蛇长到几千节那绘制和碰撞检测就会变慢。优化方向有几个碰撞检测用集合而不是列表把蛇身坐标存到一个set里判断new_head in snake_set是 O(1) 的绘制时只绘制可见区域超出屏幕的块直接跳过食物生成用空格子集合而不是每次遍历整个网格。扩展方向的话可以加障碍物、加传送门、加双人模式、加道具系统。每一个扩展都会带来新的设计问题比如双人模式怎么处理两个蛇的碰撞、道具系统怎么设计随机效果。这些都可以作为你后续练手的项目。我个人觉得贪吃蛇写完之后下一步可以试试用 Pygame 写一个简单的平台跳跃游戏那个会涉及到重力、跳跃、平台碰撞比贪吃蛇复杂一个量级但核心思路是一样的状态管理、碰撞检测、渲染循环。把这两个项目都吃透你对 2D 游戏开发的基本套路就掌握得差不多了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑