如何在运行时存储或读取动画剪辑数据?

3
我正在开发一个小程序,可以在运行时修改动画(例如当你跑得更快时,动画不仅会播放得更快,而且还会有更大的移动)所以我需要获取现有动画,更改其值,然后将其发送回去。
我发现设置新曲线到动画很有趣,但是我无法访问已有的动画。所以我要么编写文件来存储我的动画曲线(例如作为文本文件),要么找到一种在启动时读取动画的方法。
我尝试使用
AnimationUtility.GetCurveBindings(AnimationCurve);

在我的测试中它工作正常,但在某些页面上它说这是一个“编辑器代码”,如果我将项目构建为独立程序,它将不再起作用。这是真的吗?如果是,有没有办法在运行时获取曲线呢?
感谢Benjamin Zach的清晰说明和TehMightyPotato的建议,我想保留在运行时修改动画的想法,因为我认为它可以适应更多情况。
我目前的想法是编写一段编辑器代码,可以从编辑器中读取曲线,并将关于曲线(关键帧)的所有必要信息输出到文本文件中。然后在运行时读取该文件并创建新的曲线来覆盖现有的曲线。我会让这个问题保持开放状态几天,并检查是否有人对此有更好的想法。

看一下这个。 我从未尝试过,但你应该能够创建一个最大速度动画和一个最小速度动画,并在它们之间进行混合... - Nicolas
关于“编辑器代码”:是的,这是真的;每个带有using UnityEditor声明的脚本文件都无法编译成独立应用程序。 - Benjamin Zach
1个回答

10

如前所述,AnimationUtility 属于 UnityEditor 命名空间。此整个命名空间在构建时将被完全剥离,在最终应用程序中无法使用其中的任何内容,只能在 Unity 编辑器中使用。


将 AnimationCurves 存储到文件中

为了将所有必需的信息存储到文件中,您可以在构建之前编写一个脚本,将您的特定动画曲线(在编辑器中)进行序列化,例如使用 BinaryFormatter.Serialize。然后在运行时,您可以使用BinaryFormatter.Deserialize 来重新返回信息列表。

如果您希望它更加可编辑,当然也可以使用例如 JSONXML

更新:通常情况下,停止使用 BinaryFormatter

在最新的 Unity 版本中,Newtonsoft Json.NET 包 已经预先安装,因此建议改用 JSON。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Unity.Plastic.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

public class AnimationCurveManager : MonoBehaviour
{
    [Serializable]
    public sealed class ClipInfo
    {
        public int ClipInstanceID;
        public List<CurveInfo> CurveInfos = new List<CurveInfo>();

        // default constructor is sometimes required for (de)serialization
        public ClipInfo() { }

        public ClipInfo(Object clip, List<CurveInfo> curveInfos)
        {
            ClipInstanceID = clip.GetInstanceID();
            CurveInfos = curveInfos;
        }
    }

    [Serializable]
    public sealed class CurveInfo
    {
        public string PathKey;

        public List<KeyFrameInfo> Keys = new List<KeyFrameInfo>();
        public WrapMode PreWrapMode;
        public WrapMode PostWrapMode;

        // default constructor is sometimes required for (de)serialization
        public CurveInfo() { }

        public CurveInfo(string pathKey, AnimationCurve curve)
        {
            PathKey = pathKey;

            foreach (var keyframe in curve.keys)
            {
                Keys.Add(new KeyFrameInfo(keyframe));
            }

            PreWrapMode = curve.preWrapMode;
            PostWrapMode = curve.postWrapMode;
        }
    }

    [Serializable]
    public sealed class KeyFrameInfo
    {
        public float Value;
        public float InTangent;
        public float InWeight;
        public float OutTangent;
        public float OutWeight;
        public float Time;
        public WeightedMode WeightedMode;

        // default constructor is sometimes required for (de)serialization
        public KeyFrameInfo() { }

        public KeyFrameInfo(Keyframe keyframe)
        {
            Value = keyframe.value;
            InTangent = keyframe.inTangent;
            InWeight = keyframe.inWeight;
            OutTangent = keyframe.outTangent;
            OutWeight = keyframe.outWeight;
            Time = keyframe.time;
            WeightedMode = keyframe.weightedMode;
        }
    }

    // I know ... singleton .. but what choices do we have? ;)
    private static AnimationCurveManager _instance;

    public static AnimationCurveManager Instance
    {
        get
        {
            // lazy initialization/instantiation
            if (_instance) return _instance;

            _instance = FindObjectOfType<AnimationCurveManager>();

            if (_instance) return _instance;

            _instance = new GameObject("AnimationCurveManager").AddComponent<AnimationCurveManager>();

            return _instance;
        }
    }

    // Clips to manage e.g. reference these via the Inspector
    public List<AnimationClip> clips = new List<AnimationClip>();

    // every animation curve belongs to a specific clip and 
    // a specific property of a specific component on a specific object
    // for making this easier lets simply use a combined string as key
    private string CurveKey(string pathToObject, Type type, string propertyName)
    {
        return $"{pathToObject}:{type.FullName}:{propertyName}";
    }

    public List<ClipInfo> ClipCurves = new List<ClipInfo>();

    private string filePath = Path.Combine(Application.streamingAssetsPath, "AnimationCurves.dat");

    private void Awake()
    {
        if (_instance && _instance != this)
        {
            Debug.LogWarning("Multiple Instances of AnimationCurveManager! Will ignore this one!", this);
            return;
        }

        _instance = this;

        DontDestroyOnLoad(gameObject);

        // load infos on runtime
        LoadClipCurves();
    }

#if UNITY_EDITOR

