• Runtimes
  • Flow for divided animations (e.g. windup, attack, cooldown)

Related Discussions
...
6 días más tarde

My animator made an attack animation (let's say swinging a sword) that has 3 logical parts to it:

[Artist's Animation]

  1. Windup (raise sword) over 10 frames (0.333 second)
  2. Attack (swing sword) over 10 frames (0.333 second)
  3. Cooldown (follow through of swing) over 10 frames (0.333 second)

Of course, my animator does not know how fast each part should be played, because this is determined by in-game testing. Further, different enemies may use different values for each part, including potentially a pause between part 1 and 2. Through testing, say I have determined that I want it to work like this for some enemies:

[Desired Timing]

  1. Windup over 0.75 second
  2. Pause (no animation on this track) for 0.25 second
  3. Swing over 0.1 second
  4. Cooldown over 0.5 second

What would be the easiest way to translate the artist's animation into my desired timing using the existing Spine-c APIs?

I'd like to avoid custom-coding a state machine for every kind of animation that needs to be divided up like this, so it should be a generic solution. Further, I don't really want the artist to modify the original animation unless it's the only elegant way, because some of the timing may need to be based on other game events (for example, more dangerous versions of this same enemy might need a faster windup and cooldown, but the pause and swing should still be 0.5 second and 0.1 second respectively).

A little bit of background: before switching to Spine, we had been using an in-house animation system where, in code, all animations were played using a normalized value to represent the weight of the animation, from 0.0 to 1.0. So I set up a little scripting system that would do the following for this particular case:

  1. Change animation weight from 0.0 to 0.333 over 0.75 second. When that's done,
  2. Pause script for 0.25 second. When that's done,
  3. Change animation weight from 0.333 to 0.666 over 0.1 second. When that's done,
  4. Change animation weight from 0.666 to 1.0 over 0.5 second. When that's done, trigger an event callback for animation being finished.

A pseudo-code example of what I was doing would be:

// setup
script.enqueue(new LinearFunction(&animation.weight, 0.333, 0.75));
script.wait(0.25);
script.enqueue(new LinearFunction(&animation.weight, 0.666, 0.1));
script.enqueue(new LinearFunction(&animation.weight, 1.0, 0.5));

// at run time, every frame
script.update()

So a subquestion is, assuming this is what I was doing before, is there any easy way to translate this concept into Spine-c's APIs?


I can't be the only one who is trying to do this. Practically every single-player melee combat game I can think of has animations that work like this, not just games I've personally made =) Any suggestions would be appreciated!

7 días más tarde

Ideally the 3 parts of the animation would all be separate animations, but I also see that splitting them up post-hoc isn't the nicest thing to do.

You could try to emulate your old system by using the spTrackEntry.timeScale field (spine-runtimes/AnimationState.h at 3.6). When you add or set an animation on a spAnimationState, you get back such an spTrackEntry. You can modify the playback speed of that animation by setting the timeScale field. I'd imagine you could reproduce your old scripting solution with that.

Thank you, I have been working hard on a system to make this work. I will share what I come up with in case anyone else finds it useful.


Here's some code I wrote for this, to help anyone who might be trying to do the same thing. Basically it's a function that plays an animation from starting time to end time, over duration. If the animation is already playing, it will simply set the parameters of the track to the proper values. If the animation is not playing yet, it will set it on the input track.

// animRange.x is where the animation should start (in seconds),
//   and animRange.y is where the animation should end (in seconds)
// Pass in -1 for animRange.x to have the animation start wherever it is currently.
// Pass in -1 for animRange.y to have the animation end at its natural duration.
 
spTrackEntry*startAnimation(const Char* animName, int track, Point2 animRange,
                                              double duration, bool looping)
{
  spTrackEntry* trackEntry = spAnimationState_getCurrent(spineAnimState, track);
  
// start a new animation if (trackEntry == NULL || strcmp(trackEntry->animation->name, animName) != 0) { trackEntry = spAnimationState_setAnimationByName(spineAnimState, track, animName, looping ? 1 : 0); // cout << "starting new track " << animName << ", track time " << trackEntry->trackTime << endl; } // this track is already playing this animation else { trackEntry->loop = looping ? 1 : 0; // cout << "continuing track " << animName << ", track time " << trackEntry->trackTime << endl; }
double naturalDuration = getAnimationDuration(animName);
// Animation low range wasn't set. // Use either the current playback time or the end of the previously set animation, // whichever is lower (since trackTime keeps running after the animation is done) if (animRange.x < -0.01) { animRange.x = std::min(trackEntry->trackTime, trackEntry->animationEnd); }
// Animation high range wasn't set. Use the natural duration of the animation if (animRange.y < -0.01) { animRange.y = naturalDuration; }
// clamp the animation range from 0.0 - naturalDuration animRange.x = std::max(animRange.x, 0.0); animRange.y = std::min(animRange.y, naturalDuration);
// don't let the start be higher than the stop if (animRange.y <= animRange.x) { animRange.y = animRange.x + 1.0 / 60.0; }
// cout << "trackEntry->animationStart " << trackEntry->animationStart << endl;
// trackTime is the current position of the animation, but it keeps running even after the animation // stops moving because it passed getAnimationDuration() // animationLast is related to timeline keys and even triggering // trackEnd is when the track will actually being rendered, which is normally infinity. // trackTime keeps going even after the animation stops playing at animationEnd. trackEntry->trackTime = animRange.x; trackEntry->animationLast = animRange.x; // cout << "was starting at " << trackEntry->trackTime << ", setting start to " << animRange.x << endl;
// enforce the end of the range so that the animation doesn't keep playing trackEntry->animationEnd = animRange.y;
// cout << "old mix duration " << trackEntry->mixDuration << endl; double trackTimeMult = (animRange.y - animRange.x) / duration; trackEntry->timeScale = trackTimeMult; trackEntry->mixDuration *= trackTimeMult;
// change the mix times for all tracks mixing into this one spTrackEntry* currMixTrack = trackEntry->mixingFrom;
while (currMixTrack != NULL) { currMixTrack->timeScale = trackTimeMult;
currMixTrack = currMixTrack->mixingFrom; }
// cout << "new mix duration " << trackEntry->mixDuration << endl; // cout << "range " << animationRange << " desired duration " << targetDuration << " timeScale " << trackEntry->timeScale << endl; return trackEntry; }

This allows us to use it in a script to solve the example problem, by wrapping that call in a little object and feeding it into a script.

script.enqueue(new StartSpineAnimationCommand("swing_sword", 0, Point2(0.0, 0.333), 0.75));
script.wait(0.25);
script.enqueue(new StartSpineAnimationCommand("swing_sword", 0, Point2(0.333, 0.666), 0.1));
script.enqueue(new StartSpineAnimationCommand("swing_sword", 0, Point2(0.666, 1.0), 0.5));

As you can see, this method is quite clumsy. It's clear that Spine was not really designed to divide a single animation into parts like this and give each part a playback duration. I also believe that there may be some bugs in this code because the timing is sometimes off, could be due to mixing or something. But at least the basic idea is possible!

un mes más tarde

It looks way to complicated for my skills but looks also very interesting, thanks for sharing 🙂

I also felt quiet often that the API would need a way to split animations in parts like this, I ended up splitting a lot of my animations myself for -I think- quiet commun uses (like loading attacks, charging, release and could down)

Glad I could help! Would be happy to share more details, if any part is causing confusion. I now use this system (expanded a bit) for every spine animation in my game, so it ended it being vitally important, despite also being a tad clumsy.