Related Discussions
...
3 months later

I'm going to need Root Motion in a project I'm working on now. Has any native runtime support been added since this thread started 7 months ago? Otherwise, I'll need to take the route of AdamT, and roll my own.

ShaneSmit wrote

I'm going to need Root Motion in a project I'm working on now. Has any native runtime support been added since this thread started 7 months ago? Otherwise, I'll need to take the route of AdamT, and roll my own.

I'll tackle this for Unity. I'm in the middle of overhauling the Spine


Unity stuff. Will post about what I've been up to later tonight.


Had to modify SkeletonAnimation and add a callback between Update and Apply because the Time and LastTime are set to be equivalent by the time UpdateBones is called.

SkeletonAnimation.cs

using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using Spine;

[ExecuteInEditMode]
[AddComponentMenu("Spine/SkeletonAnimation")]
public class SkeletonAnimation : SkeletonRenderer {
   public float timeScale = 1;
   public bool loop;
   public Spine.AnimationState state;

   public delegate void UpdateBonesDelegate(SkeletonAnimation skeleton);
   public UpdateBonesDelegate UpdateBones;
   public UpdateBonesDelegate UpdateState;

   [SerializeField]
   private String _animationName;
   public String AnimationName {
      get {
         TrackEntry entry = state.GetCurrent(0);
         return entry == null ? null : entry.Animation.Name;
      }
      set {
         if (_animationName == value) return;
         _animationName = value;
         if (value == null || value.Length == 0)
            state.ClearTrack(0);
         else
            state.SetAnimation(0, value, loop);
      }
   }

   public override void Reset () {
      base.Reset();
      if (!valid) return;

  state = new Spine.AnimationState(skeletonDataAsset.GetAnimationStateData());
  if (_animationName != null && _animationName.Length > 0) {
     state.SetAnimation(0, _animationName, loop);
     Update(0);
  }
   }

   public virtual void Update () {
      Update(Time.deltaTime);
   }

   public virtual void Update (float deltaTime) {
      if (!valid) return;

  deltaTime *= timeScale;
  skeleton.Update(deltaTime);
  state.Update(deltaTime);

  if(UpdateState != null)   UpdateState(this);

  state.Apply(skeleton);
  if (UpdateBones != null) UpdateBones(this);
  skeleton.UpdateWorldTransform();
   }
}

SkeletonRootMotion.cs

using UnityEngine;
using System.Collections;
using Spine;

[RequireComponent(typeof(SkeletonAnimation))]
public class SkeletonRootMotion : MonoBehaviour {


   SkeletonAnimation skeletonAnimation;
   int rootBoneIndex = -1;
   AnimationCurve rootMotionCurve;

   void OnEnable(){
      if(skeletonAnimation == null)
         skeletonAnimation  = GetComponent<SkeletonAnimation>();

  skeletonAnimation.UpdateState += ApplyRootMotion;
  skeletonAnimation.UpdateBones += UpdateBones;
   }

   void OnDisable(){
      skeletonAnimation.UpdateState -= ApplyRootMotion;
      skeletonAnimation.UpdateBones -= UpdateBones;
   }

   void Start(){
      rootBoneIndex = skeletonAnimation.skeleton.FindBoneIndex( skeletonAnimation.skeleton.RootBone.Data.Name );
      skeletonAnimation.state.Start += HandleStart;
   }

