news 2026/8/30 9:34:21

用Python从零实现愤怒的小鸟:物理模拟与碰撞检测全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python从零实现愤怒的小鸟:物理模拟与碰撞检测全解析

简介:本资源是基于Python实现的经典物理弹射类游戏《愤怒的小鸟》完整开源项目,面向具备基础Python编程能力的学习者,用于深入理解游戏开发中的物理模拟、碰撞检测、图形渲染与面向对象设计等核心实践。压缩包共77个文件,包含39张PNG游戏素材(角色、场景、UI元素)、12个.zbak备份源码、9个WAV音效文件(如发射、通关、失败音效)、5个JPG/JPEG背景图及4个核心Python模块(main.py、characters.py、polygon.py、level.py),辅以README.md和流程图等说明文档,总大小26.38MB。已有34人下载学习,项目结构清晰、注释详尽,涵盖pygame坐标系应用、slingshot弹射逻辑、重力与刚体碰撞建模、关卡状态管理等关键实现细节,可直接运行调试并支持二次开发与功能拓展。 我去年花了两周时间,用Python把《愤怒的小鸟》的核心玩法从零搓了出来。不是拿现成引擎套模板,而是从物理模拟、碰撞检测到弹弓交互一点点写,最终源码可以正常跑完整个关卡流程,也能存档、计分、切换关卡。这篇文章就把这套源码实现的完整思路、关键代码和调试过程拆开讲清楚。如果你正想用Python写一个2D小游戏,或者对游戏中的物理模拟和碰撞检测感兴趣,这篇内容应该能帮你少走不少弯路。

先说明一下,这套源码完全是个人项目,没有依赖Pygame之外的重型库,所以你在自己电脑上安装Python和Pygame就能跑起来。后面的内容会从核心玩法拆解一直讲到对象池优化,顺序基本就是我当时实现的顺序,很多地方带着血泪教训,希望对你有用。

1. 从弹弓到抛物线:先把核心玩法拆清楚

1.1 所谓"愤怒的小鸟",本质上是个二维弹道游戏

愤怒的小鸟看起来花里胡哨,拆掉美术和音效之后,核心就是一个二维弹道游戏:你拖住小鸟,松手给它一个初速度,小鸟被发射出去,在重力作用下沿抛物线飞行,撞击由木块、石头、冰块搭起来的建筑,把建筑撞塌、把猪砸到,就算赢。

这句话听起来简单,但真正实现时会发现,要做出"手感"并不容易。所有玩家能感知到的反馈,都来自物理模拟和碰撞检测的精度。早期的《愤怒的小鸟》之所以火爆,就是因为它把"抛射+碰撞破坏"这套手感做得极其细腻。我们用Python复刻,不可能达到原版那种像素级物理的精细度,但至少要让抛物线的轨迹看着自然,碰撞命中判定不出错。

这里用一个生活化类比:你站在篮球场上投篮,篮球离手后只受重力和空气阻力影响,轨迹是一条抛物线的变形。游戏里的鸟也一样,只是它离手后可能还会撞到各种障碍物,撞到以后速度方向和大小都会发生变化。理解了这一点,就知道下一步该做什么:先计算鸟在每一帧的位置,再检测它是否碰到障碍物,碰到后更新速度。

1.2 技术选型:为什么用Pygame而不是Unity或Godot

最开始我其实犹豫过要不要用Unity,毕竟现成的物理引擎(Box2D、PhysX)能省很多事。但想到这是一个Python项目,目标是源码可读、可二次修改、用来学习物理模拟原理,那Pygame就是更合适的选择。

Pygame本身不提供物理引擎,只提供窗口渲染、事件处理、图片加载这些基础能力,所以重力、碰撞、反弹全都得自己写。这听起来麻烦,但恰恰是它最大的好处:你被迫理解游戏物理的实现细节,而不是拖一个Rigidbody组件就完事。

做个简单对比:

