Skip to content

Stage02|Engine Loop ​

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

Tags: #Creator2.x #Stage02 #EngineLoop #Director #Scheduler #dtDifficulty: ⭐⭐⭐⭐☆


一、本课目标 ​

本课串联 Lesson011~Lesson020,完成 Engine Loop 阶段闭环。

你需要从下面几行代码出发:

ts
cc.director.loadScene('Main');

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

解释:

text
游戏如何启动
场景如何被 Director 接管
组件如何进入调度
dt 如何推进时间
update 如何改变状态
Renderer 何时读取状态
为什么下一帧还能继续运行

二、Stage02 的核心知识树 ​

text
1. cc.game:游戏级入口
2. Director:场景级协调者
3. Scene:当前运行对象树
4. Scheduler:时间任务
5. ComponentScheduler:组件生命周期
6. update / lateUpdate:脚本阶段
7. active / enabled:调度条件
8. Action / Tween / Animation:时间驱动状态
9. dt / pause / timeScale:游戏时间
10. mainLoop:把所有阶段串成一帧

它们合起来是:

text
平台帧回调
→ cc.game
→ Director.mainLoop(dt)
→ 时间任务与组件调度
→ 生命周期和脚本更新
→ Transform / Renderer
→ 帧收尾

三、从启动到当前场景 ​

text
游戏启动
    ↓
cc.game 初始化和运行
    ↓
创建或接管 cc.director
    ↓
加载首个 Scene
    ↓
反序列化 Scene 数据
    ↓
创建 Node / Component
    ↓
执行 onLoad / onEnable / start
    ↓
设置当前运行场景
    ↓
等待平台下一次帧回调

关键认识:

Scene 文件是序列化数据,Director 管理的是运行时 Scene 对象。


四、一帧完整时间线 ​

教学模型可以画成:

text
第 N 帧平台回调
    ↓
计算 rawDt / gameDt
    ↓
检查 pause、场景切换和全局状态
    ↓
Scheduler 推进定时任务
    ↓
Action / Tween / Animation 推进
    ↓
ComponentScheduler 执行生命周期和 update
    ↓
ComponentScheduler 执行 lateUpdate
    ↓
处理 Transform、Layout 和场景状态
    ↓
Renderer 收集、排序、合批
    ↓
提交 GPU
    ↓
延迟销毁和阶段收尾
    ↓
等待第 N+1 帧

不同 2.x 小版本可能调整部分子阶段顺序。稳定的是职责边界和依赖关系,不是这张图中每一行的源码位置。


五、update 为什么会执行 ​

组件 update 需要同时满足:

text
实现 update
enabled = true
activeInHierarchy = true
未销毁
主循环正在运行

父节点关闭时:

text
child.active 仍可能是 true
child.activeInHierarchy = false
child.update 不再满足调度条件

这不是“父节点 update 没执行导致子节点没执行”,而是最终激活状态传播到了子节点。


六、一个 API 调用如何穿过一帧 ​

例子一:修改位置 ​

ts
this.node.x = 100;
text
Node setter
→ 修改 Transform 数据
→ 标记 Dirty
→ 后续需要时计算矩阵
→ Renderer 读取新的世界变换
→ GPU 显示结果

例子二:关闭节点 ​

ts
this.node.active = false;
text
Node 状态变化
→ activeInHierarchy 传播
→ 组件进入 disable 状态
→ update / 事件 / 渲染受到影响
→ 节点仍可重新启用

例子三:延迟回调 ​

ts
this.scheduleOnce(this.openReward, 1);
text
登记任务
→ Scheduler 每帧推进时间
→ 达到触发条件
→ 当前合适阶段调用回调

七、阶段综合案例:打开页面后启动倒计时 ​

场景结构:

text
Canvas
└── Panel
    ├── RewardButton
    ├── CountDownLabel
    └── PanelController

业务代码:

ts
onEnable() {
    this.remaining = 10;
    this.schedule(this.tick, 1);
}

onDisable() {
    this.unschedule(this.tick);
}

tick() {
    this.remaining -= 1;
    this.label.string = String(this.remaining);
}

引擎视角:

text
Panel.active=true
    ↓
PanelController.activeInHierarchy=true
    ↓
onEnable 注册 tick
    ↓
Scheduler 接收 dt
    ↓
累计到 1 秒
    ↓
调用 tick
    ↓
Label 数据改变
    ↓
Renderer 更新文字
    ↓
Panel.active=false
    ↓
onDisable 取消 tick

这个例子同时连接了 Node、Component、Lifecycle、Scheduler、dt、Renderer 和 active 状态。


八、阶段练习 ​

题目一:画出调用链 ​

解释下面代码从调用到画面更新的过程:

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

题目二:状态判断 ​

text
Parent.active = false
Child.active = true
Move.enabled = true

回答 Child 的 activeInHierarchy 和 Move 的 update 状态。

题目三:时间判断 ​

为什么后台恢复后,dt 可能需要限制或单独处理?

题目四:场景切换 ​

为什么 loadScene 调用下一行不能作为新场景初始化完成的判断?

题目五:调度选择 ​

一个任务每秒刷新一次,应该优先使用空 update、schedule 还是独立线程?为什么?


九、阶段练习参考答案 ​

题目一 ​

text
平台帧回调
→ cc.game
→ Director.mainLoop(dt)
→ ComponentScheduler 找到组件
→ 调用 update(dt)
→ 修改 Node Transform
→ 标记 Dirty
→ 后续计算 World Matrix
→ Renderer 收集和提交
→ GPU 显示

题目二 ​

