1. 程式人生 > >Unity 多物體混合動畫、值變動畫控制器

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

原始碼連結

一起學習和進步