方案物理引擎上手难度源码可读性适用场景
Unity + C#自带Box2D/PhysX中等低,被引擎封装商业游戏、3D项目
Godot + GDScript自带PhysicsBody中等中等独立2D/3D游戏
Pygame + Python自己实现极高学习原理、快速原型、毕设项目
Arcade/Pymunk集成Chipmunk中等需要物理但不求源码学习

如果你只是想做一款完整的游戏、不在乎底层原理,Pymunk等库会更省力。但我是冲着源码实现来的,所以选择纯手工物理。事实证明,这个选择让后面所有调试都变得更加可控。

1.3 源码目录结构:一个自底向上的模块划分

写这种项目最忌把所有代码塞进一个main.py。两三天后你自己都会找不到哪里改重力参数。我最终采用的目录结构很简单,但对单人项目来说足够清晰:

angry-birds/ ├── main.py # 程序入口,只负责启动和主循环 ├── settings.py # 全局配置项:屏幕尺寸、重力、帧率等 ├── scene/ │ ├── __init__.py │ ├── menu_scene.py # 菜单场景 │ ├── game_scene.py # 游戏主场景,处理输入和更新 │ └── level_scene.py # 关卡加载和结算 ├── entities/ │ ├── __init__.py │ ├── bird.py # 小鸟实体,包含飞行状态 │ ├── pig.py # 猪实体,被撞到会扣除生命 │ └── block.py # 木块/石头/冰块,可被推动和摧毁 ├── physics/ │ ├── __init__.py │ ├── vector.py # 二维向量运算 │ ├── collision.py # 圆形/矩形碰撞检测 │ └── world.py # 游戏世界:维护所有对象和更新顺序 ├── ui/ │ ├── __init__.py │ ├── hud.py # 计分、剩余小鸟数量显示 │ └── button.py # 菜单按钮 ├── data/ │ ├── levels/ │ │ ├── level1.json │ │ └── level2.json │ ├── images/ │ └── sounds/ └── utils/ ├── __init__.py └── assets.py # 资源加载缓存,避免重复读文件

我为什么要把物理模块单独拆出来?因为小鸟和木块都要用同一套运动规律,如果每个实体各写各的,最后改重力参数时要改好几个文件,坑死自己。统一用physics.world来管理所有实体的更新顺序,会让整个循环非常清晰:每帧调用一次world.update(dt),内部遍历所有实体,依次调用它们的apply_forceupdate

2. 物理模拟与碰撞检测:这俩是决定手感的关键

2.1 用欧拉法做运动积分:代码就这几行

物理模拟最基础的方法是欧拉法,也叫前向欧拉积分。它的思想很简单:把时间切成很多小片段,每个片段里假设速度和加速度恒定不变,然后累加位置和速度。

在2D平面里,小鸟的受力情况是重力向下,初速度由弹弓松手时决定。位置更新公式为:

vx += ax * dt vy += ay * dt x += vx * dt y += vy * dt

这里(ax, ay)是加速度,水平方向通常为0(先忽略空气阻力),垂直方向就是重力加速度g。代码如下:

class Bird: def __init__(self, x, y, vx, vy): self.x = x self.y = y self.vx = vx self.vy = vy self.radius = 15 self.alive = True def update(self, dt, gravity): self.vy += gravity * dt self.x += self.vx * dt self.y += self.vy * dt

如果你测试这段代码,会发现轨迹非常接近真实抛物线,但会有一个问题:如果dt太大,也就是每一帧间隔时间太短导致更新次数太少,那么小鸟在高速运动时可能一帧跨过好几个像素,碰撞检测就会出现穿透。解决方式是固定时间步长,这个问题在第4章会展开。

这里要特别说明:欧拉法在长时间模拟下会有能量误差,导致抛物线高度比理论值略低一点。对于游戏来说,这种误差肉眼几乎看不出来,而且容易调,所以我直接用欧拉法。如果你希望更精确,可以换用Verlet积分,但代码复杂度会高不少,性价比并不高。

2.2 圆形碰撞与矩形碰撞的实际取舍

