Skip to content

[spine] 关于 setAnimation() 和 addAnimation() 的 若干问题和建议 . #18580

@finscn

Description

@finscn

Cocos Creator version

3.8.6

System information

all

Issue description

本帖只讨论 spine 3.8 的问题. 4.2 还没有测试.

我在项目中 遇到了奇怪的问题, 先看下面两段代码:

片段 A :

        this.spine.clearTrack(trackIndex);
        const trackEntry = this.spine.setAnimation(trackIndex, animName, loop);
        trackEntry.animationStart = 2;
        trackEntry.animationEnd = 6;
        trackEntry.trackTime = 0 ;

片段 B :

        this.spine.clearTrack(trackIndex);
        const trackEntry = this.spine.addAnimation(trackIndex, animName, loop);
        trackEntry.animationStart = 2;
        trackEntry.animationEnd = 6;
        trackEntry.trackTime = 0 ;

因为 我第一行都执行了 clearTrack, 按照 spine 官方的说法 片段A 和 片段B 的运行效果应该是一样的 .

但是实际运行效果不一致. 而且 jsb模式 和 wasm模式遇到的问题还不一样.


jsb 模式下的问题

jsb 模式下, 片段B (使用 addAnimation ) 更符合预期, 片段A 始终会显示一下动画的第一帧 ,从而导致动画闪烁.

问题1 : 关于 update(0)

经过阅读源码, 发现了一个问题.

jsb 环境下, setAnimation() 方法内会强制调用一个 update(0) 方法. 而 addAnimation() 并不会.
我又查阅了一下 其他的 spine 参考资料, 因为spine 设计的"缺陷" , 这个 update(0) 有时候确实是需要被执行的.

但是 它不是一定要在 setAnimation() 后立刻被执行. 比如我这里的这个例子, 它就应该在 设置完 trackEntry 之后再调用: 比如这样:

        this.spine.clearTrack(trackIndex);
        const trackEntry = this.spine.setAnimation(trackIndex, animName, loop);
        trackEntry.animationStart = 2;
        trackEntry.animationEnd = 6;
        trackEntry.trackTime = 0 ;
        // 此时再调用  update(0)
        this.spine.update(0);

因为update() 方法中 会用到 trackEntry 上的一些关于时间的属性, 所以 如果这些属性变化了, 必须重新 update(0).
在 setAnimation() 中直接 update(0) 是不对的.

所以 spine 官方的做法 其实是把 update(0) 这个方法暴露出来, 让开发者自己决定什么时候调用.
更多的参考可以去 https://github.com/EsotericSoftware/spine-runtimes 里用关键字 update(0) 进行搜索.

所以此处有两个修改建议:

  1. 一个方案是像官方一样, 暴露 update(), 让开发者自己决定什么时候 调用update(). 这个方案可能会对现有项目产生影响, 并且增加一点点学习成本. 除非非常了解 spine, 否则什么时候调用 update(0) 是一个比较迷惑的行为.

  2. 另一个方案则是 把 update(0) 从 jsb的 setAnimation() 方法里移除, 改为设置一个标志位, 然后在游戏的 update, lateUpdate 阶段之后再去执行, 比如在 spine 的 render(), postUpdate() 一类的方法里 去执行 update(0).
    我看 spine 官方提供的 2dx runtime 里 就是用的类似思路.

Image

问题2 : 关于 markForUpdateRenderData()

其他模式的 updateAnimation 方法中, 会调用 markForUpdateRenderData() 方法.
但是 jsb-spine-skeleton.js 文件中的的 updateAnimation() 却没有.

如果 不添加 markForUpdateRenderData() , 某些情况下会出现 spine 更新不及时的问题.
所以建议添加, 从而保证所有模式 行为一致.

    skeleton.updateAnimation = function (dt) {
        const nativeSkeleton = this._nativeSkeleton;
        if (!nativeSkeleton) return;

        const node = this.node;
        if (!node) return;

        // 建议此处添加
        this.markForUpdateRenderData();

        // ......
    }


wasm 模式下的问题

wasm 模式下, 片段A (使用 setAnimation ) 更符合预期, 片段A 始终会闪烁一下.
(和 jsb模式相反)

