很多人学完 Python 的变量、循环、函数之后会卡在一个很尴尬的位置语法看得懂但不知道拿它做什么。教程里推荐的爬虫、数据分析、Web 后端当然有价值可它们的反馈链条太长——装依赖、配数据库、起服务、调接口一路走下来往往还没看到“我做的东西动起来了”热情就先耗光了。相比之下写一个能跑起来的小游戏是投入产出比极高的选择一个周末两三百行代码你就能拥有一个会动、会死、还能重开的完整程序。标题里我用“劣质”两个字并不是在自嘲式地降低标准而是一个明确的工程判断。原型阶段追求“能跑”远比追求“好”重要因为反馈速度决定了你的迭代次数而迭代次数决定了你最终能做出什么。一个画面粗糙、音效缺失、玩法单一但完整可运行的小游戏比一个构思精妙却永远停在设计文档里的“大作”有价值得多。它能让你第一次完整体验“想法 → 代码 → 运行 → 打包 → 上传 → 别人克隆下来能跑”的整条链路。这篇文章要带你走通的就是这条链路。技术栈选 pygame因为它足够轻没有引擎那套场景树、组件系统、编辑器界面的心智负担你写的每一行代码都能直接对应到屏幕上的一个像素变化。读完之后你应该能独立完成三件事一是在本地跑起一个 pygame 项目并理解它的运行机制二是掌握游戏循环、碰撞检测、帧率控制这几个最容易踩坑的点三是把代码整理成别人愿意看一眼、并且能一键跑起来的 GitHub 仓库。更重要的是我希望你能带走一个判断小游戏是 Python 初学者最合适的练手项目形态。它同时具备即时反馈、边界清晰、可无限迭代这三个特点而这些特点恰好是刻意练习所需要的全部条件。你不需要一开始就设计好完整架构先让它转起来再一块一块换掉不满意的地方这个过程中长出来的工程直觉比看十篇架构文章都实在。1. 这篇文章真正要解决的问题先把这个话题里最常见的误区说清楚。很多人觉得“写小游戏”是玩具跟工程能力没关系所以要么不屑于做要么做了也随手丢在某个文件夹里烂掉。但如果你把视角稍微挪一下会发现小游戏其实是一个被严重低估的训练场它把软件工程里最难获取的东西——快速、真实、可观测的反馈——压缩到了几分钟的循环里。想想看你改一行 Web 接口的代码要验证它是否正确可能需要启动服务、构造请求、看日志、查数据库一轮下来五六分钟。而在游戏里改一个数字刷新窗口你立刻就知道手感变没变好。这种“改一下、看一眼”的节奏是培养调试直觉最有效的方式。你在游戏里学会的定位问题的方法——画面不对看渲染、动作不对看更新、按了没反应看事件——几乎可以原封不动地迁移到任何交互式系统的排查上。第二个被低估的价值是“边界感”。小游戏的需求边界天然清晰玩家能动、障碍物会掉、碰到就结束、分数会涨。这种清晰让你可以把注意力从“需求到底是什么”转移到“我该怎么实现它”而这恰恰是初学者最缺的练习。你在公司里做业务需求需求本身经常是模糊的、变动的你很难判断问题出在理解还是实现上但在小游戏里问题几乎永远出在实现上责任明确进步也就明确。第三个价值也是这篇文章后半部分要重点处理的把它放到 GitHub 上。写代码和发布代码是两件事。很多人写了大量本地脚本却从来没经历过“让别人能跑起来”这个环节于是完全意识不到自己在路径、依赖、编码、配置这些地方留下了多少坑。而一旦你把代码推上去并且要求自己写的 README 能让一个陌生人三分钟内跑起来你的工程标准会在一个下午之内被强行拉高一个档次。那么这篇文章适合谁如果你是刚学完 Python 基础语法、想找点能动手的东西的初学者这篇文章可以从头照着做如果你已经会写业务代码但从没碰过实时循环类的程序这篇里关于帧率、时间步长、状态机的部分可能会给你一些新角度如果你是带新人的开发者这里面的项目结构和排查清单可以直接拿去当练习任务。反过来如果你想要的是做一个能上架的商业游戏或者想学 Unity、Godot 这类完整引擎这篇文章不是你要找的东西。2. 先搞懂 pygame 的游戏循环否则后面全是玄学在动手之前有一个概念必须先立住否则后面所有“为什么画面不动”“为什么按键没反应”“为什么 CPU 跑满”的问题你都只能靠猜。这个概念就是游戏循环。用一句话概括游戏程序的本质是一个不停重复执行“处理输入、更新状态、绘制画面”的无限循环直到用户关闭窗口为止。这跟我们熟悉的 Web 请求-响应模型有本质区别。Web 后端是“来一个请求处理一次返回响应然后线程去干别的”它是被动的、事件驱动的、生命周期由外部请求决定的。游戏是主动的它不管你按不按键每秒都要把这个世界往前推进 60 次。这意味着即使玩家什么都不做你的update函数依然会被调用你的障碍物依然会往下掉你的分数依然在计算。理解这一点你就不会写出“以为不按键程序就暂停了”的代码。在这个循环里有四件事依次发生第一是事件处理从 pygame 的事件队列里把这一帧积累的所有事件取出来包括窗口关闭、按键按下、鼠标移动、窗口尺寸变化等第二是状态更新根据当前输入和上一帧的状态计算出这一帧的新状态比如玩家位置、障碍物位置、分数、是否碰撞第三是渲染把当前状态画到屏幕缓冲区第四是帧率控制通过时钟对象让循环以一个稳定的节奏运行而不是尽最大努力空转。2.1 事件处理为什么按键要用 get_pressed 而不是 KEYDOWN这里是新手最容易混淆的地方值得单独说清楚。pygame 提供了两套读取键盘输入的方式用途完全不同。第一套是事件队列里的KEYDOWN和KEYUP事件它记录的是“状态发生变化的那一瞬间”。按下 A 键队列里出现一条KEYDOWN如果你一直按住不放操作系统会按自己的重复速率持续发送KEYDOWN这个速率是系统设置决定的跟你代码里的帧率毫无关系。第二套是pygame.key.get_pressed()它返回的是当前这一帧键盘的实时状态快照某个键现在是不是按着的就是是不是按着的。对于“角色持续移动”这种需求你必须用第二套。如果你用KEYDOWN事件来移动角色会得到一个非常奇怪的手感按住方向键角色一卡一卡地往前挪松开就停而且移动速度取决于你的系统键盘重复率设置换一台电脑就变成另一个手感。反过来对于“按一次触发一次”的操作比如暂停、重开、退出你必须用KEYDOWN事件。如果你用get_pressed()来判断暂停按住空格不放游戏会在一秒钟内疯狂切换暂停/继续几十次效果就是完全没有暂停。这个区别看起来很小但它是“操作手感”这类模糊评价的技术根源。2.2 帧率控制为什么不能写 while True: pass另一个必须理解的点是时钟。如果你的主循环里什么都不控制它会以 CPU 能跑多快就跑多快的速度执行可能一秒跑几千次。这带来两个问题一是白白吃满一个 CPU 核心笔记本风扇立刻起飞二是游戏速度完全不可控在性能好的机器上障碍物飞一样往下掉在性能差的机器上慢得像放幻灯片同一个游戏在不同电脑上是两种体验。clock.tick(FPS)的作用就是把循环节奏锁死。它返回一个数值距离上一次调用过去了多少毫秒。这个返回值非常关键它是你实现“帧率无关逻辑”的基础。举例来说如果你写“每帧障碍物下移 4 像素”那么在 60 帧的机器上它一秒下移 240 像素在 30 帧的机器上只有 120 像素。而如果你写“每毫秒下移 0.06 像素”再乘以tick返回的毫秒数那么在两种机器上的实际速度就是一致的。入门阶段用固定像素是可以接受的但你要知道这是有代价的简化。理解了这两点游戏循环就不再是黑箱。你可以把它想象成一个每秒钟转 60 圈的飞轮每一圈都做同样四件事只是每一圈处理的数据不一样。剩下的所有代码本质上都是在这四个环节里填充具体逻辑。3. 环境准备与项目结构动手之前先确认环境。Python 版本以你本机实际安装的为准本文的代码只用到标准库和 pygame 的常规 API不依赖特定小版本特性所以不必纠结于 3.8 还是 3.11。真正需要注意的是解释器隔离请务必在虚拟环境里安装依赖不要直接往系统 Python 里装否则以后不同项目之间的版本冲突会让你非常痛苦。创建虚拟环境并安装依赖的命令如下Windows 和 macOS/Linux 略有差异但venv模块的用法是一致的。# 文件路径项目根目录执行 # 1. 进入项目目录 cd dodge-blocks # 2. 创建虚拟环境 python -m venv .venv # 3. 激活虚拟环境 # Windows (PowerShell) .venv\Scripts\Activate.ps1 # Windows (CMD) .venv\Scripts\activate.bat # macOS / Linux source .venv/bin/activate # 4. 安装依赖 pip install pygame # 5. 验证安装是否成功 python -c import pygame; print(pygame.version.ver)最后一行如果能打印出版本号说明安装是对的。如果这里报ModuleNotFoundError多半不是安装失败而是你激活虚拟环境的终端和运行脚本的终端不是同一个或者 IDE 里配置的解释器还指向系统 Python。这个问题在后面的排查章节还会展开因为它是最常见的“明明装了却用不了”。关于依赖声明我建议养成一个习惯不要只pip install同时把依赖写进requirements.txt。这个文件是别人能否复现你环境的关键它的价值远超你的想象。版本约束建议写成范围而不是死锁单点版本给兼容性留一点余地。# 文件路径requirements.txt pygame2.0,3.0项目结构方面入门阶段我强烈建议你先写单文件。很多教程一上来就教你分层的目录结构entities/、scenes/、systems/一大堆包结果初学者把时间全花在纠结“这个类该放哪个文件夹”上游戏本身反而没写完。正确的顺序是先让一个文件跑通全部逻辑等你确实感受到“这个文件太长了、找东西很痛苦”的时候再去拆。那种从痛点出发的重构比照着模板抄目录有意义得多。等单文件版本跑通了推荐的拆分方式是这样把常量配置抽到settings.py把玩家和障碍物这两个实体类抽到entities.py把游戏主控类留在main.py。这种拆分依据的是“变化频率”——数值最常改所以单独放实体逻辑中等放一起主控流程最稳定留最后。按变化频率分层比按技术名词分层实用得多。# 文件路径game/settings.py # 所有可调参数集中在此改数值不需要翻代码 SCREEN_WIDTH 480 SCREEN_HEIGHT 640 FPS 60 BG_COLOR (24, 26, 32) PLAYER_COLOR (90, 200, 250) BLOCK_COLOR (240, 96, 96) TEXT_COLOR (235, 235, 235) PLAYER_WIDTH 48 PLAYER_HEIGHT 24 PLAYER_SPEED 6 BLOCK_SIZE 36 BLOCK_SPEED 4 SPAWN_INTERVAL_MS 700把颜色、尺寸、速度这些数字抽出来的好处不只是“好改”。它还有一个隐性收益当你把魔法数字命名之后你会被迫想清楚每个数字代表什么这个过程本身就能帮你发现设计上的模糊之处。比如你会发现“障碍物速度”和“玩家速度”应该是两个独立概念而不是随手写的 4 和 6。4. 最小可运行骨架让窗口先亮起来在写任何玩法之前先写一个只有窗口和退出逻辑的最小骨架。这一步看起来毫无产出实际上是排错效率最高的一步如果这个骨架都跑不起来那问题一定在环境而不是在你的游戏逻辑排查范围立刻缩小十倍。很多人在一个几百行的完整代码里排查“窗口不显示”那才是真正的灾难。# 文件路径game/skeleton.py import sys import pygame SCREEN_WIDTH 480 SCREEN_HEIGHT 640 FPS 60 pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(最小骨架) clock pygame.time.Clock() running True while running: # tick 返回距上一帧的毫秒数后面做帧率无关逻辑会用到 dt clock.tick(FPS) for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN and event.key pygame.K_ESCAPE: running False screen.fill((24, 26, 32)) pygame.display.flip() pygame.quit() sys.exit()这段代码里有三个地方值得反复确认。第一pygame.init()必须在set_mode之前调用它负责初始化显示、字体、音频等子系统漏掉它后面用字体渲染文字时会报“字体模块未初始化”而错误信息往往指向字体那一行容易误导你以为是字体的问题。第二pygame.display.flip()是真正把缓冲区内容推到屏幕上的那一步如果没有它你所有的绘制都发生在内存里屏幕上永远是那块初始的黑色。第三退出时必须调用pygame.quit()否则在某些系统上窗口关闭后进程仍会残留。运行这个骨架你应该看到一个纯深色背景、标题为“最小骨架”的窗口按 ESC 或点关闭按钮都能干净退出。如果窗口一闪而过说明主循环没进去或者立刻退出了如果窗口出现但标题栏显示“无响应”说明你没有持续调用事件处理系统认为这个程序卡死了。把这两种情况记住它们是后面所有交互问题的判断基准。5. 核心玩法拆解与完整代码实现玩法我选了一个极简版本屏幕底部有一个挡板玩家用左右方向键控制它移动上方会不断掉落方块躲开得分撞上结束按空格重开。这个玩法足够简单总共不到一百五十行但它完整覆盖了游戏开发里最核心的四个机制实体、生成、碰撞、状态切换。把这四个机制吃透换成贪吃蛇、打砖块、飞机大战思路是完全一样的。先说实体设计。我把玩家和障碍物各写成一个类每个类只做三件事保存自己的矩形区域、更新自己的位置、把自己画出来。这里有个关键选择用pygame.Rect而不是自己维护 x/y/width/height 四个变量。原因是Rect自带colliderect碰撞检测、自带边界对齐属性还支持整数化处理能帮你避开大量浮点数导致的偏移问题。新手常见的“明明没碰到就死了”八成就是因为碰撞区域和绘制区域不一致而用Rect至少有唯一的事实来源。再说生成逻辑。障碍物的生成不能用固定帧数计数因为帧率会浮动。我用累加毫秒数的方式每帧把dt加到计时器上超过阈值就生成一个并清零。这种写法在帧率波动时依然稳定不会出现“卡一下突然冒出三个方块”的情况。同时我给速度加了一个随分数缓慢增长的公式让游戏有一点难度曲线不至于玩十分钟都是同一个节奏。碰撞检测放在更新阶段的最后。这里要注意顺序必须先更新所有方块的位置再统一做碰撞判断而不是边更新边判断。否则会出现“上一帧还没碰到、这一帧已经穿过去”的漏检尤其在方块速度快的时候非常明显。这是实时系统里典型的时序问题理解了它你以后写任何基于轮询的状态检测都会更小心。下面是完整的单文件实现可以直接复制运行。# 文件路径game/main.py import random import sys import pygame # ---------------- 基础配置 ---------------- SCREEN_WIDTH 480 SCREEN_HEIGHT 640 FPS 60 BG_COLOR (24, 26, 32) PLAYER_COLOR (90, 200, 250) BLOCK_COLOR (240, 96, 96) TEXT_COLOR (235, 235, 235) PLAYER_WIDTH 48 PLAYER_HEIGHT 24 PLAYER_SPEED 6 BLOCK_SIZE 36 BLOCK_SPEED 4 SPAWN_INTERVAL_MS 700 class Player: 玩家控制的挡板 def __init__(self): self.rect pygame.Rect(0, 0, PLAYER_WIDTH, PLAYER_HEIGHT) self.rect.centerx SCREEN_WIDTH // 2 self.rect.bottom SCREEN_HEIGHT - 32 def move(self, dx): self.rect.x dx # 限制在窗口内避免跑出边界 if self.rect.left 0: self.rect.left 0 if self.rect.right SCREEN_WIDTH: self.rect.right SCREEN_WIDTH def draw(self, surface): pygame.draw.rect(surface, PLAYER_COLOR, self.rect, border_radius6) class Block: 从上方落下的障碍物 def __init__(self, x, y, speed): self.rect pygame.Rect(x, y, BLOCK_SIZE, BLOCK_SIZE) self.speed speed def update(self): self.rect.y self.speed def draw(self, surface): pygame.draw.rect(surface, BLOCK_COLOR, self.rect, border_radius4) def is_out_of_screen(self): return self.rect.top SCREEN_HEIGHT class Game: def __init__(self): pygame.init() self.screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(Dodge Blocks - 一个劣质小游戏) self.clock pygame.time.Clock() self.font pygame.font.SysFont(None, 26) self.big_font pygame.font.SysFont(None, 44) self.reset() def reset(self): 重置一局游戏的全部状态 self.player Player() self.blocks [] self.score 0 self.spawn_timer 0 self.running True self.game_over False def handle_events(self): for event in pygame.event.get(): if event.type pygame.QUIT: self.running False elif event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: self.running False elif event.key pygame.K_SPACE and self.game_over: self.reset() def spawn_block(self): x random.randint(0, SCREEN_WIDTH - BLOCK_SIZE) # 分数越高下落越快形成难度曲线 speed BLOCK_SPEED self.score // 10 self.blocks.append(Block(x, -BLOCK_SIZE, speed)) def update(self, dt_ms): # 1. 处理连续按键输入 keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: self.player.move(-PLAYER_SPEED) if keys[pygame.K_RIGHT]: self.player.move(PLAYER_SPEED) # 2. 按时间累加生成障碍物与帧率解耦 self.spawn_timer dt_ms if self.spawn_timer SPAWN_INTERVAL_MS: self.spawn_timer 0 self.spawn_block() # 3. 统一更新位置 for block in self.blocks: block.update() # 4. 越过底部的方块计分并移除 passed [b for b in self.blocks if b.is_out_of_screen()] self.score len(passed) self.blocks [b for b in self.blocks if not b.is_out_of_screen()] # 5. 所有位置更新完毕后再统一判定碰撞 for block in self.blocks: if self.player.rect.colliderect(block.rect): self.game_over True break def draw(self): self.screen.fill(BG_COLOR) for block in self.blocks: block.draw(self.screen) self.player.draw(self.screen) score_text self.font.render(fScore: {self.score}, True, TEXT_COLOR) self.screen.blit(score_text, (16, 16)) if self.game_over: over_text self.big_font.render(Game Over, True, TEXT_COLOR) over_rect over_text.get_rect( center(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 20) ) self.screen.blit(over_text, over_rect) tip_text self.font.render( Press SPACE to restart, ESC to quit, True, TEXT_COLOR ) tip_rect tip_text.get_rect( center(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 32) ) self.screen.blit(tip_text, tip_rect) pygame.display.flip() def run(self): while self.running: dt_ms self.clock.tick(FPS) self.handle_events() if not self.game_over: self.update(dt_ms) self.draw() pygame.quit() sys.exit() if __name__ __main__: Game().run()代码里有几个设计点需要解释否则你抄下来也只是“能跑但不知道为什么这么写”。第一reset()方法被抽出来单独存在构造函数里也调用它。这样做的直接好处是重开游戏不需要退出再启动只需要把状态清空。它不是可有可无的重构而是状态管理的正确姿势凡是能被重置的东西都应该有一个明确的重置入口。以后你要加“最高分”这种需要跨局保留的数据就会发现把它放在reset()外面天然正确不会一重开就被清掉。第二update里的注释编号对应的是执行顺序这个顺序不是随手排的。先读输入再更新位置是保证操作跟手先生成再统一更新是保证同一帧内所有实体处于同一个时间点先计分清理再判碰撞是避免已经被删掉的对象还参与判定。这类顺序问题在实时系统里极其常见写的时候顺手标注一下未来改代码能省很多时间。第三game_over状态的处理方式值得注意。我没有在游戏结束时跳出主循环而是让循环继续跑只是跳过update。这样做是因为你还需要draw来显示结束画面还需要handle_events来接收重开指令。如果把循环停了画面会定格在最后一帧窗口无响应玩家只能强杀进程。用状态位控制行为而不是用循环控制生命周期这是游戏状态机最朴素的形态。6. 运行结果与效果验证代码写完之后从项目根目录运行python game/main.py正常情况下你应该看到一个 480x640 的窗口标题是“Dodge Blocks - 一个劣质小游戏”左上角显示Score: 0。屏幕底部中央有一个蓝色圆角矩形挡板大概半秒后开始有红色方块从顶部随机位置落下。按住左右方向键挡板平滑移动撞到窗口边缘会停住而不是穿出去。成功躲过一个方块左上角分数加一。被砸中后屏幕中央出现“Game Over”和重开提示此时挡板不再响应方向键按空格可以立刻开始新的一局。验证是否真的成功建议按这个顺序检查每一项都对应一个独立的机制。第一窗口能否正常关闭点击标题栏的关闭按钮程序应该立刻退出任务管理器里没有残留进程。如果关不掉说明事件处理有问题。第二按键是否跟手连续按住右方向键挡板应该匀速右移直到贴住右边就不再动。如果移动一卡一卡说明你误用了KEYDOWN。第三分数是否只在方块完全离开屏幕后才增加观察即将落到底部的方块分数不应该在它刚接触底部边缘时就跳变。第四重开是否彻底撞死之后按空格分数必须回到 0屏幕上的方块必须全部清空不能残留上一局的障碍物。如果哪一步不符合预期第一件事不是改代码而是定位到对应的环节。窗口类问题看display相关调用按键问题看输入读取方式位置和计分问题看update的执行顺序残留状态问题看reset是否漏了字段。这个“按现象定位环节”的思路比盲目地在代码里加 print 高效得多。想要更直观地看到帧率和时间步长的关系可以在主循环里临时加一行日志观察每帧耗时# 文件路径game/main.py临时调试用验证完记得删掉 def run(self): frame_count 0 while self.running: dt_ms self.clock.tick(FPS) frame_count 1 if frame_count % 60 0: print(f第 {frame_count} 帧本帧耗时 {dt_ms} ms当前方块数 {len(self.blocks)}) self.handle_events() if not self.game_over: self.update(dt_ms) self.draw()如果你看到dt_ms稳定在 16 到 17 之间说明 60 帧跑满了如果它长期在 30 以上说明有性能问题或者机器本身吃力。方块数量持续增长而分数不涨说明清理逻辑没生效需要检查is_out_of_screen的判断条件。7. 上传到 GitHub把“劣质”变成可展示的资产代码能跑之后接下来的这一步才是真正拉开差距的地方。同样一个劣质小游戏丢在本地文件夹里它是一个练习放到 GitHub 上并且配好说明它是一个作品。这两者的差别不在于代码质量而在于你有没有完成“让别人能跑起来”这个额外的工程动作。第一步是加.gitignore。这一步千万别跳过因为虚拟环境目录动辄几十兆、上千个文件一旦提交进去你的仓库会变得又大又乱而且这些文件还会污染后续所有git status的输出让你看不清真正的改动。# 文件路径.gitignore __pycache__/ *.py[cod] .venv/ venv/ env/ build/ dist/ *.spec .idea/ .vscode/ .DS_Store Thumbs.db第二步是写README.md。判断一个仓库值不值得看多数人只花十秒钟而这十秒钟基本都花在 README 上。一个合格的 README 不需要华丽但必须回答四个问题这是什么、怎么装、怎么跑、怎么操作。如果你的 README 只写了“我的小游戏”那它对陌生人来说等于不存在。# Dodge Blocks 一个用 Python pygame 写的极简躲避小游戏用于练习游戏循环、碰撞检测与状态管理。 ## 环境要求 - Python 3.x - pygame 2.x ## 安装与运行 bash python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install -r requirements.txt python game/main.py操作说明按键功能← / →左右移动挡板空格游戏结束后重新开始ESC退出游戏玩法方块从顶部随机位置落下躲开得分撞上结束。分数越高方块下落速度越快。第三步是初始化仓库并推送。这里有几个细节值得注意分支名建议直接用 main 而不是历史遗留的 master首次提交的信息要写清楚做了什么而不是 update 或 first commit 这种等于没写的内容如果远程仓库创建时勾选了自动生成 README你的首次推送会因为历史不一致被拒绝那就需要先合并。 bash # 文件路径项目根目录执行 git init git add . git commit -m feat: 初始化躲避方块小游戏包含核心玩法与说明文档 git branch -M main git remote add origin https://github.com/your-name/dodge-blocks.git git push -u origin main如果推送时提示rejected说明远程已有提交先执行git pull --rebase origin main把远程内容合过来再推一次。如果提示认证失败说明你的凭据已失效需要重新配置访问方式不要反复重试同一个命令。最后一步也是最能体现专业度的一步验证“陌生人能否跑起来”。最可靠的做法是把仓库重新克隆到一个全新目录用全新的虚拟环境走一遍 README 里的命令。这个动作会立刻暴露你所有想当然的地方比如你本地有个未提交的配置文件、你依赖了某个全局安装的包、你的路径写法只在当前操作系统下成立。我建议你把这一步当成发布前的固定流程它比任何代码审查都更能提升仓库的可用性。8. 常见问题与排查思路下面这些问题是这类项目里出现频率最高的我把现象、原因和解决方式整理成表方便你按图索骥。但更重要的是理解排查的思维方式先确定问题出在哪个环节再在那个环节里找具体原因而不是凭感觉改代码。问题现象可能原因排查方式解决方案报ModuleNotFoundError: No module named pygame依赖未安装或装到了另一个解释器执行python -c import sys; print(sys.executable)看解释器路径用同一个解释器执行pip install pygame并在 IDE 里把解释器指向虚拟环境窗口一闪而过主循环条件一开始就是 False或没进入循环在循环入口加一条 print检查running初始值确认事件处理不会立刻置 False窗口出现但显示“无响应”主循环里没有调用pygame.event.get()查看循环体内是否漏了事件处理每帧必须清空事件队列否则系统判定程序卡死画面全黑什么都看不到漏了pygame.display.flip()或绘制顺序错误在flip前打印一次调用补上刷新调用确保绘制发生在flip之前按住方向键角色一卡一卡用了KEYDOWN事件来做持续移动检查输入读取代码改为pygame.key.get_pressed()按帧读取暂停功能疯狂切换用了get_pressed()来处理一次性操作检查暂停逻辑改为监听KEYDOWN事件障碍物下落速度在不同机器上不一致移动量按“每帧固定像素”写且未锁帧率打印clock.tick返回值用dt乘以速度系数或确保tick(FPS)被调用明明没碰到就判定死亡碰撞矩形与实际绘制区域不一致临时把rect用描边方式画出来统一用Rect作为唯一事实来源避免手算坐标方块穿过挡板没有被判定先判碰撞后更新位置或速度过大导致跨帧跳过打印每帧位置统一更新完再判定必要时对高速物体做分段检测分数在方块未离开屏幕时就增加is_out_of_screen判断条件用了或判断了bottom观察边界值用rect.top SCREEN_HEIGHT判断完全离开重开后残留上一局方块reset()漏了某个字段逐项对照__init__里的属性把需要重置的字段集中到reset()中管理中文文字显示为方块默认字体不含中文字形换成系统中文字体测试用pygame.font.Font(字体文件路径, 字号)指定字体打包后双击闪退缺少资源文件或相对路径在打包后失效用带控制台的打包方式运行看报错用pyinstaller -F -c调试确认资源路径处理正确git push被拒绝远程仓库已有提交历史不一致git log --oneline查看本地与远程差异先git pull --rebase origin main再推送仓库体积异常大虚拟环境或__pycache__被提交git count-objects -vH补充.gitignore必要时用git rm -r --cached取消跟踪表格里有两行值得再多说两句。关于“中文字体显示为方块”这是新手最容易卡住的问题之一因为代码没错、逻辑没错但屏幕上就是一排方块很容易怀疑是自己写错了渲染逻辑。根本原因是pygame.font.SysFont(None, size)使用的是系统默认字体而这个默认字体在某些平台上并不包含中文字形。稳妥的做法是显式指定一个确定存在的字体文件或者干脆在 UI 上先用英文等核心玩法稳定了再处理本地化。这不是妥协而是把问题按优先级排序。关于“打包后闪退”如果你想把游戏发给不装 Python 的朋友可以用 PyInstaller 打包成可执行文件。打包时建议先用带控制台的模式跑通确认没问题再换成无控制台模式否则一旦出错你连报错信息都看不到。# 文件路径项目根目录执行 pip install pyinstaller # 第一步带控制台打包方便看报错 pyinstaller -F -c -n dodge-blocks game/main.py # 第二步确认无误后改成无控制台打包 pyinstaller -F -w -n dodge-blocks game/main.py打包产物在dist/目录下。注意这个目录已经在.gitignore里了不要把二进制文件提交到仓库那会让仓库迅速膨胀。正确做法是在 GitHub 的 Releases 页面上传打包好的文件让仓库本身保持干净。9. 最佳实践与后续迭代方向代码能跑、能推、别人能克隆这个循环走完一遍之后接下来往哪个方向走决定了你是停留在“做过一个小游戏”还是真的从中长出了工程能力。我按投入产出比从高到低给几条建议。第一把常量彻底集中管理。你现在可能还有几个数字散落在代码里比如挡板距底部的距离、初始生成延迟。把它们全部挪到settings.py并且给每个常量加一行注释说明它影响什么。这个动作很小但它建立的习惯很重要当你以后接手一个几千行的项目你会感谢当初那个把所有魔法数字都命名的自己。第二引入一个显式的状态机。现在的代码用game_over一个布尔量控制流程短期够用但你只要加一个“开始菜单”或者“暂停界面”一个布尔量就撑不住了。更通用的写法是定义一个状态枚举比如MENU、PLAYING、PAUSED、GAME_OVER然后让update和draw根据当前状态分发到不同分支。这个改动的价值在于它把“程序现在处于什么阶段”这件事从散落的布尔变量变成了一个唯一、可枚举、可断言的事实。第三把逻辑和渲染分离。目前update里没有任何绘制draw里没有任何状态修改这个边界保持得很好请务必守住。一旦你在draw里顺手改了个变量或者为了省事在update里直接画东西测试就没法做了因为逻辑和副作用搅在一起。守住这条线你才有可能给计分规则、碰撞判定写单元测试——这些纯函数不需要窗口就能验证是这类项目里最适合补测试的部分。第四加日志而不是加 print。调试游戏时你会反复想知道“这一帧发生了什么”用print会把终端刷爆。改用标准库的logging设置一个可控的级别只在需要时打开调试输出。这个习惯从练习项目就开始培养收益会一直延续到生产环境。# 文件路径game/main.py 顶部示例片段 import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, ) logger logging.getLogger(__name__) # 在碰撞发生时记录比 print 更可控 # logger.info(游戏结束最终得分 %s, self.score)第五注意版本管理的粒度。不要攒一天再提交一次巨大的 commit。每完成一个可验证的小功能就提交一次信息写清楚做了什么、为什么这么做。比如“feat: 增加难度曲线分数每 10 分提升下落速度”就比“改了点东西”有用一百倍。当你想回退到“加难度之前”的版本时你会立刻体会到这个习惯的价值。第六警惕一个常见的性能误区不要在游戏循环里做阻塞操作。读文件、发网络请求、time.sleep、大量字符串拼接日志这些东西一旦放进update或draw都会直接拖垮帧率。正确做法是把它们挪到循环外面或者在后台线程里完成循环里只读取结果。这个原则在以后写任何实时系统时都适用。至于功能上的迭代方向我建议按这个顺序推进先加音效这是提升“完成度体感”最划算的一步几行代码就能让游戏从“作业”变成“作品”再加本地最高分存档用json模块读写一个小文件即可顺便练习文件 IO 和异常处理然后尝试把单文件拆成模块这一步一定要在你真的觉得“文件太长”之后再动手否则容易为了架构而架构最后可以考虑加一个难度选择菜单这会逼着你把状态机真正做扎实。我最后想说的其实不是技术。这个游戏的代码水平确实不高画面也确实不好看但它的价值从来不在于质量而在于它是完整的。一个人从零写下一个能跑、能推、别人能拉下来玩的东西和一个人看了十篇教程却什么也没发布两者之间的差距会随着时间不断放大。前者每完成一次都会积累一点“我能做出东西”的确信后者则在不断消耗它。所以如果你手上正好有一个想法别等它变好先让它跑起来把它推到 GitHub 上然后在一个个 commit 里把它改好——这个顺序比反过来容易实现得多。
阅读完成 · 觉得有帮助?