Unity 多物体混合动画、值变动画控制器

前言

因为工作中有用到,所以我抽出空闲把之前的LinkageAnimation优化了一下,如果有类似的需求(比如场景中有大量的物体,都按照同一频率在运动),那么这个工具可能适合你,当然如果你的环境是2017,TimeLine会是一个更好的解决方案。
不过,LinkageAnimation应该被称作值变动画才更合适,因为他支持针对所有组件(包括自定义组件)的属性做值变动画,属性满足以下要求:
1、该属性类型必须是被LinkageAnimation所识别的类型,目前有:Bool,Color,Float,Int,Quaternion,String,Vector2,Vector3,Vector4,Sprite,可以自行添加任意类型。
2、该属性必须是可读可写属性(不包括字段)。
3、该属性必须是实例属性(Instance)。
只要是满足以上要求的属性,将他所属脚本挂在场景物体上,就可以监听该物体,通过关键帧动画操控其值。

示例

1、4个Cube的联动动画

动画帧面板:(控制Transform组件的localRotation属性)
这里写图片描述

效果图:
这里写图片描述

2、UGUI Text文本动画

动画帧面板:(控制Text组件的text属性、fontSize属性)
这里写图片描述
效果图:
这里写图片描述

3、UGUI Image图片动画

动画帧面板:(控制Image组件的sprite属性)
这里写图片描述
效果图:
这里写图片描述

4、物体消隐动画

动画帧面板:(控制MeshRenderer组件的enabled属性)
这里写图片描述
效果图:
这里写图片描述

使用与解析

1、挂载LinkageAnimation脚本至场景中

这里写图片描述
一个LinkageAnimation实例对应一个动画组,点击Edit Animation按钮可以打开动画编辑界面,编辑整个动画组。

2、控制多个监听物体

这里写图片描述
1、添加新的监听物体:
① 动画编辑窗口右上角 -> Add Target按钮;
② 鼠标右键 -> Add Target选项;
2、删除监听物体:
① 物体的可移动窗口右上角 -> ‘x’按钮;
3、查找监听物体:
① 按住鼠标中间拖动视野;
② 动画编辑窗口右上角 -> Find Target按钮(查找由于拖动等原因消失在视野内的监听物体);

3、监听物体的属性

这里写图片描述
1、添加新的属性:
① 物体的可移动窗口下方 -> Add Property按钮(可以添加任意组件的任意已知、可读、可写属性);
2、删除属性:
① 属性左边的‘x’按钮;

源码解析

使用反射提取目标组件的对应属性:

if (GUI.Button(new Rect(5, h, _width - 10, 16), "Add Property"))
                {
                    GenericMenu gm = new GenericMenu();
                    //获取所有组件
                    Component[] cps = lat.Target.GetComponents<Component>();
                    for (int m = 0; m < cps.Length; m++)
                    {
                        //获取组件类型
                        Type type = cps[m].GetType();
                        //获取组件的所有属性
                        PropertyInfo[] pis = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                        for (int n = 0; n < pis.Length; n++)
                        {
                            PropertyInfo pi = pis[n];
                            string propertyType = pi.PropertyType.Name;
                            //替换属性名称为标准名称
                            propertyType = LinkageAnimationTool.ReplaceType(propertyType);
                            //检测属性类型是否为合法类型
                            bool allow = LinkageAnimationTool.IsAllowType(propertyType);

                            if (allow)
                            {
                                //属性为可读可写的属性
                                if (pi.CanRead && pi.CanWrite)
                                {
                                    gm.AddItem(new GUIContent(type.Name + "/" + "[" + propertyType + "] " + pi.Name), false, delegate ()
                                    {
                                        //添加属性成功
                                        LAProperty lap = new LAProperty(type.Name, propertyType, pi.Name);
                                        AddProperty(lat, lap);
                                    });
                                }
                            }
                        }
                    }
                    gm.ShowAsContext();
                }
4、使用关键帧制作动画

