尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python贪吃蛇开发实战:用pygame巩固编程基础,打造第一款小游戏

Python贪吃蛇开发实战:用pygame巩固编程基础,打造第一款小游戏 用Python写一个贪吃蛇小游戏最大的价值不是把游戏做完而是把Python的基础语法串起来跑一遍。输入处理、列表操作、条件判断、循环控制、函数封装这些平时单独写很容易懂但一旦放进一个不断变化的游戏循环里很多隐藏问题就会冒出来。这个练手项目尤其适合刚学完Python基础、想找一个能完整运行的小项目的人。我最近又按这个思路用pygame把贪吃蛇重新写了一遍结论是只要环境装好按“窗口创建 - 蛇移动 - 食物生成 - 碰撞判断”的顺序拆解新手也能在几个小时内跑出第一版。下面直接进入正题。我会先说明这个项目到底在练什么再给环境准备和完整源码最后把常见的报错、边界和扩展思路一起整理出来。如果你只是想复制代码可以直接跳到第四节如果你想理解每一步为什么这么写建议从头顺序看。1. 先搞懂这个练手项目到底在练什么1.1 它适合哪类Python学习者这个项目适合的并不是完全没写过代码的人而是已经认识if、for、while、列表和函数的读者。如果这些概念还没接触过我建议先花半天把基础语法过一遍否则看代码时会卡在“为什么这里用列表存蛇身”这类问题上。贪吃蛇的代码量不大一套完整可运行版本通常也就一两百行。但它的逻辑链路比普通练习复杂很多窗口要持续刷新键盘要实时响应蛇的位置每帧都要更新食物消失后要重新生成。这些东西组合在一起正好能把零散的语法点变成一套完整程序。更重要的是它能让初学者感受到“写小游戏”和“写脚本”的区别。脚本往往是顺序执行一遍就结束游戏需要在一个while循环里反复处理输入、更新状态、绘制画面。这种循环思想是很多图形程序和游戏程序的基础。1.2 一次游戏要用到的核心语法点先列一下这个项目里会反复用到的内容列表蛇身用列表存储蛇头是第一个元素蛇尾是最后一个元素。元组坐标用(x, y)表示字典用方向键映射方向向量。while循环游戏主循环每一帧执行一次更新。for循环遍历蛇身绘制矩形或者遍历事件列表。if / elif / else判断键盘事件、判断是否撞墙、是否吃到食物。函数把初始化、生成食物、绘制、检查碰撞分别封装成函数。模块导入import pygame、import random。常量定义窗口大小、格子大小、颜色、游戏速度用大写常量统一管理。这些点覆盖了Python基础中最常用的一部分。把它们放进一个项目的上下文里理解比单独背语法记得更牢。1.3 为什么游戏比打印九九乘法表更能巩固基础打印九九乘法表确实能练习循环但它的缺点是无论循环多少次外部状态几乎不变。贪吃蛇不一样蛇身列表会越来越长食物的位置会不断变化游戏速度会随着得分提高。每一步都要关心“当前状态是什么”和“状态如何迁移”这其实是编程里非常重要的思维训练。另外游戏项目天然有可视化反馈。代码出错时你能直观看到画面是否正常、蛇是否乱跑、碰撞是否误判。这种即时反馈能大大提高调试效率也能让人更有动力继续改下去。对新手来说能看到自己写的程序变成一个能动的小游戏比起单纯在终端里打印结果要有成就感得多。2. 搭建运行环境Python和pygame缺一不可2.1 检查Python版本和pip命令我默认你已经装好了Python。判断方式很简单打开终端或命令行输入python --version如果提示不是内部或外部命令再试python3 --versionWindows下一般用pythonmacOS和Linux下可能要用python3。安装Python时建议勾选“Add Python to PATH”否则后面在命令行里找不到python命令会很麻烦。接着检查pip是否可用pip --version如果pip也没找到可以试python -m pip --version。这一步的目的不是做表面检查而是避免后面安装pygame时出现“找不到pip”或“把包装到了另一个Python环境”这种问题。2.2 安装pygame并确认导入成功贪吃蛇最常用的图形库是pygame。它专门用来做2D小游戏已经封装了窗口、事件、图片和声音等常用功能。安装命令很简单pip install pygame或者使用Python模块方式python -m pip install pygame安装完成后在Python交互环境或脚本里检查import pygame print(pygame.ver)如果打印出版本号说明安装成功。如果报ModuleNotFoundError先确认你执行python和pip的环境是不是同一个。很多时候用户装了多个Python版本pip装到了3.8但命令运行的是3.11两边自然对不上。2.3 如果没有pygame还能用tkinter吗可以。Python自带tkinter不需要额外安装图形库。用tkinter写贪吃蛇也完全可以只是事件处理方式不同pygame用pygame.event.get()轮询事件tkinter用bind绑定键盘事件。从学习目的看两者都能巩固基础。不过我个人建议第一次做这个项目时直接使用pygame。原因是pygame的坐标系、帧率和矩形绘制更符合游戏开发直觉网上参考资料也多。如果你在安装pygame时遇到网络问题可以换国内镜像源安装pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后再用import pygame验证一次。环境问题解决了后面写代码才不会一头雾水。3. 拆解贪吃蛇的四个核心模块3.1 初始化窗口和游戏参数第一件事是定义游戏窗口的大小和格子大小。贪吃蛇的移动是离散的每次走一个格子所以需要把窗口分成很多小格子。我一般这样设定CELL_SIZE 20 GRID_WIDTH 30 GRID_HEIGHT 20 WINDOW_WIDTH CELL_SIZE * GRID_WIDTH WINDOW_HEIGHT CELL_SIZE * GRID_HEIGHT这里的CELL_SIZE是每个格子的边长单位是像素。GRID_WIDTH和GRID_HEIGHT分别是横向和纵向的格子数量。窗口总宽等于格子大小乘数量这样蛇头坐标可以精确落在格子里不会出现画出来对不齐的情况。为什么不让窗口宽度直接写600再让CELL_SIZE随便定因为后面判断边界和生成食物时都要依赖格子坐标。如果窗口尺寸不是格子大小的整数倍蛇走到边缘时会出现半格偏移碰撞判断就会变得很麻烦。先把窗口和格子解耦后续改起来也容易。游戏速度用一个FPS常量控制也就是每秒刷新的帧数。FPS越低蛇移动越慢FPS越高蛇移动越快。初版建议从FPS 10开始这个速度对新手比较友好。3.2 蛇的移动和数据存储方式蛇身可以用一个列表存储每个元素是一个坐标元组。比如snake [(15, 5), (14, 5), (13, 5)]这里(15, 5)是蛇头后面两个是身体。坐标含义是“第几列、第几行”不是像素位置。画图的时候再根据坐标乘以CELL_SIZE得到左上角的像素位置。蛇的移动分为四步根据当前方向dir计算新的蛇头坐标。把新蛇头插入列表最前面。如果没有吃到食物就把列表最后一个元素弹出。如果吃到了食物就不弹出蛇身长度自然增加一格。这个做法的关键是“弹出尾巴”。如果不弹出蛇每帧都会变长最终填满整个窗口。如果弹出的位置不对蛇看起来就像拖着一条断尾在跑。代码里要保证弹出的顺序先插入新头再删除旧尾。顺序反了可能吃到食物时丢掉尾巴或者没吃到食物时蛇头跑到错误位置。方向控制上需要做“禁止反向”的判断。否则蛇正在向右走时按左键蛇头会直接穿进身体。常见写法是新方向与当前方向相加不等于零向量时才允许更新方向。例如当前方向是(1, 0)新方向(-1, 0)相加等于(0, 0)说明是反向此时忽略按键。3.3 食物生成逻辑食物可以用一个坐标元组表示例如food (5, 5)。每次生成食物时需要保证食物不在蛇身上。最简单的做法是while True: food (random.randint(0, GRID_WIDTH - 1), random.randint(0, GRID_HEIGHT - 1)) if food not in snake: break这里random.randint(0, GRID_WIDTH - 1)生成的是0到29之间的整数对应横向格子索引。为什么不从1开始因为蛇头坐标也允许在0到29之间如果从1开始边缘的一圈就永远不会有食物看起来不够自然。食物生成后要判断是否被蛇吃到。判断条件通常是蛇头坐标等于食物坐标。如果相等就把一个“需要增长”的标记设为True。在下一次移动时不弹出蛇尾蛇就增长了一格。还有一个细节如果蛇很长几乎占满了整个窗口while循环可能会尝试很多次才找到空位。对于贪吃蛇来说这种概率很低可以忽略。但如果要追求严谨可以把所有空格子收集到一个列表里再从中随机选一个这样每次生成效率都稳定。初版可以不用纠结先跑通再说。3.4 碰撞检测和死亡判定贪吃蛇的死亡条件有两个撞墙和撞自己。撞墙的判断很简单if new_head[0] 0 or new_head[0] GRID_WIDTH or new_head[1] 0 or new_head[1] GRID_HEIGHT: running False注意使用而不是。因为合法下标是0到GRID_WIDTH-1一旦等于GRID_WIDTH就已经超出边界。撞自己的判断稍微复杂一点。常见写法是判断新蛇头是否在蛇身列表里。但这里有个坑如果新蛇头是用切片或复制生成的要小心列表长度。更稳妥的方式是if new_head in snake_body: running False这里的snake_body是去掉旧蛇尾后的身体。因为如果食物在尾巴的位置可能允许蛇头移动到尾巴位置时尾巴刚好移开从而不算碰撞。但初版可以不考虑这个细节点直接用整个蛇身判断也够用。等你想做“完美”版本时再根据移动顺序调整。死亡后常见处理是弹出一段文字“Game Over”然后pygame.quit()退出。也可以改成等待按键重新开始后面第五节会讲。4. 完整源码从启动到游戏结束4.1 完整代码下面是我写过的一版完整代码基于pygame实现去掉了复杂装饰保留了核心逻辑。代码可以直接保存为snake.py运行。import pygame import random # 初始化 pygame.init() # 基础参数 CELL_SIZE 20 GRID_WIDTH 30 GRID_HEIGHT 20 WINDOW_WIDTH CELL_SIZE * GRID_WIDTH WINDOW_HEIGHT CELL_SIZE * GRID_HEIGHT FPS 10 # 颜色 BLACK (0, 0, 0) WHITE (255, 255, 255) RED (200, 30, 30) GREEN (30, 180, 60) # 方向映射 DIRECTIONS { pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1), pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0), } # 创建窗口 screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(贪吃蛇) clock pygame.time.Clock() # 初始状态 snake [(GRID_WIDTH // 2, GRID_HEIGHT // 2)] snake.append((snake[0][0] - 1, snake[0][1])) snake.append((snake[0][0] - 2, snake[0][1])) direction (1, 0) # 初始向右移动 score 0 font pygame.font.Font(None, 36) def random_food(): while True: pos (random.randint(0, GRID_WIDTH - 1), random.randint(0, GRID_HEIGHT - 1)) if pos not in snake: return pos food random_food() def draw(): screen.fill(BLACK) # 画蛇 for i, (x, y) in enumerate(snake): rect pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE) # 蛇头用亮色蛇身用深色 if i 0: pygame.draw.rect(screen, GREEN, rect) else: pygame.draw.rect(screen, (0, 120, 40), rect) # 画食物 pygame.draw.rect( screen, RED, pygame.Rect(food[0] * CELL_SIZE, food[1] * CELL_SIZE, CELL_SIZE, CELL_SIZE) ) # 显示得分 score_surface font.render(fScore: {score}, True, WHITE) screen.blit(score_surface, (10, 10)) # 游戏主循环 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 in DIRECTIONS: new_dir DIRECTIONS[event.key] # 禁止反向 if new_dir[0] direction[0] ! 0 or new_dir[1] direction[1] ! 0: direction new_dir elif event.key pygame.K_SPACE and game_over: # 重新开始 snake [(GRID_WIDTH // 2, GRID_HEIGHT // 2)] snake.append((snake[0][0] - 1, snake[0][1])) snake.append((snake[0][0] - 2, snake[0][1])) direction (1, 0) score 0 food random_food() game_over False if not game_over: # 计算新蛇头 head_x, head_y snake[0] dx, dy direction new_head (head_x dx, head_y dy) # 撞墙判断 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 else: snake.insert(0, new_head) # 撞自己判断 if new_head in snake[1:]: game_over True else: # 是否吃到食物 if new_head food: score 1 food random_food() else: snake.pop() # 绘制 draw() # 游戏结束提示 if game_over: over_surface font.render(Game Over! Press SPACE to restart, True, WHITE) screen.blit(over_surface, (WINDOW_WIDTH // 2 - 200, WINDOW_HEIGHT // 2)) pygame.display.flip() clock.tick(FPS) pygame.quit()这份代码里按空格可以重新开始方便测试时不反复关闭窗口。如果你不需要这个功能可以把K_SPACE分支删掉留下QUIT处理即可。4.2 代码里几个容易改错的地方第一处是颜色元组的范围。pygame里RGB每个分量通常取0到255如果写成(0, 0, 256)有些环境会直接报溢出错误。建议保持在0到255之间。第二处是蛇的初始长度和方向。我定义了3个格子初始向右。如果你改成初始向左要注意后面的身体坐标也要保持一条直线否则蛇在启动时会扭成奇怪形状。第三处是撞自己的判断位置。代码里先snake.insert(0, new_head)再判断new_head in snake[1:]。因为新蛇头已经插入列表判断时要用snake[1:]排除掉新蛇头本身否则永远会认为撞到自己游戏一开局就结束。第四处是画食物时使用pygame.Rect它的四个参数是(x, y, width, height)不是(x1, y1, x2, y2)。写代码时要注意不要把第四参数写成结束坐标。4.3 如何验证代码是否正常第一次运行前我会做三个层面的验证。第一启动不报错。双击运行或命令行执行python snake.py后窗口能正常弹出标题栏显示“贪吃蛇”。第二初始画面正常。窗口里有黑色背景绿色蛇身一个红色食物。蛇头位置在窗口偏中间蛇身向左延伸。第三操作逻辑正确。按方向键蛇能移动按反向键不会立刻掉头吃掉食物后得分增加蛇身变长撞墙或撞自己后画面出现“Game Over”。如果这三步都通过说明核心逻辑没问题。之后再去扩展功能才有意义。5. 单局跑通后如何扩展成更像样的游戏5.1 增加计分和速度递增初版得分只是数字变化玩久了会嫌单调。更常见的做法是得分越高移动速度越快。实现方式很简单把clock.tick(FPS)中的FPS改成动态变量例如current_speed min(FPS score // 5, 30) clock.tick(current_speed)每吃5个食物速度加1上限设为30。这里设置上限是因为速度超过30后画面刷新很快键盘操作难度急剧上升反而不适合普通玩家。如果想让速度变化更平滑可以记录吃到食物的数量而不是直接依赖score。因为score可能因为扩展功能而出现加分项容易和速度挂钩混乱。5.2 增加重新开始和暂停初版代码已经加了空格重新开始。暂停功能也很简单在游戏循环里增加一个暂停标记paused False按P键切换暂停。暂停时只处理事件和绘制提示文字不更新蛇的位置。注意事件处理部分要单独写一个状态判断避免玩家在暂停时还能移动蛇。重新开始时需要把所有状态重置蛇身恢复初始长度方向恢复向右分数归零食物重新随机生成游戏结束标记设为False。这些代码可以封装成一个reset_game()函数避免在按键分支里写一大串重复代码。5.3 增加简单排行榜或最高分记录如果你想练一下文件读写可以把最高分保存到一个txt文件里。每次游戏结束时判断当前得分是否大于历史最高分如果大于则写入文件。try: with open(high_score.txt, r) as f: high_score int(f.read().strip()) except FileNotFoundError: high_score 0这里使用try except是为了处理文件不存在的情况。第一次运行时没有历史记录直接让最高分为0。当游戏结束时再判断一次更新分数。这里要注意文件编码和换行符。读取时最好用strip()去掉空白否则int()可能会因为换行符报错。写入时用str(high_score)写入整数。这个扩展能帮你练习异常处理和文件操作算是从游戏逻辑跳到通用编程能力的过渡。6. 常见报错和排查顺序6.1 启动时报 ModuleNotFoundError最常见的原因是pygame没有安装成功。先执行pip show pygame如果显示WARNING: Package(s) not found说明确实没装上。接着执行pip install pygame。如果已经装上了还是报错就要怀疑Python环境不一致。比如你用pip安装但运行脚本时用的是另一个解释器。可以这样验证python -c import pygame; print(pygame.ver)如果这个命令能正常打印版本号但运行snake.py却报错检查脚本文件是不是被命名成了pygame.py。如果脚本名字和库名相同Python导入时会优先导入当前目录下的同名文件导致找不到真实的pygame。这个问题很隐蔽我见过不少人踩坑。6.2 窗口一闪而过窗口出现后立刻关闭多半是脚本抛出了异常。因为pygame窗口模式下如果代码运行到pygame.quit()或抛出未捕获异常控制台会显示错误但窗口可能已经退出。解决办法是不要直接双击运行而是在命令行里执行脚本python snake.py这样能看到完整的报错信息。常见错误包括未初始化pygame就使用相关函数、坐标越界、函数名拼写错误。排查时先看终端里的Traceback定位到具体行再根据错误类型修正。6.3 方向键没反应或蛇反向移动方向键没反应先确认事件类型是不是pygame.KEYDOWN。有些例子用KEYUP那就可能只在按键弹起时触发手感会很奇怪。本项目使用KEYDOWN按下即触发。反向移动的原因是方向更新逻辑没有过滤掉180度掉头。检查代码里是否写了下述判断if new_dir[0] direction[0] ! 0 or new_dir[1] direction[1] ! 0: direction new_dir如果你直接写成direction new_dir那么快速连按“上、左”时蛇可能瞬间掉头并撞进自己身体。注意这个判断要在同一帧事件处理中反复执行时仍然有效如果玩家在同一个帧内按了多个键最终方向会以最后一个合法方向为准这是正常现象。6.4 蛇撞到身体却没有结束这个现象通常是“允许蛇头移动到蛇尾原位置”造成的。因为蛇在移动时如果尾部会同时移开那么蛇头可以安全地移动到尾巴之前的位置。但判断碰撞如果用整体蛇身在新蛇头等于旧尾巴时会被误判为碰撞于是游戏提前结束。解决办法是先判断是否吃到食物。如果吃到食物尾巴不弹出此时蛇头撞到尾巴就是真撞如果没吃到食物尾巴即将弹出新蛇头等于旧尾巴不算撞。具体实现可以这样if new_head food: # 增长不弹出尾巴所以旧尾巴还在撞到就算撞 snake.insert(0, new_head) if new_head in snake[1:]: game_over True else: score 1 food random_food() else: # 先弹出尾巴再判断碰撞 tail snake[-1] snake.pop() snake.insert(0, new_head) # 注意此时旧尾巴已经不在蛇身里 if new_head in snake[1:]: game_over True不过初版直接用整体蛇身判断也能玩只是偶尔会在靠近尾巴时提前判定。如果你想做出“完美”手感就需要关注这个细节。7. 容易被忽略的边界和性能细节7.1 窗口大小的取整问题pygame窗口大小可以是任意整数但贪吃蛇这种格子游戏最好保证总宽和总高是格子大小的整数倍。不然你设置GRID_WIDTH 29、CELL_SIZE 20窗口宽580没问题但如果GRID_WIDTH取小数pygame会直接报错。另外格子数量也不要设置太大。比如横向100格、纵向80格窗口会非常大普通屏幕根本放不下。我建议横向30到40格纵向20到30格配合20像素格子大小窗口在600x400到800x600之间比较合适。7.2 速度并非越快越好初版FPS 10意味着每秒刷新10帧蛇每秒移动10格。这个速度适合第一次测试。如果你开局就把FPS调到60蛇会像飞一样方向键根本来不及反应过来。实际测试时我建议把速度增长门槛设得高一点。比如每吃10个食物速度加1上限为20。这样新手能玩一两分钟又不会觉得太慢。如果想要挑战模式可以单独做一个变量控制难度不要把速度写死在多个地方。7.3 蛇身列表的顺序千万不能搞反很多变形bug都出在列表顺序上。蛇头必须是列表第一个元素蛇尾是最后一个。绘制蛇时如果遍历顺序错了蛇头可能被画在蛇身下面看起来像身体盖住了头。计算移动时如果pop()写成pop(0)会删掉蛇头蛇身瞬间乱套。我可以给一个简单的自我检查方法打印snake[0]和snake[-1]看看是否分别对应蛇头和蛇尾。如果蛇头坐标不是当前方向应该到达的位置说明移动逻辑有问题。7.4 批量测试和自动运行思路调试游戏时每次手动操作很慢。如果想验证碰撞逻辑可以让蛇自动朝某个方向移动并在特定坐标触发食物。做法是临时把键盘事件注释掉用一套预设的方向序列让蛇自动走观察是否在预期位置game over。这种自动化测试思路对以后写更复杂项目很有用。游戏虽然是交互式的但核心状态更新函数可以独立测试。比如把移动、碰撞、食物生成拆成纯函数就可以编写测试脚本验证边界条件。这里的经验是任何项目只要逻辑足够清晰都可以把自动测试加进来。小游戏也不例外。最后一个建议当代码跑通后不要急着加各种炫酷功能。先保存一份初始版本作为对照。后面改动过程中如果出现新bug可以快速回到初始版本确认是“原有逻辑问题”还是“新扩展引入的问题”。我在实际写这个项目时就吃过亏一开始直接加音效、动画和排行榜结果一个bug查了很久最后发现只是蛇身列表顺序写反了。基础逻辑不稳功能堆得越多排查越难。贪吃蛇虽然只是一个练手项目但它把Python编程里最实用的几个部分都过了一遍。如果你能不看源码自己独立写出一版能跑的贪吃蛇说明你已经具备了用Python处理简单状态变化的能力。这个基础上再去看Web开发、数据分析或自动化脚本都会顺很多。
返回列表