Cocos Creator 从 3.8.4 升级到 3.8.7 后,Tween 作用在 inactive 节点上不执行的问题记录
最近我把项目的 Cocos Creator 版本从 3.8.4 升级到了 3.8.7,发现一个原本正常运行的按钮入场动画突然失效了。
问题不是代码语法错误,而是 3.8.7 中 Tween 对 Node 激活状态的绑定行为发生了变化。
一、原来的写法
我原来的按钮入场动画逻辑大致如下:
protected playEntranceButton(node: Node, targetScale: Vec3, delay: number, onComplete?: () => void): void {
if (!node) {
onComplete?.();
return;
}
Tween.stopAllByTarget(node);
node.setScale(this.getEntranceButtonStartScale(targetScale));
tween(node)
.delay(delay)
.call(() => {
node.active = true;
})
.to(0.24, {
scale: new Vec3(targetScale.x * 1.08, targetScale.y * 1.08, targetScale.z)
}, { easing: 'backOut' })
.to(0.12, { scale: targetScale.clone() }, { easing: 'sineIn' })
.call(() => {
onComplete?.();
})
.start();
}
这个写法在 Cocos Creator 3.8.4 中是可以正常运行的。
我的逻辑是:
先让节点保持 inactive;
Tween 开始后先 delay;
delay 结束后通过 .call() 把节点设置为 active;
然后执行缩放入场动画。
但是升级到 3.8.7 之后,这段代码就不执行了。准确地说,Tween 启动后因为目标节点是 inactive,后面的 .call() 根本不会被执行,所以节点也就不会被重新激活。
二、原因分析
我对比了本机的 Cocos Creator 3.8.4 和 3.8.7 引擎源码,发现关键差异在这个文件中:
C:\ProgramData\cocos\editors\Creator\3.8.7\resources\resources\3d\engine\cocos\tween\actions\action-manager.ts
3.8.7 中新增了如下逻辑:
const registerNodeEvent = isBindNodeState && element.actions.length === 0 && target instanceof Node;
if (registerNodeEvent) {
this._registerNodeEvent(target);
if (!target.active) {
element.paused = true;
}
}
也就是说,在 3.8.7 中,如果 Tween 的目标是一个 Node,并且这个节点当前是 inactive,那么 Tween 会根据节点激活状态自动暂停。
而 3.8.4 中虽然也会注册节点事件,但是没有这一句:
if (!target.active) {
element.paused = true;
}
所以在 3.8.4 里,即使 node.active = false,Tween 仍然会继续运行。它可以正常执行到:
.call(() => {
node.active = true;
})
但是到了 3.8.7,Tween 启动时发现目标节点是 inactive,就直接暂停了。于是后面的 .call() 永远不会触发,节点也不会被激活,后续动画自然也不会执行。
三、解决方案一:不要让 Tween 在 inactive 节点上 delay
我现在采用的改法是:不要把 delay 写在 inactive 节点的 Tween 里,而是先用 scheduleOnce 延迟,等延迟结束后先激活节点,再启动 Tween。
修改后的代码如下:
protected playEntranceButton(node: Node, targetScale: Vec3, delay: number, onComplete?: () => void): void {
if (!node) {
onComplete?.();
return;
}
this.scheduleOnce(() => {
if (!node || !node.isValid) {
onComplete?.();
return;
}
Tween.stopAllByTarget(node);
node.active = true;
node.setScale(this.getEntranceButtonStartScale(targetScale));
tween(node)
.to(0.24, {
scale: new Vec3(targetScale.x * 1.08, targetScale.y * 1.08, targetScale.z),
}, { easing: 'backOut' })
.to(0.12, { scale: targetScale.clone() }, { easing: 'sineIn' })
.call(() => onComplete?.())
.start();
}, delay);
}
这种写法的思路是:
延迟逻辑交给 scheduleOnce;
延迟结束后先判断节点是否还有效;
然后设置 node.active = true;
最后再启动 Tween。
这样可以避免 Tween 一开始绑定到 inactive 节点后被暂停。
四、解决方案二:使用 bindNodeState(false) 恢复旧行为
Cocos Creator 3.8.7 中还新增了一个接口:
bindNodeState(false)
如果希望临时恢复 3.8.4 的行为,也可以这样写:
tween(node)
.bindNodeState(false)
.delay(delay)
.call(() => {
node.active = true;
})
.to(0.24, {
scale: new Vec3(targetScale.x * 1.08, targetScale.y * 1.08, targetScale.z),
}, { easing: 'backOut' })
.to(0.12, { scale: targetScale.clone() }, { easing: 'sineIn' })
.call(() => onComplete?.())
.start();
这样 Tween 就不会再跟随 Node 的 active 状态自动暂停,可以继续执行到 .call(),从而把节点重新激活。
不过我个人更倾向于第一种写法,也就是先通过 scheduleOnce 控制延迟,再激活节点并启动 Tween。这样逻辑更清晰,也更符合 3.8.7 之后的行为设计。
五、总结
这次问题的核心原因是:
Cocos Creator 3.8.7 中 Tween 默认会绑定 Node 的激活状态。当目标节点 inactive 时,Tween 会被暂停。
因此,如果旧代码中存在这种写法:
node.active = false;
tween(node)
.delay(...)
.call(() => {
node.active = true;
})
.start();
在 3.8.7 中就可能失效。
可选解决方法有两个:
第一种是把 delay 从 Tween 中移出来,用 scheduleOnce 延迟后再激活节点并启动 Tween。
第二种是使用:
.bindNodeState(false)
让 Tween 不再绑定 Node 的 active 状态,从而恢复旧版本行为。
这类问题比较隐蔽,因为代码本身没有语法错误,报错也不一定明显。如果项目从 3.8.4 升级到 3.8.7 后发现某些 Tween 动画突然不执行,可以优先检查目标节点启动 Tween 时是否处于 inactive 状态。
这个我之前就反馈过了。 bindNodeState 就是官方那次后加的。
希望官方后续能自己开一个高质量的游戏开发项目吧。