How can i store or read a animation clip data in runtime?

与世无争的帅哥 提交于 2020-01-25 10:06:06

问题


I'm working on a small program that can modify the animation at run time(Such as when you run faster the animation not only play faster but also with larger movement). So i need to get the existing animation, change its value, then send it back.

I found it is interesting that i can set a new curve to the animation, but i can't get access to what i already have. So I either write a file to store my animation curve (as text file for example), or i find someway to read the animation on start up.

I tried to use

AnimationUtility.GetCurveBindings(AnimationCurve);

It worked in my testing, but in some page it says this is a "Editor code", that if i build the project into a standalone program it will not work anymore. Is that true? If so, is there any way to get the curve at run time?

Thanks to the clearify from Benjamin Zach and suggestion from TehMightyPotato I'd like to keep the idea about modifying the animation at runtime. Because it could adapt to more situations imo.

My idea for now is to write a piece of editor code that can read from the curve in Editor and output all necesseary information about the curve (keyframes) into a text file. Then read that file at runtime and create new curve to overwrite the existing one. I will leave this question open for a few days and check it to see if anyone has a better idea about it.


回答1:


As said already AnimationUtility belongs to the UnityEditor namespace. This entire namespace is completely stripped of in a build and nothing in it will be available in the final app but only within the Unity Editor.


Store AnimationCurves to file

In order to store all needed information to a file you could have a script for once serializing your specific animation curve(s) in the editor before building using e.g. BinaryFormatter.Serialize. Then later on runtime you can use BinaryFormatter.Deserialize for returning the info list again.

If you wanted it more editable you could as well use e.g. JSON or XML of course

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif

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 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 fileStream = new FileStream(Path.Combine(Application.streamingAssetsPath, "AnimationCurves.dat"), FileMode.Create);

        // Construct a BinaryFormatter and use it to serialize the data to the stream.
        var formatter = new BinaryFormatter();
        try
        {
            formatter.Serialize(fileStream, ClipCurves);
        }
        catch (SerializationException e)
        {
            Debug.LogErrorFormat(this, "Failed to serialize. Reason: {0}", e.Message);
        }
        finally
        {
            fileStream.Close();
        }

        AssetDatabase.Refresh();
    }
#endif

    private void LoadClipCurves()
    {
        var filePath = Path.Combine(Application.streamingAssetsPath, "AnimationCurves.dat");

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

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

        try
        {
            var formatter = new BinaryFormatter();

            // Deserialize the hashtable from the file and 
            // assign the reference to the local variable.
            ClipCurves = (List<ClipInfo>)formatter.Deserialize(fileStream);
        }
        catch (SerializationException e)
        {
            Debug.LogErrorFormat(this, "Failed to deserialize. Reason: {0}", e.Message);
        }
        finally
        {
            fileStream.Close();
        }
    }

    // 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;
    }
}

Then you can do something like e.e.

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

the second parameter pathToObject is an empty string if the property/component is attached to the root object itself. Otherwise it is given in the hierachy path as usual for Unity like e.g. "ChildName/FurtherChildName".

Now you can change the values and assign a new curve on runtime.


Assigning new curve on runtime

On runtime you can use animator.runtimeanimatorController in order to retrieve a RuntimeAnimatorController reference.

It has a property animationClips which returns all AnimationClips assigned to this controller.

You could then use e.g. Linq FirstOrDefault in order to find a specific AnimationClip by name and finally use AnimationClip.SetCurve to assign a new animation curve to a certain component and property.

E.g. something like

// 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);

Note: Typed on smartphone so no warranty! But I hope the idea gets clear...


Also checkout the example in AnimationClip.SetCurve &rightarrow; You might want to use the Animation component instead of an Animator in your specific use case.



来源:https://stackoverflow.com/questions/57846333/how-can-i-store-or-read-a-animation-clip-data-in-runtime

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!