外观
Stage02|Engine Loop
Lesson017|update、lateUpdate 与 executionOrder
Tags: #Creator2.x #Stage02 #update #lateUpdate #executionOrderDifficulty: ⭐⭐⭐⭐☆
一、本课目标
本课解决:
多个组件都在每帧工作时,谁先执行、谁后执行,业务应该依赖什么?
重点理解:
update和lateUpdate的职责差异。- 为什么不能依赖节点树视觉顺序猜 update 顺序。
- execution order 适合解决什么问题,不适合解决什么问题。
- 如何减少组件之间的隐式时间依赖。
二、update 和 lateUpdate 的核心区别
text
update:本帧主要状态计算
lateUpdate:读取其他对象结果后的后置处理例如:
ts
// PlayerMove.ts
update(dt: number) {
this.node.x += this.speed * dt;
}
// CameraFollow.ts
lateUpdate() {
this.node.x = this.target.x;
}这里的设计意图是:Player 先更新,Camera 再读取结果。
但 lateUpdate 不是替代所有 update 的“更晚 update”。如果逻辑不依赖其他组件的本帧结果,应该继续使用 update。
三、为什么节点树顺序不是可靠契约
编辑器里看到:
text
Root
├── A
└── B不代表 A 上脚本的 update 一定永远先于 B。实际顺序可能受:
- 组件注册时机。
- 组件执行顺序配置。
- 调度器内部阶段。
- 动态启用和禁用。
- 场景加载顺序。
影响。
因此不要写出依赖“Hierarchy 上谁在上面”的业务逻辑。
四、executionOrder 适合解决什么问题
当两个脚本确实存在稳定的阶段依赖,可以使用引擎提供的执行顺序配置或建立显式管理器:
text
输入采集系统
↓
角色状态系统
↓
相机跟随系统
↓
UI 表现系统execution order 适合表达少量、明确、稳定的优先级关系。
它不适合:
- 给几十个脚本排出复杂全序。
- 代替模块之间的数据接口。
- 掩盖循环依赖。
- 让系统依赖一个难以维护的数字表。
如果必须配置大量数字才能让项目工作,通常说明系统边界需要重构。
五、一个典型的错误依赖
ts
// A.ts
update() {
this.value += 1;
}
// B.ts
update() {
this.label.string = String(this.value);
}如果 B 在 A 之前执行,UI 会慢一帧;如果顺序改变,结果又不同。这种隐式依赖很难排查。
更清晰的方式是由一个控制器统一组织:
ts
update(dt: number) {
this.model.update(dt);
this.view.refresh(this.model);
}或者明确使用事件、数据快照和阶段接口。
六、lateUpdate 的常见应用
1. 相机跟随
ts
lateUpdate() {
this.camera.position = this.target.position;
}2. UI 同步
ts
lateUpdate() {
this.hpBar.progress = this.player.hp / this.player.maxHp;
}3. 读取物理或动画结果
ts
lateUpdate() {
this.effect.position = this.actor.position;
}这些案例共同点是:当前脚本需要读取本帧其他系统先产生的状态。
七、lateUpdate 也可能带来问题
如果所有脚本都把逻辑放到 lateUpdate,会导致:
- 逻辑阶段失去层次。
- 不清楚谁先产生数据。
- 仍然存在同阶段内的顺序依赖。
- 本帧渲染前堆积大量工作。
正确原则不是“依赖别的组件就用 lateUpdate”,而是先明确数据生产和消费关系,再选择:
text
显式调用
事件
数据快照
阶段管理器
executionOrder
lateUpdatelateUpdate 只是其中一种工具。
八、当前帧修改状态的可见时机
text
update 修改 Player
↓
lateUpdate 读取 Player
↓
Transform / Render 读取结果这是一种常见的教学模型。实际模块可能在自己的阶段更新,例如 Animation、Physics、Action 和 UI Layout 不一定都由同一个脚本阶段完成。
遇到“为什么读取到旧值”时,要问:
- 这个值由哪个系统产生?
- 产生系统在本帧什么时候执行?
- 当前脚本在哪个阶段读取?
- 读取的是本地值、世界值还是渲染缓存?
九、案例:相机跟随抖动
错误结构:
text
Player.update 修改位置
Camera.update 读取位置
动画系统稍后又修改 Player可能出现:
- 相机跟随慢一帧。
- 角色和相机不同步。
- 动画和业务互相覆盖位置。
改进思路:
text
输入
→ 角色逻辑
→ 动画 / 物理结果
→ 相机 lateUpdate
→ 渲染并且明确谁是 Player 位置的最终写入者,避免多个系统同时写同一属性。
十、常见误区
误区一:lateUpdate 一定在所有 update 之后且所有组件都已完成
它只能说明脚本生命周期阶段的相对意图,其他引擎模块仍可能有独立阶段。
误区二:executionOrder 数字越小越晚
不要凭记忆猜具体数值规则,应以 Creator 2.x 当前版本文档或源码确认。
误区三:给所有脚本设置顺序就能解决架构问题
复杂的全序依赖通常说明模块耦合过高。
误区四:同一 Node 上的组件一定按书写顺序更新
组件顺序不能替代明确的调度契约。
十一、练习与答案
练习
- 什么时候适合使用 lateUpdate?
- 为什么不能依赖 Hierarchy 面板顺序?
- executionOrder 的正确使用边界是什么?
- 如何设计一个避免相机慢一帧的更新链?
- 如果多个系统同时写 Player.position,应该先解决什么问题?
参考答案
- 当逻辑需要读取其他系统本帧产生的结果时。
- 实际调度还受注册、配置、动态状态和内部队列影响。
- 用于少量明确的阶段依赖,不用于掩盖复杂架构耦合。
- 明确输入、角色结果、动画/物理、相机和渲染的阶段顺序。
- 先确定唯一的状态生产者,其他系统通过数据读取或事件消费。
十二、Creator 2.4.x 可复现实验:验证更新阶段
创建 Player、Camera、HUD 三个节点,分别挂载:
ts
// PlayerProbe
update() { this.node.x += 10; cc.log('[player:update]', this.node.x); }
// CameraProbe
lateUpdate() { cc.log('[camera:late]', this.node.x); }
// HudProbe
lateUpdate() { cc.log('[hud:late]', this.node.x); }先不设置 executionOrder,记录多次运行结果;再只给 Player、Camera 设置明确的顺序,比较日志是否稳定。把 Camera 的读取从 update 改到 lateUpdate,观察相机跟随是否少一帧。实验重点不是背一个固定数字,而是找出“谁生产状态、谁消费状态、在哪个阶段消费”。
故障复现
让 Player 和 Camera 都在 update 写同一个位置,故意制造抖动;然后改为 Player 唯一写入、Camera 只读并在 lateUpdate 同步。若仍抖动,检查 Animation、Tween、物理系统是否也是写入者。
十三、版本边界与诊断
Creator 2.4.x 提供 executionOrder 作为组件级排序手段,但同一阶段内部的完整顺序、动态启用时的插入点和引擎系统相对位置不应靠猜测。精确判断时记录版本、平台、节点状态,并查看当前版本 ComponentScheduler 实现。Hierarchy 顺序不是稳定的业务契约。
十四、应用题与推导答案
题目: 角色移动偶尔被 UI 脚本覆盖,应该先调大 UI 的 executionOrder 吗?
答案: 不应先改数字。先用日志记录所有写入 position 的调用点,确定唯一生产者;UI 应读取角色状态而不是写角色 Transform。只有在生产者和消费者边界明确后,才用 executionOrder 表达少量必要的阶段关系。否则排序只是把竞态隐藏到另一个数字里。
十五、本课总结
text
update 负责本帧主要计算。
lateUpdate 适合读取其他系统结果并做后置同步。
Hierarchy 顺序不是完整的调度契约。
executionOrder 应少量、明确地使用。
复杂顺序依赖应通过数据和系统设计解决。十六、下一课预告
text
Lesson018|active、activeInHierarchy、enabled 在当前帧如何改变调度?