3.8.1:Spine监听器无效

测试代码如下

import { sp } from 'cc';
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('SpineTest')
export class SpineTest extends Component {
    private _spine!: sp.Skeleton;
    onLoad() {
        this._spine = this.getComponent(sp.Skeleton)!
    }
    start() {
        console.log("start")
        let tr = this._spine.setAnimation(0, "walk", false)!
        this._spine.setTrackCompleteListener(tr, () => {
            console.log("completed")
        })
        this._spine.setEndListener(() => {
            console.log("end")
        })
    }

    update(deltaTime: number) {

    }
}


控制台只输出了一个 start,completed 和 end 都没有输出。
经过测试,onEvent 也无效,似乎是所有监听器事件都无效。

还有一个问题
在场景中挂载 spine 组件后,默认动画是<None>,这时候如果选择一个已经有的动画,再选择回<None>, 那么以后每次播放场景,都会输出:spine: Animation not found: <None>

唉,3.x 系列的版本,真的要这样子来回反复的摩擦使用者么?

@jare 辛苦看下有没有临时的解决方案哈

用这个 setCompleteListener

这个确实有效。
我修改了下测试代码

import { sp } from 'cc';
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('SpineTest')
export class SpineTest extends Component {
    private _spine!: sp.Skeleton;
    onLoad() {
        this._spine = this.getComponent(sp.Skeleton)!
    }
    start() {
        console.log("start")
        let tr = this._spine.setAnimation(0, "animation", false)!
        // this._spine.setTrackCompleteListener(tr, () => {
        //     console.log("completed")
        //     setTimeout(()=>{
        //         this._spine.setAnimation(0, "animation", false)!
        //     },10)
        // })
        this._spine.setCompleteListener(() => {
            console.log("completed2")
            setTimeout(()=>{
                this._spine.setAnimation(0, "animation", false)!
            },10)
        })
        // this._spine.setTrackEventListener(tr, (tr, ev) => {
        //     let event = ev as sp.spine.Event
        //     console.log("event:" + event.data.name)
        // })
        this._spine.setEventListener((sp, ev) => {
            let event = ev as sp.spine.Event
            console.log("event2:" + event.data.name)
        })
        // this._spine.setTrackEndListener(tr, (sp) => {
        //     console.log("end")
        // })
        this._spine.setEndListener(() => {
            console.log("end2")
        })
    }

    update(deltaTime: number) {

    }
}



这个代码确实可以达到预期。看起来是 setTrackListener 系列的失效了。
这样话,用 set
Listener 也有问题,这些监听器在切换播放新动画时,不会清除,很多时候我们的需求是播放一个动画,监听这个动画的事件触发、完成,并且回调特定上下文的回调函数。如果用set***Listener这个系列的方法的话,就需要每次播放新动画前,都清理一遍之前设置的监听器。

请问下,是什么平台出现的问题?Web、Windows、Mac、Android还是IOS

macOS 和微信都试过了。
我们大概猜到问题原因了,spine 底层(WASM 层?)每次通知 js 对应事件的时候,track 对象并不是同一个 js 对象,setTrack***Listener是不是把监听器存在的 track 的 js 对象上了?
我们写了一个临时的补丁修复这个问题,贴出来供遇到这个问题的小伙伴做参考,也方便引擎开发团队定位问题。

补丁如下:

import { VERSION, __private, sp } from "cc";

