物理模拟在 ECS 中的确定性积分步长:固定帧率物理调度器实现

在 Unity DOTS/ECS 环境下构建多人联机对战或动作游戏时,很多刚接触面向数据技术栈的开发者会直接在常规的 SystemBase.OnUpdate 里使用 SystemAPI.Time.DeltaTime 来更新实体的刚体速度与位置坐标:$$\\vec{x}_{t+1} = \\vec{x}_t + \\vec{v}_t \\cdot \\Delta t$$
这种基于可变渲染帧耗时(Variable Delta Time)的欧拉积分,是联机确定性模拟(Deterministic Simulation)的死敌。因为不同客户端的渲染帧率是动态波动的(一台手机跑 58.2 FPS,另一台跑 61.5 FPS,$\\Delta t$ 在 16.1ms 到 17.2ms 之间反复横跳)。即使两次运行的输入按键完全一致,由于浮点乘法在不同微小时间步长下的累积舍入误差,角色的运动轨迹也会在短短两秒内产生巨大的空间偏离,导致物理碰撞判定完全分叉。
要保证物理模拟在所有客户端上绝对确定且可复现,必须实现一套严格固定逻辑步长(Fixed Timestep)的累加器物理调度器(Accumulator-based Physics Scheduler),并对渲染层执行精准的状态插值(Render State Interpolation)。
固定步长累加器(Fix Your Timestep)算法模型
[ 真实渲染帧耗时 deltaTime ] ──► [ 注入全局时间累加器 (accumulator += dt) ]
│
┌─────────────────────────┴─────────────────────────┐
▼ ▼
[ accumulator >= FIXED_DELTA_TIME ? ] [ 剩余时间残差 residual ]
│ 是 │
├──────────────────────┐ │
▼ │ (循环消费) │
[ 执行一次确定性 ECS 物理步进 ] │ │
– 速度积分 / 碰撞求解 │ │
– 保存 PreviousState / CurrentState │ │
– accumulator -= FIXED_DELTA_TIME │ │
│ │ │
└──────────────────────┘ │
▼
[ 计算渲染插值因子 alpha = accumulator / FIXED_DELTA_TIME ]
│
▼
[ 渲染系统执行表现层姿态平滑插值: Lerp(Prev, Curr, alpha) ]
Unity ECS 确定性物理调度系统实现
下面展示了基于 Unity DOTS ECS 的完整确定性调度架构:
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
// 1. 物理状态双缓冲组件(用于表现层无撕裂插值)
public struct PhysicsInterpolationState : IComponentData
{
public float3 PreviousPosition;
public float3 CurrentPosition;
public quaternion PreviousRotation;
public quaternion CurrentRotation;
}
public struct DeterministicVelocity : IComponentData
{
public float3 Linear;
public float3 Angular;
}
// 2. 确定性固定物理更新组(Fixed Step Simulation Group)
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial class DeterministicPhysicsSchedulerSystem : SystemBase
{
private const float FIXED_TIMESTEP = 1.0f / 30.0f; // 固定 30 FPS 逻辑步长
private const int MAX_SUB_STEPS = 4; // 防螺旋死锁最大补偿步数
private float _accumulator = 0.0f;
protected override void OnUpdate()
{
float frameDeltaTime = math.min(SystemAPI.Time.DeltaTime, 0.2f);
_accumulator += frameDeltaTime;
int subSteps = 0;
while (_accumulator >= FIXED_TIMESTEP && subSteps < MAX_SUB_STEPS)
{
// 执行单次逻辑帧物理模拟
StepDeterministicPhysics(FIXED_TIMESTEP);
_accumulator -= FIXED_TIMESTEP;
subSteps++;
}
// 计算渲染插值权重 alpha ∈ [0, 1)
float alpha = _accumulator / FIXED_TIMESTEP;
// 调度表现层插值 Job,消除 30 帧逻辑在 120 帧高刷屏幕上的卡顿抖动
Dependency = new InterpolateTransformJob
{
Alpha = alpha
}.ScheduleParallel(Dependency);
}
private void StepDeterministicPhysics(float fixedDelta)
{
// 调度物理积分与碰撞 Job
Dependency = new IntegrateVelocityJob
{
FixedDeltaTime = fixedDelta
}.ScheduleParallel(Dependency);
}
}
// 3. 确定性积分 Job (Burst 编译,保证跨线程极致性能)
[BurstCompile]
public partial struct IntegrateVelocityJob : IJobEntity
{
public float FixedDeltaTime;
private void Execute(
ref PhysicsInterpolationState interp,
ref LocalTransform transform,
in DeterministicVelocity velocity)
{
// 记录历史状态快照
interp.PreviousPosition = interp.CurrentPosition;
interp.PreviousRotation = interp.CurrentRotation;
// 确定性欧拉积分
interp.CurrentPosition += velocity.Linear * FixedDeltaTime;
// 更新瞬时逻辑坐标
transform.Position = interp.CurrentPosition;
transform.Rotation = interp.CurrentRotation;
}
}
// 4. 表现层平滑插值 Job
[BurstCompile]
public partial struct InterpolateTransformJob : IJobEntity
{
public float Alpha;
private void Execute(
ref LocalTransform transform,
in PhysicsInterpolationState interp)
{
// 平滑插值出屏幕最终呈现的世界坐标
transform.Position = math.lerp(interp.PreviousPosition, interp.CurrentPosition, Alpha);
transform.Rotation = math.slerp(interp.PreviousRotation, interp.CurrentRotation, Alpha);
}
}
确定性物理落地三大军规
将时间掌控在确定性积分步长之内,不仅让网络对战与录像回放坚不可摧,更能让低帧率物理逻辑在高端高刷设备上呈现如丝般顺滑的视觉体验。

