ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用Pygame完善愤怒的小鸟:物理碰撞与关卡设计实战

用Pygame完善愤怒的小鸟:物理碰撞与关卡设计实战 简介这款《完善制作的愤怒的小鸟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 精灵划分鸟、猪、方块与场景背景精灵基类关键属性主要行为Birdpygame.sprite.Spritepos, vel, rotation, state蓄力、发射、飞行、撞击后减速Pigpygame.sprite.Spritepos, hp, radius被撞击掉血、死亡消失Blockpygame.sprite.Spritepos, size, material被撞击位移、掉血、销毁material 是木块、石块、冰块区分的开关写成字符串属性而不是建三个子类是为了让第四章的关卡配置文件能直接写 wood、stone解析阶段不需要做类型分发。每个精灵类至少要重写 update 和 drawupdate 里更新位置和旋转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都靠这行日志定位到具体是在哪一次点击、哪一帧发生的。本文还有配套的精品资源点击获取
返回列表