仓库里的木箱
阿诚正在做一个仓库解谜 Demo。场景中央堆着十几只木箱,玩家可以推箱子、拉货架,也可以从二楼把一袋水泥推下去。他在 UE4 时代就做过类似的玩法,当时底层是 PhysX:箱子叠到四层以上会开始高频抖动,快速抛出的物体偶尔穿透地面,睡着的刚体被碰撞唤醒时还会抽搐一下。升级到 UE5.8 之后,物理核心换成了 Chaos。阿诚发现同样数量的箱子稳定了许多,破坏系统也更容易调试,但一些旧项目的调参经验不再完全适用。这篇文章从这次仓库搬箱子的经历出发,把 Chaos 里最常见的几个基础概念串一遍。不打算罗列每一个参数,而是把阿诚踩过的坑和对应的原理讲清楚,让读者能根据自己的场景做判断。
Chaos 与旧 PhysX 的差异
PhysX 是 NVIDIA 的闭源物理引擎,Epic 在 UE4 中把它深度集成到 FPhysScene 与 UBodyInstance 里。开发者大部分时候只接触蓝图节点和碰撞预设,黑盒内部却很难干预。Chaos 是 Epic 自研的求解器,从 UE5 开始全面替代 PhysX。两者最直观的区别并不是 API,而是所有权结构:Chaos 把几何、刚体、约束、求解器都放进了引擎自己的模块,调试、扩展和定制都更直接。
flowchart TD
subgraph engine ["Unreal Engine 5.8"]
game["Game Thread"]
tick["Physics Tick"]
chaos["Chaos Solver"]
broad["Broadphase"]
narrow["Narrowphase"]
end
game --> tick
tick --> chaos
chaos --> broad
broad --> narrow上面的示意图只画到碰撞阶段。真实管线还会经过约束求解、积分、同步渲染位置等步骤。PhysX 时代这些步骤在第三方库内部完成;Chaos 时代 Epic 把管线拆得更开,方便后续做 Determinism、Replay、网络同步和破坏系统。阿诚的实际感受是:同样的蓝图调用,Add Impulse 和 Set Physics Linear Velocity 行为基本一致,但堆叠稳定性、CCD 表现和报错信息都变好了。
Chaos 还统一了刚体与破坏的数据表示。PhysX 的破坏需要 APEX 或单独插件;Chaos 的 Geometry Collection 直接复用同一套刚体与约束框架,因此可以做出“一堵墙被砸开后碎片继续参与物理交互”的效果,而不用在多个子系统之间切换。对阿诚来说,这意味着他可以用同一批物理材质和碰撞预设同时管理完整箱子与碎块。
迁移时最常见的问题是旧项目里直接读取 BodyInstance 内部指针的做法会编译失败,因为 Chaos 的代理对象变成了 FPhysicsActorHandle 和 FPhysicsShapeHandle。大多数蓝图项目不会感知这一层,但 C++ 插件需要重写相关代码。
刚体模拟基础
刚体是物理世界最常见的抽象:它假设物体内部任意两点之间的距离不变,因此只需要记录位置、朝向、线速度、角速度、质量和惯性张量。阿诚在仓库里放了一只圆桶,让它从斜坡上滚下来。圆桶的质量是 ,重力加速度取 ,那么重力给它的合力就是 。
动力学方程用最朴素的形式写就是:
\mathbf{F} = m\mathbf{a}, \quad \boldsymbol{\tau} = \mathbf{I}\boldsymbol{\alpha}其中 是合外力, 是质量, 是线加速度;\boldsymbol{\tau} 是合外力矩, 是惯性张量,\boldsymbol{\alpha} 是角加速度。游戏引擎里不会真的去解这个连续方程,而是把每帧切成很小的时间片,做数值积分。
最常用的显式欧拉积分写成递推式:
这里 是当前步长。步长越大,误差越明显:圆桶可能直接滚进坡底,也可能因为速度过大而穿透地面。Chaos 默认以固定频率运行物理 tick,具体数值可以在 Project Settings > Physics > Physics Simulation 里找到,默认通常是 ,也就是每步约 。
Chaos 内部实际使用的是半隐式欧拉,也就是先更新速度再更新位置。这种方式比纯显式欧拉更稳定,能量不会爆炸式增长。角速度则通过四元数更新,避免万向节锁问题。
刚体还有两个容易被忽略的属性:质心和阻尼。质心决定了力施加在哪里会产生纯平动还是转动;阻尼则像空气阻力一样持续消耗动能。阿诚一度把圆桶的线性阻尼设得很高,以为这样滚动会更稳,结果箱子像泡在水里一样慢吞吞。后来他只在需要快速稳定的地方提高角阻尼,保留线性阻尼默认值。惯性张量由碰撞体的形状自动计算,细长的物体绕长轴旋转更容易,绕短轴旋转则更费力,这直接影响圆桶滚下斜坡时的姿态。
碰撞检测与响应
圆桶滚下斜坡后撞上了一摞木箱。碰撞检测并不是一上来就计算精确接触点,而是分两步走。
第一步是 Broadphase。Chaos 会给每个参与物理的物体维护一个轴对齐包围盒(AABB),只在包围盒重叠的物体对之间进入下一步。这一步把 的配对降到接近 。
第二步是 Narrowphase。对于真正靠近的几何体,Chaos 使用 GJK 和 EPA 算法计算最近距离、穿透深度和接触法线。GJK 擅长判断两个凸体是否相交,EPA 在相交时给出最小穿透向量。复杂网格通常会被预处理为凸包或三角网格,用于不同精度的查询。
flowchart TD
A["Game Tick 开始"] --> B["Broadphase AABB 筛选"]
B --> C["Narrowphase GJK/EPA"]
C --> D{是否存在接触}
D -->|"是"| E["构建 Contact Manifold"]
D -->|"否"| F["跳过该物体对"]
E --> G["求解接触约束"]
G --> H["计算冲量并应用"]
H --> I["积分更新速度与位置"]
I --> J["同步到渲染线程"]碰撞响应基于冲量。两个刚体在接触点交换的冲量大小由相对速度、质量、恢复系数和摩擦系数共同决定。Chaos 内部会把接触约束和其他约束一起放进求解器,循环多轮后得到满足所有约束的速度。阿诚之前用 PhysX 时,箱子叠到五层以上就开始高频抖动,原因往往是接触法线在不断翻转,求解器每一帧给出的冲量方向都不一致。Chaos 在 Manifold 持久化和 contact offset 上做了不少改进,同样堆叠高度下明显更稳。
Chaos 用 contact offset 允许两物体在视觉上轻微重叠,避免浮空;再用 Baumgarte stabilization 把穿透慢慢推回合法位置,而不是一次性爆开。这两个参数在项目设置里可以调,默认值已经能应付大多数场景。
对于高速小物体,还可以开启 CCD(Continuous Collision Detection)。CCD 会在两帧之间沿运动方向扫出一个形状,检查路径上是否错过静态障碍。开启 CCD 的物体通常用于子弹、箭矢或弹弓石块。仓库里那袋高速飞出的水泥就应该打开 CCD,否则很可能直接穿透货架底板。阿诚在测试时确实遇到了这个问题,打开 CCD 后石块 reliably 砸中目标,再也没穿过货架。
Sleep / Wake 机制
木箱被撞散后,有几只滚到墙角停了下来。如果引擎继续为它们做积分、碰撞查询和约束求解,帧时间会白白浪费。Sleep 机制就是为了解决这个问题:当刚体的线速度和角速度都持续低于某个阈值,求解器就把它标记为 Sleeping,暂时停止模拟。
阿诚在场景里放了一座由八只箱子堆成的金字塔。金字塔静止几秒后全部进入 Sleep 状态。此时他控制玩家从二楼扔下一袋水泥,水泥袋撞到底部箱子,Chaos 会把被撞的箱子 Wake 起来,箱子再带动上面一层,整个金字塔像多米诺一样逐层苏醒。
sequenceDiagram
participant Player as "玩家"
participant Bag as "水泥袋"
participant Bottom as "底层木箱"
participant Top as "顶层木箱"
participant Solver as "Chaos Solver"
Player->>Bag: 投掷
Solver->>Bag: 速度高于阈值,保持 Wake
Bag->>Bottom: 碰撞
Solver->>Bottom: Wake
Bottom->>Top: 接触并传递力
Solver->>Top: Wake
Solver->>Solver: 求解整帧约束
Top-->>Bottom: 位移与速度更新Sleep 阈值可以在 Project Settings > Physics 里调整,也能在单个 Primitive Component 上覆盖。阈值通常由线速度和角速度两个分量组成,Chaos 会检查它们是否连续几帧都低于设定值。Sleep 不是单帧判定,而是连续满足条件若干帧后才会生效。这个帧数叫 Sleep Counter,可以在组件上微调。Counter 太短会让物体在微风中入睡,太长又起不到优化作用。
需要注意的是,Sleep 并不代表物体从世界中移除,只是不再主动积分。如果其他物体撞上它,或者代码调用 WakeRigidBody,它会立刻恢复模拟。阿诚踩过的一个坑是:他把 Sleep Threshold 设得太低,导致轻微抖动的箱子迟迟不能入睡,CPU 开销居高不下;设得太高又会让应该静止的箱子在玩家注视下微微滑动,破坏沉浸感。
Sub-stepping 与物理频率
阿诚的 Demo 里还有一段弹弓玩法:玩家把石块高速弹射出去,击中远处货架。在 的默认物理频率下,石块飞行速度如果太快,单帧位移可能超过自身尺寸,这时 Narrowphase 就来不及捕获接触,石块会直接穿过货架。这种穿透问题叫 Tunneling。
Sub-stepping 是 Chaos 用来缓解 Tunneling 的主要手段。它会在一帧游戏 tick 内把物理时间切成更小的片段,在每个子步里重新跑碰撞检测和积分。Sub-stepping 的数量可以在项目设置里限制,常见做法是 Max Substeps = 5,Fixed Time Step = 0.0167 秒。
graph TD
A["游戏帧 tick"] --> B{当前帧剩余时间 > 0}
B -->|"是"| C["取最小子步长"]
C --> D["在该子步内积分"]
D --> E["碰撞检测与约束求解"]
E --> F["更新刚体状态"]
F --> G["剩余时间 -= 子步长"]
G --> B
B -->|"否"| H["本帧结束,同步结果"]子步带来更高的模拟精度,但代价是 CPU 时间。阿诚在普通仓库场景里保持默认子步,只在弹弓关卡把 Max Substeps 临时调高。他还在代码里用 FMath::Min 钳制了石块的最大角速度,避免过快的自转让接触点频繁跳变。
如果一帧的实际 delta 时间很长,子步会把它拆成多个固定步长。剩余的极小时间会被累积到下一帧,避免时间漂移。子步越多,异步物理 tick 与游戏逻辑的相位差越大,读取位置时要留有余量。调试时可以用控制台命令 p.Chaos.Solver.DebugDraw 观察子步内的碰撞对,快速判断是不是子步不足导致穿透。
开发者在蓝图中写物理交互时要小心:不要在 Tick 里直接读取当前帧的物理位置做精确判定,因为读取到的可能是上一物理步的结果。需要精确事件判定时,应该使用碰撞回调或射线检测,而不是把位置插值当成物理事实。
物理材质与摩擦力、弹性
仓库地面有时是水磨石,有时是结冰的卸货台。阿诚用 Physical Material 给不同表面区分摩擦和弹性。Physical Material 不是材质球,而是单独的资源,可以在 Static Mesh 的 Collision 部分指定。
两个物体接触时,摩擦力和反弹力由双方的 Physical Material 共同决定。摩擦系数 的常见混合方式有 Average、Minimum、Maximum 和 Multiply。假设木箱与地面的 分别是 和 ,使用 Average 混合后等效摩擦就是 。Multiply 模式下两个很滑的材质叠在一起会变得更滑;Minimum 模式取最保守值,常用于角色脚部与任意地面的交互。
库仑摩擦模型把摩擦力限制在:
其中 是接触法向力。也就是说,法向压力越大,能提供的最大摩擦力也越大。如果切向力没有超过这个上限,物体就不会滑动。
弹性用恢复系数 表示,范围通常在 到 之间。 是完全非弹性碰撞,碰撞后两物体贴在一起; 是理想弹性碰撞,动能没有损失。现实中的橡胶球 可能在 左右,沙袋接近 。阿诚把水泥袋的恢复系数调到很低,触地时不会乱蹦;把弹弓石块的弹性适中调高,击中货架时更有打击感。
Physical Material 里还有密度参数,会影响刚体质量。如果美术给的碰撞体比实际大很多,质量会被高估,箱子会像铁块一样重。阿诚习惯在导入后用简化碰撞再重新计算质量。实际调试时,他会先锁定一个参考材质,把木箱的摩擦调到参考值,再依次调整地面,避免两个变量同时变化。
UE5.8 里 Physical Material 的界面和 PhysX 时代看起来差不多,但底层参数已经映射到 Chaos 的摩擦与恢复模型。迁移项目时要重新抽查几个关键表面的手感,旧数值不一定能一比一直接复用。
简单物理交互示例
阿诚写了一个蓝图函数,在玩家点击箱子时给它一个水平冲量。如果用 C++ 实现,可以在继承自 AActor 的类里这样写:
void AMyCrate::OnPoked()
{
UPrimitiveComponent* Prim = Cast<UPrimitiveComponent>(GetRootComponent());
if (!Prim || !Prim->IsSimulatingPhysics())
{
return;
}
FVector Impulse = GetActorForwardVector() * 800.0f;
FVector Location = GetActorLocation();
Prim->AddImpulseAtLocation(Impulse, Location);
}这段代码先判断根组件是不是物理模拟状态,然后沿角色正前方向施加 的冲量。冲量作用点是物体中心,所以不会产生额外转矩。如果想让箱子翻倒,可以把作用点抬高到箱子顶部。
另一个常用调试命令是显示物理休眠状态。在控制台输入:
p.Awake 1
p.Chaos.DebugDraw.Sleep 1运行时 Sleeping 的物体会被染成深色,Wake 的物体呈亮色。阿诚靠这个快速定位哪些箱子因为阈值不当而一直醒着。
UE5.8 Chaos 的改进
UE5.8 并不是 Chaos 的第一次登场,而是把前几年积累的东西打磨得更适合生产。官方 release notes 把 Dataflow 标为 Production-Ready,这意味着破坏资产可以用节点化、非破坏式的方式反复调整。Chaos Cloth 也进入 Production-Ready,并通过新的 Dataflow-based Cloth Panel Editor 与服装制作流程对齐。Chaos Hair、Chaos Fluids、Chaos Destruction 和模拟核心本身都收到了大量小幅度更新。
对阿诚这种主要用刚体做 Gameplay 的开发者来说,UE5.8 最实在的变化是稳定性和调试体验。堆叠 Solver 在接触法线跳变时更稳,异步 tick 的边界情况处理得更干净,Chaos Visual Debugger 也能把物理状态录下来逐帧回放。CVD 文件还可以导出给同事,方便定位只在特定输入下才会出现的抖动。
Epic 还在统一 Chaos Flesh 与 Chaos Cloth 的求解器,未来肌肉与皮肤之间的交互会自然许多。Control Rig Physics 在 5.8 进入 Beta,动画师可以在 Sequencer 里实时看到物理反馈。这对需要角色与场景物理互动的过场动画很有帮助,也让动画与物理之间的界限变得更模糊。对独立开发者来说,这些改进意味着可以用更少的工程人力做出可信的物理交互。
小结
仓库里的木箱、圆桶和水泥袋,把 Chaos Physics 的几块基础拼图画了出来。Chaos 与 PhysX 最大的不同在于所有权和管线结构;刚体模拟靠质量、惯性、力和积分推进;碰撞检测分 Broadphase 与 Narrowphase;Sleep/Wake 控制静止物体的开销;Sub-stepping 用更多 CPU 换取高速运动时的稳定性;Physical Material 则把摩擦和弹性从美术材质里抽离出来。
阿诚最后把仓库 Demo 的物理频率保持默认,只在弹弓关卡临时提高子步;把箱子堆叠层数放宽到八层而不抖;给不同地面分配了 Physical Material。写物理玩法时,建议先用默认参数做出原型,再针对抖动、穿透、睡眠异常三类问题分别调整。不要一次性改太多参数,否则很难定位真正起作用的设置。保持参数文档化也很重要,否则三个月后再次打开项目会忘记某个奇怪数值的来历。UE5.8 的 Chaos 已经能撑起这些常见玩法,理解底层机制之后,调参就不再是盲目试错。