• Runtimes
  • [spine-c] 3.5 Animation Blending

I'm having trouble with the new mixing/blending.

My game script applies animation state data in the following order:

DefaultBlend :  { duration : 0.2 } ,  /*  Default must be first  */ 
Blend :  { from : "jump", to : "walk", duration : 0.400000 } , 
Blend :  { from : "jump", to : "run", duration : 0.400000 } , 
Scene :  { name : "idle", loop : true } , 
Chain :  { name : "chain1", 
   Scene :  { name : "walk", loop : true, delay : 0 } , 
   Scene :  { name : "jump", loop : false, delay : 0 } , 
   Scene :  { name : "run", loop : true, delay : 0 } , 
   Scene :  { name : "jump", loop : false, delay : 3 } , 
   Scene :  { name : "walk", loop : true, delay : 0 } , 
   Scene :  { name : "idle", loop : true, delay : 1 } 
 } 

DefaultBlend loops over and applies to->from for all animations, plus it sets 'defaultBlend' parameter of the animation data.
Blend sets a specific blend time for to->from animations.
Chain is a sequence of animations that are applied in the following manner:

std::vector<SceneChain>::iterator itr = mSceneChains[aSequenceID].begin(), end = mSceneChains[aSequenceID].end();
 if(itr != end) // set the first animation
 {
     spAnimationState_setAnimationByName(pAnimationState, track, itr->mSceneName.c_str(), itr->bLoop);
     ++itr;
     mCurrentSequence = aSequenceID;
 }

 for (; itr != end; ++itr) // add the rest into the animation queue
 {
     spAnimationState_addAnimationByName(pAnimationState, track, itr->mSceneName.c_str(), itr->bLoop, itr->mDelay);
 }

And everything is updated:

spSkeleton_update(pSkeleton, theFrac);
      spAnimationState_update(pAnimationState, theFrac);
      spAnimationState_apply(pAnimationState, pSkeleton);

I'm testing on the latest spineboy example

I have read Set + AddAnimation Behaviour bugged? and several associated topics.

Question: What is the new method for mixing animations?

Related Discussions
...
  • 編集済み

AnimationState setAnimation and addAnimation should mostly work as they did before. Your example may be a bit more complicated than necessary. Have you tried one of the examples in the runtime? Maybe modify it in a simple way and then if it doesn't work like you expect, we can easily reproduce that.

Ok, I've setup an reproduced the error in the Cocos2dx SpineboyExample.cpp:

skeletonNode->setMix("walk", "jump", 0.4f); // increase the mix time
skeletonNode->setMix("jump", "run", 0.4f); // increase the mix time
skeletonNode->setAnimation(0, "walk", true);
spTrackEntry* jumpEntry = skeletonNode->addAnimation(0, "jump", false, 3);
skeletonNode->addAnimation(0, "run", true);

So I decreased the mix time in my project and everything seems to be working as expected.

However, why is it that longer blend times cause the jumping? What has changed from before that causes this new behavior? Is there some way I can calculate a reasonable MAX blend time?

Thanks.

Any mix time should be fine, as shown by the Skeleton Viewer:
Skeleton Viewer
We'll look into reproducing your issue today, thanks for the repro steps!

Awesome, guys. Thanks again!

This was a bug in the new AnimationState implementation in the spine-c runtime. I've just pushed a fix for it to the master branch on GitHub. Let me know if that works for you (it should!).

Confirmed, fixed.

Arm rotation looks weird, but after reading the improvements on the rotational timeline mixing I think that's a non-issue.

Thank you so much!

Oh, if it looks weird, please tell us! We'd need a .spine file and the order of animations you apply at runtime (e.g. setAnimation, addAnimation etc.) so we can reproduce. Essentially, tell us what it should look like and give us a way to reproduce how it does look like in reality 🙂

+1, especially if it looks different in Skeleton Viewer than it does in the runtime you are using.

Ok, edited the SpineboyExample.cpp in Cocos2dx. I was also able to reproduce in SFML.

Click a couple times when the example boots up to slow down the animation, then watch his right arm swing wildly.

