UE5.8 物理性能与确定性

📑 目录

UE5.8 物理性能与确定性

测试大厅里,两台显示器并排亮着。左侧玩家朝一堆油桶发射火箭弹,爆炸后一个桶滚向墙角,卡在门边;右侧玩家看到的却是同一个桶穿墙而过,静静躺在门外。两台机器运行同一版本、同一关卡、同一输入,结果却不一致。制作人盯着屏幕说:如果正式服出现这种情况,玩家会直接退款。

这并非单纯的显示 bug。物理引擎在离散时间步上积分刚体运动,帧率波动、浮点舍入、网络延迟、客户端与服务器权限划分,都会让同一堆刚体在不同机器上走出不同轨迹。更糟的是,当场景中物理对象超过预算,主线程会被拖住,帧时间从 16 毫秒跳到 40 毫秒,操作响应变得黏滞。本文围绕 UE5.8 的 Chaos 物理体系,讨论如何把物理模拟稳定在可控成本内,并让多人环境里的结果足够一致。

1. 物理模拟频率与固定时间步

Chaos 默认把物理步长绑定到渲染帧。帧率 60 fps 时,每帧调用一次 Physics Tick,步长 $ \Delta t $ 约 16.67 毫秒;帧率跌到 30 fps,步长变成 33.33 毫秒。刚体速度 $ v $、角速度 $ \omega $、位置 $ x $ 都按这个变量步长积分:

x(t+Δt)=x(t)+v(t)Δt+12a(t)Δt2x(t+\Delta t) = x(t) + v(t)\Delta t + \frac{1}{2}a(t)\Delta t^2

$ \Delta t $ 一旦变大,同样弹射力会让物体飞得更远,碰撞穿透也会变多。竞技游戏里,玩家 A 在 144 fps 机器上被炸飞的距离,可能和玩家 B 在 60 fps 机器上完全不同。这就是要把物理步长固定下来的原因。

UE5.8 提供 Fixed Time StepSubstepping 两条路径。固定时间步把渲染帧拆成若干等长子步:

[/Script/Engine.PhysicsSettings]
bSubstepping=True
MaxSubstepDeltaTime=0.008333
MaxSubsteps=8

渲染帧累积时间 $ A $,只要 $ A \ge \Delta t_{fixed} $ 就推进一次物理,剩余时间留到下一帧。这样无论渲染帧率如何,刚体积分使用的 $ \Delta t $ 都一样。子步数量设置上限是为了防止极端掉帧时一次性追太多步,把主线程卡死。

flowchart TD
    A["读取渲染帧耗时"] --> B["累加到 accumulator"]
    B --> C{accumulator >= fixed dt?}
    C -->|"是"| D["执行一次物理子步"]
    D --> E["accumulator -= fixed dt"]
    E --> C
    C -->|"否"| F["插值显示姿态"]
    F --> G["提交渲染"]

子步会提高模拟稳定性,却不会降低单步计算量。它只是把大时间片切小,让 $ \frac{1}{2}a\Delta t^2 $ 项不会过大。开发者需要在稳定性和 CPU 预算之间取舍。对于载具、布娃娃、可破坏物体这些对穿透敏感的对象,建议启用子步;对于静止装饰物,关闭子步更划算。

2. 异步物理

传统流程里,物理模拟和游戏逻辑跑在同一线程。角色输入、动画、AI、Gameplay Ability 都要等到物理步完成才能继续。当场景里有几百个刚体,主线程会被物理拖住,造成帧时间尖峰。

UE5.8 的 Async Physics 把 Chaos 求解器放到独立任务线程。游戏线程在 TG_PrePhysics 收集输入,提交到物理线程后立刻继续执行Gameplay 逻辑;物理线程在后台积分、解碰撞,完成后把结果写回场景代理,供渲染线程使用。

flowchart TD
    subgraph game ["Game Thread"]
        input["输入采样"]
        gameplay["Gameplay 逻辑"]
        proxy["Async Physics Proxy"]
    end
    subgraph physics ["Physics Task Thread"]
        solver["Chaos Solver"]
        islands["Island Manager"]
    end
    subgraph render ["Render Thread"]
        scene["场景代理"]
    end

