Spine 动画导入 Unity3D 之后,是作为一个整体导入的,所以动画中 root 骨骼的移动是
位于局部空间的,如果需要像 Mecanim 的 Root Motion 功能一样,把 root 骨骼的移动
赋给 GameObject 本身,需要通过自己写 Script 扩展来实现。

在 Spine 的论坛中有一篇 post 讨论了这个问题,地址在这 Applying Root Motion

但是论坛的代码是在动画播放前把移动曲线 bake 到 Unity3D 自己的 Animation 里面,
对瞬间的性能负荷比较大,而且移动的时候因为时间帧对齐问题会产生误差,所以我修改
成了直接使用 Spine 记录的数据来计算当前的位置,而且做了少量的优化。

下面是 Script 扩展的实现,保存成 SkeletonRootMotion.cs 放入项目中,然后在
SkeletonAnimation 上添加这个 Component 即可,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
using UnityEngine;
using System.Collections;
using Spine;
using Spine.Unity;
[RequireComponent(typeof(SkeletonAnimation))]
public class SkeletonRootMotion : MonoBehaviour
{
SkeletonAnimation skeletonAnimation;
int rootBoneIndex = -1;
TrackEntry track;
TranslateTimeline transTimeline;
float lastTime;
Vector2 lastPos;
Vector2 fullDelta;
void OnEnable()
{
// add events
skeletonAnimation.UpdateComplete += ApplyRootMotion;
skeletonAnimation.UpdateLocal += UpdateBones;
skeletonAnimation.state.Start += HandleStart;
skeletonAnimation.state.End += HandleEnd;
}
void OnDisable()
{
// remove events
skeletonAnimation.UpdateComplete -= ApplyRootMotion;
skeletonAnimation.UpdateLocal -= UpdateBones;
skeletonAnimation.state.Start -= HandleStart;
skeletonAnimation.state.End -= HandleEnd;
}
void Awake()
{
if(skeletonAnimation == null)
skeletonAnimation = GetComponent<SkeletonAnimation>();
rootBoneIndex = skeletonAnimation.Skeleton.FindBoneIndex(skeletonAnimation.skeleton.RootBone.Data.Name);
}
void HandleStart(Spine.AnimationState state, int trackIndex)
{
//must use first track for now
if(trackIndex != 0) return;
track = state.GetCurrent(trackIndex);
// get current animation
Spine.Animation anim = track.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)
{
transTimeline = tt;
lastTime = 0;
lastPos = GetXYAtTime(transTimeline, 0);
fullDelta = GetXYAtTime(transTimeline, track.animation.Duration) - lastPos;
break;
}
}
}
void HandleEnd(Spine.AnimationState state, int trackIndex)
{
if (trackIndex != 0) return;
ApplyRootMotion(skeletonAnimation);
track = null;
transTimeline = null;
}
void ApplyRootMotion(ISkeletonAnimation skelAnim)
{
if (transTimeline == null) return;
float duration = track.animation.Duration;
int loopCount = Mathf.FloorToInt(track.Time/duration);
float time = track.Time - loopCount * duration;
Vector2 pos = GetXYAtTime(transTimeline, time);
Vector2 delta = pos - lastPos;
delta += fullDelta * (loopCount - Mathf.FloorToInt(lastTime/duration));
if (skelAnim.Skeleton.FlipX) delta.x = -delta.x;
transform.Translate(delta.x, delta.y, 0);
lastTime = track.Time;
lastPos = pos;
}
void UpdateBones(ISkeletonAnimation skelAnim)
{
if (transTimeline == null) return;
//reset the root bone's x component to stick to the origin
skelAnim.Skeleton.RootBone.X = 0;
skelAnim.Skeleton.RootBone.Y = 0;
}
//borrowed from TranslateTimeline.Apply method
Vector2 GetXYAtTime(TranslateTimeline timeline, float time)
{
const int ENTRIES = 3;
const int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1;
const int X = 1, Y = 2;
float x, y;
float[] frames = timeline.Frames;
if (time < frames[0]) return new Vector2(0, 0); // Time is before first frame.
if (time >= frames[frames.Length - ENTRIES]) // Time is after last frame.
{
x = frames[frames.Length + PREV_X];
y = frames[frames.Length + PREV_Y];
return new Vector2(x, y);
}
// Interpolate between the previous frame and the current frame.
int frame = Spine.Animation.binarySearch(frames, time, ENTRIES);
float prevX = frames[frame + PREV_X];
float prevY = frames[frame + PREV_Y];
float frameTime = frames[frame];
float percent = timeline.GetCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
x = prevX + (frames[frame + X] - prevX) * percent;
y = prevY + (frames[frame + Y] - prevY) * percent;
return new Vector2(x, y);
}
}