Sorry for the delay!
Thanks for the reproduction code, it is helpful. For posterity, here's simplified code to repro in the SV/reference runtime:
float delay;
...
if (delay > 0f)
delay -= Gdx.graphics.getDeltaTime() * ui.speedSlider.getValue();
else if (state.getCurrent(0) == null) {
state.setAnimation(0, "portal", false);
delay = 0.7f;
} else if (!Gdx.input.isTouched()) {
TrackEntry entry = state.setAnimation(0, "idle", false);
entry.mixDuration = 0.5f;
delay = 0.35f;
}
First some words about the problem, mostly in case it helps future me comprehend without needing to start from scratch. It comes from both HOLD_FIRST
and HOLD_MIX
behavior:
"
A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap to the mixed out position.
The problem is D never finishes mixing in because new animations are set continuously. HOLD_MIX
is used for portal
-> idle
and new idle
animations are set such that there is always an idle
animation mixing in or out. An idle
animation never mixes in completely, so the influence from portal
persists.
Loading Image
To avoid dipping we need an animation to provide a "reasonable pose" that the other animations are mixing on top of. That's why A (portal
) is kept around until an animation mixes in all the way, effectively taking over. Getting rid of A before another animation mixes in all the way will cause snapping because the setup pose would instantly become the "reasonable pose". That may be more reasonable in many cases, but could also be just as bad.
If that is acceptable, meaning snapping is acceptable, one solution could be to limit the "mixing from" linked list to only 2 entries. The way to do that is:
TrackEntry entry = state.setAnimation(0, "idle", false);
entry.mixDuration = 0.5f;
if (entry.mixingFrom != null && entry.mixingFrom.mixingFrom != null) entry.mixingFrom.setMixDuration(0);
Exactly the same thing could be done before setting the new entry, and may be a little easier to understand:
TrackEntry current = state.getCurrent(0);
if (current != null && current.mixingFrom != null) current.setMixDuration(0);
TrackEntry entry = state.setAnimation(0, "idle", false);
entry.mixDuration = 0.5f;
This code (either one) means that if a mix is in progess, eg A -> B, and animation C is set, A is discarded so you have B -> C, where B is mixing out to the setup pose instead of mixing out to A.
Setting the mix duration to 0 is the best way to terminate a mix because it allows the track entry objects to be cleaned up (some runtimes keep a pool) the next time AnimationState update
is called. I'll add this to the docs.
Another option might be TrackEntry holdPrevious
. That means for a mix A -> B, normally A would be mixed out while B is mixed in. With holdPrevious
, A is applied as normal, then B is mixed in. When the mix is complete, A is no longer applied, which means usually you want to key everything in B that you keyed in A. Since you likely change between many different animations, that can quickly lead to needing to key nearly everything in every animation.
The original problem is actually worse with holdPrevious = true
because the linked list grows indefinitely: A -> B where B has holdPrevious
, meaning A is applied as normal while B mixes in. Currently we keep A until there are no mixing from entries, so for A -> B -> C we keep A until both A and B mixes are complete. If new animations are added, mixing doesn't complete and everything is kept.
Loading Image
This could be changed in updateMixingFrom
by adding a condition to if (from.totalAlpha == 0 || to.mixDuration == 0) {
. To get rid of A as soon as the A -> B mix (with holdPrevious
) is complete:
if (from.totalAlpha == 0 || to.mixDuration == 0 || (finished && to.holdPrevious)) {
I'm not sure we want to make this change, as someone could be relying on the old behavior, but you could try it out by modifying your runtime source. You'd then set your animation like:
TrackEntry entry = state.setAnimation(0, "idle", false);
entry.mixDuration = 0.5f;
entry.holdPrevious = true;
Lastly, while of course seeing such weird animation is bad, it shouldn't affect gameplay. Ideally the MVC or similar design pattern is used to decouple the animation system from the game state. That can simplify game logic and managing game state and also helps if you need to serialize the game state. You can Google for the "MVC design pattern". Super Spineboy uses it:
https://github.com/EsotericSoftware/spine-superspineboy/tree/master/src/com/esotericsoftware/spine/superspineboy