愤怒的小鸟里的鸟、猪、木块,形状其实都不规则,但如果完全按像素级碰撞来检测,每一帧处理几百个对象的像素掩码,性能会非常难看。实际开发中,我采用的方案是:所有对象都有逻辑碰撞体,小鸟和猪用圆形,木块用矩形。

圆形和矩形的碰撞检测公式网上很多,最好用的是"圆心到矩形最近点距离小于半径则碰撞"。核心代码:

def circle_rect_collision(cx, cy, cr, rect): # 找到矩形上离圆心最近的点 closest_x = max(rect.left, min(cx, rect.right)) closest_y = max(rect.top, min(cy, rect.bottom)) dx = cx - closest_x dy = cy - closest_y return dx * dx + dy * dy <= cr * cr

这里rect是Pygame的Rect对象,left/right/top/bottom分别代表矩形的四边坐标。这个检测方法比想象中快很多,因为只需要几次比较和一次平方运算。

圆形之间碰撞就更简单了:两个圆心距离的平方小于半径和平方,即碰撞。

def circle_circle_collision(b1, b2): dx = b1.x - b2.x dy = b1.y - b2.y dist_sq = dx * dx + dy * dy r_sum = b1.radius + b2.radius return dist_sq <= r_sum * r_sum

为什么不直接用矩形作为小鸟的碰撞体?因为旋转后矩形碰撞检测会很麻烦,而圆形没有方向性问题。木块我一般默认不旋转,直接用矩形就能满足大部分场景。这里引出一个经验:物理碰撞的形状不需要和美术视觉完全一致,但要保证"视觉上看起来撞到了"和"逻辑上判定撞到了"相差不大。

2.3 亲身踩坑:为什么小鸟穿透了木块却判定碰撞成功

这块必须拿出来单独说,因为我在测试时遇到过非常诡异的情况:小鸟明明飞到了木块上方,屏幕上都看不出接触,但方块已经碎了。一开始我还以为是自己碰撞算法写错了,白白排查了三天。

后来偶然发现,问题出在pygame.Rect的坐标体系和碰撞半径的设置不一致。我的小鸟贴图是64x64像素,但背景透明,实际鸟的身体只占中间圆形部分,半径大概20像素左右。我设置bird.radius = 20,木块矩形也按贴图尺寸直接取整,看起来没问题。但我的图片素材中,小鸟绘制的中心点并没有对齐图片中心,偏右下方了大约8个像素,这个偏差平时看不出,但在高速运动下就会让视觉位置和物理位置明显分离。

最后解决方案很简单:所有实体在加载图片时,统一把原点设置到图片几何中心,并且用调试模式绘制碰撞体。我写了一个DEBUG_DRAW开关,打开后会在每个对象周围画一个绿色圆圈或矩形。持续开着几局就能发现偏移。

if settings.DEBUG_DRAW: pygame.draw.circle(screen, (0, 255, 0), (int(camera_x), int(camera_y)), int(obj.radius), 2)

所以,如果你也在做相似项目,记得第一件事就是开启碰撞体可视化,不要靠肉眼判断。视觉上"贴着边"不等于物理上"碰撞了",这个误差在高速物体上会被放大得非常明显。

3. 弹弓交互与视觉跟随:把"手感"做出来才叫游戏

3.1 拖拽发射的状态机:蓄力、瞄准、释放

核心玩法是拖拽弹弓把鸟射出去。这需要一个状态机来管理当前时刻玩家的操作是否有效。我定义了三态:

  • READY:小鸟待在弹弓上,等待鼠标按下。
  • DRAGGING:鼠标已经按下并拖拽,计算拉力方向和大小。
  • RELEASED:鼠标松开,小鸟按初速度飞出去,之后不再响应鼠标拖拽。

代码结构可以这样组织:

class Slingshot: def __init__(self, pos): self.pos = pos self.dragging = False self.drag_offset = (0, 0) self.max_power = 800 # 最大初速度像素/秒 self.state = "READY" def handle_event(self, event): if event.type == pygame.MOUSEBUTTONDOWN: if self.state == "READY" and self.pos.distance_to(event.pos) < 40: self.dragging = True self.state = "DRAGGING" elif event.type == pygame.MOUSEMOTION: if self.dragging: dx = event.pos[0] - self.pos[0] dy = event.pos[1] - self.pos[1] # 限制最大拖拽距离,防止飞出去 dist = math.hypot(dx, dy) max_dist = 60 if dist > max_dist: dx = dx / dist * max_dist dy = dy / dist * max_dist self.drag_offset = (dx, dy) elif event.type == pygame.MOUSEBUTTONUP: if self.dragging: self.release() def release(self): dx, dy = self.drag_offset # 注意:松手时速度方向是拖拽方向的反方向 self.bird.vx = -dx * 10 self.bird.vy = -dy * 10 self.state = "RELEASED"

注意这里有一个物理直觉:弹弓向后拉,松手后物体向前飞,所以速度方向是拖拽偏移的反方向。我记得第一次实现时反了,鸟朝后射,测试了半小时才意识到。这个太容易犯,大家留意一下。

蓄力大小和拖拽距离的比例也是一个手感参数。我设成初速度 = 拖拽距离 * 系数,然后限制最大拖拽距离,这样玩家不会一拉就飞出屏幕外。max_power我设成800,太大会导致游戏难度下降,太小则飞不过第一道墙。这个参数需要反复试,建议放到settings.py里方便改。

3.2 摄像机跟随与视差背景:场景大于屏幕时怎么处理

游戏场景通常比窗口大,尤其是关卡中需要放很多木块和猪。要显示超出屏幕的部分,就需要摄像机偏移。实现思路很简单:所有绘制对象时,把世界坐标减去摄像机偏移量,得到屏幕坐标。

class Camera: def __init__(self, viewport_width, viewport_height): self.offset_x = 0 self.offset_y = 0 self.viewport_w = viewport_width self.viewport_h = viewport_height def follow(self, target_x, target_y, world_width, world_height): # 让目标处于屏幕中心附近 desired_x = target_x - self.viewport_w // 2 desired_y = target_y - self.viewport_h // 2 desired_x = max(0, min(desired_x, world_width - self.viewport_w)) desired_y = max(0, min(desired_y, world_height - self.viewport_h)) self.offset_x = desired_x self.offset_y = desired_y def apply(self, world_x, world_y): return world_x - self.offset_x, world_y - self.offset_y

这样做的效果是,小鸟飞行时镜头会跟着移动,玩家始终能看到鸟。如果不加边界限制,镜头会滑出关卡边界,背景出现黑色空隙,很难看。

视差背景是在摄像机基础上加一个缩放系数形成的"虚拟深度"效果。比如天空背景移动速度只有主场景的0.2倍,山丘是0.5倍,前景是1倍。绘制时用不同的偏移量:

def draw_background(screen, camera, bg_image, factor): bg_x = -camera.offset_x * factor screen.blit(bg_image, (bg_x, 0))

这个实现很便宜,但视觉提升非常明显。我建议就算用简单的色块渐变,也一定要加分层的视差滚动,否则会感觉场景很死。

3.3 坐标转换翻车记录:鼠标点不到目标物的真实原因

这个坑困扰了我一个下午。现象是:当我拖拽小鸟后,鼠标箭头明明就在小鸟旁边,但松手后发射角度总是偏得离谱。我一度以为是事件处理的问题,后来才发现是鼠标坐标和世界坐标混用了。

Pygame的mouse.get_pos()返回的是窗口屏幕坐标,而我的弹弓位置是存储在世界坐标里的。当摄像机偏移不为零时,屏幕坐标到世界坐标需要加上偏移量:

mouse_world_x = mouse_screen_x + camera.offset_x mouse_world_y = mouse_screen_y + camera.offset_y

这个转换如果不做,就会出现在关卡左侧时,玩家实际要向后拉,但程序认为鼠标在右侧,发射方向完全错误。发现后我在Slingshot.handle_event里统一把所有鼠标坐标都转换成世界坐标再处理,问题立刻消失。