if (VERSION.startsWith("3.8")) {
    console.log("patch cocos 3.8.x spine listener bug!")
    interface Listeners {
        completeListener?: __private._cocos_spine_skeleton__TrackListener
        endListener?: __private._cocos_spine_skeleton__TrackListener
        eventListener?: __private._cocos_spine_skeleton__TrackListener2
        interruptListener?: __private._cocos_spine_skeleton__TrackListener
        disposeListener?: __private._cocos_spine_skeleton__TrackListener
        startListener?: __private._cocos_spine_skeleton__TrackListener
    }
    function clearTrackListeners(obj: sp.Skeleton, tr: sp.spine.TrackEntry) {
        let $$ = (tr as any)["$$"]
        if ($$) {
            let ptr = $$.ptr.toString()
            let objAny = obj as any
            let trackListeners = objAny.__p_trackListeners
            if (trackListeners) {
                delete trackListeners[ptr]
            }
        }
    }

    function listeners(obj: sp.Skeleton, tr: sp.spine.TrackEntry | null, create: boolean): Listeners | null | undefined {
        let holder: any
        if (tr) {
            let $$ = (tr as any)["$$"]
            if ($$) {
                let ptr = $$.ptr.toString()
                let objAny = obj as any
                let trackListeners = objAny.__p_trackListeners || (objAny.__p_trackListeners = {})
                holder = trackListeners[ptr] || (trackListeners[ptr] = {})
            } else {
                holder = tr
            }
        } else {
            holder = obj
        }

        let listeners = holder.__p_listeners
        if (!listeners && create) {
            listeners = holder.__p_listeners = {}
        }
        return listeners
    }

    let setCompleteListener_old = sp.Skeleton.prototype.setCompleteListener
    let __p_setEndListener = sp.Skeleton.prototype.setEndListener
    let __p_setEventListener = sp.Skeleton.prototype.setEventListener
    let __p_setInterruptListener = sp.Skeleton.prototype.setInterruptListener
    let __p_setDisposeListener = sp.Skeleton.prototype.setDisposeListener
    let __p_setStartListener = sp.Skeleton.prototype.setStartListener
    function setOldListenerIfNeed(obj: sp.Skeleton, key: string, oldSetter: Function, listener: Function) {
        let objAny = obj as any
        if (objAny[key]) {
            return
        }
        oldSetter.apply(obj, [listener.bind(obj)])
        objAny[key] = true
    }

    function p_onComplete(this: sp.Skeleton, tr: sp.spine.TrackEntry) {
        let l1 = listeners(this, null, false)
        let l2 = listeners(this, tr, false)
        l1 && l1.completeListener && l1.completeListener(tr)
        l2 && l2.completeListener && l2.completeListener(tr)
    }

    function p_onEnd(this: sp.Skeleton, tr: sp.spine.TrackEntry) {
        let l1 = listeners(this, null, false)
        let l2 = listeners(this, tr, false)
        clearTrackListeners(this, tr)
        l1 && l1.endListener && l1.endListener(tr)
        l2 && l2.endListener && l2.endListener(tr)
    }

    function p_onEvent(this: sp.Skeleton, tr: sp.spine.TrackEntry, ev: sp.spine.Event) {
        let l1 = listeners(this, null, false)
        let l2 = listeners(this, tr, false)
        l1 && l1.eventListener && l1.eventListener(tr, ev)
        l2 && l2.eventListener && l2.eventListener(tr, ev)
    }
    function p_onInterrupt(this: sp.Skeleton, tr: sp.spine.TrackEntry) {
        let l1 = listeners(this, null, false)
        let l2 = listeners(this, tr, false)
        l1 && l1.interruptListener && l1.interruptListener(tr)
        l2 && l2.interruptListener && l2.interruptListener(tr)
    }
    function p_onDispose(this: sp.Skeleton, tr: sp.spine.TrackEntry) {
        let l1 = listeners(this, null, false)
        let l2 = listeners(this, tr, false)
        l1 && l1.disposeListener && l1.disposeListener(tr)
        l2 && l2.disposeListener && l2.disposeListener(tr)
    }
    function p_onStart(this: sp.Skeleton, tr: sp.spine.TrackEntry) {
        let l1 = listeners(this, null, false)
        let l2 = listeners(this, tr, false)
        l1 && l1.startListener && l1.startListener(tr)
        l2 && l2.startListener && l2.startListener(tr)
    }

    sp.Skeleton.prototype.setCompleteListener = function (listener) {
        setOldListenerIfNeed(this, "__p_setCompleteListener", setCompleteListener_old, p_onComplete)
        listeners(this, null, true)!.completeListener = listener
    }

    sp.Skeleton.prototype.setTrackCompleteListener = function (tr, listener) {
        setOldListenerIfNeed(this, "__p_setCompleteListener", setCompleteListener_old, p_onComplete)
        //参数不匹配
        //@ts-ignore
        listeners(this, tr, true)!.completeListener = listener
    }



    sp.Skeleton.prototype.setEndListener = function (listener) {
        setOldListenerIfNeed(this, "__p_setEndListener", __p_setEndListener, p_onEnd)
        listeners(this, null, true)!.endListener = listener
    }
    sp.Skeleton.prototype.setTrackEndListener = function (tr, listener) {
        setOldListenerIfNeed(this, "__p_setEndListener", __p_setEndListener, p_onEnd)
        listeners(this, tr, true)!.endListener = listener
    }



    sp.Skeleton.prototype.setEventListener = function (listener) {
        setOldListenerIfNeed(this, "__p_setEventListener", __p_setEventListener, p_onEvent)
        listeners(this, null, true)!.eventListener = listener
    }
    sp.Skeleton.prototype.setTrackEventListener = function (tr, listener) {
        setOldListenerIfNeed(this, "__p_setEventListener", __p_setEventListener, p_onEvent)
        listeners(this, tr, true)!.eventListener = listener
    }



    sp.Skeleton.prototype.setInterruptListener = function (listener) {
        setOldListenerIfNeed(this, "__p_setInterruptListener", __p_setInterruptListener, p_onInterrupt)
        listeners(this, null, true)!.interruptListener = listener
    }
    sp.Skeleton.prototype.setTrackInterruptListener = function (tr, listener) {
        setOldListenerIfNeed(this, "__p_setInterruptListener", __p_setInterruptListener, p_onInterrupt)
        listeners(this, tr, true)!.interruptListener = listener
    }



    sp.Skeleton.prototype.setDisposeListener = function (listener) {
        setOldListenerIfNeed(this, "__p_setDisposeListener", __p_setDisposeListener, p_onDispose)
        listeners(this, null, true)!.disposeListener = listener
    }
    sp.Skeleton.prototype.setTrackDisposeListener = function (tr, listener) {
        setOldListenerIfNeed(this, "__p_setDisposeListener", __p_setDisposeListener, p_onDispose)
        listeners(this, tr, true)!.disposeListener = listener
    }


    sp.Skeleton.prototype.setStartListener = function (listener) {
        setOldListenerIfNeed(this, "__p_setStartListener", __p_setStartListener, p_onStart)
        listeners(this, null, true)!.startListener = listener
    }
    sp.Skeleton.prototype.setTrackStartListener = function (tr, listener) {
        setOldListenerIfNeed(this, "__p_setStartListener", __p_setStartListener, p_onStart)
        listeners(this, tr, true)!.startListener = listener
    }



}

