技术博客

大规模刚体性能实测:一千个盒子的帧率曲线

第 79 / 149 篇 · 2026-03-15 · godot · 物理引擎性能优化实测

物理引擎的选型讨论里最常见的两个问题是:"一千个刚体卡不卡"和"Jolt 比 Godot Physics 快多少"。这两个问题都没有脱离场景的答案——堆叠的盒子、散落的碎片、持续翻滚的球,三种负载压在引擎的不同部位上。这篇用一个可复现的压测场景把两个引擎跑了一遍,把帧率曲线拆开看,差异落在哪个环节就说得清了。

压测场景与测量方法

测试场景刻意做得无聊:$20 \times 20 \times 20 = 8000$ 个 0.5m 的盒子排成点阵,从 30m 高度自由落体到一块静态地面上。选择自由落体压测而不是持续搅拌,是因为它有明确的三个阶段:下落(几乎没有接触,纯 broadphase 和积分)、触地瞬间(接触对数量爆炸)、沉降(大部分盒子进入休眠,剩少数还在碰撞)。三个阶段分别压不同的子系统。

测量口径固定在物理步上,不看渲染帧率:physics_ticks_per_second = 60,用 Engine.get_physics_frames() 配合 Time.get_ticks_usec() 累计每步耗时,取落稳后 5 秒的滑动中位数。机器是 M 系 Mac,Godot 4.4-stable,两个引擎用同一份场景文件,只切换项目设置里的物理引擎选项。

三条曲线

阶段 Godot Physics Jolt 主要成本
自由下落(~2s) 2.1 ms/步 0.9 ms/步 积分 + broadphase 更新
触地瞬间(峰值) 14.6 ms/步 5.8 ms/步 接触生成 + solver 迭代
完全休眠后 0.4 ms/步 0.3 ms/步 休眠岛屿的惰性跳过

触地瞬间的峰值最值得展开。8000 个盒子触地不是同时的——先落地的盒子形成支撑层,后落地的砸在它们上面,接触对数量在约 0.5 秒内冲到峰值然后回落。两个引擎的峰值差了 2.5 倍,拆开来归因:

每步耗时随时间的变化:三个阶段与两个引擎(M 系 Mac,8000 盒自由落体) ms/步 0 8 12 16 Godot Physics 峰值 14.6ms Jolt 峰值 5.8ms ① 下落 ② 触地(接触爆炸) ③ 沉降(休眠接管) 差异来源:触地阶段的接触生成与 solver 迭代(Jolt JobSystem 并行),而非积分;休眠后两者都接近零

Broadphase 占比比直觉大。 下落阶段两个引擎都不碰任何碰撞对,纯做位置积分和 AABB 树更新,Godot Physics 已经落后一倍多。它的 broadphase 是基于哈希格的(hash grid,详见四叉树对比那篇),盒子均匀分布时格子的负载尚好,但格子的查询常数比树结构差;8000 个盒子的 AABB 更新加两两粗测就是 2ms 的量级。Jolt 的四叉树加 SIMD 宽相查询在这个阶段就显出优势。

触地峰值的差距来自并行,不来自算法 smarter。 接触对数峰值时,两个引擎的单线程求解器数学是同一族的(Sequential Impulse),单次迭代成本相当;但 Jolt 的 JobSystem 把不相连的 island 丢到多个工作线程上同时迭代,8 核机器上有效吞吐直接乘 4~6。Godot Physics 的 solver 是单线程的,这一步没有可比性。代价前面那篇(确定性)讲过:开了并行,逐比特回放就别想了。

休眠质量决定长尾。 沉降阶段两个引擎都掉到亚毫秒,这是休眠(sleeping)的功劳:速度低于阈值的 island 整体冻结,solver 直接跳过。注意 0.4ms 的残差——那是 broadphase 仍然在维护所有盒子的 AABB,以及每帧的休眠检测扫描。休眠的 island 不是免费的,只是打了折。

数字外的三个工程结论

第一,"一千个刚体卡不卡"的正确问法是"一千个刚体在什么接触状态下卡不卡"。8000 个盒子散在地上休眠时,Godot Physics 也只占 0.4ms;真正烧时间的是接触对数量,而不是 body 数量。估算负载时用接触对数 $p$ 而不是 body 数 $n$:solver 的单步成本大约是 $O(p \times \text{迭代次数})$,盒子堆这种 $p \approx O(n)$,而平铺地面这种 $p \approx O(n)$ 但 island 分解后大量并行。

第二,渲染帧率会被物理峰值打穿。触地那 0.5 秒里 Godot Physics 单步 14.6ms,物理已经吃掉 60fps 预算的 88%。游戏里这种"事件性物理峰值"(爆炸、建筑倒塌)比稳态负载更危险,缓解手段有两个:physics_ticks_per_second 临时降档(视觉上有损但避免螺旋死亡),或者直接把碎片类刚体交给 Jolt。

第三,别用 release_debug 模板跑基准。调试模板里的断言和日志会让 solver 慢 20% 上下,对比数据会被污染。两个引擎都要用 release 模板测,编辑器里看数只配用来找瓶颈归属,不配用来报绝对值。

实测数据来自 M 系 Mac 的一次采样,绝对值随机器浮动;阶段划分和两个引擎的相对差距在多次复跑中稳定。测试项目已简化到核心场景,需要的可以在评论里留言拿 .tscn。