
在社区带入门班的时候我经常遇到这样一个需求学员学完了基础语法、条件循环和函数之后总觉得这些东西是散的不知道能拼出什么有意思的东西。后来我干脆把“手把手教你用Python编写March大逃亡”做成了一次完整的项目实战课。March是我给主角起的名字一个小机器人它要在一条危机四伏的通道里不断向右逃亡躲开障碍、跳过坑洞跑得越远分数越高。这个项目用到的核心知识非常典型Pygame游戏框架、精灵类、键盘事件、碰撞检测、状态管理还有最让人头疼的跳跃和重力模拟。这篇文章我会把整个项目的设计思路、代码拆解、实测排坑和打包方案一次讲清楚。你不需要有游戏开发经验只要你学过Python基础语法、会用pip装库哪怕从来没碰过Pygame也能照着一步步做出来。里面每一段代码我都会解释为什么这么写而不是直接甩给你一堆能跑但看不懂的东西。适合人群很明确想拿Python做点实际项目的初学者、带学生的老师和培训讲师以及想了解2D游戏开发基本套路的好奇派。很多朋友问我为什么不直接用Unity或者Godot非要拿Python写游戏玩。我的回答是教程项目的目的从来不是做出一款商业大作而是在一个足够小而完整的项目里把编程思维串起来。Python配合Pygame做2D小游戏代码量适中、报错信息相对友好、依赖就一个库对初学者来说压力最小。March大逃亡这个项目体量刚刚好代码压一压不到400行但已经包含了完整游戏该有的所有核心机制做完之后你会有一种“我也是能写游戏的人了”的实感这种正反馈比看十遍语法书都管用。1. 项目概览March大逃亡到底是个什么游戏1.1 核心玩法与功能清单先给没玩过这类游戏的朋友解释一下。March大逃亡是典型的横版自动跑酷玩法玩家不控制March的前进方向它自己会一直向右跑摄像机跟着它移动。你只需要控制两件事跳跃按空格或向上键和快速下落按向下键。游戏里会不断从右侧生成障碍物和平台缺口March要么跳过去要么被撞到之后游戏结束。这个玩法看着简单但做起来一点都不简单因为它在技术上几乎涵盖了2D游戏的所有基础要素一个持续刷新画面的窗口帧率稳定在60FPS左右玩家角色需要响应键盘事件做出跳跃动作重力系统让角色跳起来之后能落回地面而不是悬浮在空中障碍物按特定节奏生成并且随着分数提高而越来越密集角色和障碍物之间要有精确的碰撞检测边界要贴合游戏要有“死亡-重开”的完整流程也就是状态管理。为了方便你验收自己的学习成果我把功能清单列在这里做完之后逐项勾选玩家角色可以在地面上左右移动其实只需要跳跃但加上左右移动会更有操控感按下空格跳跃跳跃过程中可以按向下键加速落地障碍物分为地面障碍和空中障碍两类随机生成画面右侧会滚动生成新的障碍物左侧是已经走过的区域得分随时间增长距离越远分数越高撞到障碍物之后显示Game Over画面按R键重新开始游戏有开始菜单、运行中、结束三个状态。1.2 为什么选择Pygame而不是其他方案Python能写游戏的方案其实有好几种但Pygame是最适合这个项目的。Tkinter虽然内置、零安装但性能太差刷新个动画都费劲做游戏体验不好。Pygame Zero封装得确实更简单但它隐藏了太多的底层细节学完之后你可能会觉得“我好像什么都没学到”。Pygame则刚刚好它替你处理了窗口创建、图片加载、声音播放这些脏活但游戏主循环、事件分发、精灵管理和碰撞检测这些核心逻辑全都要你自己写而这恰恰是做游戏最有价值的部分。我听到过有的朋友说Pygame太老了不适合做游戏。这话我认同一半。Pygame确实没有现代引擎那么方便没有可视化的编辑器没有自动寻路资源管理全靠手写但它便宜在“教学透明明度”上。你写的每一行代码都直接影响游戏行为没有黑盒没有魔法。等你用Pygame把一个游戏从零写到能跑再去看Unity或者Godot的文档你会发现那些引擎里的概念你全都认识只不过换了个叫法和操作界面。1.3 你将从项目里学到的核心知识点在做这个项目之前我建议你先确认自己已经掌握这些Python基础变量和类型、if条件判断、while循环、函数定义、类的基本用法。如果还有含糊的地方回头翻翻基础教材不用精通看得懂就行。通过这个项目你会掌握的技能包括如何用面向对象的方式组织游戏代码、如何处理实时键盘输入、如何用数学公式模拟简单的物理效果重力和跳跃、如何检测两个矩形是否重叠以及如何用状态机让你的程序不至于乱成一锅粥。这些技能不只是游戏开发有用写自动化脚本、做图形界面的工具、处理实时数据流都会碰到类似的思想。我见过太多刚开始学编程的人学完了所有语法点但看到空白的代码文件还是会懵不知道第一行该写什么。这个项目的价值就在于给你一个完整的地图让你知道代码是怎么样从零长出来的。2. 动手前的环境准备别在装环境这一步劝退2.1 Python与Pygame的安装这个项目用的Python版本我推荐3.8以上我用的是3.10和3.11都跑得很稳。你可能在网上看到过各种Python安装教程这里我用自己的经验给你一个最省心的步骤先去Python官网下载对应你操作系统的安装包Windows用户注意安装时一定勾选“Add Python to PATH”这个选项否则后面你在命令行里输入python会提示找不到命令。安装完成之后打开命令行工具输入python --version看到版本号就说明安装成功。macOS用户如果不想装系统级的Python可以装Homebrew之后用brew install python也可以直接用python3命令。Pygame的安装就更简单了只要Python装好了pip通常会一起带上。在命令行里执行pip install pygame如果你是在macOS或者Linux上可能还需要确认一下pip指向的是不是Python3的版本必要时用pip3。装完之后在Python交互环境里输入import pygame不报错就是成功了。我遇到很多学员卡在这一步其实多半是PATH没配置好或者同时装了多个Python版本导致pip指向了错误环境解决办法是用python -m pip install pygame来强制指定当前Python对应的pip。2.2 编辑器选择与项目目录编辑器我推荐VS Code免费、跨平台而且对Python的支持非常友好。装好之后记得安装Python扩展这样你才会有语法提示和调试功能写代码的舒适度直接提升一个档次。当然你用PyCharm或者Sublime Text也完全没问题编辑器只是工具别在这一步纠结太久。关于项目目录建议专门建一个文件夹比如叫march_escape里面放两个子目录images用来存放游戏素材sounds用来存放音效。为什么一开始就要养成这种结构化的习惯因为等你的项目变大素材文件多起来之后乱七八糟的目录会让你找东西找到怀疑人生。就算现在就两个图片文件也要按标准来。这个项目里我们基本用纯色矩形代替图像素材方便你专心看代码逻辑但目录结构先立好。2.3 用纯色形状代替美术素材的思路我想单独说一下素材这个问题因为很多初学者在动手之前就被“我不会画画”劝退了。这个项目里我们完全不需要美术功底只需要用Pygame的绘图函数在屏幕上画矩形、圆形、线条。March小机器人就用一个蓝色矩形加一对眼睛来表示地面用绿色矩形障碍物用红色矩形背景用深色渐变。这样处理的好处有两个一是你的注意力可以全部集中在游戏逻辑上二是后面你想换成真正的图片素材只需要把绘制代码替换成加载图片的代码逻辑完全不受影响。等到你做完了这个版本再想去网上找免费的像素风素材替换那时候你已经知道代码里哪些部分管显示、哪些部分管逻辑替换起来一点不慌。从简单到复杂从方块到美术这个路径才是正确的打开方式。3. 核心框架搭建窗口、主循环与事件响应3.1 初始化与窗口参数老规矩任何Pygame项目的第一步都是初始化。这里有一条铁律先初始化Pygame再创建窗口最后设置窗口标题。import pygame import sys import random pygame.init() # 窗口参数 SCREEN_WIDTH 800 SCREEN_HEIGHT 450 FPS 60 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(March大逃亡) clock pygame.time.Clock()我解释一下每个参数的含义。SCREEN_WIDTH和SCREEN_HEIGHT定义了游戏窗口的像素尺寸800x450是一个比较舒服的横版比例既不会太小看不清楚也不会大到需要高性能显卡。FPS是每秒帧数代表游戏画面每秒钟刷新多少次60FPS是当前主流标准低了会觉得卡顿高了在普通屏幕上意义不大。set_mode是创建窗口的函数返回的screen对象你可以理解成一块画布之后所有绘制操作都是往这块画布上画。set_caption设置窗口顶部标题栏的文字。clock对象是帧率控制器后面主循环里会用到。为什么要单独做一个时钟因为如果不控制帧率游戏运行速度会直接取决于你电脑的性能配置好的人跑得快配置差的人跑得慢这显然是错误的游戏逻辑。3.2 游戏主循环一切的核心发动机如果你看过几份Pygame代码一定会发现它们都有一个长得很像的while循环。这个循环就是游戏的心脏每一帧它都会做三件事处理用户输入、更新游戏状态、重绘画面。伪代码长这样running True while running: # 1. 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 2. 更新游戏逻辑移动、碰撞、得分…… # 3. 绘制画面 screen.fill((30, 30, 40)) # ……绘制各种物体 pygame.display.flip() # 4. 控制帧率 clock.tick(FPS) pygame.quit() sys.exit()这段代码最重要的思想是“帧与状态的分离”。你不需要去精确控制March何时候移动你只需要在每一帧都根据当前键盘状态决定它要不要移动。游戏的世界不是“事件驱动”的而是“循环驱动”的每一帧都重新计算一遍整个世界。这种模型在2D小游戏里非常简单可靠你只需要保证每一帧的计算量足够小能在0.016秒内完成游戏就会保持流畅。pygame.event.get()这一行是在从事件队列里取事件。窗口被关闭、键盘被按下、鼠标被点击这些都会变成事件放进队列里。比如玩家点了窗口右上角的X就会产生一个QUIT事件我们检测到之后就退出循环然后调用pygame.quit()来清理资源。很多新人会在主循环内部随意return或者break结果发现窗口关了但进程还在就是因为忘了在退出前做清理。3.3 屏幕坐标系与绘制基础在开始写角色之前我们得先搞清楚Pygame的坐标系这是很多新人第一次犯糊涂的地方。Pygame的坐标原点在窗口左上角x轴向右增大y轴向下增大。也就是说屏幕顶部的y坐标是0越往下越大。这个和我们数学课上的直角坐标系正好是反的。所以当你设置March的初始位置时如果想让它在窗口中央偏下的位置那么它的y坐标应该是比较大的值比如300。这个坐标系直接影响后面对跳跃高度的计算理解不了的话写跳跃逻辑时你会发现角色跳的方向总是反的。绘制一个基本的长方形很简单# screen: 画布对象 # (100, 200, 20, 30): 前两个数是左上角x和y坐标后两个数是矩形的宽和高 pygame.draw.rect(screen, (0, 128, 255), (100, 200, 20, 30))(0, 128, 255)是RGB颜色分别代表红、绿、蓝三原色的亮度数值范围0到255。平时你可以用现成的颜色常量比如pygame.Color(blue)但我更喜欢直接写元组因为更直观、可控范围更大。这个绘制函数返回一个Rect对象我们后面做碰撞检测时还会用到所以最好把它存下来。4. 玩家角色March的实现跳跃、重力与动画4.1 精灵类的设计思路现在我们开始写真正的主角March。用一个类来管理角色是让代码变清晰的第一步。March这个类负责自己的位置、速度、状态和绘制对外只暴露几个简单的接口update更新逻辑、jump跳跃指令、draw绘制自己。class Player: def __init__(self, x, y): self.x x self.y y self.width 30 self.height 40 self.color (0, 128, 255) self.vel_x 0 self.vel_y 0 self.is_jumping False self.max_jump_height 120 def jump(self): if not self.is_jumping: self.is_jumping True self.vel_y -6 def update(self, ground_y): # 重力 self.vel_y 0.3 self.y self.vel_y # 落到地面 if self.y ground_y - self.height: self.y ground_y - self.height self.vel_y 0 self.is_jumping False def draw(self, screen): pygame.draw.rect(screen, self.color, (self.x, self.y, self.width, self.height)) pygame.draw.circle(screen, (255, 255, 255), (self.x 8, self.y 10), 4) pygame.draw.circle(screen, (255, 255, 255), (self.x 22, self.y 10), 4)这里有几个细节要重点说明。vel_y是y方向的速度注意我们的坐标系y向下增大所以vel_y为正数时角色向下运动为负数时角色向上运动。跳跃时我们给vel_y赋一个负值这样就产生了一个向上的速度。每次更新时先对速度施加重力vel_y 0.3。这里的0.3是重力加速度的简化模拟它表示每一帧往下加速0.3像素每帧。你可能觉得这个数值太小了但帧率是60FPS一秒下来速度会增加18像素每帧效果已经很可观。这个数值需要反复调试调大了角色掉下来太快像铅球调小了跳起来又飘得很。0.3是我试出来比较舒服的一个值。4.2 跳跃数学从速度到高度跳跃机制的背后其实是一道简单的物理题。我在这里把它讲透虽然你现在只是写代码但理解了原理之后调整手感就非常轻松了。角色跳起来的初速度是-6向上每帧重力加速度是0.3。根据物理公式上升阶段的速度从-6逐渐减小到0这个过程需要的帧数是6 / 0.3 20帧。在匀减速运动中上升的最大高度等于平均速度乘以时间平均速度是(0 6) / 2 3像素每帧时间是20帧所以最大跳跃高度大约是60像素。如果你想让跳跃高度翻倍其实不需要把初速度翻倍只需要把初速度提高到原来的1.4倍左右。因为跳跃高度与初速度的平方成正比速度变成1.4倍高度就变成约2倍。很多人凭感觉乱调参数调了半天手感还是不对其实就是没搞明白这个平方关系。想要调节跳跃手感最有效的三个旋钮是初速度、重力加速度、以及帧率。初速度决定跳多高重力加速度决定下落节奏是干脆还是飘。我的建议是不要直接抄我的参数自己多调几次体会一下各个数值对手感的影响。游戏开发里所谓的手感本质上就是这些参数调出来的。4.3 加入左右移动与动画帧March虽然主要是自动向右跑但为了操控灵活我给它加了左右移动能力。左右移动不改变自动前进的主方向只是让玩家在狭小空间里做微调。这个功能在障碍物密集的时候非常有用。def handle_input(self, keys): if keys[pygame.K_LEFT]: self.x - 5 if keys[pygame.K_RIGHT]: self.x 5 if keys[pygame.K_UP] or keys[pygame.K_SPACE]: self.jump() if keys[pygame.K_DOWN]: self.vel_y 1.0keys是pygame.key.get_pressed()返回的一个元组里面记录着当前所有按键的按下状态。你不需要像事件处理那样遍历所有事件直接检查你要用的按键即可。这背后的区别是事件处理适合“按一下触发一次”的场景比如菜单选择、输入文字而get_pressed适合“按住期间持续生效”的场景比如移动、蓄力。向下键加速下落是我强烈建议加的功能它让玩家在跳起来之后可以快速落地应对连续障碍的时候手感好得多。原理很简单在按下键期间给vel_y额外增加一个较大的值让下落速度短时间变大。因为每次检查都加1.060帧每秒的话一秒就加了60效果非常显著。关于动画这里我说一个大多数人会犯的错误一上来就到处找素材做帧动画结果被图片加载、透明处理和逐帧切换搞崩溃。正确做法是先画一个简单的静态方块游戏逻辑全部跑通之后再回来加动画。因为动画只是“每N帧换一张图片”的事情它不改变任何逻辑。March我也建议这样先纯色方块后面你可以在draw里让他左右摆动一下制造出跑动的感觉。5. 障碍物生成动态世界的核心逻辑5.1 障碍物设计不只是一种矩形March大逃亡的障碍物我做了两种类型地面障碍需要跳过去和空中障碍需要低身钻过去。这两种障碍交替出现游戏的趣味性和难度就上来了。如果只有地面障碍玩家只需要无条件按跳跃键体验很单调。class Obstacle: def __init__(self, x, y, width, height, obstacle_type): self.x x self.y y self.width width self.height height self.type obstacle_type # ground 或 air def update(self, speed): self.x - speed def draw(self, screen): if self.type ground: pygame.draw.rect(screen, (220, 60, 60), (self.x, self.y, self.width, self.height)) else: pygame.draw.rect(screen, (220, 120, 30), (self.x, self.y, self.width, self.height)) def get_rect(self): return pygame.Rect(self.x, self.y, self.width, self.height)地面障碍的y坐标应该贴合地面所以高度可以随便设计但底部始终贴着地面。空中障碍的y坐标则要设计成“玩家不跳就会撞上但跳起来也可能撞上”的样子这样玩家需要判断是该跳还是该等待。空中障碍的下边缘到地面留出足够的空隙能让March顺利钻过去。这里我特别想说一个经验不要用完全随机的方式生成障碍物。如果你纯随机会出现连续两个障碍间隔只有50像素玩家根本来不及反应或者连续五分钟一个障碍都不出来无聊得要死。正确的做法是“间隔控制随机”给障碍物设置一个合理的间距范围保证玩家有时间落地。5.2 障碍物生成器间隔控制的思路我维护了一个生成计时器每次主循环里让计时器累加当计时器超过设定的间隔值时就生成一个新障碍物同时重置计时器。间隔值是一个区间内的随机数而且随着游戏进度会越来越小。obstacle_timer 0 obstacle_interval random.randint(70, 110) speed 6 def spawn_obstacle(): global obstacle_interval # 随机决定生成地面障碍或空中障碍 obs_type ground if random.random() 0.6 else air if obs_type ground: height random.randint(30, 60) return Obstacle(SCREEN_WIDTH, GROUND_Y - height, 20, height, obs_type) else: width random.randint(40, 70) height 15 return Obstacle(SCREEN_WIDTH, GROUND_Y - 90, width, height, obs_type) # 在主循环里 obstacle_timer 1 if obstacle_timer obstacle_interval: obstacles.append(spawn_obstacle()) obstacle_timer 0 obstacle_interval random.randint(70, 110)注意地面障碍的宽度我故意设置的比较小这是为了给玩家留出一条路径。有的游戏让你在缝隙极小的障碍群中穿梭那是难度曲线后期的事情。起步阶段还是宽容一点让玩家觉得“我能行”后面再通过提高速度和缩小间隔来增加挑战。空中障碍的y坐标我建议固定不要随机浮动太多。因为玩家的跳跃高度是一样的如果空中障碍一会儿高一会儿低会让玩家判断失误产生不公平感。所有游戏设计都要遵守一条原则玩家的失败必须源于他自己的操作失误而不是随机数耍流氓。5.3 碰撞检测矩形重叠的数学Pygame的碰撞检测做得很人性化你不需要手写矩形相交公式直接调用colliderect就行。但为了让你理解背后原理我还是简单说一下两个矩形如果重叠说明左边界的x小于右边界的x且上边界的y小于下边界的y两者同时满足就说明相交。player_rect pygame.Rect(player.x, player.y, player.width, player.height) for obs in obstacles: if player_rect.colliderect(obs.get_rect()): game_state game_over break这里有一个新手常踩的坑Rect对象在update之后必须重新获取或者你在每次碰撞检测前都用当前位置重新创建Rect。因为Rect是一份快照March的x和y变了旧的Rect并不会自动跟着变。我前面代码里每次检测前都会重新生成player_rect就是为了避免这个bug。矩形碰撞的精度问题在这个项目里可以忽略因为March是一个实心方块。但等你以后做更复杂的游戏会发现矩形碰撞太“楞”两个图形明明看起来没碰到Rect却已经重叠了因为矩形的边界超出了图形实际像素的范围。到时候可以考虑用掩码碰撞pygame.mask但那是后话现阶段矩形碰撞足够好用。5.4 计分与难度递增计分我用了两种方式的结合距离分和生存时间分本质上是一样的因为March一直在自动向右跑所以记录累计的位移就等同于计分。我用一个变量distance_score来累加每帧加上speed的数值然后除以一个常数把它换算成看起来舒服的分数单位。难度递增是靠“速度随时间变快”实现的def increase_difficulty(): global speed, obstacle_interval speed 6 distance_score // 500 obstacle_interval max(50, 110 - distance_score // 1000)这个设计里隐含了一个平衡点速度变快的同时障碍物生成间隔变小两件事同时发生会让难度快速上升。我在实际测试中发现如果速度增加得比障碍密度快玩家会先适应不过来反过来又会觉得无聊。所以两个参数的变化速率需要配合。这里的数字不是最优解你可以按自己的偏好调整。我还给March加了像素级的小碰撞边缘也就是碰撞时让判定区域比视觉区域小一点这在实践里能显著改善玩家体验。玩家的视觉看到角色贴边了但实际判定还没碰到给了玩家一丝容错空间。做法是把player_rect缩小几个像素player_rect pygame.Rect(player.x 2, player.y 2, player.width - 4, player.height - 4)这个“宽容度设计”在格斗游戏里很常见高手对战时判定比体型小是为了避免“看起来没碰到却被判定击中”的挫败感。March这个项目里这个小细节能让游戏体验好了不少。6. 游戏状态管理从菜单到Game Over的完整闭环6.1 为什么需要状态机很多新手写的游戏会出现一个经典问题游戏结束了不知道按哪里重新开始要么关掉窗口重开要么代码混乱成一团。根本原因在于游戏的不同阶段需要处理不同的输入和逻辑。菜单界面里March不需要跳跃障碍物不需要生成游戏结束时键盘输入应该只响应R键重开而不是还能控制角色乱跑。状态机就是为解决这个问题而生的。March大逃亡我设计了三个状态menu开始菜单、playing游戏进行中、game_over结束。用一个字符串变量game_state来记录当前状态。主循环的结构对应变成running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False if game_state menu: # 显示标题、按键提示 keys pygame.key.get_pressed() if keys[pygame.K_RETURN]: game_state playing elif game_state playing: # 更新玩家、障碍物、碰撞检测、计分 keys pygame.key.get_pressed() player.handle_input(keys) player.update(GROUND_Y) # 更新障碍物 for obs in obstacles: obs.update(speed) obstacles [obs for obs in obstacles if obs.x -50] # 碰撞检测 player_rect pygame.Rect(player.x 2, player.y 2, player.width - 4, player.height - 4) for obs in obstacles: if player_rect.colliderect(obs.get_rect()): game_state game_over # 计分 distance_score speed increase_difficulty() elif game_state game_over: # 显示结束画面 keys pygame.key.get_pressed() if keys[pygame.K_r]: # 重置游戏 reset_game() # 最后统一绘制 draw_all() pygame.display.flip() clock.tick(FPS)这个写法的优点是每个状态下的代码逻辑互不干扰。极端一点说你甚至可以先把三个状态当成三个虚拟的“世界”它们之间的切换只发生在特定的条件上。March进入playing状态时一切从初始参数开始进入game_over状态时场景静止不动只显示评分和提示。6.2 菜单与结束画面的实现菜单和结束画面我没有单独做场景跳转只用一个flag变量去控制绘制函数里面画什么。因为这两个画面的本质就是在同一块画布上绘制不同内容没必要为它们单独开辟一套渲染循环。def draw_all(): screen.fill((30, 30, 40)) pygame.draw.rect(screen, (60, 160, 60), (0, GROUND_Y, SCREEN_WIDTH, SCREEN_HEIGHT - GROUND_Y)) if game_state menu: font pygame.font.SysFont(simhei, 48) title_surface font.render(March大逃亡, True, (255, 255, 255)) screen.blit(title_surface, (SCREEN_WIDTH // 2 - title_surface.get_width() // 2, 120)) tip_font pygame.font.SysFont(simhei, 24) tip_surface tip_font.render(按回车开始游戏, True, (200, 200, 200)) screen.blit(tip_surface, (SCREEN_WIDTH // 2 - tip_surface.get_width() // 2, 220)) elif game_state playing: player.draw(screen) for obs in obstacles: obs.draw(screen) score_font pygame.font.SysFont(simhei, 24) score_surface score_font.render(f距离: {distance_score}, True, (255, 255, 0)) screen.blit(score_surface, (10, 10)) elif game_state game_over: font pygame.font.SysFont(simhei, 48) over_surface font.render(游戏结束, True, (255, 60, 60)) screen.blit(over_surface, (SCREEN_WIDTH // 2 - over_surface.get_width() // 2, 150)) tip_font pygame.font.SysFont(simhei, 24) tip_surface tip_font.render(f得分: {distance_score} 按R重新开始, True, (255, 255, 255)) screen.blit(tip_surface, (SCREEN_WIDTH // 2 - tip_surface.get_width() // 2, 220))这里我用pygame.font.SysFont加载了系统的中文字体。Pygame自带的默认字体不支持中文直接渲染中文会显示成方框。如果你机器上没有simhei可以改成microsoftyaheipingfangsc之类你系统里有的字体或者干脆用英文显示菜单文字。这个坑我在第一次做教程时踩过当时打印出来的中文全是方框还以为是显卡问题后来才反应过来是字体问题。菜单画面的交互逻辑很简单检测到回车键就切到playing状态。结束画面检测到R键就调用reset_game重置所有数据和列表。这里重置函数要小心你必须把所有影响游戏过程的变量都恢复到初始值包括玩家的位置和速度、障碍物列表、计时器、分数、难度参数。漏掉任何一个都会出现“重开之后世界还是乱的”bug。6.3 重启逻辑的细节处理reset_game函数是防止玩家“精神污染”的关键。我把它单独抽出来而不是散落在各个地方就是因为它需要同时处理的东西太多了。def reset_game(): global game_state, distance_score, speed, obstacle_timer, obstacle_interval game_state playing distance_score 0 speed 6 obstacle_timer 0 obstacle_interval random.randint(70, 110) player.x 100 player.y GROUND_Y - player.height player.vel_x 0 player.vel_y 0 player.is_jumping False obstacles.clear()create a function单独一个函数的另一个好处是你后续想要添加“再次挑战”按钮、或者实现不同难度切换都只需要往里面加改动不会影响其他地方。状态机的好处在这里体现得很明显当你的逻辑分散在不同状态下重置逻辑也必须清晰否则用户在重开时看到残影或者奇怪的障碍物位置游戏的口碑就完了。7. 实测排坑与打包建议7.1 新手最容易踩的5个坑我把带学员过程中最常出现的五个问题整理一下你在自己动手时如果遇到可以直接对照排查第一个坑pygame窗口闪退代码看起来没问题。这个大概率是你的主循环开头少了事件处理或者循环条件写错了导致while跑完就退出。排查方法是加一行print(loop running)在循环开头看它到底有没有被反复执行。第二个坑角色按跳跃键没反应。检查你的事件处理用的是event.type pygame.KEYDOWN还是pygame.key.get_pressed()。如果是前者你要去遍历事件队列如果是后者你要确保它是在主循环里每一帧被调用。很多新手把get_pressed()写到事件循环外面去了结果按键状态不再更新。第三个坑中文乱码或方框。前面已经说过是字体问题。换成SysFont加载具体字体或者在代码文件开头加一行import pygame和pygame.init()也没用必须指定中文字体。第四个坑帧率不稳游戏速度时快时慢。这种问题多半是因为你在主循环里做了太多耗时的操作或者忘记调用clock.tick。如果电脑性能不够可以把屏幕尺寸调小分辨率低了绘制负担立马下降。第五个坑碰撞判定太严格玩家觉得不公平。参考我前面说的把碰撞矩形缩小几个像素给自己留点容错空间。另外对障碍物也做同样的处理让判定区域不超出视觉区域。7.2 调试技巧与print策略Pygame的界面是实时刷新的你不能像调试命令行程序那样轻松看到变量状态。所以我的调试三板斧是print关键变量的周期性输出、用屏幕绘制调试信息、用日志记录死亡时的状态。print的典型用法是在主循环里隔段时间打印一次玩家的位置和速度frame_count 0 # 在主循环中 frame_count 1 if frame_count % 30 0: print(fpos: ({player.x}, {player.y}), vel_y: {player.vel_y}, state: {game_state})每一秒输出一次足够你判断逻辑是否正常。对于碰撞检测我习惯在检测到碰撞时打印出两个矩形的位置这样能看出是哪个障碍物撞到了March以及重叠的幅度有多大。在屏幕上绘制调试信息也很实用特别适合看跳跃曲线。你可以在draw_all里把player_rect画成半透明色这样能直观看到碰撞判定区域和视觉区域之间的差异。当你觉得判定奇怪的时候看一眼就能明白是不是矩形缩得太多了。7.3 打包成独立可执行文件游戏写完之后你肯定想让朋友也能玩。但Python环境不是每个人都有的你不能指望朋友先去装Python再跑你的脚本。这时候就需要打包成exe。我用的是PyInstaller它是一个把Python程序打包成独立可执行文件的工具。安装和打包命令很简单pip install pyinstaller pyinstaller --onefile --windowed march_escape.py--onefile表示打包成单个exe文件方便分享--windowed表示不显示黑色控制台窗口适合图形界面程序。打包时间根据电脑性能从几十秒到几分钟不等生成的文件在dist目录下面。打包时要注意的一个坑如果你的程序里有加载外部图片或音效文件的代码PyInstaller默认不会把这些素材打包进去所以最终出现“运行时找不到文件”的报错。解决方法是把素材放在打包目录下或者用PyInstaller的--add-data参数把素材目录一并加进去。我们这版因为全是形状绘制根本没加载外部文件打包就能一次通过这算是纯代码游戏的一个小优势。exe文件体积有时候会到几十兆因为这个体积里包括了Python解释器和Pygame库。这个自带运行时的特点换个角度看也是优点用户的机器上什么都不用装解压即玩。如果你的游戏要发布给别人记得把exe放进一个整洁的文件夹里附一张操作说明图体验会专业很多。7.4 性能优化让它即使帧率波动也稳定我这个项目的物理参数是基于60FPS设计的这意味着如果你把FPS改成了30跳跃高度会严重缩水因为每一帧更新的次数少了一半但物理计算还是同一套参数。如果你的电脑跑不到60FPS游戏会不以因难度的原因偏离预期设计。最简单的解决方法是固定物理帧率。你可以把游戏的逻辑更新和画面绘制分开逻辑更新固定每秒跑60次画面绘制则跟随显示刷新率。在Pygame里实现方式可以这样记录上一帧的时间积累到一定量后才做一次逻辑更新。不过对于March这个项目来说杀鸡用牛刀了。只要运行环境不太老60FPS都能轻松跑满我写了三四十个学员版本没遇到性能问题。但我还是建议你做一点基本优化把障碍物列表里已经完全滚出屏幕的对象及时清除掉。如果只生成不清理游戏运行个十来分钟列表里可能有几千个障碍物对象每帧都要遍历一遍再简单的判断也会拖慢速度。一行列表推导式就能搞定obstacles [obs for obs in obstacles if obs.x -50]这个操作我理解为“内存管理意识”虽然它短但它传递了一个重要理念游戏世界里过时的数据要清理不能一直堆在内存里吃资源。8. 扩展玩法从能玩到好玩8.1 增加金币收集玩法March大逃亡做完了如果你还有余力可以给它加一个收集系统。最简单的做法是生成一些金币黄色小圆片玩家碰到之后加分。金币对象和障碍物类似有自己的坐标更新时向左移动检测到与玩家碰撞就把它从列表里移除并给分数。8.2 加入音效和背景音乐Pygame提供了简单的音频播放功能。你可以用pygame.mixer.Sound播放短音效比如跳跃声、碰撞声、吃金币声。背景音乐建议先把mp3转成wav或者ogg格式再通过pygame.mixer.music模块循环播放。这一步能让游戏体验提升一个档次你的朋友在试玩的时候都会“哇”一声。8.3 把纯色方块换成真正的素材等你的逻辑稳定了你可以在网上找免费的像素风素材替换掉纯色矩形。把绘制代码换成加载图片和blit绘制就行。这里需要注意图片的透明背景处理如果是PNG格式通常没问题jpg格式会因为没有透明通道而显示成带背景色的方块。我个人在实际操作中的体会是March大逃亡这个项目最打动学习者的地方不是它玩法多炫而是它把几十个Python零散知识点全串到了一起。我第一次完整跑通它的时候跟学员说“你看这就是编程”然后大家一起测试跳跃手感怎么调、碰撞尺度怎么改那种把代码改一下行为就跟着变的即时反馈体验很多学编程的人可能学了一整年都没有感受过。最后再分享一个小技巧在你提交最终代码之前花点时间把代码里那些魔法数字改成带名字的常量比如把0.3命名为GRAVITY把6命名为BASE_SPEED。这样做不只是为了代码整洁更是为了你以后想改游戏参数时能在一个地方找到它们而不是满文件地翻。当时我带着学员从方块版扩展到加音效、加金币、加二段跳靠的就是一开始这种清晰的参数管理让后来的扩展轻松了不少。