外观
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 能解决所有后台恢复问题
它只是防止单帧跳跃,还需要考虑状态同步、倒计时和资源恢复。
十三、练习与答案
练习
- 60 FPS 和 30 FPS 的单帧预算分别是多少?
- 为什么移动逻辑通常要乘 dt?
- pause 和 timeScale=0 的核心语义区别是什么?
- 后台恢复后超大 dt 会带来什么问题?
- 为什么物理或确定性模拟可能需要固定时间步?
参考答案
- 约 16.67 ms 和 33.33 ms。
- 让速度更接近按秒定义,减少帧率对结果的影响。
- pause 偏向停止推进,timeScale=0 偏向让游戏时间不流逝,但仍可能有帧和外部系统运行。
- 角色瞬移、倒计时跳过、插值异常和模拟不稳定。
- 让核心模拟使用稳定步长,降低渲染帧率波动的影响。
十四、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|阶段复习:从游戏启动到一帧结束