问题1 : 关于 _animState->apply(*_skeleton) (更新)

setAnimation() 调用的是 spine-skeleton-instance.cpp 中的 TrackEntry *SpineSkeletonInstance::setAnimation() 方法.

addAnimation() 调用的是 3.8/spine/AnimationState.cpp 中的 TrackEntry *AnimationState::addAnimation() 方法.

前者(SpineSkeletonInstance) 最后其实调用的也是 AnimationState::setAnimation() , 但是 前者会在调用完 AnimationState::setAnimation() 后, 再去执行一下 _animState->apply(*_skeleton) 方法.

经过验证 _animState->apply(*_skeleton) 是问题的关键.

修改建议:

在 spine-skeleton-instance.cpp 中封装一个 addAnimation() 方法, 此方法 在调用 AnimationState::addAnimation() 后, 也执行一次 _animState->apply(*_skeleton) , 抹平 setAnimation() 和 addAnimation() 的差异

问题2 : 多余的 updateWorldTransform() (更新)

spine-skeleton-instance.cpp 中的 TrackEntry *SpineSkeletonInstance::setAnimation() 方法里会调用

#ifdef CC_SPINE_VERSION_3_8
    _skeleton->updateWorldTransform();
#else
    _skeleton->updateWorldTransform(Physics::Physics_Update);
#endif

经过验证, 此处调用 updateWorldTransform() 是多余的, 因为后面 updateRenderData() 方法中会再次调用, 而updateRenderData() 方法每帧都会被执行.

问题3: 多余的 clearTracks()

在 spine-skeleton-instance.cpp 的 setAnimation() 方法是这样的:

TrackEntry *SpineSkeletonInstance::setAnimation(float trackIndex, const spine::String &name, bool loop) {
    if (!_skeleton) return nullptr;
    spine::Animation *animation = _skeleton->getData()->findAnimation(name);
    if (!animation) {
        _animState->clearTracks();
        _skeleton->setToSetupPose();
        return nullptr;
    }
    auto *trackEntry = _animState->setAnimation(trackIndex, animation, loop);
    _animState->apply(*_skeleton);
#ifdef CC_SPINE_VERSION_3_8
    _skeleton->updateWorldTransform();
#else
    _skeleton->updateWorldTransform(Physics::Physics_Update);
#endif
    return trackEntry;
}

没找到 animation 时, 直接返回就可以了 . 不应该 清空 所有的 track状态 重置整个动画.

这种做法有问题的. 目前没有暴露出问题, 是因为在 ts里 提前做了判断 , 所以不会走到这段逻辑.
但是并不意味着这段逻辑是正确的. 为了安全起见, 建议删除 clearTracks() 和 setToSetupPose() 这两行.

问题4: 关于 setAnimation() 中多余的 markForUpdateRenderData()

因为 updateAnimation() 方法中 会调用 markForUpdateRenderData() .
所以 setAnimation() 中的 markForUpdateRenderData() 建议删除.

问题5: 关于 findAnimation()

目前 spine-skeleton-instance.cpp 中暴露的方法 setAnimation() 方法 参数是 animName:string.

但是阅读代码 不难发现, 其实 在 js 端 已经 通过 findAnimation(animName) 找出了 animation对象.

如果 使用字符串传递给 wasm环境, 在wasm 中 还要再 findAnimation() 一次, 有一些性能损耗.
此处 可以参考 AnimationState中的 setAnimationWith 和 addAnimationWith 方法, 直接传入 已经找到 animation对象, 从而优化一点性能.


总结

提出本 issue 的最终目的是为了确保 前面提到的 片段A 和 片段B , 在web jsb wasm 环境下, 都能有一样的行为, 且行为正确.

因为spine 的设计过于诡异, 以及我本人对 cpp wasm 并不擅长, 所以这里的很多建议 都是我自己xjb试 以及 参考各种其他平台的实现 整理总结的. 并不排除有隐藏的问题和风险.
不过有一点可以保证, 这些并不是纯脑洞, 我已经在我的自定义引擎里实践了.

希望官方可以考虑下.


Metadata

Metadata

Assignees

Labels

BugNeeds TriageNeeds to be assigned by the team

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions