Nate Our game loop is a lot more complicated that that, but we're not calling Apply manually. When I tried that based on the suggestions in the post you linked to, by manually calling Apply in the AnimationState's Start callback it causes too many issues, like freezing unity when using the following logic.
Abstract character class:
protected virtual void OnSpineStart(Spine.TrackEntry trackEntry)
{
SetAnimationDebugString(trackEntry.Animation.Name);
// TODO: Figure out why this freezes Unity when the Florida Man starts to take his jug back:
// Force skeleton to update instantly on SetAnimation:
Skeleton.AnimationState.Apply(Skeleton.Skeleton);
}
Concrete character class:
PlayGatorCatchAnimation(_gatorSide);
var track = AddGatorDrinkingAnimation(_gatorSide, false);
track.Complete += (Spine.TrackEntry track) => {
AddGatorDrinkingAnimation(_gatorSide, true);
FloridaManStateMachine.SetTakeJugBackState();
};
Here the "false" passed into AddGatorDrinkingAnimation tells us whether or not to loop the gator drinking animation, to solve the issue that if it loops from the start the track.Complete listener gets called repeatedly.
The only listeners we have in AnimationState are for Event, Start, and Complete (though we aren't actually using the Complete listener for anything at the moment). Our game logic largely relies on individual track entry listeners as you suggest (virtually always on Complete, as above).
However because this is a fighting game (very similar to Punch-Out!!, and a sequel to our published game Election Year Knockout) the player can interrupt these track entries (and thus Complete listeners) at specific times when the player gives their character control inputs.
Nate If the events you mean are event keys, those are only fired when apply is called, just before the method returns.
Yes these are the events I'm referring to, as that's what your documentation calls them. They are keyed by the artists in the Spine Editor and our AnimationState listener is what handles those, which means they are only fired after Apply is called, as you mention. This means that say we get an event that tells us we should change animations at a specific point, call it the "swap" event, our listener could listen for "swap" and then call SetAnimation at this point, but because Apply was already called the AnimationState fires the Start callback immediately on SetAnimation, however the AnimationState doesn't change until the following frame as you pointed out, and thus doesn't fire off it's events that are keyed to the first frame of the animation: despite the fact that the AnimationState's Start callback is fired which leads to undesirable single frame delay between SetAnimation and the AnimationState being applied and it's events being fired.
Another example of this can be seen just in a variation of the code above. If the second AddGatorDrinkingAnimation instead of calling AddAnimation used SetAnimation, then this would mean that because the Complete callback listener was already fired the AnimationState must've been applied already, meaning that the AnimationState will fire the Start callback, but wont actually change until the following frame when Apply is called again by the update loop (and thus the events keyed at frame 0 of the animation set by SetAnimation won't be fired until the following frame, again leading to a single frame delay between AnimationState firing the Start callback, and the AnimationState applying the new animation).
The behavior I expected was that SetAnimation would immediately apply the AnimationState on that frame, so that the AnimationState's Start callback is fired, and then the AnimationState is applied: thus firing any events keyed to frame 0 in the callstack for SetAnimation. Or that the AnimationState's Start callback listeners would be fired the following frame immediately before automatically applying the AnimationState, but neither seems to be the case. To be honest to me, it seems like a bug that the AnimationState's Start callback is fired the frame before the AnimationState is actually applied, but it's more likely that I'm just misunderstanding how the runtimes work in Unity.
Nate I'm not sure what you are seeing, but if you set a new animation and then apply, you'll see the pose from the new animation, not the last frame of the previous animation (depending on mixing).
Yeah that's what I'm seeing. But if that was done on the Complete listener of a TrackEntry, then the final frame of that TrackEntry didn't seem to complete, instead immediately being replaced with the new animation from SetAnimation, when manually calling Apply from the AnimationState's Start callback listener like the sample code above. I could be wrong here however, but that's what it looked like to me when manually analyzing it frame by frame.
Nate You were setting the animation but not posing the skeleton by calling apply, so you don't get events until the next frame and you need it on the same frame, yet you want to see the last frame of the old animation so you don't want to pose the skeleton on the same frame you change the animation.
Yes but the actual issue is that the AnimationState's Start callback listener is triggered immediately when calling SetAnimation, and not when the AnimationState is actually applied (which is the next frame based on the Script Execution Order). This is what was breaking our logic and causing a single frame delay between the Start callback listener and when the AnimationState is actually applied. Maybe I'm just dense and not understanding what you're getting at though, but the behavior I expected was that:
- TrackEntry's Complete listener is fired at the end of the last frame of the TrackEntry's animation
- Any events keyed to the last frame of this TrackEntry's animation are fired
- SetAnimation is called in that listener
- The following frame the AnimationState's Start callback listener is fired
- The AnimationState is applied (thus posing the Skeleton to the new animation in the new TrackEntry)
- Any events keyed to frame 0 of the new TrackEntry's animation are fired
However what I'm actually observing is the following:
- TrackEntry's Complete listener is fired at the end of the last frame of the TrackEntry's animation
- Any events keyed to the last frame of this TrackEntry's animation are fired
- SetAnimation is called in that listener
- In this frame the AnimationState's Start callback listener is fired
- The following frame AnimationState is applied (thus posing the Skeleton to the new animation in the new TrackEntry)
- Any events keyed to frame 0 of the new TrackEntry's animation are fired
Nate It sounds like you should wait one frame before setting the new animation in response to the event. Do that on the next frame in a callback that happens before apply.
This would seem to be incredibly tedious to implement in a way that isn't just tossing coroutines everywhere that we set animations (which is everywhere, as it's an animation-event based game). But again maybe I'm just not understanding you or how the runtimes work, or maybe I'm just stupid π¦
Sorry for this long-winded response as well, I'm not sure how to accurately explain what I'm observing more concisely however.
Again thanks for the response and your assistance though!