Unity-Spine导出动画位移的实现方法

       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类添加如下代码:

总之,这个方法比较方便程序逻辑控制,但是需要导出配置文件,并且动画有更新就需要重新导出,各有利弊吧!

猜你喜欢

转载自blog.csdn.net/wangzizi12300/article/details/126832413