我如何在运行时存储或读取动画剪辑数据?
How can i store or read a animation clip data in runtime?
我正在开发一个小程序,可以在 运行 时间修改动画(比如当你 运行 更快时,动画不仅播放速度更快,而且移动幅度更大)。所以我需要获取现有动画,更改其值,然后将其发回。
我发现我可以为动画设置一条新曲线很有趣,但我无法访问我已有的曲线。所以我要么写一个文件来存储我的动画曲线(例如文本文件),要么我想办法在启动时读取动画。
我尝试使用
AnimationUtility.GetCurveBindings(AnimationCurve);
它在我的测试中有效,但在某些页面中它说这是一个 "Editor code",如果我将该项目构建为独立程序,它将不再有效。真的吗?如果是这样,有什么办法可以得到运行时间的曲线吗?
感谢 Benjamin Zach 的澄清和 TehMightyPotato 的建议
我想保留在 运行 时间修改动画的想法。因为它可以适应更多的情况。
我现在的想法是编写一段编辑器代码,可以从编辑器中的曲线读取并将有关曲线(关键帧)的所有必要信息输出到文本文件中。然后在 运行 时间读取该文件并创建新曲线以覆盖现有曲线。我会把这个问题悬而未决几天,看看是否有人对此有更好的想法。
如前所述,AnimationUtility
属于 UnityEditor
命名空间。整个命名空间在构建中被完全剥离,其中的任何内容在最终应用程序中都将不可用,但只能在 Unity 编辑器中使用。
将动画曲线存储到文件
为了将所有需要的信息存储到一个文件中,您可以有一个脚本,用于在构建之前在编辑器中序列化您的特定动画曲线一次使用例如BinaryFormatter.Serialize
. Then later on runtime you can use BinaryFormatter.Deserialize
再次返回信息列表。
如果你想让它更具可编辑性,你也可以使用例如JSON or XML当然
更新:一般来说Stop using BinaryFormatter
!
在最新的 Unity 版本中,Newtonsoft Json.NET package 已经预装,所以只需使用 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"
);
如果 property/component 附加到根对象本身,则第二个参数 pathToObject
是一个空字符串。否则,它会像往常一样在层次结构路径中为 Unity 提供,例如“ChildName/FurtherChildName”。
现在您可以更改值并在运行时分配新曲线。
正在运行时分配新曲线
在运行时您可以使用 animator.runtimeanimatorController
in order to retrieve a RuntimeAnimatorController
参考。
它有一个 属性 animationClips
which returns all AnimationClip
分配给这个控制器。
然后您可以使用例如Linq FirstOrDefault
in order to find a specific AnimationClip
by name and finally use 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
→ You might want to use the Animation
组件中的示例,而不是您的特定用例中的 Animator
。
我正在开发一个小程序,可以在 运行 时间修改动画(比如当你 运行 更快时,动画不仅播放速度更快,而且移动幅度更大)。所以我需要获取现有动画,更改其值,然后将其发回。
我发现我可以为动画设置一条新曲线很有趣,但我无法访问我已有的曲线。所以我要么写一个文件来存储我的动画曲线(例如文本文件),要么我想办法在启动时读取动画。
我尝试使用
AnimationUtility.GetCurveBindings(AnimationCurve);
它在我的测试中有效,但在某些页面中它说这是一个 "Editor code",如果我将该项目构建为独立程序,它将不再有效。真的吗?如果是这样,有什么办法可以得到运行时间的曲线吗?
感谢 Benjamin Zach 的澄清和 TehMightyPotato 的建议 我想保留在 运行 时间修改动画的想法。因为它可以适应更多的情况。
我现在的想法是编写一段编辑器代码,可以从编辑器中的曲线读取并将有关曲线(关键帧)的所有必要信息输出到文本文件中。然后在 运行 时间读取该文件并创建新曲线以覆盖现有曲线。我会把这个问题悬而未决几天,看看是否有人对此有更好的想法。
如前所述,AnimationUtility
属于 UnityEditor
命名空间。整个命名空间在构建中被完全剥离,其中的任何内容在最终应用程序中都将不可用,但只能在 Unity 编辑器中使用。
将动画曲线存储到文件
为了将所有需要的信息存储到一个文件中,您可以有一个脚本,用于在构建之前在编辑器中序列化您的特定动画曲线一次使用例如BinaryFormatter.Serialize
. Then later on runtime you can use BinaryFormatter.Deserialize
再次返回信息列表。
如果你想让它更具可编辑性,你也可以使用例如JSON or XML当然
更新:一般来说Stop using BinaryFormatter
!
在最新的 Unity 版本中,Newtonsoft Json.NET package 已经预装,所以只需使用 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"
);
如果 property/component 附加到根对象本身,则第二个参数 pathToObject
是一个空字符串。否则,它会像往常一样在层次结构路径中为 Unity 提供,例如“ChildName/FurtherChildName”。
现在您可以更改值并在运行时分配新曲线。
正在运行时分配新曲线
在运行时您可以使用 animator.runtimeanimatorController
in order to retrieve a RuntimeAnimatorController
参考。
它有一个 属性 animationClips
which returns all AnimationClip
分配给这个控制器。
然后您可以使用例如Linq FirstOrDefault
in order to find a specific AnimationClip
by name and finally use 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
→ You might want to use the Animation
组件中的示例,而不是您的特定用例中的 Animator
。