Skip to content

Stage02|Engine Loop ​

Lesson013|Director 是什么?为什么它是引擎调度中心? ​

Tags: #Creator2.x #Stage02 #Director #Scene #EngineLoopDifficulty: ⭐⭐⭐☆☆


一、本课目标 ​

上一课回答了“每一帧从哪里开始”。本课继续回答:

谁长期保存当前运行状态,并把场景、调度、时间和渲染串起来?

完成本课后,你应该能够:

  • 说明 cc.director 和 cc.game 的职责边界。
  • 解释 Director 为什么需要保存当前 Scene、暂停状态和帧时间。
  • 区分“场景切换请求”和“场景已经切换完成”。
  • 理解 Director 是协调者,不是所有子系统的实现者。
  • 在排查问题时判断应该先看 Director、Scene 还是 Component。

二、上节课回顾 ​

Lesson012 建立了这条链路:

text
平台帧回调
→ cc.game
→ cc.director.mainLoop(dt)
→ Scheduler / ComponentScheduler
→ update / lateUpdate
→ Transform / Renderer

mainLoop 是每帧入口,但它需要一个对象保存“当前游戏处于什么状态”。这个对象就是 Director。


三、Director 的核心职责 ​

可以先把 Director 理解成场景运行时的总协调器:

text
Director
├── 当前 Scene
├── 场景切换状态
├── 主循环状态
├── 暂停和时间信息
├── Scheduler / ComponentScheduler
├── 场景更新和绘制入口
└── 引擎阶段事件

它通常负责:

  1. 保存当前运行场景。
  2. 接收和执行场景切换请求。
  3. 进入每帧 mainLoop。
  4. 驱动调度器和场景更新。
  5. 组织渲染前后的阶段事件。
  6. 处理暂停、重启或清理等全局状态。

Director 不负责:

  • 直接实现 Button 点击逻辑。
  • 直接保存每个业务对象的血量。
  • 替代 Renderer 生成全部顶点。
  • 替代资源系统读取所有文件。

它的价值在于连接这些系统,而不是把所有代码都写进一个巨型对象。


四、cc.game 和 cc.director 的边界 ​

问题更接近谁
游戏是否启动cc.game
平台帧回调从哪里来cc.game / 平台层
当前运行哪个场景cc.director
场景何时切换cc.director
当前帧如何推进cc.director.mainLoop
一个脚本的 update 是否执行ComponentScheduler
画面如何提交Renderer

可以用两句口诀记忆:

text
cc.game 让游戏跑起来。
cc.director 让当前场景持续向前推进。

常见错误理解 ​

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

在 Creator 2.x 中,场景加载通常通过 Director 相关 API 完成,例如:

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

具体 API 以当前 2.x 小版本为准,但职责边界不变:场景切换属于 Director 管理的范围。


五、Director 为什么需要保存当前 Scene ​

每帧更新不能没有“更新谁”:

text
Director.currentScene
    ↓
场景根节点
    ↓
子节点树
    ↓
可调度的 Component

如果当前 Scene 为空,Director 不能正常推进场景节点和组件。场景切换时,Director 需要处理:

text
旧 Scene
    ↓
退出、清理或保留常驻对象
    ↓
加载新 Scene
    ↓
创建节点和组件
    ↓
进入新 Scene 生命周期
    ↓
继续 mainLoop

这也是为什么切场景不是简单地把一个根节点换成另一个根节点。


六、场景切换请求和完成不是一回事 ​

业务代码写下:

ts
cc.director.loadScene('Battle');
cc.log('request sent');

这里首先发生的是“提出切换请求”。真正的切换还可能包括资源加载、旧场景退出、新场景创建和生命周期执行。

text
loadScene('Battle')
    ↓
记录目标场景
    ↓
准备资源和序列化数据
    ↓
清理旧场景或切换常驻根节点
    ↓
创建 Battle Scene
    ↓
执行新节点生命周期
    ↓
新场景进入调度

因此不要在调用 loadScene 的下一行假设新场景的所有节点已经完成 start。


七、Director 是协调者,不是巨型业务类 ​

一个合理的职责分工是:

text
Director:什么时候推进
Scheduler:哪些任务到时间
ComponentScheduler:哪些组件可执行
Scene:当前节点树是什么
Node:层级、Transform、active
Component:具体能力
Renderer:如何画出来

如果把所有业务逻辑都放到 Director,最终会出现:

  • 场景逻辑和引擎逻辑互相依赖。
  • 场景切换时难以清理业务状态。
  • 任何一个系统变化都可能影响全局。
  • 测试和定位问题变得困难。

高级开发不是把所有机制都归因于 Director,而是能继续向下找到真正负责的子系统。


八、Director 相关的排查方法 ​

遇到“场景没有更新”时,可以按层排查:

text
1. 游戏是否启动或处于 pause?
2. Director 是否有当前 Scene?
3. 当前 Scene 是否已经进入运行状态?
4. 目标 Node 是否在 Scene 树中?
5. Node 的 activeInHierarchy 是否为 true?
6. Component.enabled 是否为 true?
7. Component 是否实现了 update?
8. Scheduler 是否仍在推进?

不要一看到 update 没打印,就直接认为“Director 坏了”。大多数问题发生在节点状态、组件状态或业务对象已经被销毁。


九、综合案例:点击按钮切换场景 ​

