在FixedUpdate中如何手动补偿因timeScale导致的物理步长
解读
国内项目普遍会暂停游戏(timeScale=0)或子弹时间(timeScale∈(0,1)),而Unity的FixedUpdate仍按固定间隔(默认0.02 s)被调用,与timeScale无关。若逻辑里依赖Time.fixedDeltaTime做积分、计时或动画插值,timeScale变小后物理步长被“压缩”,会导致模拟速度变慢、刚体拖拽感异常、网络同步漂移等问题。面试官想看你是否理解物理步长与timeScale解耦的本质,并能在不改动引擎源码的前提下,用纯C#层补偿把“被吞掉”的时间追回来,同时兼顾数值稳定性与跨平台一致性。
知识点
- Time.fixedDeltaTime在运行时只读,修改Time.fixedDeltaTime只能在Edit→Project Settings→Time里预设,无法动态缩放。
- Time.maximumDeltaTime、Time.maximumParticleDeltaTime只对主线程更新生效,不影响FixedUpdate。
- Time.timeScale只会影响Time.fixedUnscaledDeltaTime的返回值,而Time.fixedDeltaTime本身不变。
- Manual Simulation(Physics.Simulate)可在timeScale=0时手动步进物理世界,但会关闭自动FixedUpdate,需要完全接管循环。
- 补偿公式:
累积欠时acc += Time.unscaledDeltaTime;
步进次数steps = (int)(acc / targetFixedDelta);
剩余欠时acc %= targetFixedDelta;
其中targetFixedDelta = 0.02f / timeScale; - 必须在FixedUpdate里累加unscaled时间,否则timeScale=0时Time.time不再增长,无法驱动补偿。
- 补偿后单帧可能跑多次Physics.Simulate,需限制最大步数(通常4步)防止螺旋死亡。
- 若项目使用Hybrid CLR或lua热更,补偿逻辑要放在C#层避免il2cpp裁剪导致Physics.Simulate被strip。
答案
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; // 强制收敛,防止雪球
}
}
}
使用方式:
- 把脚本挂在任意激活的GameObject上。
- 关闭Auto Simulation:
Physics.autoSimulation = false;(建议在Start里执行)。 - 之后无论timeScale如何变化,物理世界都会按恒定“真实时间”步进,刚体速度、关节驱动、布娃娃表现与timeScale=1时完全一致。
拓展思考
- 网络同步:帧同步项目里,服务器以固定30 Hz广播快照,客户端timeScale=0.5时若不做补偿,插值时钟会落后服务器。可把上述accumulator作为逻辑时钟,驱动状态回滚与前瞻,保证tick号对齐。
- 粒子与动画:ParticleSystem和Animator受timeScale影响,但Physics.Simulate不会驱动它们。需要手动调用ParticleSystem.Simulate(delta, true, false)并传入unscaledDelta,否则烟雾、布料会慢放。
- 性能权衡:移动端发热场景下,maxStepsPerFrame可动态下调,牺牲精度换帧率;XR应用需与Display.refreshRate对齐,targetFixedDelta最好设为1/72或1/90。
- JobSystem兼容:Unity 2022+的Physics Step支持多线程模拟,Manual Simulation会强制同步,主线程阻塞明显。可拆分小步长并插入WaitForFixedUpdate,把耗时摊平到多帧。