ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+Pygame开发贪吃蛇:核心循环与碰撞检测实战解析

Python+Pygame开发贪吃蛇:核心循环与碰撞检测实战解析 简介一份面向Python初学者与游戏开发爱好者的毕业设计报告围绕用Python和Pygame实现贪吃蛇小游戏展开适合正在做课程设计、毕业设计或入门游戏开发的同学借鉴。包内只有1个docx文档整包约197KB报告按软件工程流程组织从游戏产业与贪吃蛇发展背景到Python 3.8与PyCharm环境搭建再依次覆盖可行性分析、需求分析、界面与功能设计、代码实现、测试与调试、性能优化等环节。实现部分重点讲解了窗口初始化、图形绘制、事件处理、碰撞检测、得分累计与游戏结束条件并给出多级难度、速度调整等可扩展思路。目前已有3753人学习下载对想快速理解Pygame游戏逻辑、梳理程序开发步骤以及撰写规范毕业设计报告均有实用价值。1. 为什么把贪吃蛇作为 Python 入门项目贪吃蛇这个项目第一眼看上去太简单一条蛇、一个食物、四个方向键200 行代码就能跑起来。但真正把它拆开做一遍才会发现它完整覆盖了游戏开发最核心的循环——输入响应、状态更新、碰撞检测、界面渲染还要额外处理食物随机生成、得分累计、重开逻辑。这个毕业设计报告正好围绕这些点展开技术栈锁定 Python 3.8 Pygame。适合哪些人刚开始接触 Python、想找一个不依赖 Web 前端的图形界面项目或者已经在写爬虫和数据分析、想补一点游戏交互逻辑的人。我建议别急着往上加花活先把蛇的移动和碰撞摸透后面加难度、存档都顺理成章。2. 从需求分析到坐标建模2.1 技术选型Python 3.8、PyCharm 与 Pygame 的组合做这个项目时开发机是 Windows10 64 位语言选的 Python 3.8。选它不是因为它最新而是当时 Pygame 和 PyCharm 对 3.8 的支持已经非常稳定遇到问题能立刻搜到解决方案。Python 是解释型语言源码先编译成字节码再交给自己实现的虚拟执行环境运行这让开发阶段能快速改、快速跑不需要像 C 那样反复编译链接非常适合小游戏迭代。Pygame 负责窗口创建、图形绘制和事件监听它是基于 SDL 库封装的。用 Pygame 时整个游戏就是在一个 while 循环里反复做三件事处理用户输入、更新游戏状态、重新绘制画面。这个模型和现在前端游戏引擎的 requestAnimationFrame 思路一致先建立这个认知后面读任何游戏代码都不会乱。PyCharm 在这里的角色就是纯 IDE提供语法高亮、调试断点和项目文件管理。它不会帮你写逻辑但能让你在调试snake_coords这样的列表数据时直接看到每一步的增删变化。对小项目来说PyCharm 最大的价值其实是 F8 逐行执行能逼着你确认每行代码对蛇的坐标做了什么。2.2 可行性分析技术、经济、运行三层把关这份报告里做了一个标准的三维可行性分析在实际开发前走一遍很有必要。可行性维度分析要点结论技术可行Python 标准库 Pygame 模块可以完成窗口、绘图、随机数、键盘事件全部需求坐标模型简单无复杂网络通信可行经济可行Python 与 Pygame 均开源免费只需要一台能跑 Windows10 的普通电脑无额外授权成本可行运行可行游戏常驻内存小CPU 占用低数年前的入门级电脑也能流畅运行跨平台特性支持后续移植可行互联网休闲游戏市场早在 2019 年就已经趋于饱和移动游戏用户规模达到 6.2 亿增长率降到 3.2%。这意味着一个轻量级小游戏的竞争点不在画面而在逻辑是否完整、操作是否顺手。贪吃蛇这类规则简单的游戏反而最适合用来验证一套游戏程序的基本架构事件循环、状态机、碰撞检测、资源释放。技术可行性里最需要关注的不是“能不能做出蛇”而是“蛇的坐标数据怎么组织”这个决策决定了后续所有函数的复杂度。2.3 游戏规则的形式化用坐标元组描述贪吃蛇在写任何 Pygame 绘制代码之前先用数据结构把游戏规则定义清楚。原始报告中的做法是用一个元组列表存储蛇身体每一节的坐标每一节是一个字典包含x和yimport random # 地图尺寸单位是网格数 MAP_WIDTH 20 MAP_HEIGHT 20 # 每个网格在屏幕上占用的像素 CELL_SIZE 20 # 方向常量 UP up DOWN down LEFT left RIGHT right # 蛇头永远在下标 0 的位置 HEAD 0 # 初始化蛇长度 3初始方向向右 snake_coords [ {x: 5, y: 5}, {x: 4, y: 5}, {x: 3, y: 5} ] direction RIGHT逻辑说明MAP_WIDTH和MAP_HEIGHT定义地图是 20x20 的网格CELL_SIZE决定每个网格在屏幕上占 20 像素所以窗口尺寸是 400x400。snake_coords列表的头部是蛇头尾部是蛇尾移动时只在列表头部插入新坐标尾部按情况删除。HEAD常量让代码可读性更高避免到处写snake_coords[0]。方向用字符串常量而不是数字调试时打印出来的值一目了然。参数说明这里的random.randint(0, MAP_WIDTH - 1)生成食物坐标时用的是0到19不是1到20因为蛇在边界碰撞检测里用的也是-1和MAP_WIDTH这两个临界值。如果地图宽高设置成奇数也不会影响逻辑但视觉效果不居中建议保持偶数。3. 核心循环实现移动、食物与碰撞3.1 蛇的绘制从坐标到像素的映射绘制是 Pygame 里最直观的部分不需要手动管理每个像素只需要调用pygame.draw.rect()在指定位置画方块。原始报告里的draw_snake()做了两层绘制外层深蓝色内层亮绿色制造一点立体感。import pygame def draw_snake(screen, snake_coords, cell_size): for coord in snake_coords: x coord[x] * cell_size y coord[y] * cell_size # 外层身体 outer_rect pygame.Rect(x, y, cell_size, cell_size) pygame.draw.rect(screen, (30, 144, 255), outer_rect) # 内层装饰缩进 4 像素 inner_rect pygame.Rect(x 4, y 4, cell_size - 8, cell_size - 8) pygame.draw.rect(screen, (0, 255, 127), inner_rect)逻辑说明coord[x] * cell_size把网格坐标换算成像素坐标。比如蛇头在网格 (5, 5)换算后是 (100, 100)。inner_rect故意比outer_rect小 8 像素四周留出 4 像素边框这样蛇身每节之间有间隙视觉效果更清晰。如果省略内层绘制蛇就是一片纯色块移动时容易看不清方向。参数说明pygame.Rect接受四个参数左上角 x、左上角 y、宽度、高度。screen是主画布 Surface 对象所有绘制都在它上面完成。颜色值用 RGB 元组表示深蓝色是(30, 144, 255)亮绿色是(0, 255, 127)。内层缩进 4 像素是经验值如果cell_size小于 16 像素缩进要改成 2否则内层会挤成一团。3.2 键盘控制与方向反转问题控制蛇移动的核心不是直接修改坐标而是修改direction状态再由统一的移动函数去更新蛇身。这样做的好处是移动逻辑只依赖方向状态和具体按键解耦。按键映射关系如下按键direction 值头部坐标变化↑UPy 减 1↓DOWNy 加 1←LEFTx 减 1→RIGHTx 加 1移动函数的核心是“头部插入新坐标尾部按情况删除”def move_snake(direction, snake_coords): # 根据方向计算新头坐标 if direction UP: new_head {x: snake_coords[HEAD][x], y: snake_coords[HEAD][y] - 1} elif direction DOWN: new_head {x: snake_coords[HEAD][x], y: snake_coords[HEAD][y] 1} elif direction LEFT: new_head {x: snake_coords[HEAD][x] - 1, y: snake_coords[HEAD][y]} elif direction RIGHT: new_head {x: snake_coords[HEAD][x] 1, y: snake_coords[HEAD][y]} # 把新头插入到列表头部蛇整体向前移动一格 snake_coords.insert(0, new_head) return snake_coords逻辑说明insert(0, new_head)先把新蛇头放进去此时蛇的长度比原来多了 1 节。后面在判断吃到食物时如果没吃到就执行del snake_coords[-1]删掉尾节如果吃到了保留尾节蛇身变长。这个“先插头、后删尾”的顺序是固定的不能反过来否则蛇头坐标会被尾节覆盖。参数说明函数里没有做方向合法性校验。也就是说如果当前方向是 LEFT玩家按 RIGHT新头会被计算到蛇头的左侧但此时蛇身第二节就在左侧下一次碰撞检测会立刻判定死亡。所以真正的事件处理里必须加“不能反向移动”的判断。常见做法是只有在当前方向不是 RIGHT 时才允许把方向改成 LEFT以此类推。3.3 食物随机生成与吃掉判定食物生成很简单核心约束是必须在墙体范围之内避免生成到玩家看不到的区域。吃掉食物的判定本质是“蛇头坐标是否等于食物坐标”因为蛇和食物都是网格坐标不需要处理像素级碰撞精度刚好。def get_random_location(map_width, map_height): return { x: random.randint(0, map_width - 1), y: random.randint(0, map_height - 1) } def snake_is_eat_food(snake_coords, food): head snake_coords[HEAD] if head[x] food[x] and head[y] food[y]: # 吃到食物重置食物位置 food[x] random.randint(0, MAP_WIDTH - 1) food[y] random.randint(0, MAP_HEIGHT - 1) return True else: # 没吃到删除尾部一节保持长度不变 del snake_coords[-1] return False逻辑说明snake_is_eat_food做了两件事。第一判断蛇头是否与食物重合如果是就把食物坐标改成新的随机位置返回True第二如果没吃到就删除蛇尾一节模拟向前移动。注意这里对food字典直接修改了x和y字段所以函数不需要返回值调用方看到的food已经是新位置。这在 Python 里是因为字典是可变对象传引用。参数说明random.randint(0, map_width - 1)的上限是map_width - 1配合边界碰撞检测里的map_width把地图坐标限制在 0 到 19 的闭区间内。一个容易踩的坑是重新生成食物时只靠random.randint可能出现新食物生成在蛇身上。虽然不影响程序运行但玩家会看到食物在蛇身体里闪烁体验很差。后文会在进阶部分给出改进方案。4. 组装完整游戏与实战排错4.1 游戏主循环把三个子系统串起来Pygame 游戏的主循环是一个while True里面的执行顺序必须是处理事件、更新状态、绘制画面、刷新屏幕。顺序颠倒会出现一个经典现象蛇已经撞墙但画面还停留在撞墙前一帧。def main(): pygame.init() screen pygame.display.set_mode((MAP_WIDTH * CELL_SIZE, MAP_HEIGHT * CELL_SIZE)) pygame.display.set_caption(贪吃蛇) snake_coords [{x: 5, y: 5}, {x: 4, y: 5}, {x: 3, y: 5}] direction RIGHT food get_random_location(MAP_WIDTH, MAP_HEIGHT) clock pygame.time.Clock() while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() return elif event.type pygame.KEYDOWN: if event.key pygame.K_UP and direction ! DOWN: direction UP elif event.key pygame.K_DOWN and direction ! UP: direction DOWN elif event.key pygame.K_LEFT and direction ! RIGHT: direction LEFT elif event.key pygame.K_RIGHT and direction ! LEFT: direction RIGHT elif event.key pygame.K_ESCAPE: pygame.quit() return if not snake_is_alive(snake_coords, MAP_WIDTH, MAP_HEIGHT): break move_snake(direction, snake_coords) snake_is_eat_food(snake_coords, food) screen.fill((0, 0, 0)) draw_snake(screen, snake_coords, CELL_SIZE) draw_food(screen, food, CELL_SIZE) pygame.display.update() clock.tick(10) # 每秒 10 帧 pygame.quit()逻辑说明screen.fill((0, 0, 0))用黑色清空整个画布不清空的话上一帧的蛇身会残留在画面上形成长尾巴。pygame.display.update()把绘制好的 Surface 一次性呈现到窗口。clock.tick(10)控制游戏速度这里 10 表示每秒最多循环 10 次也就是蛇每秒移动 10 格。帧率越低蛇移动越慢这个值就是难度调节的直接入口。参数说明事件处理里的direction ! DOWN就是方向反转保护原理是禁止蛇头直接掉头。注意这里判断用的是当前时刻的direction所以狂按按键时只有后一帧进入事件循环才生效不会出现同一帧内连续两次转向导致蛇反向。MAP_WIDTH * CELL_SIZE算出来是 400如果你把CELL_SIZE改成 30窗口会变成 600 像素宽蛇移动速度不变但视觉上每格更大。4.2 Pygame 环境配置与帧率控制python 安装和 PyCharm 配置 python 环境是这类入门项目的第一道坎。Windows 上建议直接到 Python 官网下载 3.8 安装包安装时勾选“Add Python to PATH”这样不会出现python not found。PyCharm 里配置 python 环境时用File - Settings - Project - Python Interpreter选到本地 Python 安装目录下的python.exe即可。Pygame 安装只需要一条命令pip install pygame如果你用的是 Python 3.8 配套的 PyCharm 新项目建议先创建虚拟环境再安装避免污染全局 Python。安装完成后用下面这段代码验证 Pygame 能否正常初始化python -c import pygame; print(pygame.version.ver)能输出版本号就说明环境通了。帧率控制方面clock.tick(10)是固定帧率但它有一个副作用如果某帧逻辑复杂导致耗时超过 100ms后续帧率会被压缩。更稳妥的方式是用clock.tick(30)并降低控制台输出频率保证游戏逻辑计算速度和绘制速度分离。实际调试时我一般先把tick设为 5 验证移动逻辑确认没问题再调回 10。4.3 常见 Bug方向反转、食物刷在蛇身上、边界判定不一致异常现象可能原因处理方式按左键蛇先往上再往左缺失方向反转保护同帧内多次转向在 KEYDOWN 事件里增加direction ! RIGHT判断食物出现在蛇身体内部get_random_location()没有避开蛇身生成食物后遍历snake_coords判断碰撞冲突则重新生成蛇头穿墙后死亡位置偏一格边界判定用的是 map_width而不是 map_width统一使用head[x] -1 or head[x] map_width游戏结束后按任意键重启失败主循环里pygame.quit()后没有重新初始化把pygame.init()放在main()函数入口重开时递归调用或循环调用逻辑说明方向反转 bug 的根源在于move_snake()只负责根据direction计算新坐标不校验方向合法性。如果玩家在一帧内快速按了“上”和“左”而当前方向是“下”程序可能先处理“上”再处理“左”导致蛇向左但视觉上蛇身第二节在左侧下一帧就撞死了。食物刷在蛇身上这个问题原始设计里random.randint确实可能生成到蛇身坐标上。我通常这样改进def get_random_location_avoid_snake(snake_coords, map_width, map_height): while True: candidate { x: random.randint(0, map_width - 1), y: random.randint(0, map_height - 1) } if candidate not in snake_coords: return candidate注意这里candidate not in snake_coords能正常工作的前提是snake_coords里的元素都是字典且结构一致。如果蛇身长度接近地图总格子数这个 while 循环会有极低概率陷入死循环。实际开发时可以在循环里加一个attempt计数超过 100 次就把蛇身坐标集合用 set 排除再随机取。5. 用单元测试锁定游戏逻辑再谈扩展5.1 对 move_snake 和碰撞做单元测试贪吃蛇的图形逻辑跑起来容易回归测试难。手动按键盘测十几次每次都要重新定位“第几步出错”效率太低。我在做完基本功能后会用unittest把纯逻辑函数全部覆盖一遍这样后续调整方向处理或添加加速功能时能立刻知道有没有破坏原有行为。import unittest class TestSnakeLogic(unittest.TestCase): def test_move_right(self): snake [{x: 5, y: 5}, {x: 4, y: 5}, {x: 3, y: 5}] move_snake(right, snake) self.assertEqual(snake[0], {x: 6, y: 5}) self.assertEqual(len(snake), 4) # 移动后未删除尾部 def test_eat_food(self): snake [{x: 5, y: 5}, {x: 4, y: 5}, {x: 3, y: 5}] food {x: 5, y: 5} result snake_is_eat_food(snake, food) self.assertTrue(result) self.assertEqual(len(snake), 3) # 吃到食物不删尾 def test_collision_wall(self): snake [{x: -1, y: 5}, {x: 0, y: 5}] self.assertFalse(snake_is_alive(snake, 20, 20)) if __name__ __main__: unittest.main()逻辑说明测试move_right时先构造一个水平方向的蛇调用move_snake后断言蛇头变成(6, 5)且列表长度是 4因为此时还没调用snake_is_eat_food尾节没有被删除。测试eat_food时食物坐标和蛇头坐标故意设置成一样断言返回值是True且蛇身长度不变。测试碰墙时直接把蛇头坐标塞成-1验证碰撞检测函数能正确返回False。这个测试集的缺点是依赖snake_is_alive函数的边界判断逻辑如果你把边界判定改成“出界后从另一边穿出来”测试用例要同步修改但测试本身的价值在于当你重构移动函数时test_move_right会第一时间告诉你蛇头是否还按预期前进。5.2 扩展速度、存档与计分板单元测试稳定后就可以放心加扩展。第一个建议是增加速度等级把clock.tick(10)的值做成全局变量每得 5 分自动加 2让蛇越跑越快。第二个建议是最高分存档用 JSON 文件存储游戏结束界面读取并显示import json def save_score(score, filenamescore.json): try: with open(filename, r) as f: data json.load(f) except (FileNotFoundError, json.JSONDecodeError): data {high_score: 0} if score data[high_score]: data[high_score] score with open(filename, w) as f: json.dump(data, f)第三个建议是在游戏结束界面增加“按 R 重开”的功能注意重开时要重置蛇身长度、方向、得分和时钟速度否则会出现蛇身残留或速度不回退。这三个扩展点都建立在正确的坐标模型之上。先把移动、碰撞、食物这三件事的单元测试写扎实再往上加功能你会发现整个项目从“能玩”到“能给别人玩”中间只差一个可靠的回归基线。本文还有配套的精品资源点击获取
返回列表