   void HandleStart (Spine.AnimationState state, int trackIndex)
   {
      //must use first track for now
      if(trackIndex != 0)
         return;

  rootMotionCurve = null;

  Spine.Animation anim = state.GetCurrent(trackIndex).Animation;

  //find the root bone's translate curve
  foreach(Timeline t in anim.Timelines){
     if(t.GetType() != typeof(TranslateTimeline))
        continue;

     TranslateTimeline tt = (TranslateTimeline)t;
     if(tt.boneIndex == rootBoneIndex){

        //sample the root curve's X value
        //TODO:  cache this data?  Maybe implement RootMotionTimeline instead and keep it in SkeletonData
        rootMotionCurve = new AnimationCurve();

        float time = 0;
        float increment = 1f/30f;
        int frameCount = Mathf.FloorToInt(anim.Duration / increment);

        for(int i = 0; i <= frameCount; i++){
           float x = GetXAtTime(tt, time);
           rootMotionCurve.AddKey(time, x);
           time += increment;
        }

        break;
     }
  }
   }
   
//borrowed from TranslateTimeline.Apply method float GetXAtTime(TranslateTimeline timeline, float time){ float[] frames = timeline.frames; if (time < frames[0]) return frames[1]; // Time is before first frame.
Bone bone = skeletonAnimation.skeleton.RootBone; if (time >= frames[frames.Length - 3]) { // Time is after last frame. return (bone.data.x + frames[frames.Length - 2] - bone.x); } // Interpolate between the last frame and the current frame. int frameIndex = Spine.Animation.binarySearch(frames, time, 3); float lastFrameX = frames[frameIndex - 2]; float frameTime = frames[frameIndex]; float percent = 1 - (time - frameTime) / (frames[frameIndex + -3] - frameTime); percent = timeline.GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); return (bone.data.x + lastFrameX + (frames[frameIndex + 1] - lastFrameX) * percent - bone.x); } void ApplyRootMotion(SkeletonAnimation skelAnim){ if(rootMotionCurve == null) return; TrackEntry t = skelAnim.state.GetCurrent(0); if(t == null) return; int loopCount = (int)(t.Time / t.EndTime); int lastLoopCount = (int)(t.LastTime / t.EndTime); //disregard the unwanted if(lastLoopCount < 0) lastLoopCount = 0; float currentTime = t.Time - (t.EndTime * loopCount); float lastTime = t.LastTime - (t.EndTime * lastLoopCount); float delta = 0; float a = rootMotionCurve.Evaluate(lastTime); float b = rootMotionCurve.Evaluate(currentTime); //detect if loop occurred and offset if(loopCount > lastLoopCount){ float e = rootMotionCurve.Evaluate(t.EndTime); float s = rootMotionCurve.Evaluate(0); delta = (e-a) + (b-s); } else{ delta = b - a; } if(skelAnim.skeleton.FlipX) delta *= -1; //TODO: implement Rigidbody2D and Rigidbody hooks here transform.Translate(delta,0,0); } void UpdateBones(SkeletonAnimation skelAnim){ //reset the root bone's x component to stick to the origin skelAnim.skeleton.RootBone.X = 0; } }
Mitch wrote

I'll tackle this for Unity. I'm in the middle of overhauling the Spine


Unity stuff. <snip>

Thanks Mitch! :handshake: I'll give this a shot as soon as possible and let you know how it goes.


I had to change a couple things:

1) In one instance I am just triggering the animation via the inspector's drop-down list, and in that case HandleStart() was never being called. So I called it manually in Start().

2) My spine characters are scaled quite a bit, so I had to apply local scale to the 'delta' before Translate().

But yeah, it works great so far! Thanks again Mitch!

ShaneSmit wrote
Mitch wrote

I'll tackle this for Unity. I'm in the middle of overhauling the Spine


Unity stuff. <snip>

Thanks Mitch! :handshake: I'll give this a shot as soon as possible and let you know how it goes.


I had to change a couple things:

1) In one instance I am just triggering the animation via the inspector's drop-down list, and in that case HandleStart() was never being called. So I called it manually in Start().

2) My spine characters are scaled quite a bit, so I had to apply local scale to the 'delta' before Translate().

But yeah, it works great so far! Thanks again Mitch!

Welcome 🙂

Heh yea, I encountered those too, figured it'd be better to get somethin up here before I really try and figure out a more permanent solution for root motion.

3 months later

Hello! I'm countering this problem, too.
My character need to perform very complex movement in move animations, attack animations....
So i have to animate root motion in Spine to get better result. This post helps a lot, thanks!

