Skip to content

Stage02|Engine Loop ​

Lesson019|schedule、Action、Tween、Animation 的时间由谁推进? ​

Tags: #Creator2.x #Stage02 #Scheduler #Action #Tween #AnimationDifficulty: ⭐⭐⭐⭐☆


一、本课目标 ​

游戏中的“过一秒”“移动到某个位置”“播放一段动画”,本质上都需要时间推进。本课回答:

schedule、Action、Tween、Animation 看起来不同,为什么它们都必须依赖 Engine Loop?

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

  • 区分定时回调、属性插值、动作序列和动画采样。
  • 理解这些系统都需要 dt 或游戏时间。
  • 判断暂停、节点禁用和时间缩放分别影响什么。
  • 避免多个系统同时写同一 Transform 属性。

二、它们都是“时间驱动的状态变化” ​

text
当前状态
    ↓ dt 推进
计算新状态
    ↓
应用到 Node / Component
    ↓
下一帧继续

例如移动动画:

text
起点 x = 0
目标 x = 100
持续时间 = 1 秒
本帧经过 dt
    ↓
计算当前进度
    ↓
得到新的 x

区别只在于:谁保存状态、谁计算进度、谁应用结果。


三、schedule:到时间执行一次逻辑 ​

ts
this.scheduleOnce(() => {
    this.showResult();
}, 1);

高层模型:

text
登记回调
    ↓
Scheduler 每帧推进时间
    ↓
达到 1 秒
    ↓
调用回调

它适合:

  • 延迟打开 UI。
  • 周期性刷新。
  • 低频轮询。
  • 超时检查。

它不适合代替每帧连续运动或高频插值。


四、Action:描述可组合的动作 ​

Action 更接近“对节点执行一段动作”:

ts
const move = cc.moveTo(1, 100, 0);
const wait = cc.delayTime(0.5);
const sequence = cc.sequence(move, wait);

this.node.runAction(sequence);

内部需要保存:

text
动作当前进行到哪一步
已经经过多少时间
当前插值进度
下一个动作是什么

每帧推进可以抽象成:

text
Action.update(dt)
    ↓
更新当前动作进度
    ↓
计算节点新状态
    ↓
节点 Transform 被修改

Action 不是独立线程,它仍然依赖引擎主循环。


五、Tween:对属性进行插值 ​

Tween 通常表达“让某个属性从 A 平滑到 B”:

ts
cc.tween(this.node)
    .to(1, { scale: 2 })
    .start();

它需要处理:

text
开始值
目标值
持续时间
缓动函数
当前时间

不同缓动函数只改变进度曲线:

text
linear:匀速
easeIn:开始慢
easeOut:结束慢
easeInOut:两端慢、中间快

但无论曲线怎样,最终仍然要在每帧根据时间算出一个属性值。


六、Animation:按时间采样关键帧 ​

Animation 更接近“在时间轴上采样关键帧”:

text
时间 0.0s:位置 A
时间 0.2s:位置 B
时间 0.5s:位置 C
时间 1.0s:位置 D

每帧需要:

text
累计动画时间
    ↓
根据播放速度和循环规则取样
    ↓
插值关键帧
    ↓
写入绑定属性

Animation 可能同时修改多个属性,因此更需要避免业务脚本在同一帧持续覆盖动画结果。


七、谁来推进这些系统 ​

可以用这个简化模型理解:

text
Director.mainLoop(dt)
    ↓
时间调度器推进 schedule
    ↓
Action / Tween / Animation 根据 dt 更新
    ↓
ComponentScheduler 调用 update / lateUpdate
    ↓
Transform / Renderer 使用最终状态

不同 Creator 2.x 小版本可能改变 Action、Animation、Scheduler 的具体调用位置。稳定结论是:

没有主循环推进,就没有正常的游戏时间,也就没有这些系统的持续变化。


八、游戏时间和真实时间 ​

一个系统可能使用:

text
游戏时间:受 pause、timeScale、游戏规则影响
真实时间:来自平台时钟,不一定随游戏暂停

例如:

  • 战斗动画通常跟随游戏时间。
  • 登录超时可能需要真实时间或网络时间。
  • 后台恢复检测需要平台时间。
  • UI 动画是否暂停取决于产品设计。

不要只看到 dt 就认为所有业务时间含义相同。


九、多个系统同时写同一个属性 ​

错误组合:

text
Tween 写 node.x
Animation 写 node.x
Player.update 也写 node.x

结果可能是:

  • 位置抖动。
  • 动画被覆盖。
  • 结束回调时状态不符合预期。
  • 顺序变化后出现不同结果。

更好的设计是指定单一写入者:

text
业务层决定目标状态
    ↓
动画层负责表现插值
    ↓
Renderer 只读取最终状态

或者把业务位置和表现位置分开:

text
modelPosition:逻辑位置
visualPosition:动画表现位置

十、暂停和停止的区别 ​

text
pause:保留当前进度,之后继续
stop:结束当前动作,可能回到初始或结束状态
destroy:对象生命周期终止

节点 active=false、组件 enabled=false、Action 暂停和 cc.game.pause() 可能作用于不同层级。

设计暂停功能时要列清楚:

text
角色输入是否暂停
动画是否暂停
倒计时是否暂停
网络心跳是否暂停
UI 提示是否继续

不要用一个全局布尔值假设所有系统都应同样暂停。


十一、案例:倒计时结束后播放奖励动画 ​

ts
startCountDown() {
    this.scheduleOnce(() => {
        this.showReward();
        cc.tween(this.rewardNode)
            .to(0.3, { scale: 1.2 })
            .to(0.2, { scale: 1 })
            .start();
    }, 3);
}

运行链路:

text
Scheduler 推进 3 秒倒计时
    ↓
回调触发
    ↓
showReward 修改 active / 数据
    ↓
Tween 被登记
    ↓
后续每帧推进 Tween
    ↓
修改 rewardNode.scale
    ↓
Renderer 显示动画结果

如果界面在 3 秒内被关闭,应在 onDisable 中取消任务或检查对象状态,避免回调访问已经失效的节点。


十二、常见误区 ​

误区一:Tween 自己有独立计时器 ​

它通常依赖引擎帧推进,不是后台线程。

误区二:scheduleOnce 的 1 秒一定在精确 1.000 秒执行 ​

它受帧回调、主线程阻塞和平台调度影响。

误区三:Animation 和业务脚本同时写同一属性没问题 ​

最后结果取决于更新顺序和写入者,容易产生覆盖和抖动。

误区四:暂停游戏一定等于暂停所有网络和系统任务 ​

全局暂停主要控制游戏主循环,平台回调和外部服务需要单独设计。


十三、练习与答案 ​

练习 ​

  1. schedule、Action、Tween、Animation 的共同点是什么?
  2. 为什么 Tween 仍然需要 mainLoop?
  3. 同一属性被 Animation 和业务 update 同时写会怎样?
  4. 倒计时 UI 关闭后,为什么要取消 schedule 或检查对象状态?
  5. 游戏暂停时,哪些时间任务可能应该继续?

参考答案 ​

  1. 它们都需要时间输入,在每帧推进状态变化。
  2. Tween 要根据每帧 dt 计算插值结果。
  3. 结果可能互相覆盖或抖动,具体取决于更新顺序。
  4. 防止回调访问无效对象、重复刷新或产生泄漏。
  5. 例如网络保活、系统提示或后台恢复检测,但要按产品语义单独决定。

十四、Creator 2.4.x 可复现实验:四种时间驱动 ​

在一个节点上同时创建 schedule、Action、Tween 和 Animation,给同一个 Label 或 Sprite 设置不同属性,分别记录开始、结束和取消时刻。示例:

ts
this.scheduleOnce(() => cc.log('[scheduleOnce]'), 1);
cc.tween(this.node).to(1, { opacity: 0 }).call(() => cc.log('[tween:done]')).start();
this.node.runAction(cc.sequence(cc.moveBy(1, 100, 0), cc.callFunc(() => cc.log('[action:done]'))));

运行 2 秒后将节点设为 inactive,再重新启用。观察组件归属的 schedule、Action、Tween 和 Animation 是否暂停或继续,并检查重新启用后是否重复创建。不同系统对 active、pause 和 stop 的处理应以实验为准,不能仅凭“都是动画”推断。

故障诊断 ​

现象可能原因
动画结束回调执行两次start/runAction 被重复调用,或旧实例未 stop
关闭页面后仍改 UI全局调度、Tween owner 未清理,或回调持有旧节点
同一属性抖动Animation、Tween、update 存在多个写入者

十五、版本边界 ​

课程示例覆盖 Creator 2.4.x 常用 schedule、runAction、cc.tween 和 Animation 组件。Tween 的内部实现、Action 与 ComponentScheduler 的相对调用点以及暂停传播细节可能随版本变化;稳定工程原则是明确 owner、唯一写入者和对称取消。

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

题目: 一个倒计时在 timeScale=0 时应该停住,但网络超时仍要生效,怎样拆分?

答案: 倒计时显示使用游戏时间或组件 dt,受 timeScale 影响;网络超时使用真实时间源或平台计时器,由网络层独立管理。回到主线程后再检查页面 session,避免把两种时间语义混在同一个 schedule 中。

十七、本课总结 ​

text
schedule 是时间到点执行回调。
Action 是可组合的动作过程。
Tween 是属性插值。
Animation 是时间轴采样。
它们都依赖 Engine Loop 推进。
同一属性应尽量只有一个明确的写入者。

十八、下一课预告 ​

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