Unity自定义骨骼动画播放
缘由由于项目是足球竞技类的,对于动作的要求比较高,Unity提供的动画管理器消耗比较大,以为一个球员的动作都几百上千个,真的使用unity的动画,移动设备真心是抗不住的!所以才有了这个需求!
Unity的动画优化技术
有兴趣的可以去这里看看
原理
将AnimationCurve的keyframe数据序列化成文本,然后在运行解释是将keyframe的数据生成AnimationCurve,在Play的时候,将对应的Target的Transform数值按照数据生成AnimationCurve去设定就可以了
序列化Animation数据
前面讲了大概的原理,现在开始来讲讲怎么去序列化数据!
说明
要说明的是这里用来做例子的是骨骼动画,如果是其他的animation,原理也差不多,可以自行参考修改
Unity的官方提供了一套工具类给我们用来获取Animation的Curve->AnimationUtility.GetCurveBindings(clip)
该方法返回的是EditorCurveBinding
EditorCurveBinding有两个比较重要的属性path和propertyName
[*]path 是只该curve的名称,在unity的Animation Editor界面是下图的红色框框的名称
[*]propertyName 这是AnimationCurve的名称,通常是m_LocalPosition.X(y,z)之类的
通过上述的方法获取了Bind的数据,那么Unity也提供另外一个方法去提取Bind对应的Curve,那就是AnimationUtility.GetEditorCurve(clip, bind)
通过上述的方法获取了AnimationCurve后,那么就可以把AnimationCurve的keyframe数据读取出来,并存起来
上述就是大概的过程,代码如下
static void GenerateCurves()
{
// 需要直接选中AnimationClip
var animation_go = Selection.activeObject;
if (animation_go.GetType() == typeof(AnimationClip))
{
var clip = animation_go as AnimationClip;
var binds = AnimationUtility.GetCurveBindings(clip);
Dictionary<string, WDAnimationCurve> anim = new Dictionary<string, WDAnimationCurve>();
// 遍历Bind
foreach (var bind in binds)
{
WDAnimationCurve wdCurve;
// 一个path下有m_LocalPosition.x/m_LocalPosition.y/m_LocalPosition.z
// 和m_LocalRotation.x/m_LocalRotation.y/m_LocalRotation.z/m_LocalRotation.w
// 和m_LocalScale.x/m_LocalScale.y/m_LocalScale.z
if (!anim.ContainsKey(bind.path))
{
// 用一个WDAnimationCurve去存放上述的m_LocalPosition/m_LocalRotation/m_LocalScale
wdCurve = new WDAnimationCurve();
anim.Add(bind.path, wdCurve);
}
else
{
anim.TryGetValue(bind.path, out wdCurve);
}
if (wdCurve == null)
continue;
// 获取Curve
var curve = AnimationUtility.GetEditorCurve(clip, bind);
List<WDKeyFrame> list = new List<WDKeyFrame>();
// 关键帧
var keys = curve.keys;
for (int index=0; index< keys.Length; index++)
{
var keyframe = keys;
// 关键帧的数值
WDKeyFrame frame = new WDKeyFrame();
frame.m_Time = keyframe.time;
frame.m_Value = keyframe.value;
frame.m_InTangent = keyframe.inTangent;
frame.m_OutTangent = keyframe.outTangent;
list.Add(frame);
}
// 存
wdCurve.AddCurve(bind.propertyName, list);
}
// 移除根节点,也可以不移除,看具体需求
anim.Remove(&#34;&#34;);
// 开启一个序列化内容的流
var steam = File.Open(Path.Combine(Application.dataPath, $&#34;{Selection.activeObject.name}.txt&#34;),
FileMode.OpenOrCreate);
// 开启一个用来说明内容流的每条数据大小的int流
var steamindex = File.Open(Path.Combine(Application.dataPath, $&#34;{Selection.activeObject.name}Index.txt&#34;),
FileMode.OpenOrCreate);
BinaryWriter ms = new BinaryWriter(steam);
BinaryWriter msL = new BinaryWriter(steamindex);
// 写入动画的数量
msL.Write(anim.Count);
foreach (var d in anim)
{
byte[] byteArray = System.Text.Encoding.UTF8.GetBytes(d.Key);
ms.Write(byteArray);
msL.Write(byteArray.Length);
// 依序写入每个数据
d.Value.Write(ms, msL);
}
ms.Flush();
ms.Dispose();
ms.Close();
ms = null;
steam.Dispose();
steam.Close();
steam = null;
msL.Flush();
msL.Dispose();
msL.Close();
msL = null;
steamindex.Dispose();
steamindex.Close();
steamindex = null;
}
}
解释和执行Animation数据
上述已经讲述了怎么去序列化动画数据了,那么现在开始讲讲怎么去执行这些数据的;
直接看代码,代码里有注释了
/// <summary>
/// 加载动画的数据文件
/// </summary>
/// <param name=&#34;fileName&#34;>文件名称</param>
/// <returns>自定义动画</returns>
public CustomAnimationConfig LoadAnimation(string fileName)
{
// 已经加载过的配置
if (_cacheConfig.ContainsKey(fileName))
{
_cacheConfig.TryGetValue(fileName, out var config);
return config;
}>>
// 构建一个自定义的动画文件
CustomAnimationConfig animationConfig = new CustomAnimationConfig(fileName);
// 开启一个动画内容的流
var stream = File.OpenRead(Path.Combine(Application.dataPath, $&#34;CustomAnimation/{fileName}.txt&#34;));
// 开启一个动画内容字节位的流
var streamIndex = File.OpenRead(Path.Combine(Application.dataPath, $&#34;CustomAnimation/{fileName}Index.txt&#34;));
BinaryReader brAnim = new BinaryReader(stream);
BinaryReader brAnimIndex = new BinaryReader(streamIndex);
// 动画数量
var count = brAnimIndex.ReadInt32();
while (count > 0)
{
count--;
var length = brAnimIndex.ReadInt32();
var bytesCont = brAnim.ReadBytes(length);
// Animation Path
var aniPath = System.Text.Encoding.UTF8.GetString(bytesCont);
// 截取最后的一个objectName 作为名称(在demo的例子中,该节点的名称是唯一的)
var lIndex = aniPath.LastIndexOf(&#34;/&#34;, StringComparison.Ordinal);
var path = aniPath.Substring(lIndex+1);
// 因为只考虑Position和Rotation,所以就7个Curve,不懂的同学可以去看看Animation的优化章节
for (var i = 0; i < 7; i++)
{
// keyframe的数量
length = brAnimIndex.ReadInt32();
if (length != 0)
{
// 将keyframe转换成为AnimationCurve
AnimationCurve curve = new AnimationCurve();
for (int j = 0; j < length; j++)
{
Keyframe frame = new Keyframe(brAnim.ReadSingle(), brAnim.ReadSingle(),
brAnim.ReadSingle(), brAnim.ReadSingle());
curve.AddKey(frame);
}
// 添加到自定义的结构里记录
animationConfig.Add(path, curve, i);
}
}
}
brAnim.Close();
brAnimIndex.Close();
stream.Close();
streamIndex.Close();
// cache
_cacheConfig.Add(fileName, animationConfig);
return animationConfig;
}在这里,还需要对上面的部分代码进行相应的说明
CustomAnimationConfig
这个类主要是存放从数据中加载及解释出来的配置文件,并且能根据骨骼节点生成新的动画播放器
该文件内存放着n个CustomAnimationPlay
CustomAnimationPlay
自定义的动画播放器,只允许通过CustomAnimationConfig.CreateAnimationPlay创建,里面记录了一个完整的Animation的动画
由CustomAnimationPath组成,就像Unity Animation一样,都是有一个个path组成
CustomAnimationPath
前面的Animation数据序列原理里已经说过了,所以为了更加接近Unity的视图化,这里的CustomAnimationPath就是记录了Bind.Path下的所有Curve,在次demo里,因为我不需要使用Scale,所以一个CustomAnimationPath下就只有7个Curve
由有限个AnimationCurve组成
AnimationCurve
这个就是unity自带的,本文就不多做讲述了
需要播放动画的代码
var _animationConfig = CustomAnimationPlayManager.Instance.LoadAnimation(&#34;AI_recover_F_left&#34;);
customAnimationPlay = _animationConfig.CreateAnimationPlay(skin.bones);
// mTime就是希望播放到哪个时间点
customAnimationPlay.Play(mTime);最后放上代码! CustomAnimationConfig.cs
using System.Collections.Generic;
using UnityEngine;
namespace CustomAnimation.AnimationPlay
{
public class CustomAnimationConfig
{
/// <summary>
/// 动画名称
/// </summary>
private string _name;
/// <summary>
/// 该动画节点的所有animation Path的动画
/// </summary>
private readonly Dictionary<string, CustomAnimationPath> _dict = new Dictionary<string, CustomAnimationPath>();
public CustomAnimationConfig(string fileName)
{
_name = fileName;
}
public void Add(string path, AnimationCurve curve, int i)
{
CustomAnimationPath animation;
if (!_dict.ContainsKey(path))
{
animation = new CustomAnimationPath();
_dict.Add(path, animation);
}
else
{
_dict.TryGetValue(path, out animation);
}
animation?.Add(i, curve);
}
/// <summary>
/// 通过骨骼构建一个动画播放器
/// </summary>
/// <param name=&#34;newBones&#34;>新的骨骼节点</param>
/// <returns>动画播放器</returns>
public CustomAnimationPlay CreateAnimationPlay(Transform[] newBones)
{
var play = new CustomAnimationPlay();
foreach (var bone in newBones)
{
if (!_dict.ContainsKey(bone.name)) continue;
_dict.TryGetValue(bone.name, out var path);
play.Add(bone, path);
}
return play;
}
}
}CustomAnimationPath.cs
using UnityEngine;
namespace CustomAnimation.AnimationPlay
{
public class CustomAnimationPath
{
/// <summary>
/// 作用的Transform对象
/// </summary>
private Transform _target = null;
/// <summary>
/// 对象上的所有曲线,目前只考虑Position和Rotation
/// </summary>
private readonly AnimationCurve[] _curves = new AnimationCurve;
/// <summary>
/// 添加Curve
/// </summary>
/// <param name=&#34;i&#34;>下标</param>
/// <param name=&#34;curve&#34;>曲线</param>
public void Add(int i, AnimationCurve curve)
{
_curves = curve;
}
/// <summary>
/// 绑定的骨骼
/// </summary>
/// <param name=&#34;newBone&#34;>骨骼</param>
public void BindBone(Transform newBone)
{
_target = newBone;
}
/// <summary>
/// 播放
/// </summary>
/// <param name=&#34;time&#34;>时间</param>
public void Play(float time)
{
if (_target == null)
return;
// Position
var localPosition = _target.localPosition;
var x = _curves?.Evaluate(time) ?? localPosition.x;
var y = _curves?.Evaluate(time) ?? localPosition.y;
var z = _curves?.Evaluate(time) ?? localPosition.z;
localPosition = new Vector3(x, y, z);
_target.localPosition = localPosition;
// Rotation
var localRotation = _target.localRotation;
var rx = _curves?.Evaluate(time) ?? localRotation.x;
var ry = _curves?.Evaluate(time) ?? localRotation.y;
var rz = _curves?.Evaluate(time) ?? localRotation.z;
var rw = _curves?.Evaluate(time) ?? localRotation.w;
localRotation = new Quaternion(rx, ry, rz, rw);
_target.localRotation = localRotation;
}
}
}CustomAnimationPlay.cs
using System.Collections.Generic;
using UnityEngine;
namespace CustomAnimation.AnimationPlay
{
public class CustomAnimationPlay
{
/// <summary>
/// 所有的骨骼动画
/// </summary>
private readonly List<CustomAnimationPath> _list = new List<CustomAnimationPath>();
/// <summary>
/// 绑定骨骼的动画
/// </summary>
/// <param name=&#34;newBone&#34;>骨骼</param>
/// <param name=&#34;animationPath&#34;>动画</param>
public void Add(Transform newBone, CustomAnimationPath animationPath)
{
animationPath.BindBone(newBone);
_list.Add(animationPath);
}
/// <summary>
/// 播放动画
/// </summary>
/// <param name=&#34;time&#34;>动画的时间进度</param>
/// <returns>是否播放完成</returns>
public bool Play(float time)
{
foreach (var curve in _list)
{
curve.Play(time);
}
return true;
}
}
} 没必要过unity,直接fbxsdk解析就行了 感谢大佬点评,改天我去试试 直接抄slate源码里面的动画序列化和播放系统就行 这也是一种,ue的时候会顺便写一下[握手]
页:
[1]