news 2026/9/8 11:25:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零到一:用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)表示 x=100、y=100 的格子,整个蛇就是一串这样的坐标,列表头部是蛇头,尾部是蛇尾。

理解了这个结构,移动逻辑就变得很清晰。蛇的每次移动,本质上只有三步:

  1. 根据方向计算出新的蛇头坐标;
  2. 把这个新蛇头插入列表头部;
  3. 如果本轮没吃到食物,就把列表尾部的坐标弹出。

这其实是数据结构里“队列”的经典应用。如果你把蛇的移动理解成“头进尾出”,就会发现不需要真的把每一节都往前挪一格,那既麻烦又容易出错。用列表的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.py222.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-game

Windows 用户直接在文件夹上右键压缩成 zip 也一样。压缩之前,一定要先把__pycache__目录、.DS_Store文件这种东西清掉。它们里面装的是 pyc 缓存和系统元数据,对运行没有任何帮助,只会让压缩包变大变乱。如果是用 git 管理项目,那.git目录也绝对不要打进去。

4.2 README 与 requirements.txt:别人能不能跑的全靠它们

我见过太多代码本身不错、却因为没写说明而没法跑起来的项目。收到 zip 的人第一件事永远是打开 README,你的 README 至少要写清楚这几项:

  1. 项目名称和一句话简介;
  2. 运行环境,比如 Python 3.8+;
  3. 安装依赖的命令,pip install -r requirements.txt
  4. 运行命令,python main.py
  5. 操作说明,方向键控制蛇的移动。

requirements.txt 里也别偷懒,写上依赖名和版本限定:

pygame>=2.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_overpygame.quit()
蛇不能移动direction初始化或更新错误在循环内把change_to赋值给direction
吃到食物但没变长尾部没有正确处理只有吃到食物时才不执行pop()
食物刷在蛇身上随机坐标未排除蛇身while (x, y) in snake重新随机
中文显示方块系统缺少中文字体用项目内字体文件加载

写在最后

把“贪吃蛇.zip”从一份随手交差的作业,变成一份结构规范、开箱即跑的分享包,这件事本身没多难,难的是愿意多走那几步。而我个人在实际操作中体会最深的一点是:当你开始为“别人拿到后能不能顺利跑起来”操心时,你的编码习惯就已经在改变了。

最后再分享一个小技巧:如果你以后打算长期维护这类小项目,建议用 git 管理版本,每次打完 zip 后打一个 tag,比如v1.0。这样 github 上也好、本地也好,别人拿到手的压缩包永远对应一个明确的版本。想继续扩展的话,加音效、加障碍物、加 AI 自动移动的敌人,从这份代码往上叠加都很轻松。希望这个简陋但完整的“贪吃蛇.zip”,能成为你第一个拿得出手的项目作品。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 11:24:41

纯Win32 API实现标题栏自定义按钮:非客户区自绘实战

简介&#xff1a;这是一份面向Visual C开发者的窗口界面增强示例工程&#xff0c;目标是在Windows窗口标题栏紧挨最小化按钮处添加自定义按钮。资源通过OfficeXPMenuSDI示例项目&#xff0c;演示了CreateWindowEx、SetWindowLong、GetSystemMenu、GetMenuItemRect、ScreenToCli…

作者头像 李华
网站建设 2026/9/8 11:24:28

计算机单片机毕设实战-基于 STM32 的环境感知智能消毒柜体装置设计 基于 STM32 的红外感应语音控制智能柜设计与实现(012007)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/8 11:23:56

纯CSS3实现“加载中”文字动效:逐字弹跳、呼吸与波浪效果详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:22:59

mbed OS源码深度剖析:从HAL到RTOS的嵌入式架构设计

1. 从一次真实项目说起&#xff1a;为什么我要啃 mbed OS 源码如果你做过物联网设备&#xff0c;尤其是那种需要在多种 Arm 芯片上快速切换方案的量产项目&#xff0c;你一定遇到过这组连环问题&#xff1a;前期在 STM32 上调好的逻辑&#xff0c;换到 NXP 或者瑞萨的片子&…

作者头像 李华
网站建设 2026/9/8 11:22:35

RKNN NPU推理实战:78.78ms耗时背后的版本对齐与优化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:21:46

Java面试避坑指南:从搞笑程序员翻车现场拆解面试官真实考点

面试这事儿吧&#xff0c;我经历过太多轮了。坐在桌子这边当候选人时慌&#xff0c;坐在桌子那边当面试官时也慌——只不过慌的内容不一样。前段时间朋友发我一个视频&#xff0c;标题就叫“互联网大厂Java面试&#xff1a;严肃面试官与搞笑程序员的对决”&#xff0c;我点进去…

作者头像 李华