    // Call this from the ContextMenu (or later via editor script)
    [ContextMenu("Save Animation Curves")]
    private void SaveAnimationCurves()
    {
        ClipCurves.Clear();

        foreach (var clip in clips)
        {
            var curveInfos = new List<CurveInfo>();
            ClipCurves.Add(new ClipInfo(clip, curveInfos));

            foreach (var binding in AnimationUtility.GetCurveBindings(clip))
            {
                var key = CurveKey(binding.path, binding.type, binding.propertyName);
                var curve = AnimationUtility.GetEditorCurve(clip, binding);

                curveInfos.Add(new CurveInfo(key, curve));
            }
        }

        // create the StreamingAssets folder if it does not exist
        try
        {
            if (!Directory.Exists(Application.streamingAssetsPath))
            {
                Directory.CreateDirectory(Application.streamingAssetsPath);
            }
        }
        catch (IOException ex)
        {
            Debug.LogError(ex.Message);
        }

        // create a new file e.g. AnimationCurves.dat in the StreamingAssets folder
        var json = JsonConvert.SerializeObject(ClipCurves);
        File.WriteAllText(filePath, json);

        AssetDatabase.Refresh();
    }
#endif

    private void LoadClipCurves()
    {
        if (!File.Exists(filePath))
        {
            Debug.LogErrorFormat(this, "File \"{0}\" not found!", filePath);
            return;
        }

        var fileStream = new FileStream(filePath, FileMode.Open);

        var json = File.ReadAllText(filePath);

        ClipCurves = JsonConvert.DeserializeObject<List<ClipInfo>>(json);
    }

    // now for getting a specific clip's curves
    public AnimationCurve GetCurve(AnimationClip clip, string pathToObject, Type type, string propertyName)
    {
        // either not loaded yet or error -> try again
        if (ClipCurves == null || ClipCurves.Count == 0) LoadClipCurves();

        // still null? -> error
        if (ClipCurves == null || ClipCurves.Count == 0)
        {
            Debug.LogError("Apparantly no clipCurves loaded!");
            return null;
        }

        var clipInfo = ClipCurves.FirstOrDefault(ci => ci.ClipInstanceID == clip.GetInstanceID());

        // does this clip exist in the dictionary?
        if (clipInfo == null)
        {
            Debug.LogErrorFormat(this, "The clip \"{0}\" was not found in clipCurves!", clip.name);
            return null;
        }

        var key = CurveKey(pathToObject, type, propertyName);

        var curveInfo = clipInfo.CurveInfos.FirstOrDefault(c => string.Equals(c.PathKey, key));

        // does the curve key exist for the clip?
        if (curveInfo == null)
        {
            Debug.LogErrorFormat(this, "The key \"{0}\" was not found for clip \"{1}\"", key, clip.name);
            return null;
        }

        var keyframes = new Keyframe[curveInfo.Keys.Count];

        for (var i = 0; i < curveInfo.Keys.Count; i++)
        {
            var keyframe = curveInfo.Keys[i];

            keyframes[i] = new Keyframe(keyframe.Time, keyframe.Value, keyframe.InTangent, keyframe.OutTangent, keyframe.InWeight, keyframe.OutWeight)
            {
                weightedMode = keyframe.WeightedMode
            };
        }

        var curve = new AnimationCurve(keyframes)
        {
            postWrapMode = curveInfo.PostWrapMode,
            preWrapMode = curveInfo.PreWrapMode
        };

        // otherwise finally return the AnimationCurve
        return curve;
    }
}

那么您可以做类似于e.e.的事情。

AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
    clip, 
    "some/relative/GameObject", 
    typeof<SomeComponnet>, 
    "somePropertyName"
);

pathToObject是第二个参数,如果属性/组件附加到根对象本身,则为空字符串。否则,像Unity一样通常在层次路径中指定,例如“ChildName/FurtherChildName”。

现在,您可以在运行时更改值并分配一个新的曲线。


在运行时分配新曲线

在运行时,您可以使用animator.runtimeanimatorController来检索RuntimeAnimatorController引用。

它有一个属性animationClips,返回分配给此控制器的所有AnimationClip

然后,您可以使用例如Linq FirstOrDefault按名称查找特定的AnimationClip,最后使用AnimationClip.SetCurve将新动画曲线分配给某个组件和属性。

例如,像下面这样:

// you need those of course
string clipName;
AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
    clip, 
    "some/relative/GameObject", 
    typeof<SomeComponnet>, 
    "somePropertyName"
);

// TODO 
AnimationCurve newCurve = SomeMagic(originalCurve);

// get the animator reference
var animator = animatorObject.GetComponent<Animator>();
// get the runtime Animation controller
var controller = animator.runtimeAnimatorController;
// get all clips
var clips = controller.animationClips;
// find the specific clip by name
// alternatively you could also get this as before using a field and
// reference the according script via the Inspector 
var someClip = clips.FirstOrDefault(clip => string.Equals(clipName, clip.name));

// was found?
if(!someClip)
{
    Debug.LogWarningFormat(this, "There is no clip called {0}!", clipName);
    return;
}

// assign a new curve
someClip.SetCurve("relative/path/to/some/GameObject", typeof(SomeComponnet), "somePropertyName", newCurve);

注意:此内容通过智能手机输入,不提供保证!但我希望您的想法可以更清晰明了...
另外,请查看示例 AnimationClip.SetCurve → 在您特定的用例中,您可能想使用Animation组件而不是Animator

3
非常清楚,感谢您提供详细的帮助。但我想在这个答案中补充一些内容:当我使用setCurve时,它会修改原始游戏文件(因此即使重新启动程序,效果仍然存在)。为了避免这种情况,我创建了一个动画剪辑的副本,然后将来自游戏文件的数据添加到新的动画剪辑中的曲线上。因此,修改不再影响游戏文件。 - Ice.Rain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接