Skip to content

Stage02|Engine Loop ​

Lesson020|dt、FPS、pause、resume 与 timeScale ​

Tags: #Creator2.x #Stage02 #dt #FPS #Pause #timeScaleDifficulty: ⭐⭐⭐⭐☆


一、本课目标 ​

本课把“时间”从一个参数提升为引擎运行模型:

  • dt 表示什么,为什么不能假定固定值。
  • FPS 和 Frame Time 如何互相转换。
  • pause、resume、timeScale 分别影响什么。
  • 后台恢复和超大 dt 应该如何处理。

二、FPS 和 Frame Time ​

FPS 表示每秒完成多少帧,Frame Time 表示一帧花了多少时间:

text
Frame Time ≈ 1 / FPS

常见预算:

目标帧率单帧预算
60 FPS约 16.67 ms
30 FPS约 33.33 ms
120 FPS约 8.33 ms

如果某一帧耗时 40 ms,即使其他帧很快,玩家也可能感受到卡顿。

FPS 是结果指标,Frame Time 更适合分析一帧到底超出了多少预算。


三、dt 是什么 ​

dt 通常表示本帧和上一帧之间经过的时间,单位一般是秒:

ts
update(dt: number) {
    this.node.x += this.speed * dt;
}

如果 speed = 300,含义是每秒移动 300 个单位,而不是每帧 300 个单位。

帧率变化时:

text
60 FPS:每帧 dt 较小,调用次数较多
30 FPS:每帧 dt 较大,调用次数较少

理想情况下,两者经过同样的真实时间后移动距离接近。


四、为什么 dt 不会永远等于 1 / 60 ​

实际设备存在:

  • CPU 或 GPU 突发负载。
  • 后台切换。
  • 垃圾回收。
  • 资源解码。
  • 平台帧回调抖动。
  • 屏幕刷新率变化。

所以不要写:

ts
this.node.x += 5;

除非你明确想表达“每帧移动固定量”。通常连续运动应该使用:

ts
this.node.x += 300 * dt;

五、dt、游戏时间和真实时间 ​

可以区分三种概念:

text
平台真实时间:设备时钟
帧间隔 dt:两次主循环之间的时间差
游戏时间:经过暂停、timeScale 等规则后的时间

一个倒计时可能使用游戏时间:

ts
this.leftTime -= dt;

一个网络超时可能需要真实时间:

text
即使游戏暂停,网络连接是否仍需超时?

不能把所有计时器都简单归为同一种时间。


六、timeScale 的意义 ​

时间缩放可以表达:

text
timeScale = 1:正常速度
timeScale = 0.5:游戏时间变慢
timeScale = 2:游戏时间加速
timeScale = 0:游戏时间停止

概念上:

text
rawDt
    ↓
gameDt = rawDt × timeScale
    ↓
游戏系统使用 gameDt

但不是所有系统都一定自动遵循同一个 timeScale。UI、网络、原生平台和自定义计时器可能采用不同时间来源。


七、pause 和 timeScale=0 的区别 ​

两者都可能让游戏对象停止变化,但语义不同:

text
pause:主循环或调度推进进入暂停状态
timeScale=0:仍可能有帧,但游戏时间增量为 0

差异可能影响:

  • 平台事件是否继续接收。
  • UI 是否继续响应。
  • 自定义真实时间计时器是否继续。
  • 渲染是否仍然发生。
  • 网络和音频系统如何运行。

产品需要“暂停菜单”时,应先定义暂停策略,再选择实现方式。


八、超大 dt 的风险 ​

应用切后台后恢复,可能出现:

text
正常 dt:0.016
恢复 dt:2.5

简单运动会瞬间移动:

ts
this.node.x += this.speed * dt;

简单倒计时会瞬间结束:

ts
this.leftTime -= dt;

常见处理方式:

ts
const safeDt = Math.min(dt, 0.1);

但限制 dt 会改变真实时间语义,因此不能对所有系统统一套用。角色控制、物理、网络同步和 UI 倒计时可能需要不同策略。


九、固定时间步的思想 ​

物理或确定性模拟常用固定时间步:

text
积累真实 dt
    ↓
只要累积值 >= fixedStep
    ↓
用固定 fixedStep 更新一次逻辑
    ↓
扣除 fixedStep,继续判断

这样可以让核心模拟不完全依赖渲染帧率。代价是:

  • 一帧可能需要执行多次模拟。
  • 低性能设备可能积压计算。
  • 需要处理最大补帧次数。

本课只建立概念,具体物理和同步实现留到专项课程。


十、案例:不同帧率下的移动 ​

