• Runtimes
  • [spine-ts / webgl] track complete -> set animation?

Related Discussions
...
21일 후

I have seen the warnings about setting another animation inside some of the listener callbacks (and experienced the issue first hand, when I switched animations in a complete listener with a non-zero animation mix and the original animation complete listener would fire again with the original animation eventTrack.)

What is the suggested pattern for updating an animation based on a listener complete callback? I am thinking now it might be something like adding an event queue which is set in the callback, but processed later outside the callback in the game loop.

Any suggestions or examples? Thanks in advance for the help.


Checking in for suggestions?

5일 후

We are very sorry that you haven't received any reply earlier!

MikalDev wrote

I have seen the warnings about setting another animation inside some of the listener callbacks (and experienced the issue first hand, when I switched animations in a complete listener with a non-zero animation mix and the original animation complete listener would fire again with the original animation eventTrack.)

In general it is not discouraged to call SetAnimation from e.g. complete callbacks. The documentation page also states "It is always safe to call AnimationState methods when receiving events.".

You should receive no duplicate complete callbacks. Could you perhaps share the code snippets that led to your issue of receiving a complete callback twice? I just tested the scenario locally but could not reproduce the issue, received the callback only once as expected.

Thanks for the help!

It's for the Construct plugin, so it triggers a construct function which then calls set animation and triggers another complete callback (so there's not really code to share on the C3 side, but the C3 side calls this other set animation function, so I'll show both of those. Also note that the issue only happen when animation mix is set to non zero. When it's zero, there is no error. The this.trackAnimations was a hack I had to do, so it would be the same animation triggered twice.

This is the listener, the triggers below in the code call functions in C3 events which in turn call setAnimation.

state.tracks[trackIndex].listener = {
    complete: (trackEntry, count) => {
        this.completeAnimationName = this.trackAnimations[trackEntry.trackIndex];
        this.completeTrackIndex = trackEntry.trackIndex;
        this.Trigger(C3.Plugins.Gritsenko_Spine.Cnds.OnAnimationFinished);
        this.Trigger(C3.Plugins.Gritsenko_Spine.Cnds.OnAnyAnimationFinished);
    },
    event: (trackEntry, event) => {
        this.completeEventName = event.data.name;
        this.completeEventTrackIndex = trackEntry.trackIndex;
        this.Trigger(C3.Plugins.Gritsenko_Spine.Cnds.OnEvent);
    }

Here's set animation, note that the animation state is updated later in the game tick (and if starting at a later time, add the listener after that, so earlier events are not triggered.)

updateCurrentAnimation(loop,start,trackIndex, animationName) {
    
if (!this.skeletonInfo) return; if (!this.skeletonInfo.skeleton) return; if (!this.animationNames) return; if (!(this.animationNames.includes(animationName))) { if (this.debug) console.warn('[Spine] updateCurrentAnimation, animation does not exist.', animationName, this.uid); return; } try { const state = this.skeletonInfo.state; const skeleton = this.skeletonInfo.skeleton; const track = state.tracks[trackIndex]; let currentTime = 0; let currentRatio = 0; if (track) { // calculate ratio and time currentTime = track.trackTime; if (track.animationEnd != track.animationStart && track.animationEnd > track.animationStart) { currentRatio = (track.animationLast+track.trackTime-track.trackLast)/(track.animationEnd-track.animationStart); } } state.setAnimation(trackIndex, animationName, loop); switch (start) { case 0: break; // Start from beginning case 1: state.tracks[trackIndex].trackTime = currentTime; break; case 2: state.tracks[trackIndex].trackTime = currentRatio * (state.tracks[trackIndex].animationEnd-state.tracks[trackIndex].animationStart); break; default: break; } // Record animation assigned for listener this.trackAnimations[trackIndex] = this.animationName; if (start == 0 || (start == 2 && currentRatio == 0)) // If starting from beginning or 0 ratio add listners so they'll trigger at 0 { this.setTrackListeners(state, trackIndex); } else // If starting later, apply time, then enable listeners so they do not trigger on past events { // state.apply(skeleton); // skeleton.updateWorldTransform(); this.delayedTrackListeners.push(trackIndex); // this.setTrackListeners(state, trackIndex); } } catch (ex) { if (this.debug) { console.error('[Spine] setAnimation error', ex, trackIndex, animationName); } this.spineError = 'setAnimation error '+ex; this.Trigger(C3.Plugins.Gritsenko_Spine.Cnds.OnError); } }

I'm not sure I am able to follow your code entirely. I cannot tell from the above code that your added layers of code don't lead to side effects that could lead to multiple callbacks. E.g. registering complete as well as event, triggering OnAnimationFinished as well as OnAnyAnimationFinished, are you sure that setAnimation is only called once from any of the callbacks, and that your maintained state is always correct?

Could you perhaps perform a simpler reproduction test which only performs registering at the callback and setting the animation from there, without any additional layers of code?

16일 후

I'll be looking at doing a another simpler test case next week, for now I have a work around, but it will be good to understand more if the issues is in my code/c3 or the runtime.