这个补丁目前可以让我们的使用场景恢复正常,但是不确定是否覆盖全面,有使用到的小伙伴如果发现问题,欢迎完善下哈。

3赞

点了赞,但是看到这么高级的代码,但是又没用的代码(说不准下个版本就没用了),突然又觉得心情复杂,

3.8.0源码:
/**

 * @en Sets the complete event listener for specified TrackEntry.

 * @zh 用来为指定的 TrackEntry 设置动画一次循环播放结束的事件监听。

 * @param entry

 * @param listener @en Listener for registering callback functions. @zh 监听器对象,可注册回调方法。

 */

public setTrackCompleteListener (entry: spine.TrackEntry, listener: TrackListener2) {

    // TODO

    // TrackEntryListeners.getListeners(entry).complete = function (trackEntry) {

    //     const loopCount = Math.floor(trackEntry.trackTime / trackEntry.animationEnd);

    //     listener(trackEntry, loopCount);

    // };

}

看这源码,没调用回调很正常吧(

好一个 TODO,这是怎么过测试的 :joy:

3.8.1 修复了
image

我们用的就是3.8.1,没修复噢

我用windows验证,trackCompleteListener是可以回调的;前提是动画选择realTime,是否有js报错

没有报错噢,我们断点调用 setTrack***Listener 是成功返回了的。动画也是正常播放了。

你这个播放正常是你改过的版本吧?

不是,没改之前,也是正常播放的。我们之所以发现没回掉是因为我们技能释放过程中监听了 spine 事件,但是动画播放完了事件都没触发。我改过的版本只是让 setTrack***Listenter 正常回调。

我给你一个测试工程
测试组件代码

import { _decorator, Component, Node, sp } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('SpineTest')
export class SpineTest extends Component {
    private _spine: sp.Skeleton;
    protected onLoad(): void {
        this._spine = this.getComponent(sp.Skeleton)!
    }
    start() {
        this._spine.setCompleteListener(() => {
            console.log("setCompleteListener called")
        })
        this._spine.setEventListener(() => {
            console.log("setEventListener called")
        })
        let tr = this._spine.setAnimation(0, "animation", false)
        this._spine.setTrackCompleteListener(tr, () => {
            console.log("setTrackCompleteListener called")

        })
        this._spine.setTrackEventListener(tr, () => {
            console.log("setTrackEventListener called")
        })
    }

    update(deltaTime: number) {

    }
}


运行输出:

Init Base: 0.301025390625 ms
game.ts:776 Init Infrastructure: 33.632080078125 ms
debug.ts:64 [PHYSICS2D]: register box2d.
game.ts:825 Init SubSystem: 83.25390625 ms
debug.ts:64 Cocos Creator v3.8.1
game.ts:866 Init Project: 24.911865234375 ms
SpineTest.ts:15 setEventListener called
SpineTest.ts:12 setCompleteListener called

工程如下:
SpineTest.zip (73.8 KB)

我也遇到了 请问有结果了么

可以用这个补丁临时解决。
我们自己目前就是用的这个方案。
https://forum.cocos.org/t/topic/154238/6?u=leeho0108

382已修复,涉及到wasm,所以需要新版本才能用

我的天啦噜