ARTICLE DETAIL

资讯详情

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

从零到一:用Python和pygame打造规范可分享的贪吃蛇项目

从零到一:用Python和pygame打造规范可分享的贪吃蛇项目 简介基于STM32战舰V3开发板的贪吃蛇游戏完整工程面向单片机初学者与嵌入式系统开发者演示如何在STM32平台上从零实现经典小游戏。工程覆盖开发环境搭建、LCD屏幕显示、按键中断、定时器帧率控制以及蛇移动、食物生成、碰撞检测、得分等游戏逻辑并针对性能做了简单优化代码基于STM32标准外设库编写涉及定时器、时钟、GPIO、LCD驱动等模块配有可编译的Keil工程。压缩包共206个文件包含h头文件、c源文件、uvprojx工程配置、编译生成的hex/axf可执行文件、bat清理脚本与说明文档等总计约31.86MB。已有1348人学习/下载适合希望完整走通STM32外设编程、理解嵌入式游戏设计思路的读者。通过这份资料可直接烧录体验游戏也可参考其模块划分与代码风格迁移到其他开发板或复杂交互项目中是一份不错的实践范例。 说来也怪我电脑里有个文件名一直没舍得删就叫“贪吃蛇.zip”。最早是大学课程设计交作业时随手打的包后来工作了又有一版是给新同事做入门演示重新整理的。别看名字不起眼这个 zip 里装的东西其实很能反映一个开发者对“练手项目”的态度代码写没写干净、说明文档有没有、别人解压能不能直接跑。很多人以为贪吃蛇很简单——不就是一条蛇吃格子吗可真要你从头写一遍还要写得规范、能打包分享问题就来了蛇怎么平滑拐弯食物为什么偶尔生成在蛇身上窗口为什么一闪就退这些坑我基本都踩过。这篇就从一份“能拿得出手的贪吃蛇.zip”出发把项目设计、核心逻辑、完整代码、打包流程和排错经验一次性讲透。适合刚学完 Python 想用 pygame 做第一个小游戏的读者也适合想把自己练手项目整理成作品集的开发者。1. 项目整体设计与思路拆解1.1 为什么选贪吃蛇当练手项目选贪吃蛇不是因为网上代码多可以抄而是因为这个项目在一个极小的规模里覆盖了游戏开发几乎所有的核心概念循环、输入处理、数据更新、碰撞检测、随机生成、状态切换。它的逻辑量刚好比“计算器”复杂一个量级又比“俄罗斯方块”简单一个量级做完之后不会因为代码太多而劝退又能真正建立“把需求拆解成代码”的信心。我在带新手时经常说一句话如果你能独立写出一版不抄别人的贪吃蛇你就已经具备写任何 2D 小游戏的基础能力了。因为后续做平台跳跃、打砖块、甚至简单 Roguelike底层都是同一套“游戏循环 状态更新 碰撞响应”的骨架只是表现层和规则不同。从课程设计角度看贪吃蛇也特别好交差界面直观、可演示性强、扩展点丰富——加音效、加关卡、加速、存档每一样都是现成的“加分项”。这也是为什么各种“贪吃蛇.zip”在毕业季反复出现。可惜大多数打包版本质量堪忧代码和资源混在一起没有 README甚至连入口文件都要猜。所以我把自己的项目重新整理了一遍按“可交付、可复现、可分享”的标准来做。1.2 技术方案选型为什么锁定 Python pygame实现贪吃蛇的语言选择很多C、JavaScript、甚至 Excel 宏都能做。但我在这个项目里选 Python pygame理由是“上手门槛最低环境搭建最快代码表达最贴近逻辑本身”。用 C 写光是窗口初始化和事件循环就要几十行重点全被底层细节带走了用 JavaScript 写效果可以直接放到网页里但还没有接触过前端的读者会被 HTML、CSS、DOM 这些东西干扰用 Python 自带的 turtle 库也能做但 turtle 可视化速度慢做动画效果还行做需要即时响应的游戏体验不够顺畅。pygame 的主要优势在于它把窗口创建、键盘事件、图形绘制、帧率控制这些脏活都封装好了代码写起来几乎和思路一一对应。你说“蛇向右移动”代码里就是direction (20, 0)你说“蛇头撞到身体”就是一个new_head in snake的成员判断。这种“所想即所写”的体验对初学者建立正反馈很重要。当然pygame 也有它的脾气比较典型的是坐标系统从左上角为原点、x 轴向右、y 轴向下而且默认窗口不会帮你限制帧率。所以我常在代码里加上tick()控制速度同时把蛇的移动单位、窗口尺寸、格子大小这些“魔法数字”统一放在文件顶部方便后续调整。2. 核心逻辑解析与实现要点2.1 蛇的移动坐标数组就是一切贪吃蛇最核心的数据结构不是“蛇”而是一个坐标列表。每一节身体都是一个二元组比如(100, 100)表示 x100、y100 的格子整个蛇就是一串这样的坐标列表头部是蛇头尾部是蛇尾。理解了这个结构移动逻辑就变得很清晰。蛇的每次移动本质上只有三步根据方向计算出新的蛇头坐标把这个新蛇头插入列表头部如果本轮没吃到食物就把列表尾部的坐标弹出。这其实是数据结构里“队列”的经典应用。如果你把蛇的移动理解成“头进尾出”就会发现不需要真的把每一节都往前挪一格那既麻烦又容易出错。用列表的insert(0, ...)和pop()就能完成整个移动过程。很多初学朋友写贪吃蛇卡壳就是卡在把蛇想成了“一整条”而不是“一串点”。方向控制也有一个容易踩的坑蛇不能瞬间掉头。如果当前正在向右移动你按了左键下一帧蛇头就会直接扎进自己的身体。解决思路是在按键事件里先判断“新方向是否与当前方向相反”只有方向一致或垂直时才允许改变。判断方法是对比坐标增量比如向右是(20, 0)那么向左是(-20, 0)两者相加正好等于(0, 0)。我在代码里就是利用这个特性做的限制当新方向的增量加上当前方向的增量不等于零时才更新方向。2.2 食物生成与碰撞检测随机与判定的平衡食物生成看起来简单就是取一个随机坐标嘛。但有几个细节直接影响体验食物必须落在网格交叉点上否则蛇永远吃不到它。因为蛇的坐标是(100, 100)这种按格子对齐的值食物的随机坐标也必须按格子大小取整食物最好不要生成在蛇身上。如果随机坐标准备生成时刚好落在蛇的身体范围里常见做法是重新随机一次食物和蛇头用“坐标相等”判断而不是“距离小于某个值”。因为蛇头移动单位恰好等于格子大小所以只要坐标完全相等就认为吃到了。碰撞检测则是整个游戏的生死线。我把它分成两类一类是撞墙检测检查新蛇头的 x、y 是否超出窗口边界另一类是撞自己检测检查新蛇头是否已经存在于蛇身坐标列表中。这两个条件只要有一个成立游戏就立即结束。这里要特别注意检测时机应该在“插入新蛇头之前”判断的是“下一步会不会死”而不是等死完再去补救。我在实际写的时候还加了一个小优化检测撞自己时允许蛇头与蛇身最后一位“重叠”。这是因为蛇在移动时尾部会同步移走视觉上蛇头即将到达的位置如果是旧尾位置并不会真的相撞。不过这个属于锦上添花新手版本不加也不影响。2.3 游戏循环与状态管理一个状态机搞定全部pygame 程序本质上就是一个“无限循环”不断读事件、更新数据、重绘画面、限制帧率。这个循环的控制变量我习惯叫running它一旦变成False游戏就退出。除了主循环我还把游戏状态拆成了三个运行中、临时暂停、游戏结束。初学者往往想用一堆if嵌套来控制很快代码就乱成一团。更清晰的做法是用一个状态变量比如game_state取值playing、over、restart然后在不同状态下执行不同逻辑。我的这个版本里做了简化只用game_over一个布尔值控制结束状态。但即便只是这样一个开关也够说明状态管理的重要性程序判断输入的位置、更新蛇的逻辑、绘制的内容都会因为“游戏是否结束”而不同。等读者以后做更复杂的游戏再把它推广成完整的有限状态机也不迟。3. 实操过程与核心代码实现3.1 规范的项目结构从 zip 解压开始收到一个“贪吃蛇.zip”之后最怕看到什么一堆文件名是111.py、222.py没有任何说明资源的图片也不知道放哪。所以我这次打包前先规定了结构snake-game/ ├── main.py # 入口文件直接运行这个 ├── requirements.txt # 依赖清单 ├── README.md # 项目说明 └── assets/ # 资源目录放音效、图标 └── .gitkeep这个结构不复杂但它说清楚了三件事怎么启动、需要装什么、每个文件是干嘛的。assets 目录先留空用.gitkeep占位等以后加音效和图标时不用改动代码里的路径逻辑。requirements.txt是给使用pip install -r requirements.txt的人准备的也是判断一个 Python 项目“专业与否”的标尺之一。如果可以我还会把游戏版本写进README.md哪怕只是v1.0。别小看这行字几周后你自己再看这个 zip能立刻知道自己当时做到哪一步。3.2 核心代码分步实现下面这份代码是根据我自己项目的核心版精简出来的功能完整逻辑清楚直接复制就能跑import pygame import random import sys # 常量统一放这里方便调整 WINDOW_WIDTH 640 WINDOW_HEIGHT 480 CELL_SIZE 20 FPS 10 pygame.init() screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(贪吃蛇) clock pygame.time.Clock() # 蛇的初始状态三段水平排列向右移动 snake [(100, 100), (80, 100), (60, 100)] direction (20, 0) change_to direction # 随机生成食物保证落在网格点上 def random_food(): x random.randint(0, WINDOW_WIDTH // CELL_SIZE - 1) * CELL_SIZE y random.randint(0, WINDOW_HEIGHT // CELL_SIZE - 1) * CELL_SIZE while (x, y) in snake: x random.randint(0, WINDOW_WIDTH // CELL_SIZE - 1) * CELL_SIZE y random.randint(0, WINDOW_HEIGHT // CELL_SIZE - 1) * CELL_SIZE return (x, y) food random_food() score 0 game_over False def draw(): screen.fill((20, 30, 20)) for segment in snake: pygame.draw.rect(screen, (50, 220, 80), (segment[0], segment[1], CELL_SIZE, CELL_SIZE)) pygame.draw.rect(screen, (30, 30, 30), (segment[0], segment[1], CELL_SIZE, CELL_SIZE), 1) pygame.draw.rect(screen, (220, 50, 50), (food[0], food[1], CELL_SIZE, CELL_SIZE)) font pygame.font.SysFont(simhei, 24) text font.render(f得分: {score}, True, (255, 255, 255)) screen.blit(text, (10, 10)) pygame.display.update() while not game_over: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() if event.type pygame.KEYDOWN: if event.key pygame.K_UP and direction ! (0, 20): change_to (0, -20) elif event.key pygame.K_DOWN and direction ! (0, -20): change_to (0, 20) elif event.key pygame.K_LEFT and direction ! (20, 0): change_to (-20, 0) elif event.key pygame.K_RIGHT and direction ! (-20, 0): change_to (20, 0) direction change_to head_x, head_y snake[0] new_head (head_x direction[0], head_y direction[1]) # 撞墙和撞自己检测 if (new_head[0] 0 or new_head[0] WINDOW_WIDTH or new_head[1] 0 or new_head[1] WINDOW_HEIGHT or new_head in snake): game_over True continue snake.insert(0, new_head) if new_head food: score 1 food random_food() else: snake.pop() draw() clock.tick(FPS) # 结束画面 font pygame.font.SysFont(simhei, 36) text font.render(f游戏结束得分: {score}, True, (255, 255, 255)) screen.blit(text, (WINDOW_WIDTH // 2 - 150, WINDOW_HEIGHT // 2 - 30)) pygame.display.update() pygame.time.delay(2000) pygame.quit()这段代码里有几个细节值得你细看。方向限制那里direction ! (0, 20)表示“如果当前不是向下才允许改为向上”。但这里有个隐藏问题如果玩家在一帧内快速按下两个键比如先按上再按左由于direction还没有被更新为向上程序可能因为旧方向判断而允许一个不合理的转向。严格的处理应该把change_to也纳入判断用“即将生效的方向”而不是“当前方向”来限制。我这里的核心版为了可读性先用了简单判断理解原理之后可以自己加强。random_food()函数里有一个while (x, y) in snake的循环保证了食物不会生成在蛇身上。这一步在蛇很长的时候尤其重要否则食物会“埋”在蛇身体里玩家怎么都吃不到然后开始怀疑人生。虽然极端情况下会出现死循环但正常游戏里蛇身很难占满整个屏幕所以这个方案足够优先使用。吃掉食物后只是给身体增加一个新头尾部不弹出这样蛇的长度自然增加一格。这个“增一不减”的操作正是得分系统的本质吃到食物并不是加分数这么简单背后是链条的长度变化。3.3 跑起来之后的体验调优代码能跑只是第一步手感才是游戏体验的分水岭。我第一次写完试玩时感觉蛇的移动有点“愣”后来总结出几个优化点帧率控制FPS 10表示每秒移动 10 格这个速度对新手正好。想要难度递增可以让分数每加 5 分FPS就增加 1。我在进阶版里写过一个FPS 10 score // 5的公式实测下来难度曲线比较平滑蛇身的绘制身体节之间加了一圈深色描边pygame.draw.rect的最后一个参数为 1 时就是只画边框。这样蛇看起来是一节一节的而不是一根面条视觉上更好判断碰撞位置结束画面的停留时间pygame.time.delay(2000)给玩家 2 秒看清得分再退出不然窗口“啪”地一下没了体验非常差。这些优化都不难但就是它们决定了这个项目是“能跑的 demo”还是“能玩的游戏”。4. 打包成 zip 的规范化流程4.1 压缩包里的组织方式与命名规范现在项目文件齐全了下一步就是把它变成“贪吃蛇.zip”。我建议压缩包内部保留一层文件夹比如snake-game/而不是把所有文件直接扔在 zip 根目录。这样别人解压后得到一个独立目录不会把文件散落到当前文件夹里。命名方面也有一些共识全英文小写加连字符是相对安全的选择snake-game比贪吃蛇项目更不容易在某些系统上出怪问题。中文做展示没问题但作为目录名容易遇到编码问题。zip 压缩的时候如果用的是 macOS 或 Linux 的命令行我一般这样操作cd 项目所在目录 zip -r 贪吃蛇.zip snake-gameWindows 用户直接在文件夹上右键压缩成 zip 也一样。压缩之前一定要先把__pycache__目录、.DS_Store文件这种东西清掉。它们里面装的是 pyc 缓存和系统元数据对运行没有任何帮助只会让压缩包变大变乱。如果是用 git 管理项目那.git目录也绝对不要打进去。4.2 README 与 requirements.txt别人能不能跑的全靠它们我见过太多代码本身不错、却因为没写说明而没法跑起来的项目。收到 zip 的人第一件事永远是打开 README你的 README 至少要写清楚这几项项目名称和一句话简介运行环境比如 Python 3.8安装依赖的命令pip install -r requirements.txt运行命令python main.py操作说明方向键控制蛇的移动。requirements.txt 里也别偷懒写上依赖名和版本限定pygame2.1.0只写pygame不写版本的话万一遇到旧版本 API 不同安装者就会踩坑。加个下限版本是最省心的做法既保证新功能可用又不会把版本卡死。4.3 打 zip 的细节操作与避坑我有一次打包时忘记放进 README分享出去之后被问了一整天“怎么运行”。从那以后我给自己定了一条规矩把 zip 打完之后先解压到一个全新目录严格按照 README 里的步骤跑一遍。这个“自测打包”的流程虽然多花两分钟但能挡住绝大多数低级问题。用我自己总结的检查清单是有没有把__pycache__或.git打进去解压后入口文件能不能直接执行README 里的路径和文件名是否和实际一致依赖版本是否标清楚了。如果你是课程设计提交我还会建议在压缩包内附加一份简短的报告文件写上你的设计思路、核心代码片段和运行截图。这个习惯在评审时能带来不少好感分。5. 常见问题与排查技巧实录5.1 蛇动不动就“掉头”撞死自己这是新手最容易遇到的问题。症状是蛇向右走你按了一下左游戏立刻结束。原因很简单代码没有加入反向限制或者限制判断用的是旧方向而不是即将生效的方向。我的建议是把方向判断写成“四个按键各自判断是否与当前方向相反”。同时注意如果游戏对帧率极其敏感一次按键事件触发后要尽快更新change_to避免多次方向累积。更稳健的做法是设置一个“当前帧是否已经转向”的标志确保一帧内只响应一次方向键。老实说在FPS 10的低帧率下这个问题的发生概率不高但在速度上来之后会变得非常致命。5.2 碰撞检测不准确蛇头“擦边”过去如果你发现蛇明明应该撞墙却还能多走半步多半是因为坐标没有按网格对齐。比如蛇头在(98, 100)下一帧移动到(118, 100)而窗口宽度是 640看起来快出去了却没触发越界判断。解决方法是确保所有移动增量都是CELL_SIZE的整数倍并且初始位置也在网格交叉点上。我在代码里强制了增量为 20就是为了避免这类问题。如果发现蛇头穿过了自己的身体但没游戏结束另一种情况是撞自己检测用了蛇头旧位置而不是新位置。正确逻辑应该是先算出new_head再检测new_head in snake顺序反了就会漏判。5.3 中文显示成方块或窗口闪退这条几乎每个 pygame 中文用户都会遇到。pygame 默认字体不一定包含中文字形我代码里用的pygame.font.SysFont(simhei, 24)是优先找 Windows 的“黑体”。但在没有安装中文字体的 Linux 环境下这句可能回退到默认字体中文就显示成方块。比较靠谱的做法是在项目 assets 目录里放一个开源字体文件比如思源黑体的 ttf然后用pygame.font.Font(assets/font.ttf, 24)加载。这样无论用户系统里有没有中文字体都能正常显示。窗口闪退则多半是因为代码里没有pygame.quit()或底部事件循环写错。我的调试经验是把整个程序用python main.py从终端运行闪退时看终端里的报错信息比在 IDE 里瞎找快得多。下面是几个常见问题的排查速查表问题现象可能原因解决方案窗口一闪而过主循环没写或异常退出检查while not game_over和pygame.quit()蛇不能移动direction初始化或更新错误在循环内把change_to赋值给direction吃到食物但没变长尾部没有正确处理只有吃到食物时才不执行pop()食物刷在蛇身上随机坐标未排除蛇身用while (x, y) in snake重新随机中文显示方块系统缺少中文字体用项目内字体文件加载写在最后把“贪吃蛇.zip”从一份随手交差的作业变成一份结构规范、开箱即跑的分享包这件事本身没多难难的是愿意多走那几步。而我个人在实际操作中体会最深的一点是当你开始为“别人拿到后能不能顺利跑起来”操心时你的编码习惯就已经在改变了。最后再分享一个小技巧如果你以后打算长期维护这类小项目建议用 git 管理版本每次打完 zip 后打一个 tag比如v1.0。这样 github 上也好、本地也好别人拿到手的压缩包永远对应一个明确的版本。想继续扩展的话加音效、加障碍物、加 AI 自动移动的敌人从这份代码往上叠加都很轻松。希望这个简陋但完整的“贪吃蛇.zip”能成为你第一个拿得出手的项目作品。本文还有配套的精品资源点击获取
返回列表