错误写法 ​

ts
update() {
    this.node.x += 5;
}

60 FPS 时每秒约 300 单位,30 FPS 时每秒约 150 单位。

改进写法 ​

ts
update(dt: number) {
    this.node.x += 300 * dt;
}

仍需处理的情况 ​

ts
update(dt: number) {
    const safeDt = Math.min(dt, 0.1);
    this.node.x += 300 * safeDt;
}

是否限制 dt,应根据系统是否允许“补上后台期间的真实运动”决定。


十一、性能诊断中的时间判断 ​

看到 FPS 下降时,不要只看 FPS:

text
FPS 降低
    ↓
Frame Time 是否出现尖峰?
    ↓
CPU 时间还是 GPU 时间超预算?
    ↓
dt 是否只是结果,还是根因?

dt 变大通常说明前一帧或平台调度出现了问题,它本身不是卡顿根因。


十二、常见误区 ​

误区一:60 FPS 就代表每帧一定是 16.67 ms ​

这是目标平均值,不是每一帧的硬保证。

误区二:所有计时器都必须使用同一个 dt ​

游戏时间、真实时间、网络时间和 UI 时间可能有不同语义。

误区三:timeScale=0 等于所有系统停止 ​

外部事件、网络、原生任务和自定义计时器可能不受影响。

误区四:限制 dt 能解决所有后台恢复问题 ​

它只是防止单帧跳跃,还需要考虑状态同步、倒计时和资源恢复。


十三、练习与答案 ​

练习 ​

  1. 60 FPS 和 30 FPS 的单帧预算分别是多少?
  2. 为什么移动逻辑通常要乘 dt?
  3. pause 和 timeScale=0 的核心语义区别是什么?
  4. 后台恢复后超大 dt 会带来什么问题?
  5. 为什么物理或确定性模拟可能需要固定时间步?

参考答案 ​

  1. 约 16.67 ms 和 33.33 ms。
  2. 让速度更接近按秒定义,减少帧率对结果的影响。
  3. pause 偏向停止推进,timeScale=0 偏向让游戏时间不流逝,但仍可能有帧和外部系统运行。
  4. 角色瞬移、倒计时跳过、插值异常和模拟不稳定。
  5. 让核心模拟使用稳定步长,降低渲染帧率波动的影响。

十四、Creator 2.4.x 可复现实验:帧率、暂停和恢复 ​

创建一个每帧累加 dt 的探针,并在页面上显示 FPS:

ts
private elapsed = 0;
private frames = 0;
update(dt: number) {
    this.elapsed += dt;
    this.frames += 1;
    if (this.elapsed >= 1) {
        cc.log('[time]', 'fps=', this.frames, 'gameSeconds=', this.elapsed.toFixed(3));
        this.elapsed = 0;
        this.frames = 0;
    }
}

分别测试:正常运行、cc.game.pause()、cc.game.resume()、cc.director.getScheduler().setTimeScale(0.5),再切后台 5 秒恢复。记录 dt、计数器、Tween 和网络回调。预期不同平台和浏览器后台策略可能不同;重点是识别是否出现超大 dt,并在业务层做上限、补偿或重同步。

失败诊断 ​

text
移动突然瞬移 → 检查恢复帧 dt 是否异常
倒计时多跳几秒 → 区分游戏时间和真实时间
pause 后 UI 仍动 → UI 是否使用独立时间源
timeScale 修改无效 → 是否修改了正确 Scheduler,任务是否归属其他时钟

十五、版本边界 ​

Creator 2.4.x 的 dt 是当前调度路径提供的帧时间输入,但是否经过 timeScale、pause 和后台修正要结合具体 Scheduler 与平台实现验证。不要把 dt 视为固定 1/60,也不要假设 Web、Native 和小游戏后台都给出相同恢复行为。

十六、应用题与推导答案 ​

题目: 如何实现一个不会因后台恢复而瞬移的角色?

答案: 对渲染移动使用 Math.min(dt, 0.1) 之类的上限只能防止单帧爆炸,不能修复真实进度;对于战斗、冷却等关键状态,应在恢复时读取服务器或单调时钟重新计算,再把结果同步到表现层。也就是说,限制 dt 是表现保护,重同步才是状态正确性保证。

十七、本课总结 ​

text
FPS 是频率,Frame Time 是单帧成本。
dt 是本帧时间输入,不是固定常数。
游戏时间不一定等于真实时间。
pause、timeScale 和节点关闭是不同层级。
超大 dt 需要按系统语义处理。

十八、下一课预告 ​

text
Lesson021|阶段复习:从游戏启动到一帧结束