text
Child.active = true
Child.activeInHierarchy = false
Move.enabled = true
Move.update 不执行

因为父节点关闭导致 Child 最终激活状态为 false。

题目三 ​

平台可能暂停帧回调,恢复时两帧间隔变大。直接使用超大 dt 可能导致移动、倒计时或模拟瞬间跳跃。

题目四 ​

场景切换还包含资源加载、反序列化、对象创建和生命周期执行。调用 API 只表示提出请求,不表示所有步骤已经完成。

题目五 ​

优先使用 schedule。它适合低频时间任务,能避免无意义的每帧调用;独立线程也不能直接替代 Creator 主线程对象更新。


十、Stage02 面试题 ​

1. cc.game 和 cc.director 分别负责什么? ​

cc.game 负责游戏级启动和平台帧驱动,cc.director 负责当前 Scene 的管理和每帧推进。

2. Director 是不是所有业务逻辑的入口? ​

不是。Director 是运行时协调层,业务逻辑应由组件、系统或管理器承载。

3. Scheduler 和 ComponentScheduler 的区别是什么? ​

前者主要处理时间任务,后者主要处理组件生命周期和每帧方法。

4. lateUpdate 是整个引擎的最后阶段吗? ​

不是。它是脚本更新阶段的后置回调,后面仍可能有 Transform、Renderer 和帧收尾。

5. 为什么修改 Node 属性不等于立即提交 GPU? ​

修改首先改变运行时状态,后续 Transform、渲染数据和提交阶段才会把状态交给 GPU。

6. active=false、enabled=false、pause 的作用范围有什么不同? ​

分别是节点树级、组件级和游戏主循环级控制。

7. 为什么移动逻辑要使用 dt? ​

为了按照时间而不是按照帧数定义速度,减少帧率差异带来的行为变化。

8. 为什么需要在 onDisable 中取消事件或定时器? ​

防止隐藏或离开场景的对象继续收消息、重复执行或被外部引用持有。


十一、Stage02 综合实验:EngineLoopLab ​

实验目标 ​

用一个最小场景把 Stage02 的核心链路一次串起来:cc.game 帧驱动、Director、Scene、Scheduler、组件生命周期、active/enabled、dt、update/lateUpdate 和渲染结果。

场景结构 ​

text
EngineLoopLab
├── ProbeRoot(LifecycleProbe)
├── MovingNode(MoverProbe)
├── CameraNode(CameraProbe)
└── Controls(Pause/Toggle/Load 按钮)

探针脚本 ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class EngineLoopProbe extends cc.Component {
    @property(cc.Node) target: cc.Node = null;
    private frame = 0;
    onLoad() { cc.log('[LAB] onLoad'); }
    onEnable() { cc.log('[LAB] onEnable'); }
    start() { cc.log('[LAB] start'); }
    update(dt: number) {
        this.frame += 1;
        if (this.frame <= 5) cc.log('[LAB] update', this.frame, dt.toFixed(4), this.node.activeInHierarchy, this.enabled);
        if (this.target) this.target.x += 30 * dt;
    }
    lateUpdate() {
        if (this.frame <= 5) cc.log('[LAB] lateUpdate', this.frame, this.target && this.target.x);
    }
    onDisable() { cc.log('[LAB] onDisable'); }
    onDestroy() { cc.log('[LAB] onDestroy'); }
}

执行步骤与验收 ​

  1. 正常运行,保存一帧的完整日志。
  2. 切换 target.active,确认 activeInHierarchy 变化并观察调度停止/恢复。
  3. 切换探针 enabled,确认只影响该组件。
  4. 点击暂停和恢复,比较 dt、移动距离和外部 UI。
  5. 切换场景,确认旧探针销毁、新探针重新经历生命周期。
  6. 给目标添加 Tween,再让探针也写位置,定位抖动的多个写入者。

故障定位矩阵 ​

现象定位层证据
所有日志停止game/pause 或主循环cc.game.isPaused()、平台前后台事件
只有一个组件停止Component/Node 状态enabled、activeInHierarchy
状态更新但画面晚一帧update/lateUpdate/Transform同帧位置日志与相机日志
切场景后旧日志仍出现常驻节点/全局任务owner、事件和 Scheduler 注册表
恢复后位置跳变dt/时间语义恢复首帧 dt、真实时间戳

阶段验收标准 ​

完成者应能提交:一张调用链图、一份包含版本/平台的日志、一张故障定位表,以及对“组件不更新、画面晚一帧、切场景内存不降、后台恢复跳变”四个问题的证据化结论。只有写出“可能是引擎问题”不算通过。

十二、Stage02 总结 ​

text
cc.game 让游戏获得帧驱动。
Director 管理当前 Scene 并推进 mainLoop。
Scene 保存运行时节点树。
Scheduler 推进时间任务。
ComponentScheduler 管理组件生命周期。
update / lateUpdate 修改和整理状态。
dt 决定时间推进的量。
Transform / Renderer 把状态变成画面。

Stage02 最终形成的能力是:

看到一个“为什么这一帧没有更新”或“为什么画面晚一帧”的问题时,能够沿着平台帧回调、Director、调度器、组件状态和渲染阶段逐层定位。


十三、下一阶段预告 ​

Stage02 完成,下一阶段进入:

text
Stage03|Runtime Objects & Assets
Lesson022|Scene 和 Prefab 的序列化数据如何还原成运行时对象?

下一阶段会从“Scene 已经被 Director 接管”继续向下追踪:

text
Scene / Prefab 文件
→ UUID / AssetDB
→ 反序列化
→ Node / Component 实例
→ 资源依赖和运行时缓存