自定义骨骼动画播放

缘由

由于项目是足球竞技类的,对于动作的要求比较高,Unity提供的动画管理器消耗比较大,以为一个球员的动作都几百上千个,真的使用unity的动画,移动设备真心是抗不住的!所以才有了这个需求!

Unity的动画优化技术

有兴趣的可以去这里看看

原理

AnimationCurvekeyframe数据序列化成文本,然后在运行解释是将keyframe的数据生成AnimationCurve,在Play的时候,将对应的Target的Transform数值按照数据生成AnimationCurve去设定就可以了

序列化Animation数据

前面讲了大概的原理,现在开始来讲讲怎么去序列化数据!

说明

要说明的是这里用来做例子的是骨骼动画,如果是其他的animation,原理也差不多,可以自行参考修改

Unity的官方提供了一套工具类给我们用来获取AnimationCurve->AnimationUtility.GetCurveBindings(clip)

该方法返回的是EditorCurveBinding

EditorCurveBinding有两个比较重要的属性pathpropertyName

  • path 是只该curve的名称,在unity的Animation Editor界面是下图的红色框框的名称

jpg

  • propertyName 这是AnimationCurve的名称,通常是m_LocalPosition.X(y,z)之类的

通过上述的方法获取了Bind的数据,那么Unity也提供另外一个方法去提取Bind对应的Curve,那就是AnimationUtility.GetEditorCurve(clip, bind)

通过上述的方法获取了AnimationCurve后,那么就可以把AnimationCurve的keyframe数据读取出来,并存起来

上述就是大概的过程,代码如下

[MenuItem("Assets/CustomAnimation/生成动画数据")]
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[index];
                // 关键帧的数值
                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("");
        // 开启一个序列化内容的流
        var steam = File.Open(Path.Combine(Application.dataPath, $"{Selection.activeObject.name}.txt"),
            FileMode.OpenOrCreate);
        // 开启一个用来说明内容流的每条数据大小的int流
        var steamindex = File.Open(Path.Combine(Application.dataPath, $"{Selection.activeObject.name}Index.txt"),
            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;
    }
}

gif

解释和执行Animation数据

上述已经讲述了怎么去序列化动画数据了,那么现在开始讲讲怎么去执行这些数据的;

直接看代码,代码里有注释了

/// <summary>
/// 加载动画的数据文件
/// </summary>
/// <param name="fileName">文件名称</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, $"CustomAnimation/{fileName}.txt"));
    // 开启一个动画内容字节位的流
    var streamIndex = File.OpenRead(Path.Combine(Application.dataPath, $"CustomAnimation/{fileName}Index.txt"));
    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("/", 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自带的,本文就不多做讲述了

png

需要播放动画的代码

var _animationConfig = CustomAnimationPlayManager.Instance.LoadAnimation("AI_recover_F_left");
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="newBones">新的骨骼节点</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[7];
        /// <summary>
        /// 添加Curve
        /// </summary>
        /// <param name="i">下标</param>
        /// <param name="curve">曲线</param>
        public void Add(int i, AnimationCurve curve)
        {
            _curves[i] = curve;
        }
        /// <summary>
        /// 绑定的骨骼
        /// </summary>
        /// <param name="newBone">骨骼</param>
        public void BindBone(Transform newBone)
        {
            _target = newBone;
        }
        /// <summary>
        /// 播放
        /// </summary>
        /// <param name="time">时间</param>
        public void Play(float time)
        {
            if (_target == null)
                return;
            // Position
            var localPosition = _target.localPosition;
            var x = _curves[0]?.Evaluate(time) ?? localPosition.x;
            var y = _curves[1]?.Evaluate(time) ?? localPosition.y;
            var z = _curves[2]?.Evaluate(time) ?? localPosition.z;
            localPosition = new Vector3(x, y, z);
            _target.localPosition = localPosition;
            // Rotation
            var localRotation = _target.localRotation;
            var rx = _curves[3]?.Evaluate(time) ?? localRotation.x;
            var ry = _curves[4]?.Evaluate(time) ?? localRotation.y;
            var rz = _curves[5]?.Evaluate(time) ?? localRotation.z;
            var rw = _curves[6]?.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="newBone">骨骼</param>
        /// <param name="animationPath">动画</param>
        public void Add(Transform newBone, CustomAnimationPath animationPath)
        {
            animationPath.BindBone(newBone);
            _list.Add(animationPath);
        }
        /// <summary>
        /// 播放动画
        /// </summary>
        /// <param name="time">动画的时间进度</param>
        /// <returns>是否播放完成</returns>
        public bool Play(float time)
        {
            foreach (var curve in _list)
            {
                curve.Play(time);
            }

            return true;
        }
    }
}