ts
onClickStart() {
    cc.director.loadScene('Battle');
}

从引擎视角可以拆成:

text
Button 事件触发
    ↓
业务脚本调用 Director API
    ↓
Director 记录场景切换请求
    ↓
旧场景进入退出流程
    ↓
加载并反序列化 Battle
    ↓
创建 Node / Component
    ↓
执行 onLoad / onEnable / start
    ↓
Battle 进入 Director 的主循环

这里 Button 只负责交互,业务脚本负责提出请求,Director 负责场景运行时切换,Scene 和资源系统负责具体对象创建。每一层职责都不同。


十、常见误区 ​

误区一:Director 就是当前场景的根节点 ​

不是。Director 管理当前场景,但它不是场景树中的普通 Node。

误区二:调用 loadScene 后旧场景马上消失、新场景马上可用 ​

场景切换可能涉及异步加载、序列化、生命周期和资源处理。应该通过加载回调或新场景生命周期确认切换完成。

误区三:Director 直接调用每个脚本的 update ​

Director 负责推进调度流程,具体组件筛选和生命周期调用由组件调度系统完成。

误区四:只要 Director 在跑,所有 update 都会执行 ​

组件还必须满足 enabled、activeInHierarchy、实现方法和未销毁等条件。

误区五:把业务状态放到 Director 里最方便 ​

短期看似方便,长期会让场景、系统和全局状态耦合。业务对象应由业务组件或独立服务管理。


十一、综合练习 ​

请回答:

  1. cc.game 和 cc.director 的职责边界是什么?
  2. 为什么 Director 需要保存当前 Scene?
  3. 调用 cc.director.loadScene('Battle') 后,为什么不能立刻使用 Battle 场景中的所有对象?
  4. 如果当前 Scene 正常显示,但某组件的 update 没执行,应该先查 Director 还是组件状态?
  5. 为什么不建议把所有业务状态都放到 Director?

参考答案 ​

  1. cc.game 管游戏级启动和平台帧驱动,cc.director 管当前场景和每帧推进。
  2. Director 必须知道当前要更新和绘制的节点树。
  3. 场景切换还包括资源、序列化、节点创建和生命周期。
  4. 先检查组件 enabled、节点 activeInHierarchy、是否被销毁和调度条件,再检查全局状态。
  5. Director 是协调层,混入业务会造成全局耦合、难以清理和难以测试。

十二、Creator 2.4.x 可复现实验:Director 状态探针 ​

实验目标 ​

验证“请求切场景”和“新场景已经接管运行”是两个时刻,并观察 Director 在切换前后的当前场景。

操作步骤 ​

  1. 用 Creator 2.4.x 新建 DirectorLab 场景,再复制出 DirectorLabB。
  2. 在两个场景根节点各挂一个脚本,脚本的 onLoad、start、update 分别打印场景名。
  3. 在 A 场景加入按钮,点击时执行:
ts
cc.log('[before]', cc.director.getScene().name);
cc.director.loadScene('DirectorLabB', () => {
    cc.log('[callback]', cc.director.getScene().name);
});
cc.log('[after-request]', cc.director.getScene() && cc.director.getScene().name);
  1. 运行预览并点击按钮,记录三组日志的顺序。

预期现象 ​

before 一定是 A;callback 应该是 B。after-request 不应被当作“B 已完成初始化”的证明,因为加载可能异步完成。B 的 onLoad、onEnable、start 日志会出现在接管阶段附近,具体相邻顺序以当前 2.4.x 小版本和平台为准。

失败诊断 ​

现象优先检查
回调不执行场景是否加入 Build Settings、路径和大小写是否正确
当前场景仍是 A是否实际点击、是否有加载错误、是否被第二次切换覆盖
B 的脚本不打印脚本是否挂载、节点是否 active、脚本编译是否成功

十三、版本边界与源码验证 ​

本课按 Creator 2.4.x 的公开 API 讲解。cc.director.loadScene、getScene 和组件调度是稳定入口;切换内部的加载器、事件名、清理时机和不同平台的帧间插入点属于实现细节。需要精确到源码时,锁定具体 2.4.x tag,再搜索 CCDirector、loadScene 和 ComponentScheduler,不要把某次日志顺序扩展成所有版本的契约。

十四、诊断题:场景切换后按钮仍响应 ​

旧场景的按钮仍能响应,不能先归咎于 Director 没有切换。按以下链路排查:

text
是否真的切到 B?
→ 旧按钮是否来自 DontDestroyOnLoad/常驻节点?
→ 旧事件监听是否注册在全局对象?
→ 回调是否持有旧节点并主动修改它?
→ 是否有第二个 Canvas 或叠加层仍可见?

解决方案是让页面级监听在 onDisable/onDestroy 对称清理,并让常驻服务只派发业务事件,不持有已离场页面的视图引用。

十五、本课总结 ​

text
cc.game:游戏级入口
Director:场景级运行时协调者
Scene:当前节点树
Scheduler:时间任务
ComponentScheduler:组件生命周期和每帧方法
Renderer:把运行状态变成画面

最终记住:

Director 不是“所有代码都放进去的地方”,而是让不同引擎系统在正确时间协同工作的中心。


十六、下一课预告 ​

text
Lesson014|Scene 是什么?加载和切换场景时发生了什么?

下一课会进一步拆解 Scene、场景树、场景切换和常驻节点。