开启异步物理需要在项目设置里勾选 Async Physics Tick,并把需要异步模拟的 UPrimitiveComponent 标记为 AsyncPhysicsTickEnabled。蓝图节点在 Async Physics Tick 事件里读取上一物理步的结果,而不是当前帧的结果,因为两个线程存在一帧延迟。

异步能抚平帧时间,但会引入时序问题。例如玩家在 tick 100 按下跳跃,物理线程在 tick 101 才处理;若Gameplay 在 tick 100 立即判断玩家离地,就会和物理状态错位。处理这类问题的方法是在 FAsyncPhysicsInput 里附带目标物理步号,让求解器在指定步执行该输入,而不是立即应用。

3. 服务器权威物理

回到油桶不同步的故事。根本原因是两台客户端各自本地模拟爆炸。爆炸产生的冲量、随机碎片、接触法线都由本机计算,浮点差异会迅速放大。解决思路很简单:让服务器成为唯一可信的物理世界,客户端只负责把输入发上去,再把服务器算出的状态接收回来。

sequenceDiagram
    participant C1 as "客户端 A"
    participant S as "服务器"
    participant C2 as "客户端 B"
    C1->>S: 火箭弹命中坐标 + 物理步号
    S->>S: 在权威世界执行爆炸
    S-->>C1: 油桶状态快照
    S-->>C2: 油桶状态快照
    C1->>C1: 本地状态与快照对齐
    C2->>C2: 本地状态与快照对齐

服务器权威模型下,客户端可以保留一份本地预测物理世界,用于即时反馈;但真相始终来自服务器。当服务器快照到达,客户端比较预测结果,若误差 $ e = |x_{client} - x_{server}| $ 超过阈值,就回滚并重放到当前 tick。

权威物理并不意味着服务器要模拟所有东西。静态地形、不可交互的装饰物、远离玩家的对象,都可以交给客户端做纯视觉模拟,或者干脆不同步。只有会影响胜负判定的对象才需要服务器仲裁。合理划分同步范围,是控制服务器 CPU 负载的关键。

4. 客户端预测与插值

如果玩家按下前进后,要等到服务器回包才看到角色移动,延迟会让人无法操作。所以客户端会提前在本地物理世界里施加输入,并把同一输入发送给服务器。服务器按权威规则计算,返回确认或修正。这个过程叫客户端预测。

预测的核心公式和权威模拟一致:

s(t+Δt)=s(t)+f(s,u,Δt)s(t+\Delta t) = s(t) + f(s, u, \Delta t)

其中 $ s $ 是状态向量,$ u $ 是输入,$ f $ 是物理积分函数。只要客户端和服务器使用相同的 $ f $ 和 $ \Delta t $,预测结果和权威结果就会接近。实际上,由于浮点环境、碰撞事件顺序、第三方对象干扰,两者会 diverge。当服务器回包显示角色在 tick 120 的位置是 $ x_{server} $,客户端在 tick 120 的预测位置是 $ x_{client} $,误差超过 10 厘米时就要回滚。

回滚不是直接 teleport。直接跳会让画面抖动。更稳的做法是重放:把历史输入队列从冲突 tick 开始,在服务器给出的状态上重新执行到当前 tick,再把重放结果线性插值回当前显示姿态。插值公式写作:

xdisplay=xcurrent+α(xreplayxcurrent)x_{display} = x_{current} + \alpha(x_{replay} - x_{current})

$ \alpha $ 通常取 0.2 到 0.4,根据延迟和误差动态调整。对于远程玩家的物体,不能预测,只能插值。服务器每 50 毫秒广播一次状态,客户端维护两个历史快照,在它们之间做 Hermite 或线性插值,得到平滑运动。

5. 浮点确定性与同步

确定性是指给定相同初始状态、相同输入序列、相同时间步,所有机器产出完全一致的物理结果。理论上,Chaos 使用 IEEE 754 单精度浮点数,$ + 、 \times 、 \sqrt{} $ 在同一 CPU 架构下是可重复的。问题出在并行求和、编译器优化、调用顺序和不同平台。

