索引
- 前言
- 示例
- 1、4个Cube的联动动画
- 2、UGUI Text文本动画
- 3、UGUI Image图片动画
- 4、物体消隐动画
- 使用与解析
- 1、挂载LinkageAnimation脚本至场景中
- 2、控制多个监听物体
- 3、监听物体的属性
- 源码解析
- 4、使用关键帧制作动画
- 源码解析
- 5、控制动画
- 源码解析
- 源码链接
前言
因为工作中有用到,所以我抽出空闲把之前的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;
}