Spine动画在使用过程中需要对动画对象进行位移控制,虽然官方给出SkeletonRootMotion的脚本,但实际项目中通常需要配合移动逻辑一起计算,这个方法就不能很好控制。
这里提供一种实现方法:通过获取动画位移数据,在移动逻辑中进行插值运算;代码如下:
#if UNITY_EDITOR
using System;
using System.IO;
using Spine;
using Spine.Unity;
using Spine.Unity.AnimationTools;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class SpineRootMotionMaker : EditorWindow
{
private static GameObject s_PreSelectGameObject = null;
private GameObject m_SelectGameObject = null;
private GameObject m_PrevSelectGameObject = null;
private List<string> m_ListBones = new List<string>();
private int m_nSelectBoneIndex = 0;
private string rootMotionBoneName = "root";
private int rootMotionBoneIndex = -1;
private Bone rootMotionBone = null;
private Dictionary<string, List<Vector3>> DicData = new Dictionary<string, List<Vector3>>();
private List<int> nListFrame = new List<int>();
private Vector2 m_v2TableScroll = Vector2.zero;
[MenuItem("Lop/GenerateSpineRootMotion")]
private static void Init()
{
s_PreSelectGameObject = null;
GetWindow<SpineRootMotionMaker>(true, "Export Root Motion Data");
}
[MenuItem("Assets/RootMotion/GenerateSpineRootMotion")]
static void OpenDialog()
{
bool bCanCreate = true;
if (Selection.activeObject == null) bCanCreate = false;
if (Selection.activeObject && Selection.activeObject.GetType() != typeof(GameObject)) bCanCreate = false;
if (bCanCreate == false)
{
EditorUtility.DisplayDialog("Error", "选中GameObject可以继续进行", "OK");
return;
}
s_PreSelectGameObject = (GameObject)Selection.activeObject;
GetWindow<SpineRootMotionMaker>(true, "Export Root Motion Data");
}
private void OnGUI()
{
if (s_PreSelectGameObject != null)
{
m_SelectGameObject = s_PreSelectGameObject;
s_PreSelectGameObject = null;
}
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Select Object", GUILayout.Width(100));
m_SelectGameObject = (GameObject)EditorGUILayout.ObjectField(m_SelectGameObject, typeof(GameObject), false);
GUILayout.EndHorizontal();
RefreshBoneList();
if (m_SelectGameObject != null)
{
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField(" - Select Bone", GUILayout.Width(100));
rootMotionBoneIndex = EditorGUILayout.Popup(rootMotionBoneIndex, m_ListBones.ToArray());
GUILayout.EndHorizontal();
}
OnGUICurve();
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
bool bCheckSize = false;
if (position.width - 200 > 0) bCheckSize = true;
if (bCheckSize) GUILayout.Space(position.width - 200);
if (GUILayout.Button("Cancel", GUILayout.Width(100), GUILayout.ExpandWidth(bCheckSize)) == true)
{
Close();
GUIUtility.ExitGUI();
}
GUI.enabled = (m_SelectGameObject != null && m_nSelectBoneIndex != -1) ? true : false;
if (GUILayout.Button("Create", GUILayout.Width(100), GUILayout.ExpandWidth(bCheckSize)) == true)
{
GameObject objInst = GameObject.Instantiate(m_SelectGameObject);
objInst.name = m_SelectGameObject.name;
objInst.hideFlags = HideFlags.HideAndDontSave;
DicData.Clear();
nListFrame.Clear();
string p = AssetDatabase.GetAssetPath(m_SelectGameObject);
string path = Path.GetDirectoryName(p) + "/" + Path.GetFileNameWithoutExtension(p);
GenerationRootMotion(path, objInst);
GameObject.DestroyImmediate(objInst);
//Close();
//GUIUtility.ExitGUI();
}
GUI.enabled = true;
GUILayout.EndHorizontal();
}
private void OnGUICurve()
{
m_v2TableScroll = GUILayout.BeginScrollView(m_v2TableScroll);
foreach (var kv in DicData)
{
int frame = kv.Value.Count;
float time = 0f;
int idx = 0;
Vector3 tmp = Vector3.zero;
AnimationCurve curve = new AnimationCurve();
foreach (var v in kv.Value)
{
time += 1f / 30f;
tmp += v;
curve.AddKey(time, Vector2.Distance(Vector2.zero, v));
}
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField(kv.Key, GUILayout.Width(100));
EditorGUILayout.CurveField(curve, GUILayout.Height(50));
GUILayout.EndHorizontal();
}
GUILayout.EndScrollView();
}
private void RefreshBoneList()
{
if (m_PrevSelectGameObject == m_SelectGameObject) return;
m_PrevSelectGameObject = m_SelectGameObject;
m_ListBones.Clear();
rootMotionBoneIndex = 0;
if (m_SelectGameObject == null) return;
GameObject objInst = GameObject.Instantiate(m_SelectGameObject);
objInst.name = m_SelectGameObject.name;
objInst.hideFlags = HideFlags.HideAndDontSave;
SkeletonAnimation animation = objInst.GetComponent<SkeletonAnimation>();
if(animation == null)
animation = objInst.GetComponentInChildren<SkeletonAnimation>();
if (animation != null)
{
Skeleton skeleton = animation.Skeleton;
int index = skeleton.FindBoneIndex(rootMotionBoneName);
if (index >= 0)
{
rootMotionBoneIndex = skeleton.FindBoneIndex(rootMotionBoneName);
rootMotionBone = skeleton.Bones.Items[index];
}
else
{
Debug.Log("Bone named \"" + rootMotionBoneName + "\" could not be found.");
rootMotionBoneIndex = 0;
rootMotionBone = skeleton.RootBone;
}
SkeletonData data = animation.SkeletonDataAsset.GetSkeletonData(true);
if (data != null)
{
foreach (var b in data.Bones.Items)
{
m_ListBones.Add(b.Name);
}
}
}
GameObject.DestroyImmediate(objInst);
}
public void GenerationRootMotion(string szFullPath, GameObject obj)
{
SkeletonAnimation skeletonAnimation = obj.GetComponent<SkeletonAnimation>();
if (skeletonAnimation == null)
skeletonAnimation = obj.GetComponentInChildren<SkeletonAnimation>();
if(skeletonAnimation == null)
{
return;
}
Spine.AnimationState state = skeletonAnimation.AnimationState;
Skeleton skeleton = skeletonAnimation.Skeleton;
if (state != null) state.ClearTrack(0);
skeleton.SetToSetupPose();
bool bExistKey = false;
EditorUtility.DisplayProgressBar("RootMotion PreCalclater", "Calculate animation", 0);
int nCount = 0;
float fFps = 30f;
foreach (var item in skeleton.Data.Animations)
{
if (item.Name.Contains("_NoMotion"))
continue;
if (DicData.ContainsKey(item.Name))
{
Debug.Log("contain key : " + item.Name);
continue;
}
int nFrame = (int)(item.Duration * fFps);
Vector2 vPrevPosition = Vector2.zero;
List<Vector3> vListPos = new List<Vector3>();
float lastTime = 0f;
for (int i = 1; i <= nFrame; i++)
{
float end = (item.Duration / nFrame) * i;
Vector3 vTemp = GetAnimationRootMotion(lastTime, end, item);
if (vTemp.sqrMagnitude > 0.0f)
bExistKey = true;
vListPos.Add(vTemp);
}
DicData.Add(item.Name, vListPos);
nListFrame.Add(nFrame);
EditorUtility.DisplayProgressBar("RootMotion PreCalclater", "Calculate animation", 1.0f / (int)skeleton.Data.Animations.Count * nCount);
nCount++;
}
EditorUtility.ClearProgressBar();
if (bExistKey == false)
{
return;
}
FileStream fs = new FileStream(szFullPath + "_RootMotionData.bytes", FileMode.Create, FileAccess.Write);
BinaryWriter bw = new BinaryWriter(fs);
int nIndex = 0;
bw.Write(DicData.Count);
foreach (var Pair in DicData)
{
Debug.Log("Export : " + Pair.Key + " " + nListFrame[nIndex]);
bw.Write(Pair.Key);
bw.Write(nListFrame[nIndex]);
for (int i = 0; i < Pair.Value.Count; i++)
{
bw.Write(Pair.Value[i].x);
bw.Write(Pair.Value[i].y);
bw.Write(Pair.Value[i].z);
}
nIndex++;
}
Debug.Log("Export : " + szFullPath + "_RootMotionData.bytes");
bw.Close();
fs.Dispose();
AssetDatabase.Refresh();
}
public Vector2 GetAnimationRootMotion(float startTime, float endTime,
Spine.Animation animation)
{
var timeline = animation.FindTranslateTimelineForBone(rootMotionBoneIndex);
if (timeline != null)
{
return GetTimelineMovementDelta(startTime, endTime, timeline, animation);
}
return Vector2.zero;
}
private Vector2 GetTimelineMovementDelta(float startTime, float endTime,
TranslateTimeline timeline, Spine.Animation animation)
{
Vector2 currentDelta;
if (startTime > endTime) // Looped
currentDelta = (timeline.Evaluate(animation.Duration) - timeline.Evaluate(startTime))
+ (timeline.Evaluate(endTime) - timeline.Evaluate(0));
else if (startTime != endTime) // Non-looped
currentDelta = timeline.Evaluate(endTime) - timeline.Evaluate(startTime);
else
currentDelta = Vector2.zero;
return currentDelta;
}
}
#endif
移动逻辑代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using Spine.Unity;
using Spine;
public class test : MonoBehaviour
{
public SkeletonAnimation spine;
public TextAsset rootmotion;
private Dictionary<string, List<Vector3>> m_DicRootPositionPart1 = null;
public bool IsRootMotion = true;
public float Speed = 1f;
public bool DirectLeft = false;
float startTime = -1f;
string curAction = string.Empty;
private float prevFrame = 0;
private void Awake()
{
Application.targetFrameRate = 30;
}
// Start is called before the first frame update
void Start()
{
m_DicRootPositionPart1 = LoadRootMotion(rootmotion);
}
private void OnEnable()
{
}
// Update is called once per frame
void Update()
{
spine.Skeleton.FlipX = DirectLeft;
if(IsRootMotion && startTime >= 0 && !string.IsNullOrEmpty(curAction))
{
TrackEntry track = spine.AnimationState.GetCurrent(0);
var animation = track.Animation;
float start = track.AnimationLast;
if (start < 0)
start = 0f;
float end = track.AnimationTime;
if (start == end)
{
startTime = -1f;
return;
}
float fFrame = (int)(animation.Duration * 30.0f);
float fPrevFrame = prevFrame;
float fCurFrame = end * 30.0f;
prevFrame = fCurFrame;
fPrevFrame = Mathf.Clamp(fPrevFrame, 0, fFrame - 0.001f);
fCurFrame = Mathf.Clamp(fCurFrame, 0, fFrame - 0.001f);
Vector3 vec = GetAniDistance(curAction, fPrevFrame, fCurFrame);
vec *= Speed;
vec.x *= DirectLeft ? -1 : 1;
transform.position += transform.TransformVector(vec);
}
if (Input.GetKeyDown(KeyCode.J))
{
startTime = Time.time;
prevFrame = 0f;
curAction = "Jump";
spine.AnimationState.SetAnimation(0, curAction, false);
}
if (Input.GetKeyDown(KeyCode.K))
{
DirectLeft = !DirectLeft;
}
if (Input.GetKeyDown(KeyCode.L))
{
spine.AnimationState.ClearTracks();
spine.Skeleton.SetToSetupPose();
spine.AnimationState.SetAnimation(0, "Jump", false);
}
}
public Dictionary<string, List<Vector3>> LoadRootMotion(TextAsset textAsset)
{
if (textAsset == null) return null;
Dictionary<string, List<Vector3>> DicPosition = new Dictionary<string, List<Vector3>>();
MemoryStream ms = new MemoryStream(textAsset.bytes);
BinaryReader br = new BinaryReader(ms);
int nClipCount = br.ReadInt32();
for (int i = 0; i < nClipCount; i++)
{
List<Vector3> vList = new List<Vector3>();
string szName = br.ReadString();
int nFrame = br.ReadInt32();
vList.Add(Vector3.zero);
for (int j = 0; j < nFrame; j++)
{
vList.Add(new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()));
}
DicPosition.Add(szName, vList);
}
br.Close();
ms.Dispose();
return DicPosition;
}
public Vector3 GetAniDistance(string szName, float fPrevFrame, float fCurFrame)
{
if (m_DicRootPositionPart1 == null)
return Vector3.zero;
List<Vector3> vList = null;
if (m_DicRootPositionPart1 != null && m_DicRootPositionPart1.ContainsKey(szName))
vList = m_DicRootPositionPart1[szName];
if (null == vList)
return Vector3.zero;
Vector3 v1 = GetInterpolationValue(vList, fPrevFrame);
Vector3 v2 = GetInterpolationValue(vList, fCurFrame);
Vector3 vDistance = v2 - v1;
vDistance.z = 0;
return vDistance;
}
private Vector3 GetInterpolationValue(List<Vector3> vList, float fFrame)
{
try
{
int nFrame = (int)fFrame;
return Vector3.Lerp(vList[nFrame], vList[nFrame + 1], fFrame - (float)nFrame);
}
catch
{
return Vector3.zero;
}
}
}
关闭Spine动作自身的动画位移代码:
Assets\Spine\Runtime\spine-csharp\Animation.cs脚本内TranslateTimeline类添加如下代码:
总之,这个方法比较方便程序逻辑控制,但是需要导出配置文件,并且动画有更新就需要重新导出,各有利弊吧!