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;
}
原始碼連結
一起學習和進步