- Editado
Interrupt event fired on setAnimation() from complete evt
Hi,
At the moment I'm working my way through events. Part of it to get tp know all the deep details about them and mixing, part of it to debug another project and find the right way to do things with Spine runtime as I'm having issues on that other project I need to solve.
Now I am bumping on some issues in the pixi-spine runtime. One of them being:
When one of the animations is finished I'd like to start another animation on the fly. I don't want and can't use the queue for that, so I use setAnimation()
inside the complete
handler of the Animation State object to start the next animation.
Now the following situation occurs, which I think is wrong, but I'm not sure if this is also an issue in the 'official' Spine runtime, or only happens in the pixi-spine one:
When animation A finishes and triggers the complete handlers, from within that handler function animation B is started with setAnimation()
. But for some strange reason this triggers the interrupt
event, while the animation was never interrupted, but finished. Otherwise the complete
event should never be fired.
[edit] The interrupt
event even gets triggered when instead of setAnimation()
we use [b]add[/b]Animation()
from within the complete event handler.
Anybody knows if this an issue in the official runtime of Spine as well, or perhaps even inteded behaviour I don't get? Or is this a pixi-spine only issue?
BTW It looks like there's something changed on this forum looking at the max amount of chars in the title of threads. This now is way too short. Even with this title it was a hastle just to fit in the words... could this be change to allow a little longer subjects? Thanks in advance!
Hello, thank you for writing in about your issue with Spine. Please see the link below about the interrupt event being triggered.
Coding for Spine Events and AnimationState callbacks
Although the link above contains information specific to Unity, this may help. If not, please let us know and we'll be happy to help you troubleshoot it further!
Oh no, please don't look at AnimationState! :yuno: If you find any bugs, they tend to be difficult to reproduce and fix! :p
When animation A finishes and triggers the complete handlers, from within that handler function animation B is started with setAnimation(). But for some strange reason this triggers the interrupt event, while the animation was never interrupted, but finished. Otherwise the complete event should never be fired.
The complete event doesn't mean a track entry is finished, it means it completed one loop. If the track entry is looping and you let the animation play, you'll see the complete event again and again.
In your scenario the animation A was indeed interrupted, meaning another entry (animation B) has replaced it as the current entry for the track. When any mix duration is finished, you'll see for animation A an end
event (because it will not be applied anymore) and then a dispose
event (because everything is done with the track entry object and it may be reused or the memory freed). That covers all the events except start
, which happens when the track entry has become the current entry for a track.
You mentioned addAnimation
and setAnimation
, but the interrupt
event should happen for both
any time the entry is no longer the current entry. If you've reached the animation duration (and you have if you've seen a complete
event) then addAnimation
and setAnimation
should be equivalent (should be, they are slightly different code paths).
Here's an interesting tidbit: TrackEntry events are collected during AnimationState update
and AnimationState apply
and fired only after those methods are finished. I'll add this note to the documentation. It means that if you make changes in an AnimationStateListener, the collected events are still fired, even if your listener makes changes, like clearing a track. This can be a surprise
you can set an animation or clear a track (from listener code) and afterward you may still get some events for the old track entry that is no longer the current entry for the track. If you come across this, you can check if the track entry for the event is the current entry for the track. In some cases it doesn't matter (eg to turn off particle effects) but in others it might (eg if you are setting the next animation in a sequence, you probably only want to do that when the event is for the current entry).
The events are collected and fired later as described because if we fired them in the middle of update
or apply
and your listener code called other AnimationState methods, it could change the internal AnimationState state, make for difficult to solve problems.
spineappletree escreveumax amount of chars in the title of threads
We didn't change this recently, but I never noticed. I suppose I rarely post new threads! Looks like it's 60 characters. There's no setting to increase it so we'd have to hack up the forums. We can do that, but not right now since we've got things going on and more effort is needed to see what might break, if the DB has a limit, etc. You'll have to be creative and concise with your titles!
Luke escreveuHello, thank you for writing in about your issue with Spine. Please see the link below about the interrupt event being triggered.
Coding for Spine Events and AnimationState callbacks
Thanks. That's the documentation I worked my way through today to get the details.
Nate escreveuOh no, please don't look at AnimationState! :yuno: If you find any bugs, they tend to be difficult to reproduce and fix! :p
Oh no! Well, let's hope for the best
Nate escreveuThe complete event doesn't mean a track entry is finished, it means it completed one loop. If the track entry is looping and you let the animation play, you'll see the complete event again and again.
I took the same from the documentation and see it happening in the demo project I've created where I created a visual timeline and indicators that pop up to show the events being fired. Thanks for confirming.
Nate escreveuIn your scenario the animation A was indeed interrupted, meaning another entry (animation B) has replaced it as the current entry for the track. When any mix duration is finished, you'll see for animation A an
end
event (because it will not be applied anymore) and then adispose
event (because everything is done with the track entry object and it may be reused or the memory freed).
Nate escreveuYou mentioned
addAnimation
andsetAnimation
, but theinterrupt
event should happen for both
any time the entry is no longer the current entry. If you've reached the animation duration (and you have if you've seen a
complete
event) thenaddAnimation
andsetAnimation
should be equivalent (should be, they are slightly different code paths).
Thanks. I understood the end
and dispose
events. But if I get you right I didn't quite understood the interrupt
event? You're saying the interrupt
event ALWAYS fires when whatever other animation replaces the current animation on the same track? Even if the animation was already completed (and loop was set to false
)?
Nate escreveuHere's an interesting tidbit: TrackEntry events are collected during AnimationState
update
and AnimationStateapply
and fired only after those methods are finished. I'll add this note to the documentation. It means that if you make changes in an AnimationStateListener, the collected events are still fired, even if your listener makes changes, like clearing a track. This can be a surprise
you can set an animation or clear a track (from listener code) and afterward you may still get some events for the old track entry that is no longer the current entry for the track. If you come across this, you can check if the track entry for the event is the current entry for the track. In some cases it doesn't matter (eg to turn off particle effects) but in others it might (eg if you are setting the next animation in a sequence, you probably only want to do that when the event is for the current entry).
The events are collected and fired later as described because if we fired them in the middle of
update
orapply
and your listener code called other AnimationState methods, it could change the internal AnimationState state, make for difficult to solve problems.
Not sure if that's what I'm bumping into. looks like you're describing something else here and only talk about events that happen pretty much at the same time.
I think I'm having a different issue here. The issue in the other project, that I'm trying to figure out now what is happening exactly. The situation I'm having now in that project is (all on the same animation track):
1) Animation A (non-looping animation) starts
2) Before completed Animation A gets swapped by Animation B with a mix transition of around 0,5 sec
3) The animation state object receives a complete
event from Animation A (which should never occur (as the eventThreshold
is never set throughout the whole project, so should still be zero, meaning no events of Animation A should be fired once Animation B got started - which btw works as expected in pixi-spine in the demo-project I've created now)
This is causing a problem in the project, because:
4) The complete handler now thinks Animation A was fully completed (which in fact is not the case, because interrupted by Animation B) and triggers animation C (which is an IDLE animation that should only play once animation A was finished)
So now Animation B is suddenly interrupted while playing, or didn't even got the time to play. Which obviously is a problem as not intended.
I don't understand why animation A still is able to trigger the complete
event while already replaced by animation B (setAnimation()
is definitely called to start animation B. I see that happening). Especially because the eventThreshold
is zero. Any clue on what to do about this? Is this something I'm missing, perhaps the situation you're describing or could this be a bug somewhere? How to solve this unwanted behaviour?
spineappletree escreveumax amount of chars in the title of threads
Nate escreveuWe didn't change this recently
Haha, ok. I've never noticed that before either and couldn't type more characters anymore, which was confusing. That's why I thought it was changed. 60 Chars is pretty stone age though. Even DOS had 80 chars on screen when we were still cave men Minor details though of course.
spineappletree escreveuYou're saying the interrupt event ALWAYS fires when whatever other animation replaces the current animation on the same track? Even if the animation was already completed (and loop was set to false)?
Yes! An animation completing one loop fires the complete
event, but otherwise nothing special happens because of it. If the animation is looping, it will play again from the start. If it is not looping, it will continue to be applied using the last frame of the animation.
Some animations needs this, for example a crane moving to a new position, you wouldn't want it to go back to the setup pose once it hits the animation duration. For animations where you don't want them to keep being applied you can either 1) queue an animation to play afterward, 2) set a new animation at the appropriate time, or 3) set TrackEntry trackEnd
to the animation duration, equivalent to clearing the track at that time. trackEnd
will cause the skeleton pose to snap back to the setup pose. Because of that, it's more common to use AnimationState addEmptyAnimation
or AnimationState setEmptyAnimation
to go back to the setup pose using a mix duration.
Not sure if that's what I'm bumping into. looks like you're describing something else here
Quite possible! AnimationState is a multifaceted beast.
spineappletree escreveu3) The animation state object receives a complete event from Animation A (which should never occur (as the eventThreshold is never set throughout the whole project, so should still be zero, meaning no events of Animation A should be fired once Animation B got started
The TrackEntry eventThreshold
applies only to event timelines. It does not affect AnimationStateListener method invocations, those always occur. It's common to refer to those listener callbacks as "events", which I see can be confusing.
If animation A is current and then animation B is set with a mix duration, then animation A will continue being applied for that mix duration. During that time, AnimationStateListener methods will still be called for animation A even though it is not the current track entry. It is valid to receive complete
for animation A even after you set animation B. It could be important in some cases, for example if you need to turn off some particle effects for animation A every time it completes a loop, and you want to keep doing that even when animation A is mixing out.
4) The complete handler now thinks Animation A was fully completed
What you can do is: when you get complete
for animation A, check if it is the current animation for the track in the listener:
if (state.getCurrent(entry.getTrackIndex()) != entry) return;
Another option, which may be more straightforward, is to check if the entry is mixing to another entry (ie the entry is mixing out):
if (entry.getMixingTo() != null) return; // The entry is mixing out.
That check is mentioned in the AnimationStateListener complete
docs, which also mentions that complete
can happen after interrupt
. It is tricky though, we should expand the runtimes guide to explain this with an example that does these checks.
Wow, that's some useful information!
I'm going to try it out now, but it looks a lot like this will solve the problem I'm having on the other project AND ups the knowledge about using these events for future projects!
Thanks for your indepth explaining and very helpful response Nate! I'll let you know if this fixed the issue I'm having.
[edit A]
@Nate Just a quick observation of the pixi-spine package and question about it:
I see mixingTo is getting filled with the new trackEntry
object when receiving a complete-event while mixing. But in other cases in pixi-spine (javascript) the mixingTo
property can be either undefined
(so the property doesn't even exist) or set to null
. Is that intentional in the 'official' runtime or an interpretation by pixi-spine?
[edit B]
Cool. This seems to have solved the issues I was having in the other project. Now doing this to detect if the complete handler should trigger stuff:
const isCurrentAnim = trackEntry.mixingTo === undefined || trackEntry.mixingTo === null;
Thanks again!
Either undefined or null is OK, but it would be better if it were null so we'll make that change, issue here:
https://github.com/EsotericSoftware/spine-runtimes/issues/2003
For now it'd be easiest like this:
const isCurrentAnim = !entry.mixingTo;
Nate escreveuEither undefined or null is OK, but it would be better if it were null so we'll make that change
Thanks. That would be nice and consistent!
It has been done this morning!
Cool! That's quick! Not sure how and when pixi-spine update their version, which they based on the official runtime, but great to see it's in the pipeline now and will probably be there in the next sync!