例如四个刚体碰撞产生的总冲量,串行相加顺序为 $ (((a+b)+c)+d) $,多线程并行可能变成 $ (a+b)+(c+d) $。两种顺序的舍入结果不同。Chaos 的 Island Solver 把接触分组到独立岛屿并行求解,岛屿边界上的对象就可能因合并顺序不同而得到不同速度。

跨平台同步更麻烦。x86 与 ARM 的默认浮点模式、FMA 指令可用性、编译器常量折叠策略都有差异。UE5.8 在 PhysicsSettings 里提供 DeterministicPhysics 开关,强制关闭部分 SIMD 重排和并行归约,代价是 10% 到 30% 的性能下降。在需要严格同步的会话里,建议所有服务器使用相同编译版本、相同 CPU 架构,并锁定 r.FloatingPointExceptionChecks

对于休闲多人游戏,不必追求逐位一致。只要保证关键对象的位置误差 $ e $ 小于玩家可感知的阈值,就可以接受少量漂移。此时优先保证帧率和响应,而不是绝对确定性。

6. 物理对象数量控制

场景中每多一个刚体,碰撞检测、约束求解、岛屿合并都要消耗 CPU。性能尖峰往往不是因为对象总数,而是因为某一刻大量对象同时进入活动状态。想象一堵砖墙被炸塌,数百块碎片同时开始模拟,每块都要与邻居做连续碰撞检测,帧时间会瞬间爆表。

控制对象数量的第一层是静态合并。建筑倒塌前,整面墙可以用一个静态网格加简易破坏效果表现;真正需要物理模拟的碎片数量限制在 20 到 30 块,其余用 Niagara 粒子或预录制动画替换。

第二层是动态 LOD。远离相机的物理对象可以降低模拟频率,甚至完全停用。UE5.8 的 Physics Object Component 支持 DistanceBasedSimulation,当对象距离摄像机超过 50 米时,把固定时间步从 120 Hz 降到 30 Hz;超过 100 米时进入睡眠或剔除。

第三层是质量与复杂度预算。复杂凸包碰撞比球体贵一个数量级。可破坏物体的碎片应尽量使用简单几何代理,并在破碎事件后把细小碎片快速合并为单一聚合体。合并后的刚体数量从 $ N $ 降到 $ M \ll N $,求解器压力大幅下降。

7. Sleep / Cull 策略

刚体一旦静止,继续积分是浪费。Chaos 提供 Sleep 机制:当线速度 $ v $ 和角速度 $ \omega $ 都低于阈值一段时间,物体进入睡眠状态,求解器跳过它。唤醒条件包括受到外力、被其他活动物体碰撞、玩家交互等。

动能判据可以写成:

Ek=12mv2+12Iω2<ϵE_k = \frac{1}{2}mv^2 + \frac{1}{2}I\omega^2 < \epsilon

$ \epsilon $ 在 Project Settings 里对应 Sleep Threshold。阈值设得太低,轻微抖动会让物体反复唤醒;设得太高,物体还没完全停稳就睡过去,会显得漂浮。一般建议默认值 0.1 到 1.0 之间,根据对象质量微调。

graph LR
    A["活动刚体"] -->|"速度低于阈值"| B["计时器启动"]
    B -->|"计时器到期"| C["进入睡眠"]
    C -->|"受到外力"| D["强制唤醒"]
    D --> A
    A -->|"超出同步范围"| E["客户端剔除"]
    E -->|"重新进入范围"| A

Cull 比 Sleep 更激进。对于远离玩家、不影响胜负的对象,可以直接停止模拟并从客户端移除。服务器仍保留一份轻量级占位,用于关键判定;当玩家靠近时,再下发最新状态并恢复模拟。Cull 策略需要配合兴趣管理,避免所有客户端都同步全地图物理对象。

Sleep 和 Cull 的边界要设计清楚。睡眠物体只是不被积分,仍然参与碰撞查询;剔除物体则不再占用网络带宽和客户端内存。油桶在被炸飞后 2 秒若速度归零,进入睡眠;若玩家跑远,再被服务器剔除。等玩家返回,服务器发回睡眠姿态,客户端直接显示,无需重新模拟坠落过程。

