David Kalina

Wondering if this sort of thing is possible with Spine:

- Animate a character such that the Root bone defines translation that I wish to apply to the character in-game.
- Specify at runtime to 'extract root motion' -- i.e. play the animation without the root bone's translation/rotation applied to the animation, save those values somewhere I can access them.
- My game code instead reads the translation/rotation off the root bone and applies it to the character.

Would do this sort of thing in 3D way back in my previous life as a console game programmer :) It's quite clearly good for implementing something like a walk cycle with a cadence perfectly matched to the animation.

(I'm currently animating insects, and their walk cycles can be really funny, so simply applying linear velocity doesn't feel right)
David Kalina
Posts: 11

Nate

At runtime you can access the animation, which has a list of timelines which each have keys. Find the timelines for the root bone, copy the translation keys, and remove those timelines from the animation.
User avatar
Nate

Nate
Posts: 7625

AdamT

How do you remove a timeline?

Trying to figure this out in Unity, but it's a bit of a merry-go-round...
AdamT
Posts: 31

Pharan

Remove a timeline? Like from the json? or the deserialized animation data?

or the Spine project?
User avatar
Pharan

Pharan
Posts: 4493

AdamT

Sorry, that wasn't the most well constructed question.

From the deserialized animation data - a script on a gameobject.

I sat down with it again with a fresh coffee this morning and worked my way through the Timeline class, figured out that there are RotateTimeline and TranslateTimeline classes extended from CurveTimeLine, which itself is derived from Timeline - none of these are mentioned in the docs or class diagram, so it never occurred to me to look further than CurveTimeLine (and I only came across that because it was required in the Timeline.Remove() method).

While I can script fine in C#, some of the deeper nuances of OO coding are still surfacing so I have to jog in wide circles a lot - Unity's probably to blame for some of that, making life too easy / hiding away a lot of things we really should learn, but end up skipping. Doesn't help much that there are namespace conflicts, and Monodevelop seems happy to suggest things that appear to belong to other classes/types...

So I've got these now anyway, the question is how to remove them gracefully? MonoDevelop suggests Timeline.Remove(CurveTimeline) but I'm getting different results with different approaches and as a result I'm having trouble interpreting the errors (and can't find the declaration for the Timeline.Remove() method).

If I step through all the timelines, I get an error trying to Remove() the first TranslateTimeline. Yet I get no error if I specify the timeline manually (in the code after the foreach).

Even so, in the second case, Remove() hasn't done what I expected - it appears to have just removed the first keyframe of that timeline - my character is still walking, but his Y position leaves off from wherever the previous animation was at.

string anim = "walk-root"; // animation with root motion
int p = 0;

// This throws an InvalidOperatorException Removing timeline[1]*
// "Collection was modified; enumeration operation may not execute."
//
foreach ( CurveTimeline c in skeletonData.FindAnimation(anim).timelines)
{
// reports alternating Rotate/TranslateTimelines
Debug.Log (p + ": " + c.GetType() );

if (c.GetType() == typeof(Spine.TranslateTimeline))
{
Debug.Log ("Removing curve "+p);
skeletonData.FindAnimation(anim).Timelines.Remove(c); // *exception here, p=1
}
p++;
}

// This returns Spline.TranslateTimeline
//
Debug.Log ( skeletonData.FindAnimation(anim).timelines[1].GetType() );

// This needs to be cast explicitly otherwise it returns Spline.Timeline
//
TranslateTimeline timeline = skeletonData.FindAnimation(anim).timelines[1] as TranslateTimeline;

// And this seems to remove only the first keyframe - the root is still moving.
//
skeletonData.FindAnimation(anim).Timelines.Remove(timeline);


No doubt I'm making a schoolboy error...?

edit: fixed an oops in my code, same results though.
AdamT
Posts: 31

AdamT

Caffeine is working, and I'm having a face-palm moment for trying to call Remove on a Generic List.

Don't understand why that didn't throw an error in the second Remove(), and I'm still stuck :/
AdamT
Posts: 31

AdamT

Ok so I got around this by just grabbing a copy of the timeline, blanking it's keys, and copying the new timeline back into the animation.timeline
// c is animation.timelines[p] (the root's TranslateTimeline)
// Discovered the Root timelines are at the *end* of the List, not the start.
// so p = 34 in my case

TranslateTimeline rootTimeline = c as TranslateTimeline;
int fc = rootTimeline.FrameCount;

for (int i = 0 ; i < fc ; i++ )
{
rootTimeline.SetFrame(i,0,0,0);
}
skeletonData.FindAnimation(anim).timelines[p] = rootTimeline;
That all works, except our guy's 0,0 root position isn't being respected when playing his root animation after another animation - even if I set the keys to non-zero, i.e. SetFrame(i,0,0,1) he still seems to inherit the Y position of the previous bone in the chain (hip in this case). So for example, if I jump then AddAnimation to the modified animation, his feet are below the ground level.

I've just got to figure out why this is happening (blending, timer, update issue?) , then work out how to read the offsets back from a cached timeline during playback.
AdamT
Posts: 31

AdamT

Got it. Rebuilding the pivot timelines, using the same code above, for all of the animations fixes the blend/inheritance problem. No idea why, gift horse mouths and all that, but it's all groovy now.

Also discovered that having Event timelines (and I assume other types i.e. Color) mean you can't just grab the last Timeline to get the pivot, easy to get around once I'd stopped scratching my head.
AdamT
Posts: 31

AdamT

Problems, problems.

Pulling movement data from the timelines is impractical without cloning them, and there's no easy way to get it to work with mixed animations.

I tried a couple of quick and dirty workarounds - first transposing the root bone's position with transform.position (of the gameobject)... this worked, sort of, as it would sometimes flicker, and worse had some weird artifact where it would play the last frame of the animation in it's "true" position --- is Spline forcing the render outside normal Update loop?

Also, this didn't work so well with animation mixes, which are of course internal to Spine.

The second workaround was to add a rootMotion bone to my character in Spine Editor ... parent the hip to this, animate, then re-parent the hip to the root. In Unity I simply read the position of the bone and += that to the GameObject's transform.position. This actually works well - and presents a strong argument for implementing root motion support in the Editor (an ever-present bone that you can toggle Edit Root Motion On/Off).

However in playback there's a problem, it pauses (or eases) between loops. Which suggests again that Spine is not playing nice with regular Render/Update timing.

Curious, I set up a gameobject to follow the head bone, and sure enough the bone actually lags behind the head as you'd expect from a smooth Lerp.

Can anyone shed any light on this?
AdamT
Posts: 31

Pharan

Sorry, I'm a bit confused about why you're doing this (removing the root bone movement from the Skeleton and using it on the Unity Transform).

As for the lag, it might be the case that your script that makes the thing follow the head bone is running before the SkeletonAnimation script updates so it followed the Bone's state from the last frame. You may have to force a specific script execution order between these two MonoBehaviours.

The Spine-Unity runtime doesn't make any render calls or mess with rendering. It just updates the Mesh attached to the GameObject. (And uses two meshes, because having double-buffered meshes seems to be good for mobile)

(BTW, to everyone else, it's Spine and not Spline. just fyi.)
User avatar
Pharan

Pharan
Posts: 4493

AdamT

Pharan wrote:Sorry, I'm a bit confused about why you're doing this (removing the root bone movement from the Skeleton and using it on the Unity Transform).
So I can move the gameobject to follow the skeleton's footstep cadence; think fake IK foot planting.

I've set up script execution orders, no difference.
(BTW, to everyone else, it's Spine and not Spline. just fyi.)
I feel like I'm back at school.

Edit: Correction, yep sorry the script exe order fixed the gameobject following bone issue. Hasn't changed the odd data I'm getting from my rootmotion bone - on closer inspection (slowing everything down to 5%) it appears to be ignoring the first half of the bone's x data; animates in-place for 15 frames, then moves forward 15 frames, repeat.
AdamT
Posts: 31

Pharan

That is kinda weird. I was going to suggest trying to use WorldX instead of X, but I guess for the root bone, that'd be the same thing. (and if it were a child bone and you already removed the timeline, WorldX would never get calculated). Or is the rootmotion bone different from the root bone?

My first instinct is to check if there's good data in the json. If the root bone moved and a separate rootmotion bone was animated too (and that rootmotion bone is inevitably a child of the rootbone) then you would get weird data.

I still don't get the use though. hehe.
I know what IK is but this transfer of animation from bone to Unity Transform doesn't make sense to me. But whatever, no need to explain. I'll just look it up. I suppose it's something common.
User avatar
Pharan

Pharan
Posts: 4493

AdamT

Got it! :)

The problem was in my code; I was rather lazily/foolishly writing all my test functions in the same script, and left an earlier coroutine running every time I changed animation, which was overriding a flag I use to process the rootmotion function. Can't believe I didn't see it - I must be getting old. I hang my head in shame.

End result is just a couple of maths calcs, and works really well - the mixing seems fine so far too. I may post a package when I've turned it into a proper controller and put it through it's paces, but first I need to create some big-ass mechs.

Thanks for the sanity checks :)
AdamT
Posts: 31

felthecat

Hi AdamT!

I'm also at the point where I need a way to use motion root. :) After reading this post I was intrigued how it goes with this package you proposed to post? It would be of really great help for me and my team. Thx! :)
felthecat
Posts: 3

Nate

FWIW, you can use offset with ghosting for a walk cycle (or jump, sort of).
User avatar
Nate

Nate
Posts: 7625

Pharan

I kinda get the use of root motion now.

Consider a character-ful walk where the displacement has explicitly timed pauses and easing. Like a sneak-walk with pauses.
This would be a huge pain to do in code if you had a lot of variations of it, but trivial if you could just animate the displacement through Spine in keys.

Admittedly, it leaves a bad taste in my mouth for mixing animation data with balanceable logic data.
I mean, you'd never do it for jumps in a platformer game... maybe. :D
But it's apparently pretty standard for 3D. Unity supports it for imported 3D animations.
Personally, I'd only ever use it for really specific things.

Not that I'm requesting.
User avatar
Pharan

Pharan
Posts: 4493

Shiu

Isn't it enough to put motion on for example a hip bone? Personally I always have a hip bone which is parented to the root bone and if I need to move something I use that and not the root.
User avatar
Shiu

Shiu
Posts: 2182

Pharan

@Shiu.
Root Motion isn't about putting motion on any specific bone.
Root Motion means specifying the displacement motion/locomotion of an animation (conventionally by animating the position of the root bone in the animation program, hence the name) and using it as movement of the logical game object.
It means using the animation system to drive logical motion, which isn't something the Spine runtimes do out of the box, though it easily could with additional code on top of what it already does. (Again, not that I'm requesting. :D)

In the sneak-walk-with-pauses example, it would be really painful to express the starting and stopping and easing of the character's movement through raw code, at least in the typical, game-logical way one would move a game character (by interpreting a stored speed into a change in position).
Just coding it correctly to apply to possibly several types of movements would be one thing, adjusting it perfectly per specific animation would be another, and having to retime it if the animation changes is an additional pain.

The easier solution is for the animator to animate the x-movement of such an animation perfectly in a GUI environment that's meant for animation (ie, Spine Editor), and then just use Root Motion.
User avatar
Pharan

Pharan
Posts: 4493

Shiu

Ohhhh I see, I didn't read all of the thread (shame on me) :x
That makes sense, especially with the ease-in/out part. Makes so much sense that I even know what kind of Math should be used for it YAY! Don't ask me to code it though :(
User avatar
Shiu

Shiu
Posts: 2182

BlackCapApps

This a nice video to illustrate the concept:
http://vimeo.com/12052874
Plus IK pinning to create accurate motion in-game.
2D Artist (Hand-Painted Style) - Game Assets And Concept Art
Portfolio: viewtopic.php?f=10&t=2709
http://hammeronart.com
User avatar
BlackCapApps
Posts: 47

ShaneSmit

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.
User avatar
ShaneSmit
Posts: 2

Mitch

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;
}
}
User avatar
Mitch

Mitch
Posts: 966

ShaneSmit

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!
User avatar
ShaneSmit
Posts: 2

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.
User avatar
Mitch

Mitch
Posts: 966

janusoo

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] !
janusoo
Posts: 12


Return to Runtimes