首页 / 资讯中心 / 文章详情

Pygame实战:从零开发接苹果小游戏完整指南

Pygame实战:从零开发接苹果小游戏完整指南 ★ FEATURED ARTICLE
如果你点进这篇内容大概率是想用 Pygame 写一个真正能跑起来的小游戏但又不知道从哪下手。我当初也是这么过来的装了 Python看了几篇教程写了几行 pygame.display.set_mode然后对着黑窗口发了好几天呆。后来在某个周末硬着头皮把一个小项目从头写到尾算是彻底打开了思路——这个过程中踩坑学到的远比看十篇教程有用。这篇就以 Pygame 开发一个“接苹果小游戏”为主线从环境准备、游戏设计、核心代码实现到性能优化和打包发布带你完整走一遍开发流程。适合刚学完 Python 基础、想用游戏项目练手的读者也适合已经写过 Pygame 小 Demo、但还没独立完成过完整项目的朋友。看完你应该能照着做出来并且理解游戏主循环、碰撞检测、资源加载这些基础概念。1. 为什么用 Pygame 写第一个游戏Pygame 是 Python 生态里最适合做游戏入门的一个多媒体库封装了 SDL 库底层处理窗口、图像、声音、事件上层暴露给 Python 的是一套相对简洁的接口。用它做游戏的最大好处是把精力集中在游戏逻辑上不用纠结图形界面那一堆底层细节。1.1 Pygame 适合什么场景Pygame 常用于 2D 小游戏、教学演示和游戏开发入门项目。像贪吃蛇、俄罗斯方块、飞机大战、接苹果这类游戏用 Pygame 都能在几百行内实现。它不追求华丽特效也不需要你掌握图形学和着色器知识只要会 Python 基础语法能理解“事件”“循环”“坐标”“碰撞检测”这几个概念就能写出一个完整的游戏。对比一下其他选择用纯 Python 控制台写游戏只能靠文本交互谈不上画面和交互体验用 Unity 或 Godot 这类游戏引擎功能强大但学习曲线陡峭而且需要学 C# 或 GDScript。Pygame 刚好夹在中间比命令行生动得多比完整引擎轻量得多。对于第一次做游戏的人来说用 Pygame 写一个能控制角色、有得分、有结束条件的完整作品是最能建立信心的路径。1.2 游戏引擎不选 Pygame 的理由有人可能会问现在做游戏不都是用 Unity 吗这里有个常见误区。Unity 适合做商业项目但在学习阶段直接扑向大型引擎会陷入剪辑界面、材质、预制体、动画状态机这些概念里光是编辑器操作就要学很久。Pygame 的优势是“代码即游戏”每一行代码对应的都是游戏里的一层逻辑比如移动、生成、碰撞、计分你能从零开始理解游戏到底是怎么运作的。另外Pygame 跨平台性好。同一份代码能在 Windows、macOS、Linux 上运行也支持导出到树莓派上配合物理按键做成街机小盒子。这类延伸玩法很多是我们这种折腾型开发者最爱的部分。纯入门选 Pygame后续上手真正的引擎反而会更有底气。2. 接苹果小游戏的整体设计思路“接苹果”这个游戏逻辑很简单玩家控制一个篮子在屏幕底部左右移动接住从屏幕顶部落下的苹果接到一个得一分漏掉一个少一条命生命用光游戏结束游戏过程和结束画面都能清楚地看到分数变化。2.1 核心玩法与规则设计细化一下规则苹果从屏幕顶部随机水平位置生成以一定速度下落。玩家通过左右方向键或鼠标移动控制篮子接苹果的容器。苹果与篮子碰撞时判断为接到分数加一苹果消失。苹果落到屏幕底部而未被接到生命减一苹果消失。初始拥有三条生命生命为零时游戏结束。游戏可以重新开始结束时按下空格键重置。规则定下来之后开发目标就非常清晰了窗口、循环、角色、移动、碰撞、得分、生命、状态切换。每个部分拆开都对应一个独立的代码模块这样组织代码时就不会写成一坨面条代码。2.2 文件结构与模块划分很多人写 Pygame 入门项目喜欢把所有代码堆在一个文件里但一旦游戏逻辑变复杂后期维护很痛苦。我这里建议从第一个项目开始就建立一个简单但清晰的目录结构catch_apple/ ├── main.py # 主程序入口负责游戏循环和状态切换 ├── config.py # 全局配置常量窗口尺寸、颜色、速度等 ├── player.py # 玩家类封装移动和碰撞框架 ├── apple.py # 苹果类封装生成、下落、绘制 ├── game.py # 游戏逻辑类整合所有元素和规则 └── assets/ ├── apple.png # 苹果图片 └── basket.png # 篮子图片可能有人觉得做这么小的游戏还分文件太小题大做但实际写下来你会发现分离文件最大的好处是改苹果的移动速度不需要在主循环里翻来翻去直接在 apple.py 里改就行。这种模块化意识越早建立后面的开发路越顺畅。2.3 状态机设计游戏不是只有一个画面任何游戏都有多个状态开始画面、游戏进行中、游戏结束、暂停。如果用一堆 if 变量在全局判断当前状态很容易出错。这里我用到的是“状态机”思路用一个整数变量表示当前状态在主循环里根据状态执行不同逻辑。这个机制不复杂比如用常量定义三个状态——GAME_READY准备开始、GAME_PLAYING游戏中、GAME_OVER结束。主循环每次检查当前状态对应调用不同的更新和绘制逻辑。这样设计的好处是游戏逻辑清晰后续增加开始菜单或暂停功能时只需新增状态不必改写原有逻辑。3. 环境配置与开发前的准备工作3.1 Python 版本选择和 Pygame 安装做 Pygame 开发建议使用 Python 3.9 及以上版本。老版本 3.7、3.8 也不是不行但新版对类型提示和语法的支持更好写代码更顺手。安装 Pygame 只有一条命令pip install pygame如果安装速度慢可以切换国内镜像源pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后在 Python 环境里运行下面代码验证python -m pygame.examples.aliens如果弹出一个飞船射击的游戏窗口说明安装成功。这个官方示例本身就是很好的参考闲时可以翻翻它的源码比看文档有用得多。3.2 编辑器和调试工具选择编辑器我用的是 VS Code配合 Python 插件。Pygame 代码调试有个特点程序跑起来之后没有终端断点也没关系可以用print()来输出坐标、分数、速度这些关键变量观察实时变化。VS Code 的“Python Interactive”窗口支持逐行执行调试逻辑时很方便。另外强烈建议在代码里设置一个FPS显示开关。开发阶段把帧率显示在窗口标题栏上一旦发现游戏变快变慢能立刻知道是否和循环时间控制有关。这个细节能省很多排查时间。4. 主循环与游戏初始化4.1 初始化窗口与游戏循环Pygame 程序的骨架就是一个永不结束的while循环循环里反复做三件事处理事件、更新游戏状态、绘制画面。这是几乎所有 Pygame 游戏的核心结构。# main.py import pygame import config from game import Game def main(): pygame.init() screen pygame.display.set_mode((config.WIDTH, config.HEIGHT)) pygame.display.set_caption(接苹果小游戏 - Pygame 入门项目) clock pygame.time.Clock() game Game(screen) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False else: game.handle_event(event) game.update() game.draw() pygame.display.flip() clock.tick(config.FPS) pygame.quit() if __name__ __main__: main()这段代码的关键点在于clock.tick(config.FPS)。FPS设置为 60意思是每秒钟游戏循环最多执行 60 次。没有这一行游戏的速度就会取决于电脑性能帧率高的机器游戏就会快到玩家反应不过来。加了它游戏在不同设备上的运行节奏一致。4.2 事件系统与键盘控制玩家移动的核心是 Pygame 的事件系统。在每一帧中pygame.event.get()返回这一段时间内发生的所有事件。键盘按键有两种处理方式一种是用KEYDOWN事件判断“按下瞬间”适合单击操作另一种是用pygame.key.get_pressed()获取当前所有按键的实时状态适合判断持续按住方向键移动。接苹果游戏用第二种方式更顺滑。在每一帧的update()中直接获取按键状态然后根据按键改变篮子的x坐标# player.py def update(self, keys): if keys[pygame.K_LEFT]: self.x - self.speed if keys[pygame.K_RIGHT]: self.x self.speed注意这里不要用elif否则如果玩家同时按了两个方向键第二个判断就被跳过了。实测中用elif会出现一种恼人的问题同时按住左右键篮子完全不动弹。如果发现这个现象改回两个独立的if就正常了。4.3 让苹果下落速度与坐标苹果的下落逻辑是每帧让它的y坐标增加一个下落的速率。为了让游戏有进步感下落速度不能是固定值而是随分数阶梯式加快# apple.py self.fall_speed config.BASE_SPEED (score // 5) * config.SPEED_STEP这里的设计思路是初始下落速度比较慢玩家容易接游戏上手快每得 5 分下落速度提升一档逐步增加难度。score // 5是整除运算分数为 0 到 4 时速度是初始档分数到 5 之后加一档以此类推。苹果的位置用一个Rect对象矩形区域保存。Rect是 Pygame 里很重要但新手容易忽视的概念它封装了x、y、width、height并且提供了colliderect()方法用于矩形碰撞检测。接苹果游戏里的碰撞判定本质就是“苹果的矩形”和“篮子的矩形”是否重叠。5. 核心游戏逻辑实现5.1 碰撞检测原理与 Pygame 中的实现碰撞检测是现代游戏开发的基本功。在 Pygame 中最简单的碰撞检测方法是pygame.Rect.colliderect()。它判断两个矩形是否有重叠区域返回布尔值。实现起来是一行代码的事if apple.rect.colliderect(player.rect): score 1 apple.reset()使用 Rect 做碰撞检测有一个重要前提苹果的图片和篮子的图片其Rect区域必须和视觉大小匹配。如果加载的图片周围有大片透明区域Rect会包含透明区域导致视觉上没有接触却判了碰撞。解决办法是用pygame.image.load()加载图片后调用set_colorkey()去掉背景色或者自己手动建立一个更精确的碰撞矩形。对于接苹果这种休闲游戏用图片实际尺寸即可。5.2 生成苹果与回收机制苹果每帧生成一个不行那样苹果会叠成密集的一堆游戏瞬间失去可玩性。正确做法是设置一个“生成间隔”用一个计时器变量控制。每经过一定的帧数生成一个新的苹果同时限制屏幕上最多同时出现的苹果数量比如不超过 5 个。更稳妥的方式是对象池思路初始化时创建固定数量的苹果对象需要显示时激活落到底部或被接住时“回收”并重置位置。这样做的优势是避免游戏中频繁创建和销毁对象减少 Python 垃圾回收对游戏性能的干扰。几百行的小项目里你察觉不出来但这是养成好习惯的开始——游戏对象管理比技巧本身重要。我的实现方式写在AppleManager类里用列表保存所有苹果每个苹果用一个active标志表示是否在场上# game.py def spawn_apple(self): if self.spawn_timer 0: self.spawn_timer - 1 return if sum(1 for a in self.apples if a.active) config.MAX_APPLES: apple self.get_inactive_apple() if apple: apple.activate() self.spawn_timer config.SPAWN_INTERVAL这种“激活/回收”的设计在后续写飞机大战、射击游戏时依然适用值得掌握。5.3 计分、生命与游戏结束判定计分和生命管理直接放在Game类里。每接到一个苹果分数加一每漏掉一个生命减一。生命的初始值是三。判断游戏结束的条件是生命小于等于零。游戏结束之后不能直接退出程序而是切换到一个结束页面显示最终分数并提示“按下空格键重新开始”。按空格键重置所有游戏状态回到准备状态。这部分的实现就是典型的“状态机切换”用主循环里的状态变量控制。def toggle_state(self, new_state): self.state new_state if new_state GAME_READY: self.reset() self.update_ui()这里有一个经验之谈重置游戏时不仅要把分数和生命归零还要把所有苹果位置重置、生成计时器清零、篮子位置移回初始点。经常有人只重置了分数结果一局结束后上一次下落的苹果还停在原地画面看起来非常奇怪。5.4 绘制文本与 UI 界面Pygame 绘制文本需要用到pygame.font.Font模块。初学者最容易踩的坑是忘记字体文件路径用默认字体时中文字符显示成方块。一个稳妥方案是把用到的文字全部用英文或者使用系统中文字体文件的路径。Windows 下可以用C:/Windows/Fonts/msyh.ttc来加载微软雅黑font pygame.font.Font(C:/Windows/Fonts/msyh.ttc, 32)macOS 下对应路径是/System/Library/Fonts/PingFang.ttc。如果不想依赖系统字体也可以把字体文件放进assets目录统一管理。分数文本建议放在窗口左上角生命值用图标或数字显示在右上角。UI 元素每帧重新渲染一次就行不需要单独做缓存优化——这是小项目里常见的过度设计。先把逻辑写对再考虑性能。6. 完整代码示例与运行效果从零到可用这里给出一个精简但功能完整的版本。注意为了实现“拿起来就能测”我用内置图形代替图片素材完全省略了加载外部图片的麻烦。这也是刚入门时推荐的测试方式验证完整体逻辑后再替换素材。6.1 简化版完整代码# catch_apple.py import pygame import random WIDTH, HEIGHT 480, 640 FPS 60 BASE_SPEED 3 SPEED_STEP 0.5 MAX_APPLES 5 LIVES 3 GAME_READY 0 GAME_PLAYING 1 GAME_OVER 2 class Player: def __init__(self): self.width 80 self.height 20 self.x (WIDTH - self.width) // 2 self.y HEIGHT - 60 self.speed 6 self.rect pygame.Rect(self.x, self.y, self.width, self.height) def update(self, keys): if keys[pygame.K_LEFT]: self.x - self.speed if keys[pygame.K_RIGHT]: self.x self.speed self.x max(0, min(WIDTH - self.width, self.x)) self.rect.x self.x def draw(self, screen): pygame.draw.rect(screen, (0, 120, 255), self.rect) class Apple: def __init__(self): self.size 20 self.x random.randint(0, WIDTH - self.size) self.y -self.size self.speed BASE_SPEED self.active False self.rect pygame.Rect(self.x, self.y, self.size, self.size) def reset(self, score): self.x random.randint(0, WIDTH - self.size) self.y -self.size self.speed BASE_SPEED (score // 5) * SPEED_STEP self.active True self.rect.x self.x self.rect.y self.y def update(self): if not self.active: return self.y self.speed if self.y HEIGHT: self.active False self.rect.y self.y def draw(self, screen): if self.active: pygame.draw.circle(screen, (200, 50, 50), (self.x self.size // 2, self.y self.size // 2), self.size // 2) class Game: def __init__(self, screen): self.screen screen self.state GAME_READY self.score 0 self.lives LIVES self.player Player() self.apples [Apple() for _ in range(MAX_APPLES)] self.spawn_timer 0 self.font pygame.font.SysFont(Arial, 30) def reset(self): self.score 0 self.lives LIVES self.player.x (WIDTH - self.player.width) // 2 self.player.rect.x self.player.x for apple in self.apples: apple.active False self.spawn_timer 0 def handle_event(self, event): if event.type pygame.KEYDOWN: if self.state GAME_OVER and event.key pygame.K_SPACE: self.reset() self.state GAME_PLAYING def update(self): if self.state ! GAME_PLAYING: return keys pygame.key.get_pressed() self.player.update(keys) self.spawn_timer - 1 if self.spawn_timer 0: self.spawn_timer 30 for apple in self.apples: if not apple.active: apple.reset(self.score) break for apple in self.apples: apple.update() if apple.active and apple.rect.colliderect(self.player.rect): apple.active False self.score 1 elif apple.active and apple.y HEIGHT: apple.active False self.lives - 1 if self.lives 0: self.state GAME_OVER def draw(self): self.screen.fill((240, 240, 240)) for apple in self.apples: apple.draw(self.screen) self.player.draw(self.screen) if self.state GAME_READY: text self.font.render(Press SPACE to Start, True, (0, 0, 0)) self.screen.blit(text, (WIDTH // 2 - text.get_width() // 2, HEIGHT // 2)) elif self.state GAME_PLAYING: score_text self.font.render(fScore: {self.score}, True, (50, 50, 50)) lives_text self.font.render(fLives: {self.lives}, True, (200, 50, 50)) self.screen.blit(score_text, (10, 10)) self.screen.blit(lives_text, (WIDTH - lives_text.get_width() - 10, 10)) elif self.state GAME_OVER: text self.font.render(fGame Over - Score: {self.score}, True, (200, 0, 0)) hint self.font.render(Press SPACE to Restart, True, (80, 80, 80)) self.screen.blit(text, (WIDTH // 2 - text.get_width() // 2, HEIGHT // 2 - 30)) self.screen.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT // 2 10)) def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(Pygame 接苹果小游戏) clock pygame.time.Clock() game Game(screen) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False else: game.handle_event(event) game.update() game.draw() pygame.display.flip() clock.tick(FPS) pygame.quit() if __name__ __main__: main()如果你想切换到图片素材只需要把draw方法里的pygame.draw换成screen.blit(image, rect)同时把碰撞用到的Rect尺寸改为图片实际大小。逻辑部分完全不用改。6.2 运行效果与关键节点检查运行代码后你会看到一个灰色窗口按下空格键红色圆形苹果从顶端零散下落蓝色长条是篮子左右方向键控制移动。接住一个苹果左上角分数加一漏掉一个右上角生命减一。生命清零后窗口中央显示 Game Over 提示。如果一切正常这个游戏框架已经能跑通了。接下来可以根据喜好调整参数把BASE_SPEED改大可以让游戏更刺激调低MAX_APPLES可以减少场上同时出现的苹果数量修改SPAWN_INTERVAL我代码里写的是 30 帧可以让苹果生成更快或更慢。这些配置是游戏手感的关键需要反复试玩调整到你满意的节奏。7. 常见 Bug 与排查技巧写 Pygame 项目的过程中有几个问题出现的频率极高我把它们和经验解法整理在一起。7.1 窗口秒退或闪退最常见的情况是代码里用了sys.exit()或者运行环境的问题导致窗口一闪就没了。排查方法是在程序入口加一个输入等待或者逐步注释掉初始化部分定位问题。另外一个典型原因是没有写pygame.init()导致创建窗口时报错直接崩溃。7.2 游戏速度时快时慢如果你发现同样的移动量在不同电脑上表现不一致那基本是帧率控制出问题了。解决方法就是前面提到的clock.tick(FPS)保证每帧之间的时间间隔可控。注意tick()要放在主循环的末尾这样一帧里所有逻辑都在同一节奏下执行。7.3 图片加载失败或中文路径问题Pygame 加载图片失败时不会抛出明确的 Python 异常经常得到的是 0 宽 0 高的 Surface。排查时可以打印一下图片的尺寸img pygame.image.load(assets/apple.png) print(img.get_size())如果输出(0, 0)说明图片加载失败检查路径和文件名。特别注意Windows 路径里的\在字符串里会被当作转义符建议使用正斜杠/或者用原始字符串rassets/apple.png。7.4 中文显示为方块这是 Pygame 老玩家都知道的经典问题。Pygame 默认字体不支持中文字符解决方案是显式指定一个系统中文字体路径而不是依赖字体名称。字体文件路径在不同操作系统上不同做得更稳妥的方法是写一个辅助函数自动寻找可用字体路径。7.5 按键不灵敏或长按无效如果你用KEYDOWN事件做移动就会遇到这个体验问题快速点按方向键时移动的响应不连续像在“蹦”而不是“滑”。原因在于KEYDOWN只在按键按下的一瞬间触发一次不会持续触发。要让移动平滑改用pygame.key.get_pressed()在每一帧里查询持续按下的键效果会好非常多。8. 从简单游戏出发的小项目迭代建议很多人第一次做完接苹果这个小项目会有一种“这就完了”的感觉。事实上这个项目已经足够承载很多扩展玩法。如果你还想继续打磨我建议按以下顺序迭代8.1 项目进阶方向加入音效背景音乐用pygame.mixer播放接住苹果时播放短促的音效。这是最容易体现出“游戏感”的一步。加入特殊道具比如接住金色苹果加两分接住黑色炸弹扣一条命需要在苹果类里增加类型字段并在碰撞时建立分支。加入粒子特效苹果被接住时出现一圈飞溅的小圆点这种简单粒子系统实现起来也不复杂。加入关卡难度分数达到阈值后自动进入下一关改变背景色和苹果下落速度。增加鼠标操控用鼠标位置控制篮子移动体验另一种交互方式。只需在update里多添加一行self.x pygame.mouse.get_pos()[0]。这些功能的本质都是对一个游戏循环中的某个环节做局部改动。有了这个底子再去做飞机大战、贪吃蛇、弹球游戏的代码时你会发现它们的内核都极其相似——主循环、对象更新、碰撞检测、状态切换换的只是表现层。8.2 用 Git 管理游戏项目哪怕是一个几百行的小项目我也建议从一开始就用git init管理代码。每功能完成一个版本就提交一次。这样做有两个直接好处一是改坏了能快速回退二是能逼自己把改动拆成小模块保持逻辑清晰。学编程不只是写出能跑的代码更是写出自己能维护、别人能看懂的代码。你完全可以从今天这份代码开始把接苹果升级成自己风格的版本——换素材、改规则、加功能。过程中遇到的问题大都可以回到这个篇内容里找到对应的模块去排查。我第一次用 Pygame 做出能玩的游戏时那种成就感至今还记得希望你也能在这次实践中找到属于你的那份。
阅读完成 · 觉得有帮助?
咨询建站