所以,做这类游戏时,一定要区分两个坐标系。输入事件拿到的坐标永远是屏幕坐标,所有和游戏物体位置比较的逻辑都应该先转换到世界坐标。我甚至在代码里刻意用screen_to_world函数封装转换,避免后续再来回改。

4. 帧率、资源与对象池:源码能跑是一回事,跑得顺是另一回事

4.1 用固定时间步长稳定物理表现,而不是让物理跟随FPS漂

Pygame的主循环如果直接用clock.tick(60)控制帧率,然后用每帧的真实时间dt去更新物理,在性能波动时会出现物理变化不一致的问题。帧率高的机器上小鸟飞得远,帧率低的机器上飞得近,这显然不合理。

正确的做法是使用固定时间步长累加器。维护一个accumulator,每帧累加真实经过的时间,只有当累加值超过固定步长时,才更新一次物理。这样无论帧率是30还是120,物理更新的频率都保持一致。

FIXED_DT = 1 / 120 # 物理更新频率120Hz accumulator = 0.0 clock = pygame.time.Clock() while running: frame_time = clock.tick(60) / 1000.0 # 秒 accumulator += frame_time while accumulator >= FIXED_DT: world.update(FIXED_DT) accumulator -= FIXED_DT

这里我把物理更新频率设成了120Hz,而不是60Hz。原因是60Hz下,高速物体每帧移动距离大,碰撞检测容易发生穿透。提升到120Hz后,碰撞检测的漏判率大幅下降,性能开销也完全可以接受。

4.2 减少Surface创建:绘制原语与图片缓存的取舍

Pygame中每创建一个Surface都是开销,尤其是在主循环里创建临时Surface会导致明显的卡顿。我最初写代码时为了让小鸟有拖影效果,每帧都创建半透明Surface,结果帧率直接降到30以下。

优化思路很简单:把不变的资源提前加载并缓存,不要在循环里创建。

class AssetManager: def __init__(self): self._cache = {} def load_image(self, path): if path not in self._cache: self._cache[path] = pygame.image.load(path).convert_alpha() return self._cache[path]

另外,Pygame的convert_alpha()非常好用,它会把图片转换成更适合当前显示模式的内存格式,绘制速度会快不少。如果你在某个界面发现图片加载变慢,大概率是没做转换。

还有一个小技巧:在很多游戏中,半透明效果可以通过预先生成的Surface反复blit,而不是每帧新建。比如弹弓的橡皮筋线条,可以直接用pygame.draw.line绘制,不需要额外Surface。我写的时候尽量用基本绘制原语,减少Alpha混合的Surface数量。

4.3 对象池在弹丸和碎块中的应用

当木块被小鸟撞碎时,会产生很多小碎块和粒子效果。如果每次碰撞都创建新的对象,之后又马上销毁,Python的垃圾回收会被频繁触发,造成偶发卡顿。一个小型项目可能不明显,但如果你把碎块数量调高到50个以上,就会感觉到帧率抖动。

我的做法是实现一个简单的对象池:

class ObjectPool: def __init__(self, obj_class, initial_size=20): self.obj_class = obj_class self.pool = [obj_class() for _ in range(initial_size)] self.active = [] def spawn(self, *args): if self.pool: obj = self.pool.pop() else: obj = self.obj_class() obj.reset(*args) self.active.append(obj) return obj def recycle_all(self): self.pool.extend(self.active) self.active.clear()

每次关卡开始时调用recycle_all(),把上一关所有对象回收。这种方式虽然简单,但能大幅降低频繁创建对象的开销。对于小鸟本体我没有用对象池,因为一次最多几只,但碎块和特效非常适合。

5. 关卡、计分与音效:补齐游戏体验的最后一公里

5.1 用JSON定义关卡,不用每个关卡写死代码

如果每个关卡都在代码里硬编码木块坐标,关卡设计会变成一场灾难。我用JSON文件定义关卡数据,程序启动时加载对应文件。