But i am trying to do the same thing on Y Axis(Height).
First, sorry for my English. Second, i am not a programmer(I am animator) and new to Spine.
I modify Mitch's code. Add a animation curve to catch y transform.
But it works totally wrong... and can not figure out how to fix it...
Seems GetXYAtTime() are returning both x transform..
SkeletonRootMotion.cs

public class SkeletonRootMotion : MonoBehaviour {
   

SkeletonAnimation skeletonAnimation; int rootBoneIndex = -1; // animation curves for copy position AnimationCurve rootMotionCurve; AnimationCurve rootMotionCurveY;
void OnEnable(){ if(skeletonAnimation == null) skeletonAnimation = GetComponent<SkeletonAnimation>(); // add events skeletonAnimation.UpdateState += ApplyRootMotion; skeletonAnimation.UpdateBones += UpdateBones; }
void OnDisable(){ // remove events skeletonAnimation.UpdateState -= ApplyRootMotion; skeletonAnimation.UpdateBones -= UpdateBones; }
void Start(){ rootBoneIndex = skeletonAnimation.skeleton.FindBoneIndex( skeletonAnimation.skeleton.RootBone.Data.Name ); skeletonAnimation.state.Start += HandleStart; }
void HandleStart (Spine.AnimationState state, int trackIndex) { //must use first track for now if(trackIndex != 0) return;
rootMotionCurve = null; rootMotionCurveY = null; // get current animation Spine.Animation anim = state.GetCurrent(trackIndex).Animation; //find the root bone's translate curve foreach(Timeline t in anim.Timelines){ if(t.GetType() != typeof(TranslateTimeline)) continue; TranslateTimeline tt = (TranslateTimeline)t; if(tt.boneIndex == rootBoneIndex){ //sample the root curve's X value //TODO: cache this data? Maybe implement RootMotionTimeline instead and keep it in SkeletonData rootMotionCurve = new AnimationCurve(); rootMotionCurveY = new AnimationCurve(); float time = 0; float increment = 1f/30f; int frameCount = Mathf.FloorToInt(anim.Duration / increment); for(int i = 0; i <= frameCount; i++){ Vector2 v = GetXYAtTime(tt, time); rootMotionCurve.AddKey(time, v.x); rootMotionCurveY.AddKey(time, v.y); time += increment; } break; } } }
//borrowed from TranslateTimeline.Apply method Vector2 GetXYAtTime(TranslateTimeline timeline, float time){ float[] frames = timeline.frames; if (time < frames[0]) return (new Vector2(frames[1], frames[1])); // Time is before first frame.
Bone bone = skeletonAnimation.skeleton.RootBone; if (time >= frames[frames.Length - 3]) { // Time is after last frame. return (new Vector2(bone.data.x + frames[frames.Length - 2] - bone.x, bone.data.y + frames[frames.Length - 2] - bone.y)); } // Interpolate between the last frame and the current frame. int frameIndex = Spine.Animation.binarySearch(frames, time, 3); float lastFrameX = frames[frameIndex - 2]; float frameTime = frames[frameIndex]; float percent = 1 - (time - frameTime) / (frames[frameIndex + -3] - frameTime); percent = timeline.GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); return (new Vector2(bone.data.x + lastFrameX + (frames[frameIndex + 1] - lastFrameX) * percent - bone.x, bone.data.y + lastFrameX + (frames[frameIndex + 1] - lastFrameX) * percent - bone.y)); }
void ApplyRootMotion(SkeletonAnimation skelAnim){ if(rootMotionCurve == null || rootMotionCurveY == null) return;
TrackEntry t = skelAnim.state.GetCurrent(0); if(t == null) return; int loopCount = (int)(t.Time / t.EndTime); int lastLoopCount = (int)(t.LastTime / t.EndTime); //disregard the unwanted if(lastLoopCount < 0) lastLoopCount = 0; float currentTime = t.Time - (t.EndTime * loopCount); float lastTime = t.LastTime - (t.EndTime * lastLoopCount); float delta = 0; float deltaY = 0; float a = rootMotionCurve.Evaluate(lastTime); float aY = rootMotionCurveY.Evaluate(lastTime); float b = rootMotionCurve.Evaluate(currentTime); float bY = rootMotionCurveY.Evaluate(currentTime); //detect if loop occurred and offset if(loopCount > lastLoopCount){ float e = rootMotionCurve.Evaluate(t.EndTime); float eY = rootMotionCurveY.Evaluate(t.EndTime); float s = rootMotionCurve.Evaluate(0); float sY = rootMotionCurveY.Evaluate(0); delta = (e-a) + (b-s); deltaY = (eY-aY) + (bY-sY); } else{ delta = b - a; deltaY = bY - aY; } if(skelAnim.skeleton.FlipX) { delta *= -1; deltaY *= -1; } //TODO: implement Rigidbody2D and Rigidbody hooks here transform.Translate(delta,deltaY,0); Debug.DrawLine(new Vector3(transform.position.x+2, 2, 0), new Vector3(transform.position.x+2, 2+deltaY, 0), Color.red, .2f); }
void UpdateBones(SkeletonAnimation skelAnim){ //reset the root bone's x component to stick to the origin skelAnim.skeleton.RootBone.X = 0; skelAnim.skeleton.RootBone.Y = 0; } }

Hope some one can give me a hand. THANKS!! 🙁


Oh... I found the problem..
Because the data of frames are written as [time, x, y, ....., time, x, y] !

a month later

@[deleted]

Did you get y-root motion working now? Could you post the fixed version of the code please?

Thanks!

edit: I think the fix is needed here ->

        for(int i = 0; i <= frameCount; i++){
           Vector2 v = GetXYAtTime(tt, time);
           rootMotionCurve.AddKey(time, v.x);
           rootMotionCurveY.AddKey(time, v.y);
           time += increment;
        }

@rootMotionCurveY.AddKey(time,vy);
to something like that ->
rootMotionCurveY.AddKey(time,?,vy); but what filling in at the x var?

Rob wrote

@[deleted]

Did you get y-root motion working now? Could you post the fixed version of the code please?

Thanks!

edit: I think the fix is needed here ->

        for(int i = 0; i <= frameCount; i++){
           Vector2 v = GetXYAtTime(tt, time);
           rootMotionCurve.AddKey(time, v.x);
           rootMotionCurveY.AddKey(time, v.y);
           time += increment;
        }

@rootMotionCurveY.AddKey(time,vy);
to something like that ->
rootMotionCurveY.AddKey(time,?,vy); but what filling in at the x var?

@Rob
I'm not a programmer, so maybe my code have some mistakes.
But i think it works well, here is my code...

public class SkeletonRootMotion : MonoBehaviour {
   
SkeletonAnimation skeletonAnimation; int rootBoneIndex = -1; // animation curves for copy position AnimationCurve rootMotionCurve; AnimationCurve rootMotionCurveY; // FOR ROOT Y
void OnEnable(){ if(skeletonAnimation == null) skeletonAnimation = GetComponent<SkeletonAnimation>(); // add events skeletonAnimation.UpdateState += ApplyRootMotion; skeletonAnimation.UpdateBones += UpdateBones; }
void OnDisable(){ // remove events skeletonAnimation.UpdateState -= ApplyRootMotion; skeletonAnimation.UpdateBones -= UpdateBones; }
void Start(){ rootBoneIndex = skeletonAnimation.skeleton.FindBoneIndex( skeletonAnimation.skeleton.RootBone.Data.Name ); skeletonAnimation.state.Start += HandleStart; }
void HandleStart (Spine.AnimationState state, int trackIndex) { //must use first track for now if(trackIndex != 0) return;
rootMotionCurve = null; rootMotionCurveY = null; // FOR ROOT Y // get current animation Spine.Animation anim = state.GetCurrent(trackIndex).Animation; //find the root bone's translate curve foreach(Timeline t in anim.Timelines){ if(t.GetType() != typeof(TranslateTimeline)) continue; TranslateTimeline tt = (TranslateTimeline)t; if(tt.boneIndex == rootBoneIndex){ //sample the root curve's X value //TODO: cache this data? Maybe implement RootMotionTimeline instead and keep it in SkeletonData rootMotionCurve = new AnimationCurve(); rootMotionCurveY = new AnimationCurve(); // FOR ROOT Y float time = 0; float increment = 1f/30f; int frameCount = Mathf.FloorToInt(anim.Duration / increment); for(int i = 0; i <= frameCount; i++){ Vector2 v = GetXYAtTime(tt, time); rootMotionCurve.AddKey(time, v.x); rootMotionCurveY.AddKey(time, v.y); // FOR ROOT Y time += increment; } break; } } }
//borrowed from TranslateTimeline.Apply method Vector2 GetXYAtTime(TranslateTimeline timeline, float time){ float[] frames = timeline.frames; if (time < frames[0]) return (new Vector2(frames[1], frames[2])); // Time is before first frame.
Bone bone = skeletonAnimation.skeleton.RootBone; if (time >= frames[frames.Length - 3]) { // Time is after last frame. return (new Vector2(bone.data.x + frames[frames.Length - 2] - bone.x, bone.data.y + frames[frames.Length - 1] - bone.y)); // FOR ROOT Y } // Interpolate between the last frame and the current frame. int frameIndex = Spine.Animation.binarySearch(frames, time, 3); float lastFrameX = frames[frameIndex - 2]; float lastFrameY = frames[frameIndex - 1]; // FOR ROOT Y float frameTime = frames[frameIndex]; float percent = 1 - (time - frameTime) / (frames[frameIndex + -3] - frameTime); percent = timeline.GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); return (new Vector2(bone.data.x + lastFrameX + (frames[frameIndex + 1] - lastFrameX) * percent - bone.x, bone.data.y + lastFrameY + (frames[frameIndex + 2] - lastFrameY) * percent - bone.y)); // FOR ROOT Y }
void ApplyRootMotion(SkeletonAnimation skelAnim){ if(rootMotionCurve == null || rootMotionCurveY == null) // FOR ROOT Y return;
TrackEntry t = skelAnim.state.GetCurrent(0); if(t == null) return; int loopCount = (int)(t.Time / t.EndTime); int lastLoopCount = (int)(t.LastTime / t.EndTime); //disregard the unwanted if(lastLoopCount < 0) lastLoopCount = 0; float currentTime = t.Time - (t.EndTime * loopCount); float lastTime = t.LastTime - (t.EndTime * lastLoopCount); float delta = 0; float deltaY = 0; float a = rootMotionCurve.Evaluate(lastTime); float aY = rootMotionCurveY.Evaluate(lastTime); // FOR ROOT Y float b = rootMotionCurve.Evaluate(currentTime); float bY = rootMotionCurveY.Evaluate(currentTime); // FOR ROOT Y //detect if loop occurred and offset if(loopCount > lastLoopCount){ float e = rootMotionCurve.Evaluate(t.EndTime); float eY = rootMotionCurveY.Evaluate(t.EndTime); // FOR ROOT Y float s = rootMotionCurve.Evaluate(0); float sY = rootMotionCurveY.Evaluate(0); // FOR ROOT Y delta = (e-a) + (b-s); deltaY = (eY-aY) + (bY-sY); // FOR ROOT Y } else{ delta = b - a; deltaY = bY - aY; // FOR ROOT Y } if(skelAnim.skeleton.FlipX) { delta *= -1; deltaY *= -1; // FOR ROOT Y } //TODO: implement Rigidbody2D and Rigidbody hooks here transform.Translate(delta,deltaY,0); // FOR ROOT Y Debug.DrawLine(new Vector3(transform.position.x+2, 2, 0), new Vector3(transform.position.x+2, 2+deltaY, 0), Color.red, .2f); // FOR ROOT Y }
void UpdateBones(SkeletonAnimation skelAnim){ //reset the root bone's x component to stick to the origin skelAnim.skeleton.RootBone.X = 0; skelAnim.skeleton.RootBone.Y = 0; // FOR ROOT Y } }

Or you can see the code on my GitHub.. (Also keep the original code)
https://github.com/januswow/TAG/blob/master/TouchActionGame/Assets/spine-unity/Assets/spine-unity/SkeletonRootMotion.cs

Thank you 🙂
Gonna make some tests with it right now : P


But if I want to support slopes and advanced movement, rootmotion will not fit my needs right?
I already setup my scripts to handle slopes at any angle. It gots some velocity and other stuff.

But if I want to support slopes and advanced movement, rootmotion will not fit my needs right?
I already setup my scripts to handle slopes at any angle. It gots some velocity and other stuff.

Depends on how you implement root motion. Rather than setting the object's position directly, you could apply it to its rigidbody, or if you aren't using physics just raycast downward and apply gravity yourself.

I use custom movement without physics. Already applied gravity by myself.
It's a raycast based script.

I just don't understand the following:
(no physics used here)

If I press right, a force is being applied to my character. He moves right and the script calculates when to move up or down because a slope is detected.

Rootmotion is just following the animations x and y position, as far as I know. Well, I could stick to my script, without rootmotion, but it offers so many advantages. Especially when it comes to complicated animations.

Could you give me a hint how to combine the rootmotionscript with a raycastercontroller that has its own speed, without physics?

Maybe using delta and deltaY values and put them somewhere as velocity.

Hi, Mitch. Thanks for the awesome script.
But I'm a little confused about the resampled rootMotionCurve. What's that for? Can't I just calculate the x delta from the TranslateTimeline?
You mentioned that maybe a RootMotionTimeline would be better. So what's the difference between RootMotionTimeline and TranslateTimeline of the root bone?
Thanks again.

But I'm a little confused about the resampled rootMotionCurve. What's that for? Can't I just calculate the x delta from the TranslateTimeline?

Sure can. I wrote this long, long before I had finished reading over the whole Spine-Unity API heh. I think it was like...my 5th post here or something. The plan was to do a proper root motion implementation with SkeletonUtility but I never got around to it. The hooks were missing from Spine-Unity to do proper Root Motion when I originally implemented this and I didn't want to destructively alter a timeline for RootMotion since it would alter the SkeletonData instance rather than just the SkeletonAnimation instance.

Feel free and modify 🙂 I have no timeline on a better one for SkeleonUtility.

8 months later

I am getting a few errors in this part of the CODE.

Here is the printscreen
http://postimg.org/image/64srv3ehb/

It seems to be sintax error.

void OnEnable(){
   
if (skeletonAnimation == null) skeletonAnimation = GetComponent<SkeletonAnimation> ();
skeletonAnimation.UpdateWorld += ApplyRootMotion; skeletonAnimation.UpdateLocal += UpdateBones; // updatebones == updatelocal && updatestate == updateworld } void OnDisable(){
skeletonAnimation.UpdateWorld -= ApplyRootMotion; skeletonAnimation.UpdateLocal -= UpdateBones;
}

Nobody else got this?

I'm assuming the method signature for UpdateWorld and UpdateLocal has changed. You need to make sure your ApplyRootMotion and UpdateBones methods match the delegate signature defined in SkeletonAnimation (actually SkeletonRenderer). Probably because they now expect a SkeletonRenderer and not a SkeletonAnimation, if I had to guess.

Oh...i see.

There is documentation about the parameters of spine/unity integration?

a month later

Would be neat if we could get either a proper solution here, or get an official UpdateXXXX callback added to SkeletonAnimation before the UpdateWorldTransform. Right now I need to maintain changes to spine-unity each time I update. 🙂