- Modifié
[spine-c] 3.5 - animation jerking
Start animation with setAnimation. Have callback spineAnimStateHandler and on SP_ANIMATION_COMPLETE I call new setAnimation. I expect to start new animation from setupPose with applying first frame from new timelines. BUT, I can see setupPose first and only after update I can see first frame of a new animation.
Is it by design? Or bug? How can I fix it on my side? I need to start new animation from inside StateHandler ...
Do you have the latest? We committed a possibly related fix minutes ago.
Yeah, tested latest.
When you call setAnimation it changes the AnimationState. The Skeleton is not affected until the next time the AnimationState is applied to the skeleton. Maybe I misunderstand the issue? Can you modify one of the examples to show the problem?
I have following order:
spSkeleton_update
spAnimationState_update
spAnimationState_apply
Here apply calls _spEventQueue_drain and listener ... on my side on listener I call spAnimationState_setAnimationByName
Now my Update() loop completed and I Draw() everything ...
So, should I call spAnimationState_apply again after I set animation?
Ok, now another bug, I have single "idle" animation with 0 frames (ie same as setup pose), call order is:
spSkeleton_update
spAnimationState_update
spAnimationState_apply ->
_spEventQueue_drain ->
callback hook ->
spAnimationState_setAnimationByName( set same "idle" animation )
spAnimationState_apply
at _spAnimationState_applyMixingFrom I get stack overflow on this line "if (from->mixingFrom) _spAnimationState_applyMixingFrom(self, from, skeleton);"
What callback are you using? If your setAnimationByName
triggers the callback then it's doing what you asked until it runs out of stack space.
I use onComplete, want to make loop (I need to control it manually). As I said recursion happens at _spAnimationState_applyMixingFrom after spAnimationState_apply. I call it only once.
Can you modify one of the samples to reproduce the issue?
Ok, add this code into MemoryTestFixture
spSkeleton* skeleton = nullptr;
static void spineAnimStateHandler(spAnimationState* state, int type, spTrackEntry* entry, spEvent* event)
{
if (type == SP_ANIMATION_COMPLETE)
{
spAnimationState_setAnimationByName(state, 0, "walk", false);
spAnimationState_apply(state, skeleton);
}
}
void MemoryTestFixture::reproduceIssue_Loop()
{
spAtlas* atlas = nullptr;
spSkeletonData* skeletonData = nullptr;
spAnimationStateData* stateData = nullptr;
spAnimationState* state = nullptr;
//////////////////////////////////////////////////////////////////////////
// Initialize Animations
LoadSpineboyExample(atlas, skeletonData, stateData, skeleton, state);
///////////////////////////////////////////////////////////////////////////
if (state)
state->listener = (spAnimationStateListener)&spineAnimStateHandler;
spAnimationState_setAnimationByName(state, 0, "walk", false);
// run normal update
for (int i = 0; i < 50; ++i) {
const float timeSlice = 1.0f / 60.0f;
spSkeleton_update(skeleton, timeSlice);
spAnimationState_update(state, timeSlice);
spAnimationState_apply(state, skeleton);
}
}
Also, don't forget to modify header:
TEST_CASE(reproduceIssue_Loop);
and
void reproduceIssue_Loop();
Sorry, I mislead you when I said you should call apply
. If it makes you feel better, badlogic and I suffered to figure out why. When you call
apply
, by design it does not have any side effects (though your callbacks may). This allows you to call update
, then call apply
on any number of skeletons. If an animation has completed, each apply
will trigger a complete
event. The fix for your stack overflow then is to call update
to advance the AnimationState internals so they know that the complete
has already happened:
spAnimationState_setAnimationByName(state, 0, "walk", false);
spAnimationState_update(state, 0);
spAnimationState_apply(state, skeleton);
Actually it looks like a hack ... this magic sequence of calls.
I think it should be done on API side, I just want to set new animation without any knowledge of your internals with update/apply logic.
No, not at all. You change the animation, but you want the skeleton modified right away, rather than wait until the next frame. This means you want to apply the animation right after changing it. To do that you advance the AnimationState to the next frame then you apply it. No hacks here.
These aren't internals. The update and applying of the AnimationState are public API and your responsibility. The only way we could remove the need to call update
would be to give apply
side effects. This would mean it could only be applied to a single skeleton, a second apply
with no update
between would not fire the complete event. It's not super common, but it can be useful to have a single AnimationState applied to many skeletons. Imagine a marching army of 100 skeletons, an AnimationState for each one may be overkill or otherwise eat too much CPU. You could use just a few AnimationStates to animate them all, randomly assigned so it's hard to notice some have the same pose.
It's true this may be a kind of API surprise, but at least you know right away (stack overflow) and it makes sense when you consider how update
and apply
work (which you should).
Ok, with update - I get exactly same stack overflow. Any ideas? This happens on animation with zero frames (on my side only now).
Ah, in that case think what happens
every time you apply
a zero duration animation, complete
will fire. You may want to deregister your listener before setting a new animation, or set the listener on a specific TrackEntry rather than the whole AnimationState.
Just edit Spineboy.json and add "empty": {} inside "animation".
Changed code to looks like this, same stack overflow, any ideas?
static void spineAnimStateHandler(spAnimationState* state, int type, spTrackEntry* entry, spEvent* event)
{
if (!handlerEnabled) return;
handlerEnabled = false;
if (type == SP_ANIMATION_COMPLETE)
{
spAnimationState_setAnimationByName(state, 0, "empty", false);
spAnimationState_update(state, 0);
spAnimationState_apply(state, skeleton);
}
handlerEnabled = true;
}
As far as I can tell, it's working correctly. You've set a listener on the AnimationState which will be notified every time any animation event happens. With a zero duration animation, your listener gets a complete event every time the animation is applied. That happens when you call apply
every frame, not just when you call apply
from the listener. So, every frame you are getting a complete event and calling setAnimationByName
. This builds up a long linked list of mixingFrom
entries. When you get enough of those, applying the mixing from animations runs out of stack space and crashes.
See TrackEntry listener
. This way you can set a listener that handles the complete on an animation but is not notified for any other animations. Or, deregister your listener using AnimationState removeListener
before setting a new animation.
Eg, to play run
then when complete happens, play empty
:
static void spineAnimStateHandler(spAnimationState* state, int type, spTrackEntry* entry, spEvent* event) {
if (type != SP_ANIMATION_COMPLETE) return;
spAnimationState_setAnimationByName(state, 0, "empty", false);
spAnimationState_update(state, 0);
spAnimationState_apply(state, skeleton);
}
...
TrackEntry* entry = spAnimationState_setAnimationByName(state, 0, "run", true);
entry.listener = (spAnimationStateListener)&spineAnimStateHandler;
Of course for such a simple example you would use AnimationState addAnimation
:
spAnimationState_setAnimationByName(state, 0, "run", true);
spAnimationState_addAnimationByName(state, 0, "empty", true, 0);
Hm ... I need to get events for all animations, so I can't use trackentry listener. And it's hard to unregister/register handler on my side, as I use complex wrapper around Spine API ... so, looks like it almost impossible to change animation from inside listener ... need to think.
Anyway, it works for non-empty animation, but for empty - crash ...