{ "name": "Level 1", "slingshot_pos": [120, 480], "birds": [ {"type": "red", "pos": [100, 470]}, {"type": "yellow", "pos": [150, 470]} ], "blocks": [ {"type": "wood", "x": 700, "y": 420, "angle": 0}, {"type": "wood", "x": 720, "y": 360, "angle": 90}, {"type": "stone", "x": 680, "y": 300, "angle": 0} ], "pigs": [ {"type": "small", "x": 710, "y": 450} ] }

解析时我写了一个LevelLoader,把JSON里的type映射到实体类。这样关卡策划(哪怕是自己)只需要改JSON,不需要碰代码。如果你后续想加新关卡,直接复制一个JSON,改坐标就行。

有一点要注意:JSON里没有注释,层级较深时容易看花眼。我会在命名上用slingshot_pos这种清晰字段,并在settings.py里留下每个字段的说明注释。

5.2 计分规则与界面刷新,以及UI和场景生命周期划分

计分规则我设计得很简单:砸中一只猪得500分,撞碎一个木块得100分,石块200分,小鸟停留在场上且关卡通关时有200分奖励。这个规则影响了结算界面和HUD的显示。

HUD使用独立于游戏世界的绘制函数,在每帧最后绘制:

def draw_hud(screen, score, birds_left): font = pygame.font.SysFont("Arial", 24) score_text = font.render(f"SCORE: {score}", True, (255, 255, 255)) birds_text = font.render(f"BIRDS: {birds_left}", True, (255, 255, 255)) screen.blit(score_text, (20, 20)) screen.blit(birds_text, (20, 60))

一开始我把HUD绘制放在世界绘制之前,结果背景图片把文字盖住了。后来调整了绘制顺序:先世界、再UI、最后闪屏特效。这是一个很小的坑,但如果你没注意顺序,UI永远显示不出来。

另外,要严格区分"游戏世界对象"和"场景管理对象"。游戏世界中不应该有menu_scene这种东西的存在,它只负责物理和实体。UI场景、菜单场景、结算场景是游戏状态机,它们控制世界是否更新。比如在结算弹出时,世界应该暂停更新,否则小鸟还在继续飞,用户没法看结果。我实现了一个简单的GameState枚举:

class GameState(Enum): MENU = 1 PLAYING = 2 LEVEL_COMPLETE = 3 GAME_OVER = 4

主循环里根据状态决定是否调用world.update()

5.3 音效资源处理和缺失时的防御代码

音效是让游戏"有感觉"的重要环节,但资源路径经常缺失,处理不好程序直接崩溃。我在AssetManager里加了一个防御性补充:如果音效文件不存在,仍返回一个空对象,但记录日志,不影响主循环运行。

def load_sound(self, path): if path not in self._sound_cache: try: self._sound_cache[path] = pygame.mixer.Sound(path) except pygame.error: self._sound_cache[path] = None print(f"Warning: sound not loaded {path}") return self._sound_cache[path]

后面在播放时,先判断是否为空:

snd = assets.load_sound("data/sounds/launch.wav") if snd: snd.play()

这样即使你换了一台机器没有声音文件,游戏也不会崩,只会静音。这个做法强烈建议复制,因为网上不少源码项目一换路径就闪退,体验很差。

6. 从运行到二次开发:环境准备与扩展建议

6.1 环境准备:Python版本、依赖安装与虚拟环境

运行这套源码对Python版本要求不高,3.8以上都可以,我最近在Python 3.11下跑也没有问题。Pygame推荐安装2.x版本,不要装老旧的1.9.x。

建议为项目创建虚拟环境,避免污染全局Python环境:

python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate pip install pygame

如果你用的是Anaconda,也可以:

conda create -n angry-birds python=3.10 conda activate angry-birds pip install pygame

安装完成后,运行python main.py即可启动。如果窗口没有出现,可能是环境变量或者Pygame初始化问题,可以检查main.py里是否调用了pygame.init()pygame.display.set_mode()

6.2 高频启动报错排查:模块找不到、窗口闪退、图片路径问题

我整理了几个自己遇到过的启动问题,方便你排查。