这里写图片描述
1、添加新的关键帧:
① 动画编辑窗口右上角 -> Add Frame按钮;
② 鼠标右键 -> Add Frame选项;
2、删除关键帧:
① 选中某一关键帧 -> Delete Frame按钮;
3、复制关键帧:
① 选中某一关键帧 -> Clone Frame按钮;
4、记录关键帧的值:
① 选中某一关键帧 -> Get Value In Scene按钮(将当前所有监听物体的被监听属性值记录到当前选中的关键帧);
5、提取关键帧的值:
① 选中某一关键帧 -> Set Value To Scene按钮(将当前选中关键帧的值赋予到场景中所有监听物体的被监听属性中);

源码解析

每一个关键帧中都有属性值仓库,可以通过索引提取属性值或是存储属性值,核心代码也是使用反射:

/// <summary>
    /// 获取目标属性值并记录到当前关键帧
    /// </summary>
    private void GetPropertyValue(int index)
    {
        for (int i = 0; i < _LA.Targets.Count; i++)
        {
            LinkageAnimationTarget lat = _LA.Targets[i];
            if (lat.Target)
            {
                LAFrame laf = lat.Frames[index];
                for (int j = 0; j < lat.Propertys.Count; j++)
                {
                    //通过名称获取组件
                    Component cp = lat.Target.GetComponent(lat.Propertys[j].ComponentName);
                    if (cp != null)
                    {
                        //通过名称获取属性
                        PropertyInfo pi = cp.GetType().GetProperty(lat.Propertys[j].PropertyName);
                        if (pi != null)
                        {
                            //获取属性值
                            object value = pi.GetValue(cp, null);
                            //重新记录到关键帧仓库
                            laf.SetFrameValue(j, value);
                        }
                        else
                        {
                            Debug.LogWarning("目标物体 " + lat.Target.name + " 的组件 " + lat.Propertys[j].ComponentName + " 不存在属性 " + lat.Propertys[j].PropertyName + "!");
                        }
                    }
                    else
                    {
                        Debug.LogWarning("目标物体 " + lat.Target.name + " 不存在组件 " + lat.Propertys[j].ComponentName + "!");
                    }
                }
            }
        }
    }
/// <summary>
    /// 设置当前关键帧数据至目标属性值
    /// </summary>
    private void SetPropertyValue(int index)
    {
        for (int i = 0; i < _LA.Targets.Count; i++)
        {
            LinkageAnimationTarget lat = _LA.Targets[i];
            if (lat.Target)
            {
                LAFrame laf = lat.Frames[index];
                for (int j = 0; j < lat.Propertys.Count; j++)
                {
                    //通过名称获取组件
                    Component cp = lat.Target.GetComponent(lat.Propertys[j].ComponentName);
                    if (cp != null)
                    {
                        //通过名称获取属性
                        PropertyInfo pi = cp.GetType().GetProperty(lat.Propertys[j].PropertyName);
                        if (pi != null)
                        {
                            //为属性设置值
                            pi.SetValue(cp, laf.GetFrameValue(j), null);
                        }
                        else
                        {
                            Debug.LogWarning("目标物体 " + lat.Target.name + " 的组件 " + lat.Propertys[j].ComponentName + " 不存在属性 " + lat.Propertys[j].PropertyName + "!");
                        }
                    }
                    else
                    {
                        Debug.LogWarning("目标物体 " + lat.Target.name + " 不存在组件 " + lat.Propertys[j].ComponentName + "!");
                    }
                }
            }
        }
    }
5、控制动画

这里写图片描述
1、播放动画:

        LinkageAnimation la;
        la.Playing = true;

2、暂停动画:

        LinkageAnimation la;
        la.Playing = false;

3、停止动画:

        LinkageAnimation la;
        la.Stop();

4、重新播放动画:

        LinkageAnimation la;
        la.RePlay();

5、添加帧回调:
① 属性面板 -> Add CallBack按钮(例:当动画执行到第一帧时会呼叫Translate函数);
6、删除帧回调:
① 属性面板 -> CallBack List -> ‘x’按钮;

源码解析

