Cocos从 3.8.4 升级到 3.8.7 后,Tween 作用在 inactive 节点上不执行的问题记录

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 状态。

引擎从3.8.3升级到3.8.6后,tween缓动细节差异导致bug
:yum: 这个我之前就反馈过了。 bindNodeState 就是官方那次后加的。
立功!

1赞

怪不得,不去翻阅文档或者源码根本看不出来哪里出了问题

:smiley: 希望官方后续能自己开一个高质量的游戏开发项目吧。
很多版本细节差异都苦了我们开发者
得让他们自己做个游戏,踩踩自己引擎的坑,才能体会到我们的不容易 qwq