简介:这款《完善制作的愤怒的小鸟Python小游戏》是针对Python初学者与游戏开发爱好者的一款完整实战项目。项目复刻经典《愤怒的小鸟》玩法,涵盖Tkinter图形界面、Canvas画布绘制、物理抛射轨迹模拟、Pillow图像处理、事件驱动交互等核心知识点,物理模拟部分涉及重力加速度、角度、初速度等计算,同时包含变量、类与对象、异常处理与调试技巧,可作为从语法学习过渡到综合项目实践的绝佳样例。压缩包共57个文件,包含32张PNG与5张JPG图片素材、9个WAV背景音效、4个.py源码文件、4个.pyc编译文件以及3个.md说明文档,总大小25.21MB。目前已有182人浏览学习。项目目录结构清晰,图片、音效、脚本分目录存放,便于对照学习。通过研究源码与文档,可掌握游戏主循环设计、碰撞检测、列表与字典数据存储等多层逻辑,并能直接运行、修改参数或二次开发,对理解Python游戏开发全流程很有帮助。
1. 完善制作的愤怒的小鸟Python小游戏:从能玩到玩得下去
网上搜“python小游戏”的代码,愤怒的小鸟类实现一大把,但绝大多数跑起来你会发现:鸟只能朝固定角度飞、撞到猪没有反应、木板和石块只是贴图、游戏结束只能关窗口重开。把它们一一补全,才是“完善制作”的真正工作量。这条路不依赖第三方物理引擎,只用 pygame 官方 API,覆盖蓄力发射、抛体运动、碰撞检测、关卡配置、计分重开与手感打磨。适合刚学完 Python 基础想完整做完一个项目的人,也适合拿到半成品代码不知从哪下手的人。后面的代码都能直接跑进一个最小骨架里,每个参数我会说明它影响什么、调不好时看哪里。
2. 用 pygame 搭出愤怒的小鸟最小可玩骨架:场景、精灵与主循环
2.1 先定架构:场景栈与精灵组的分工
一个完整的愤怒的小鸟小游戏,至少要包含开始菜单、游戏主场景、结算界面三个场景。常见做法是用一个场景栈来管理:栈顶场景负责接收事件和绘制,push 表示进入新场景,pop 表示返回上一场景。关卡结束、重开一关、回到菜单,都只是栈操作,不会把主循环写成一大串 if 分支。
精灵方面用 pygame.sprite.Group,但这里有个容易写坏的习惯:鸟、猪、方块各自建一个 group。碰撞检测是跨类别的,鸟要撞木块也要撞猪,分 group 会让检测逻辑重复写三遍。正确做法是全部放进同一个 group,用精灵的 type 属性区分身份。背景和 UI 不进精灵组,直接在场景的 draw 阶段绘制,这样血条、得分、蓄力指示器可以在所有精灵之上独立控制层级。
2.2 可复现的最小主循环代码
import pygame def run(scene_stack): pygame.init() screen = pygame.display.set_mode((1280, 720)) clock = pygame.time.Clock() while True: for event in pygame.event.get(): if event.type == pygame.QUIT: return scene_stack[-1].handle_event(event) scene_stack[-1].update() scene_stack[-1].draw(screen) pygame.display.flip() clock.tick(60)这段代码的核心是让事件处理、逻辑更新、绘制三件事分离。handle_event 只管输入,update 里做位移和碰撞,draw 只负责画。三者分开之后,后面加暂停、加快进、加慢动作回放都只需要改 update 的调度方式。clock.tick(60) 把帧率锁在 60 FPS,避免不同配置的机器上游戏速度不一样,这里是最基础的帧率控制,第五章还会讲到更严格的固定时间步。注意 scene_stack[-1] 始终是当前活跃场景,菜单和游戏场景互不感知,这正是场景栈带来的解耦。
2.3 精灵划分:鸟、猪、方块与场景背景
| 精灵 | 基类 | 关键属性 | 主要行为 |
|---|---|---|---|
| Bird | pygame.sprite.Sprite | pos, vel, rotation, state | 蓄力、发射、飞行、撞击后减速 |
| Pig | pygame.sprite.Sprite | pos, hp, radius | 被撞击掉血、死亡消失 |
| Block | pygame.sprite.Sprite | pos, size, material | 被撞击位移、掉血、销毁 |
material 是木块、石块、冰块区分的开关,写成字符串属性而不是建三个子类,是为了让第四章的关卡配置文件能直接写 "wood"、"stone",解析阶段不需要做类型分发。每个精灵类至少要重写 update 和 draw:update 里更新位置和旋转,draw 里先画矩形占位跑通逻辑,贴图最后再换——这是做小游戏最高效的路径。占位阶段就把碰撞体尺寸和绘制尺寸分开定义,后面换贴图时不会因为图片自带留白导致碰撞体偏大或偏小。
3. 抛体运动与碰撞检测:让鸟飞得像样、砸得准
3.1 手动积分 vs 物理引擎:为什么自己算
愤怒的小鸟本质是 2D 抛体运动加简单碰撞堆叠,用 Box2D、pymunk 当然能做,但自己算更可控。理由有三:一是这个游戏里刚体数量通常不超过 50 个,物理引擎的连续碰撞检测、关节、马达全用不上;二是引擎的密度、摩擦、恢复系数调起来比直接调重力系数和动量传递比更玄学,你很难跟玩家解释“为什么同样的力度这次木头没碎”;三是不引入依赖意味着源码拷到任何一台机器装上 pygame 就能跑,对分享代码的场景尤其友好。
手动积分的代价是碰撞响应要自己写,但 2D 圆与矩形的碰撞本身只有几行数学。真正需要留神的是两个问题:帧率波动导致的轨迹偏差,以及碰撞后物体嵌在一起不弹开。前者用第五章的固定时间步解决,后者用本章的“最小修正”解法。先把这两件事想清楚,物理部分就稳了。
3.2 鸟的抛物线轨迹:蓄力、发射与代码
发射前要解决两件事:方向和力度。常见做法是按住鼠标往后拖,拖拽向量取反就是发射方向,拖拽长度映射为初速度标量。这样玩家天然理解“往后拉、往前飞”,这也是愤怒的小鸟手感的核心来源。
def update(self, dt): # 鼠标按住期间实时更新蓄力向量 if self.state == "aim": mouse = pygame.mouse.get_pos() drag = pygame.Vector2(mouse) - self.press_pos self.aim = -drag self.power = min(drag.length() * 0.08, 15.0) # 松手瞬间发射 if self.power > 0 and not pygame.mouse.get_pressed()[0]: self.vel = self.aim.normalize() * self.power self.state = "fly" self.power = 0 def fly(self, dt): self.vel.y += 9.8 * dt * 50 # 重力,50 是像素缩放系数 self.pos += self.vel * dt self.rotation += self.vel.x * dt * 2参数说明:power 上限 15 是 1280x720 窗口下比较舒服的初速度,太小飞不到第一块木块,太大会直接穿屏到另一边。重力用 9.8 乘 50 是因为坐标单位是像素,直接用真实重力值在每帧位移几十像素的游戏里会显得轻飘飘,鸟飘在半空下不来。旋转由 vel.x 驱动,让鸟在上升时向后仰、下落时向前倾,比把角度锁死真实得多。注意 power 清零放进了同一帧判断里,避免松手后下一帧又检测到“未按住”重复发射一次。
3.3 圆形与 AABB 碰撞检测的选择
pygame 自带的 collide_circle 只能圆对圆,collide_rect 是矩形对矩形。这里有个容易踩的坑:鸟的贴图是方形,但碰撞体应该是圆。用矩形碰撞的话,鸟的四个角会“虚空碰物”,玩家会看到明显擦边却撞碎了木块。鸟和猪用圆形碰撞,方块用 AABB 矩形碰撞,跨类别检测时统一走圆对矩形的函数:
def circle_rect_hit(cx, cy, cr, rx, ry, rw, rh): # 找到矩形上离圆心最近的点,再算距离 px = max(rx, min(cx, rx + rw)) py = max(ry, min(cy, ry + rh)) dx, dy = cx - px, cy - py return dx * dx + dy * dy < cr * cr这个函数先把圆心坐标夹到矩形范围内,得到矩形上离圆心最近的点,然后算欧氏距离的平方与半径平方比较。用平方比较是为了省一次开方,在每帧几百对检测时能省下不少时间。它是 O(1) 的,性能完全不是问题。pygame.sprite.groupcollide 支持传自定义碰撞函数,所以这个检测可以无缝接进精灵组的碰撞逻辑里。
3.4 碰撞响应:冲量公式与最小修正
检测到碰撞后,常见做法是“最小修正加速度传递”。最小修正是把重叠对象沿法线推开,避免下一帧还卡在里面反复触发碰撞;速度传递模拟能量损失,让木块被撞飞、石块纹丝不动。
def resolve(block, bird): vn = bird.vel.length() if block.material == "wood": block.vel = bird.vel * 0.4 block.hp -= vn * 0.5 elif block.material == "stone": block.vel = bird.vel * 0.15 block.hp -= vn * 0.2 bird.vel *= 0.3 # 把 block 沿鸟速度方向推出重叠区域 if block.vel.length() > 0: block.pos += block.vel.normalize() * 20.4 和 0.15 是动量传递比,决定了木块被撞飞多远、石块基本原地不动。hp 扣血用速度大小而不是固定值,这样慢速擦碰不致命、高速直击一下碎。0.3 是鸟撞击后的保留速度,设成 0 的话鸟会像撞墙一样瞬间停住,观感非常生硬。最后一行最小修正只推 2 像素,配合下一帧的位移自然会分开;如果一次推太多,会把原本没碰到的相邻方块也挤开。这里没有用标准冲量公式,因为鸟的质量统一视为 1,木块和石块的质量通过动量传递比间接体现,反而更好调。
4. 完善制作的三件套:关卡文件、计分重开与轻量特效
4.1 用 JSON 关卡文件代替硬编码布防
把关卡数据从代码里拆出来,是“完善”和“demo”的分水岭。用 JSON 描述每个关卡里有什么方块、什么材质、猪放在哪,新增关卡只需加文件,完全不动代码。这也方便你做关卡编辑器,因为文件格式稳定之后,任何工具都能生成关卡。
{ "level": 1, "birds": 3, "blocks": [ {"type": "wood", "x": 500, "y": 610, "w": 20, "h": 120}, {"type": "stone", "x": 700, "y": 660, "w": 30, "h": 60} ], "pigs": [{"x": 560, "y": 580, "hp": 30}] }加载代码用 json.load 读取后逐项构造精灵实例就好。这里有个实际经验:先决定坐标锚点是左下角还是中心,并全文件统一。我一般用中心锚点,因为圆形碰撞体算距离方便,方块将来做旋转显示时也不会出现位置漂移。另外把 y 轴写成向下递增,与 pygame 的屏幕坐标一致,否则调试关卡时总差半个屏幕,还得做一次坐标翻转。方块支持被多段撞击,所以文件的 w 和 h 要限制成偶数,避免碰撞体因取整抖动。
4.2 计分、连击与剩余鸟数奖励
计分规则如果只有“打掉一只猪得 500 分”,玩两关就腻了。常见做法是把得分拆成三份:猪的基础分、木块碎掉的建筑分、剩余鸟数的奖励分。连击做成“按时间窗口内连续撞碎物体的数量递增倍率”,能鼓励玩家追求一次撞击解决一片。
def on_block_destroyed(self, block): now = pygame.time.get_ticks() if now - self.last_combo_time < 1500: self.combo += 1 else: self.combo = 1 self.last_combo_time = now score = block.score * self.combo self.total_score += score show_floating_text(block.pos, "+" + str(score))combo 的实现逻辑是:两次得分事件间隔不超过 1.5 秒就递增连击数,否则重置为 1。1500 毫秒这个窗口要结合鸟的飞行速度来调:太短会导致一次撞击还没结束连击就断了;太长又变成慢慢打也能拿高倍率。一次撞击触发多块碎裂通常发生在几十毫秒内,所以 1.2 到 1.8 秒都是合理区间。浮动文字(+500 飘起来再淡出)是成本最低的正反馈,比音效还直观,实现上就是一组带速度和生命周期的文本对象。
4.3 状态机:准备态、飞行态、结算态
游戏主场景最少要有四个状态:waiting 等待发射、flying 飞行中、settling 等待碰撞结束、result 显示结算。不加状态机的典型后果是:发射之后玩家还能拖拽蓄力、鸟刚落地就立刻判定关卡失败,这些正是网上大量 python 小游戏 代码里最常见的 bug。
class PlayScene: STATE_WAIT, STATE_FLY, STATE_SETTLE, STATE_RESULT = range(4) def update(self, dt): if self.state == self.STATE_WAIT: self.handle_aim() elif self.state == self.STATE_FLY: self.bird.update(dt) self.check_hits() if self.is_bird_done(): self.state = self.STATE_SETTLE elif self.state == self.STATE_SETTLE: if self.all_still(): if self.pigs_alive() == 0: self.state = self.STATE_RESULT elif self.birds_left > 0: self.spawn_bird() self.state = self.STATE_WAIT else: self.state = self.STATE_RESULTall_still() 的判断方式是遍历所有方块和猪,检查速度都小于阈值(比如 0.5)并持续若干帧。注意不要只测速度为零,浮点积分下速度很难精确归零,而且刚碰撞完有短暂回弹。加一个“连续 30 帧都静止”的计数最稳妥。每个状态转移都要能被人工打断,比如结算界面按空格直接下一关,否则玩家会觉得游戏卡死。结算时通关和失败是同一状态,只是用 pigs_alive() 的结果决定显示文案和奖励倍数。
4.4 不加依赖的音效与粒子反馈
音效和粒子不需要额外库。粒子的常见做法是一组小 Surface 矩形,在方块碎掉时生成 6 到 12 个碎片,每个碎片有独立速度、角速度和生命周期。音效用 pygame.mixer.Sound,注意转成 wav 或 ogg 格式,采样率 44100,否则部分机器上播放会爆音或延迟。
最容易踩的坑是粒子误入主精灵组:碰撞检测会对粒子进行判定,瞬间多出几百个碰撞体,逻辑就乱了。给粒子单独建一个列表,只更新位置和绘制,不参与任何碰撞。碎片数量太少没有破碎感,太多在低端机器上掉帧,每种材质控制在 8 个左右、碎片 4 到 8 像素、生命周期 0.3 到 0.6 秒,是我试过手感最稳的组合。音效和粒子都遵循同一个原则:先用最小实现让玩家有反馈,再谈效果丰富度。
5. 打磨细节:固定时间步、坐标适配与发布前自检
5.1 用固定时间步让物理结果可复现
acc = 0 step = 1 / 60 while running: acc += clock.tick(60) / 1000 while acc >= step: scene.update(step) acc -= step固定时间步的核心好处是物理可复现:同样的输入必然得到同样的轨迹,这对回放 bug、测试关卡平衡性极其有用。step 取 1/60 秒,渲染帧率可以更高也可以更低,但物理更新永远以固定节奏走。注意累加器要归零,否则游戏时间会越走越快,典型表现是十几分钟后鸟飞得明显变快。
5.2 世界坐标与屏幕坐标分离
所有物理和碰撞都跑在一个固定的世界坐标系里,比如 800x600。窗口大小变化时,在 draw 阶段做变换:screen_pos = world_pos * scale + offset。这样切到 1024x768 或全屏时,物理行为完全不变,只是画面缩放。鼠标位置要反向转换回世界坐标,否则点击位置和蓄力方向会随窗口大小漂移,这一步漏掉的话,玩家换窗口模式后会发现瞄不准。
5.3 发布前自检:从“能跑”到“能玩”
可以固定一套自检清单:连续快速点击发射,看会不会出现两只鸟;在关卡里塞入 100 个方块,看是否掉帧,掉帧就检查有没有对已移除精灵重复调用 update;关掉音效、缩小窗口各跑一遍全流程;最后对照蓄力、碰撞、计分、重开、通关五个环节逐项确认。如果你只带走一个技巧,那就是给状态机加一行转移日志:每次 state 变化时打印新状态、帧号和触发事件。发布前最后一次自检时,那些只在特定操作顺序下出现的 bug,都靠这行日志定位到具体是在哪一次点击、哪一帧发生的。
本文还有配套的精品资源,点击获取