8. 多人游戏物理同步方案

把前面所有手段组合起来,可以得到一套适用于 UE5.8 的多人物理同步方案。首先,服务器以固定时间步运行权威 Chaos 世界,启用异步物理以稳定 tick 时间。所有会影响胜负的刚体在服务器上模拟,客户端保留预测副本。

其次,输入和事件附带物理步号。火箭弹命中不是只传坐标,而是传 HitLocation + ServerPhysicsStep。客户端在本地同样按步号触发爆炸,这样预测和权威模拟的初始条件尽量一致。爆炸产生的随机碎片种子由服务器生成并广播,避免客户端随机数 diverge。

第三,状态同步采用 delta 快照。每个物理对象的位置、旋转、线速度、角速度打包成 64 到 128 比特。静止对象只发 1 比特表示仍在睡眠;活跃对象按优先级排序,超出带宽预算的低优先级对象推迟到下一帧。

第四,误差处理分三级。小于 5 厘米的误差直接 smooth;5 到 30 厘米用插值修正;超过 30 厘米或检测到穿墙则强制回滚重放。重放时客户端用服务器状态覆盖本地对象,然后从快照步号开始重放未确认的输入队列。

第五,对象生命周期由服务器主导。生成、睡眠、唤醒、剔除的决策都在服务器做出,客户端只执行。即使客户端预测了爆炸效果,最终对象的启用和禁用仍以服务器权威包为准。

这套方案并不完美。它增加了服务器负载,要求开发者把物理对象按重要性分层,还要处理回滚时的视觉抖动。但它能把开头那两台显示器上的油桶轨迹,收敛到玩家看不出差异的程度。

案例复盘:从错位到收敛

让我们把测试大厅里的油桶场景完整走一遍。火箭弹命中后,服务器首先记录命中点和当前物理步号,而不是立刻让客户端各自处理。爆炸在服务器权威世界内以固定时间步触发,碎片数量被限制在 30 块以内,细小尘埃用 Niagara 表现。冲击波作用结束后,速度低于阈值的碎片在 1 秒内进入睡眠,不再消耗 CPU。

客户端 A 和客户端 B 收到的是同一份状态快照。由于两人都按服务器给的步号做了本地预测,各自屏幕上油桶的初始飞行方向基本一致。网络抖动导致客户端 A 比客户端 B 早 30 毫秒收到快照,但快照里包含服务器模拟时刻,客户端会把状态对齐到对应 tick,再用插值平滑过渡到当前画面。最终两边看到的油桶都卡在门边,差异不到半个身位。

这个修复过程没有改动美术资源,也没有禁用物理模拟,只是把对象按重要性分层、把权限收回到服务器、把时间步固定下来。性能方面,砖墙倒塌的帧时间尖峰从 45 毫秒降到 22 毫秒,因为大量碎片在 2 秒后被剔除,只剩少量可见块参与渲染。玩家反馈操作响应变快,跳跃和受击不再黏滞。

实际开发中建议先用 stat 命令观察物理耗时。stat physics 可以看到每个岛屿的求解时间,stat game 能区分 Game Thread 和 Physics Task Thread 的占比。找到耗时最高的对象后,再决定是降模拟频率、换简单碰撞,还是改为纯视觉效果。不要一上来就关闭所有物理,那样会让世界显得呆板。调优真的不能一次性改太多参数,每次只动一个变量,看数据变化,再决定下一步。记录每次改动的帧时间和同步误差,画出趋势图,反复对比并不断地持续监控,直到指标稳定为止,才能定位真正有效的优化点。

写在最后

物理性能和确定性是一对始终需要权衡的变量。追求绝对确定性会牺牲多线程优化和跨平台便利;追求极限性能又会让不同机器上的结果分道扬镳。UE5.8 给了足够的开关:固定时间步稳住积分,异步物理降低主线程尖峰,服务器权威和客户端预测让多人同步有章可循,Sleep 与 Cull 把资源花在刀刃上。

最终目标不是让每一台机器算出完全相同的浮点位,而是让玩家在关键时刻看不到错位、感受不到卡顿。做到这一点,油桶到底停在墙角还是门外,就不再是问题。