bool SpineboyExample::init () {
   if (!LayerColor::initWithColor(Color4B(128, 128, 128, 255))) return false;

   skeletonNode = SkeletonAnimation::createWithJsonFile("spineboy.json", "spineboy.atlas", 0.6f);

skeletonNode->setStartListener( [] (spTrackEntry* entry) {
  log("%d start: %s", entry->trackIndex, entry->animation->name);
   });
    skeletonNode->setInterruptListener( [] (spTrackEntry* entry) {
        log("%d interrupt", entry->trackIndex);
    });
   skeletonNode->setEndListener( [] (spTrackEntry* entry) {
      log("%d end", entry->trackIndex);
   });
   skeletonNode->setCompleteListener( [] (spTrackEntry* entry) {
      log("%d complete", entry->trackIndex);
   });
    skeletonNode->setDisposeListener( [] (spTrackEntry* entry) {
        log("%d dispose", entry->trackIndex);
    });
   skeletonNode->setEventListener( [] (spTrackEntry* entry, spEvent* event) {
      log("%d event: %s, %d, %f, %s", entry->trackIndex, event->data->name, event->intValue, event->floatValue, event->stringValue);
   });

   skeletonNode->getState()->data->defaultMix = 0.2f;
   skeletonNode->setMix("jump", "run", 0.2);
   skeletonNode->setMix("jump", "walk", 0.2);


   skeletonNode->setAnimation(0, "walk", true);
   skeletonNode->addAnimation(0, "jump", false, 0);
   skeletonNode->addAnimation(0, "run", true);    
skeletonNode->addAnimation(0, "jump", false, 3); skeletonNode->addAnimation(0, "walk", true);
skeletonNode->addAnimation(0, "idle", true, 1); // skeletonNode->addAnimation(1, "test", true); // skeletonNode->runAction(RepeatForever::create(Sequence::create(FadeOut::create(1), FadeIn::create(1), DelayTime::create(5), NULL))); skeletonNode->setPosition(Vec2(_contentSize.width / 2, 20)); addChild(skeletonNode); scheduleUpdate();
EventListenerTouchOneByOne* listener = EventListenerTouchOneByOne::create(); listener->onTouchBegan = [this] (Touch* touch, Event* event) -> bool { if (!skeletonNode->getDebugBonesEnabled()) skeletonNode->setDebugBonesEnabled(true); else if (skeletonNode->getTimeScale() == 1) skeletonNode->setTimeScale(0.3f); else Director::getInstance()->replaceScene(GoblinsExample::scene()); return true; }; _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this); return true; }

Thanks for the repro code! Issue fixed, rest of runtimes will be updated soon.

Confirmed fixed. Thanks again.

I use mixing from animation A -> B. The question is: how does mixing happens? It looks different, if I play A -> B several times. On first play it looks fine, but on second B fades in with setupPose instead of first frame.


Probably I have reproduced it with Viewer. Make a new animation "appear" for spineboy, just smoothly fade in alpha for all images (first frame alpha = 0, last frame = 1). Now try to play it with viewer and some mix value (0.2 for example). It start to appear not from invisible state, it mixes from setupPose.


But, on my side [spine-c], on first play of "appear" it looks fine (starts from alpha = 0), but on second play - it fades same as on viewer (start alpha != 0). Any ideas?

In Skeleton Viewer, when you have no animation selected, then select an animation to play, it is doing this:

state.setEmptyAnimation(track, 0);
TrackEntry entry = state.addAnimation(track, animation, loop, 0);
entry.mixDuration = mix;

This causes the animation to mix in from the setup pose. You can see the transition from empty to appear where SV shows the tracks and mixing:
 Loading Image

If you don't want it to mix from the setup pose, do it like this:

TrackEntry entry = state.setAnimation(track, animation, loop);
entry.mixDuration = 0;

Setting the mix duration is optional of course. setAnimation uses the mix duration from AnimationStateData, so setting the mix like this overrides what AnimationStateData has configured.

I'm not sure how you are applying the animations in spine-c, so it's hard to say if what you see is correct. I can say that if you are playing an animation, then play appear, it will use the mix duration from AnimationStateData. If you want the appear animation to play without mixing, use 0 for mixDuration.