针对被监听目标的组件和属性,我这里选择只将组件名称和属性名字做序列化,在运行时才会动态去获取组件和属性,如果获取失败,则这个动画无效,这样做的好处是降低了数据结构的耦合性、序列化的复杂度:

    /// <summary>
    /// 初始化运行时控件
    /// </summary>
    private void InitComponent()
    {
        for (int i = 0; i < Targets.Count; i++)
        {
            LinkageAnimationTarget lat = Targets[i];

            if (lat.Target)
            {
                if (lat.PropertysRunTime == null)
                {
                    lat.PropertysRunTime = new List<LAPropertyRunTime>();
                }

                for (int j = 0; j < lat.Propertys.Count; j++)
                {
                    LAProperty lap = lat.Propertys[j];
                    //获取组件
                    Component cp = lat.Target.GetComponent(lap.ComponentName);
                    //获取属性
                    PropertyInfo pi = cp ? cp.GetType().GetProperty(lap.PropertyName) : null;
                    //该属性动画是否有效
                    bool valid = (cp != null && pi != null);
                    LAPropertyRunTime laprt = new LAPropertyRunTime(valid, cp, pi);
                    lat.PropertysRunTime.Add(laprt);
                }
            }
        }
    }

播放动画时,每种类型的属性都会采用线性插值算法进行播放(当然有些类型无法做到线性插值,比如bool,所以这取决于具体的实现代码):

    /// <summary>
    /// 更新动画帧
    /// </summary>
    private void UpdateFrame(LinkageAnimationTarget lat, int currentIndex, int nextIndex)
    {
        if (lat.Target)
        {
            LAFrame currentLAF = lat.Frames[currentIndex];
            LAFrame nextLAF = lat.Frames[nextIndex];

            for (int i = 0; i < lat.PropertysRunTime.Count; i++)
            {
                //当前属性名
                LAProperty lap = lat.Propertys[i];
                //当前属性运行时实例
                LAPropertyRunTime laprt = lat.PropertysRunTime[i];

                //属性动画有效
                if (laprt.IsValid)
                {
                    //根据播放位置进行插值
                    object value = LinkageAnimationTool.Lerp(currentLAF.GetFrameValue(i), nextLAF.GetFrameValue(i), lap.PropertyType, _playLocation);
                    //重新设置属性值
                    laprt.PropertyValue.SetValue(laprt.PropertyComponent, value, null);
                }
            }
        }
    }

关于插值方法Lerp的实现,其实很简单,很多类型可以直接调用官方的插值方法,如果要添加自定义的类型,这里必须要实现他的插值算法:

    /// <summary>
    /// 根据类型在两个属性间插值
    /// </summary>
    public static object Lerp(object value1, object value2, string type, float location)
    {
        object value;
        switch (type)
        {
            case "Bool":
                value = location < 0.5f ? (bool)value1 : (bool)value2;
                break;
            case "Color":
                value = Color.Lerp((Color)value1, (Color)value2, location);
                break;
            case "Float":
                float f1 = (float)value1;
                float f2 = (float)value2;
                value = f1 + (f2 - f1) * location;
                break;
            case "Int":
                int i1 = (int)value1;
                int i2 = (int)value2;
                value = (int)(i1 + (i2 - i1) * location);
                break;
            case "Quaternion":
                value = Quaternion.Lerp((Quaternion)value1, (Quaternion)value2, location);
                break;
            case "String":
                string s1 = (string)value1;
                string s2 = (string)value2;
                int length = (int)(s1.Length + (s2.Length - s1.Length) * location);
                value = s1.Length >= s2.Length ? s1.Substring(0, length) : s2.Substring(0, length);
                break;
            case "Vector2":
                value = Vector2.Lerp((Vector2)value1, (Vector2)value2, location);
                break;
            case "Vector3":
                value = Vector3.Lerp((Vector3)value1, (Vector3)value2, location);
                break;
            case "Vector4":
                value = Vector4.Lerp((Vector4)value1, (Vector4)value2, location);
                break;
            case "Sprite":
                value = location < 0.5f ? (Sprite)value1 : (Sprite)value2;
                break;
            default:
                value = null;
                break;
        }
        return value;
    }

源码链接

github源码链接:https://github.com/SaiTingHu/LinkageAnimation

一起学习和进步

猜你喜欢

转载自blog.csdn.net/qq992817263/article/details/78487996
今日推荐