症状可能原因解决方案
ModuleNotFoundError: No module named 'pygame'没有安装Pygame,或安装在别的Python环境pip install pygame并检查当前虚拟环境
窗口闪退,没有报错图片路径错误但在try/except中被吞掉检查AssetManager.load_image的路径,缺失时打印完整路径
窗口黑屏主循环中没有screen.fill()或没有pygame.display.flip()在绘制前填充背景色,绘制后调用flip()
运行后CPU占用高主循环没有限制帧率调用clock.tick(60)
游戏物理速度在高低配机器上不一致没有用固定时间步长参考4.1节的累加器写法

其中最烦的是图片路径问题。我建议所有资源路径都基于项目根目录的相对路径,不要使用绝对路径。我一开始在Windows上写死C:/Users/...,换到Mac上就崩了。后来改成:

BASE_DIR = Path(__file__).resolve().parent.parent ASSETS_DIR = BASE_DIR / "data" / "images"

这样就完全跨平台了。

6.3 想让源码变成真正好玩的游戏,可以从这些参数下手

源码能跑之后,你可能会觉得手感还是不够好。这时候不要急着改结构,先去调参数。重点看settings.py中的这几个值:

  • GRAVITY:默认我设为980,单位是像素/秒^2。调高会让鸟飞得更"坠手",调低则更像太空漫步。
  • SLINGSHOT_POWER:默认800,影响最大初速度。调大会让鸟更容易飞过整个关卡,调小则更考验角度。
  • RESTITUTION:碰撞后的反弹系数,我设在0.3到0.6之间。调太高物体会像弹力球一样弹来弹去,太低则会显得很生硬。
  • FIXED_DT:物理更新的时间步长。如果发现高速物体穿透,可以把FIXED_DT调小,但代价是CPU占用上升。

如果你想让游戏更多样化,可以从这几个方向扩展:多只不同鸟(红鸟直线、黄鸟加速、炸弹鸟爆炸)、猪的AI躲避、风力系统、甚至联机对战。这些扩展都不需要推翻现有框架,只需在实体基类上增加新的行为方法即可。

我个人在实际操作中的体会是,这种"从零手写物理"的项目,最值钱的不是最终能玩的Demo,而是调试过程中对各种坐标、帧率、碰撞概念的理解。尤其当你把物理步长、对象池、资源缓存这些细节逐一解决后,你会发现很多看似复杂的游戏机制,底层其实就是这些基础模块的组合。如果你也想用Python写一个小游戏,千万不要急着找现成引擎,像我这样一步步从弹弓写到碰撞,做完之后对游戏开发的理解会完全不同。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 9:31:11

AI决策审计服务:构建可观测、可审核、可回滚的系统设计

AI 的能力边界在快速刷新&#xff0c;但比模型能力更值得讨论的&#xff0c;是使用 AI 的人如何在不确定中做出判断。历史学家尤瓦尔赫拉利在关于 AI、人类愚蠢与文明未来的访谈中反复提醒一件事&#xff1a;AI 是历史上第一个能够自主做出决策&#xff0c;并独立生成观点、作品…

作者头像 李华
网站建设 2026/8/30 9:28:08

组件库日常巡检的关键检查项

组件库日常巡检的关键检查项组件库的问题很少只停留在组件库里。一个属性类型变化、全局样式泄漏或错误的导出方式&#xff0c;都会传到许多业务应用。日常巡检的价值&#xff0c;是在发布前发现这些影响&#xff0c;并告诉维护者具体变了什么&#xff0c;而不是等业务团队升级…

作者头像 李华
网站建设 2026/8/30 9:25:13

OpenClaw 智能体框架实战:从部署配置到 Skill 开发与排错

最近 OpenClaw 维护者圆桌视频上线后&#xff0c;社区里关于这个开源智能体框架的讨论明显多了起来。从安装部署、模型配置&#xff0c;到接入微信、飞书、钉钉&#xff0c;再到 Skill 开发和 Active Memory 长期记忆&#xff0c;网上能搜到的资料不少&#xff0c;但大多比较零…

作者头像 李华