-
Notifications
You must be signed in to change notification settings - Fork 2k
Description
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)
进行搜索.
所以此处有两个修改建议:
-
一个方案是像官方一样, 暴露 update(), 让开发者自己决定什么时候 调用update(). 这个方案可能会对现有项目产生影响, 并且增加一点点学习成本. 除非非常了解 spine, 否则什么时候调用 update(0) 是一个比较迷惑的行为.
-
另一个方案则是 把 update(0) 从 jsb的 setAnimation() 方法里移除, 改为设置一个标志位, 然后在游戏的 update, lateUpdate 阶段之后再去执行, 比如在 spine 的 render(), postUpdate() 一类的方法里 去执行 update(0).
我看 spine 官方提供的 2dx runtime 里 就是用的类似思路.

问题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试 以及 参考各种其他平台的实现 整理总结的. 并不排除有隐藏的问题和风险.
不过有一点可以保证, 这些并不是纯脑洞, 我已经在我的自定义引擎里实践了.
希望官方可以考虑下.