一般来说,我们做动画的时候尽量要求动画师做原地动画,也就是说动画本身是没有位移的。这种方法在做匀速运动的动画的时候是没有问题的,比如主角跑步是个匀速运动。但如果某些动画本身不是匀速运动,比如丧失的走路动画,可能会是一个非线性运动,这个时候如果要策划在编辑器通过调曲线来调出走路的非线性移动是相当困难的,而spine-unity并没有提供像3dmax一样将移动转换为unity速度的方法,这个问题只能我们自己来解决。
解决方案有两种,先来看第一种。
第一种要求动画师在编辑动画时,让动画的根骨骼也就是“root”跟随动画一起运动,保持根骨骼一直在人物脚底。
然后我们直接看spine-unity源码,直接看Bone.cs源码:
这里只截取一部分
public void UpdateWorldTransform (float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY) {
priorX = ax;
priorY = ay;
ax = x;
ay = y;
arotation = rotation;
ascaleX = scaleX;
ascaleY = scaleY;
ashearX = shearX;
ashearY = shearY;
appliedValid = true;
Skeleton skeleton = this.skeleton;
Bone parent = this.parent;
if (parent == null) { // Root bone
float rotationY = rotation + 90 + shearY;
float la = MathUtils.CosDeg(rotation + shearX) * scaleX;
float lb = MathUtils.CosDeg(rotationY) * scaleY;
float lc = MathUtils.SinDeg(rotation + shearX) * scaleX;
float ld = MathUtils.SinDeg(rotationY) * scaleY;
if (skeleton.flipX) {
x = -x;
la = -la;
lb = -lb;
}
if (skeleton.flipY != yDown) {
y = -y;
lc = -lc;
ld = -ld;
}
a = la;
b = lb;
c = lc;
d = ld;
//worldX = x + skeleton.x;
//worldY = y + skeleton.y;
//
// worldSignX = Math.Sign(scaleX);
// worldSignY = Math.Sign(scaleY);
return;
}
先把根骨骼位移去掉,直接注释掉worldX=x+skeleton.x,worldY=y+skeleton.y,直接在更新骨骼位置的时候拿到根骨骼当前跟上一帧的位置差,将这个位移应用到gameobject本身就可以了,也就是每帧移动的时候获取ax-priorX(ay-priorY)的值,然后乘以本地缩放值,附加给gameobject就可以了。
但是,这个方法有个明显的问题,spine在切动画的时候,会把根骨骼根据mix差值回去,这就表明我们要给每个动画指明方向,比如这个动画只能往前走,往后走的位移会被忽略掉,这样就不会出现瞬移问题了。
第二种方法,自己根据每个动画之间的mix时间计算差值位移(记得乘本地缩放),直接上核心代码
public void SetSourceBone(string name)
{
var skeleton = skeletonComponent.Skeleton;
int bi = skeleton.FindBoneIndex(name);
if (bi >= 0)
{
this.boneIndex = bi;
this.bone = skeleton.bones.Items[bi];
}
else
{
Debug.Log("Bone named \"" + name + "\" could not be found.");
this.boneIndex = 0;
this.bone = skeleton.RootBone;
}
}
void HandleUpdateLocal(ISkeletonAnimation animatedSkeletonComponent)
{
if (!this.isActiveAndEnabled) return; // Root motion is only applied when component is enabled.
Vector2 localDelta = Vector2.zero;
TrackEntry current = state.GetCurrent(0); // Only apply root motion using AnimationState Track 0.
TrackEntry track = current;
TrackEntry next = null;
int boneIndex = this.boneIndex;
while (track != null)
{
var a = track.Animation;
var tt = a.FindTranslateTimelineForBone(boneIndex);
if (tt != null)
{
float start = track.animationLast;
float end = track.AnimationTime;
Vector2 currentDelta;
if (start > end)
currentDelta = (tt.Evaluate(end) - tt.Evaluate(0)) + (tt.Evaluate(a.duration) - tt.Evaluate(start)); // Looped
else if (start != end)
currentDelta = tt.Evaluate(end) - tt.Evaluate(start); // Non-looped
else
currentDelta = Vector2.zero;
//Apply alpha to the delta position (based on AnimationState.cs)
float mix;
if (next != null)
{
if (next.mixDuration == 0)
{
mix = 1;
}
else
{
mix = next.mixTime / next.mixDuration;
if (mix > 1) mix = 1;
}
float mixAndAlpha = track.alpha * next.interruptAlpha * (1 - mix);
currentDelta *= mixAndAlpha;
}
else
{
if (track.mixDuration == 0)
{
mix = 1;
}
else
{
mix = track.alpha * (track.mixTime / track.mixDuration);
if (mix > 1) mix = 1;
}
currentDelta *= mix;
}
// 3. Add the delta from the track to the accumulated value.
localDelta += currentDelta;
}
// Traverse mixingFrom chain.
next = track;
track = track.mixingFrom;
}
// 4. Apply flip to the delta position.
var skeleton = animatedSkeletonComponent.Skeleton;
if (skeleton.flipX) localDelta.x = -localDelta.x;
if (skeleton.flipY) localDelta.y = -localDelta.y;
// 5. Apply root motion to Transform or RigidBody;
if (!useX) localDelta.x = 0f;
if (!useY) localDelta.y = 0f;
if(skeletonAnimation.state.UseAniMovement)
{
Move(new Vector2(localDelta.x * skeletonAnimation.transform.localScale.x, localDelta.y * skeletonAnimation.transform.localScale.y));
}
// 6. Position bones to be base position
// BasePosition = new Vector2(0, 0);
foreach (var b in siblingBones)
{
if (useX) b.x -= bone.x;
if (useY) b.y -= bone.y;
}
if (useX) bone.x = 0;
if (useY) bone.y = 0;
foreach (var b in fixedBones)
{
b.x = 0;
b.y = 0;
}
}
}
public static class TimelineTools
{
public static TranslateTimeline FindTranslateTimelineForBone(this Spine.Animation a, int boneIndex)
{
foreach (var t in a.timelines)
{
var tt = t as TranslateTimeline;
if (tt != null && tt.boneIndex == boneIndex)
return tt;
}
return null;
}
public static Vector2 Evaluate(this TranslateTimeline tt, float time, SkeletonData skeletonData = null)
{
const int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1;
const int X = 1, Y = 2;
var frames = tt.frames;
if (time < frames[0]) return Vector2.zero;
float x, y;
if (time >= frames[frames.Length - TranslateTimeline.ENTRIES])
{ // Time is after last frame.
x = frames[frames.Length + PREV_X];
y = frames[frames.Length + PREV_Y];
}
else
{
int frame = Spine.Animation.BinarySearch(frames, time, TranslateTimeline.ENTRIES);
x = frames[frame + PREV_X];
y = frames[frame + PREV_Y];
float frameTime = frames[frame];
float percent = tt.GetCurvePercent(frame / TranslateTimeline.ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
x += (frames[frame + X] - x) * percent;
y += (frames[frame + Y] - y) * percent;
}
Vector2 o = new Vector2(x, y);
if (skeletonData == null)
{
return o;
}
else
{
var boneData = skeletonData.bones.Items[tt.boneIndex];
return o + new Vector2(boneData.x, boneData.y);
}
}
但是这个方法也有个问题,自己计算差值会导致一个问题,mix时间越长位移误差就越大。