在FixedUpdate中如何手动补偿因timeScale导致的物理步长

解读

国内项目普遍会暂停游戏(timeScale=0)或子弹时间(timeScale∈(0,1)),而Unity的FixedUpdate仍按固定间隔(默认0.02 s)被调用,与timeScale无关。若逻辑里依赖Time.fixedDeltaTime做积分、计时或动画插值,timeScale变小后物理步长被“压缩”,会导致模拟速度变慢刚体拖拽感异常网络同步漂移等问题。面试官想看你是否理解物理步长与timeScale解耦的本质,并能在不改动引擎源码的前提下,用纯C#层补偿把“被吞掉”的时间追回来,同时兼顾数值稳定性跨平台一致性

知识点

  1. Time.fixedDeltaTime在运行时只读,修改Time.fixedDeltaTime只能在Edit→Project Settings→Time里预设,无法动态缩放
  2. Time.maximumDeltaTimeTime.maximumParticleDeltaTime只对主线程更新生效,不影响FixedUpdate
  3. Time.timeScale只会影响Time.fixedUnscaledDeltaTime返回值,而Time.fixedDeltaTime本身不变
  4. Manual Simulation(Physics.Simulate)可在timeScale=0手动步进物理世界,但会关闭自动FixedUpdate,需要完全接管循环
  5. 补偿公式
    累积欠时 acc += Time.unscaledDeltaTime;
    步进次数 steps = (int)(acc / targetFixedDelta);
    剩余欠时 acc %= targetFixedDelta;
    其中 targetFixedDelta = 0.02f / timeScale;
  6. 必须在FixedUpdate累加unscaled时间,否则timeScale=0Time.time不再增长,无法驱动补偿
  7. 补偿后单帧可能跑多次Physics.Simulate,需限制最大步数(通常4步)防止螺旋死亡
  8. 若项目使用Hybrid CLRlua热更,补偿逻辑要放在C#层避免il2cpp裁剪导致Physics.Simulatestrip

答案

public class PhysicsTimeCompensator : MonoBehaviour
{
    [SerializeField] float targetFixedDelta = 0.02f; // 期望的物理步长
    [SerializeField] int maxStepsPerFrame = 4;       // 防止卡死

    float accumulator = 0f;

    void FixedUpdate()
    {
        // 1. 计算“真实”需要步长
        float desiredDelta = targetFixedDelta / Mathf.Max(Time.timeScale, 0.0001f);

        // 2. 用unscaled时间累加,防止timeScale=0时停滞
        accumulator += Time.unscaledDeltaTime;

        // 3. 计算本次需要补几次物理步
        int steps = 0;
        while (accumulator >= desiredDelta && steps < maxStepsPerFrame)
        {
            Physics.Simulate(desiredDelta);
            accumulator -= desiredDelta;
            steps++;
        }

        // 4. 溢出时间留给下一帧,避免误差累积
        if (steps == maxStepsPerFrame && accumulator > desiredDelta)
        {
            accumulator = desiredDelta; // 强制收敛,防止雪球
        }
    }
}

使用方式:

  1. 把脚本挂在任意激活的GameObject上。
  2. 关闭Auto SimulationPhysics.autoSimulation = false;(建议在Start里执行)。
  3. 之后无论timeScale如何变化,物理世界都会按恒定“真实时间”步进刚体速度关节驱动布娃娃表现与timeScale=1时完全一致

拓展思考

  1. 网络同步:帧同步项目里,服务器以固定30 Hz广播快照,客户端timeScale=0.5时若不做补偿,插值时钟落后服务器。可把上述accumulator作为逻辑时钟驱动状态回滚与前瞻,保证tick号对齐
  2. 粒子与动画ParticleSystemAnimatortimeScale影响,但Physics.Simulate不会驱动它们。需要手动调用ParticleSystem.Simulate(delta, true, false)传入unscaledDelta,否则烟雾、布料慢放
  3. 性能权衡移动端发热场景下,maxStepsPerFrame动态下调,牺牲精度帧率XR应用与Display.refreshRate对齐,targetFixedDelta最好设为1/721/90
  4. JobSystem兼容Unity 2022+Physics Step支持多线程模拟Manual Simulation强制同步主线程阻塞明显。可拆分小步长插入WaitForFixedUpdate,把耗时摊平到多帧。