动画事件
在Unity中动画事件是一种重要的辅助工具,能在动画播放到特定时间点时发送通知给脚本,方便进行自定义的处理。
通常情况下想要接收动画事件需要进行如下一番设置。
- 在Animator组件所在对象上挂载脚本
- 在脚本中编写接收事件通知的方法
- 在动画面板中为动画添加事件
- 在事件配置面板上下拉选择定义好的方法
此后便可以使用该事件了。
可以看得出来,动画事件的配置过程相对复杂,而且十分依赖和Animator组件同级别的脚本实现,为了方便使用应当尝试对其进行封装。
封装方式并不复杂,使用统一的脚本接受动画事件并按照事件传递过去的参数自动分发即可,外部使用前需要先注册回调方法。
代码如下
public class AnimationEventDispatcher : MonoBehaviour { public bool isDebug = false; // 动画事件分发表,可以为每个动画事件注册对应的分发方法 private Dictionary<string, HashSet<Action<string>>> dispatchTable = new Dictionary<string, HashSet<Action<string>>>(); // 注册事件参数接收器 public void RegisterReceiver(string name, Action<string> callback) { if (!string.IsNullOrEmpty(name)) { if (!dispatchTable.ContainsKey(name)) { dispatchTable[name] = new HashSet<Action<string>>(); } dispatchTable[name].Add(callback); } else { Debug.LogWarning("Cannot register Animation Event Receiver with empty name!"); } } // 反注册接收器 public void UnregisterReceiver(Action<string> callback) { if (!string.IsNullOrEmpty(name)) { foreach (string name in dispatchTable.Keys) { dispatchTable[name].Remove(callback); } } } // 事件分发入口,配置动画事件时选择该方法 public void DispatchAnimationEvent(string name) { if (isDebug) { Debug.Log("Animator Event: " + name); } if (!string.IsNullOrEmpty(name) && dispatchTable.ContainsKey(name)) { HashSet<Action<string>> callbacks = dispatchTable[name]; foreach (Action<string> func in callbacks) { func.Invoke(name); } } } }
使用时将该脚本挂载到Animator组件所在对象上,并且在动画事件配置中选择DispatchAnimationEvent方法,给定事件参数,随后外部获取到AnimationEventDispatcher组件并注册事件回调方法即可。
AnimationEventDispatcher dispatcher = animatorObject.GetComponent<AnimationEventDispatcher>(); dispatcher.RegisterReceiver("TestEvent", ev => { // 事件处理代码 });
经过这样的简单封装后,动画事件的使用会相对更加方便一些。
Animator状态机
Animator本身其实是个状态机实现,而且能够通过编辑器面板进行可视化的配置,在开发过程中有时候会需要让自定义的控制器与Animator的状态在一定程度上同步,默认情况下只能通过在Update方法中获取当前AnimatorStateInfo结构来追踪状态。
这样的用法会导致大量冗余代码,即便将获取AnimatorStageInfo的代码封装起来使用,总是需要重复判断当前所处状态以及前后切换路径依然会造成迷惑和阻碍。
因此可以考虑将这些追踪状态变化的代码封装到一个独立的脚本里,外部通过注册特定状态的进入,更新以及退出方法来实现控制。
实现代码如下
public class AnimatorStateMachine : MonoBehaviour { public bool autoUpdate; public Animator Animator { get { return _animator; } } protected Animator _animator; // 状态更新代理方法集合 protected Dictionary<int, HashSet<Action>> stateUpdateMethods = new Dictionary<int, HashSet<Action>>(); // 状态进入代理方法集合 protected Dictionary<int, HashSet<Action>> stateEnterMethods = new Dictionary<int, HashSet<Action>>(); // 状态退出方法集合 protected Dictionary<int, HashSet<Action>> stateExitMethods = new Dictionary<int, HashSet<Action>>(); protected Dictionary<int, string> hashToAnimString; protected int[] _lastStateLayers; private void Awake() { _animator = GetComponent<Animator>(); _lastStateLayers = new int[_animator.layerCount]; } private void Update() { if (autoUpdate) { StateMachineUpdate(); } } // 每帧检查状态变化情况 protected virtual void StateMachineUpdate() { for (int layer = 0; layer < _lastStateLayers.Length; layer++) { int _lastState = _lastStateLayers[layer]; int stateId = _animator.GetCurrentAnimatorStateInfo(layer).fullPathHash; if (_lastState != stateId) { if (stateExitMethods.ContainsKey(_lastState)) { foreach (Action action in stateExitMethods[_lastState]) { action.Invoke(); } } if (stateEnterMethods.ContainsKey(stateId)) { foreach (Action action in stateEnterMethods[stateId]) { action.Invoke(); } } } if (stateUpdateMethods.ContainsKey(stateId)) { foreach (Action action in stateUpdateMethods[stateId]) { action.Invoke(); } } _lastStateLayers[layer] = stateId; } } protected void InsertStateEnterMethod(int hash, Action method) { if (!stateEnterMethods.ContainsKey(hash)) { stateEnterMethods[hash] = new HashSet<Action>(); } stateEnterMethods[hash].Add(method); } protected void InsertStateUpdateMethod(int hash, Action method) { if (!stateUpdateMethods.ContainsKey(hash)) { stateUpdateMethods[hash] = new HashSet<Action>(); } stateUpdateMethods[hash].Add(method); } protected void InsertStateExitMethod(int hash, Action method) { if (!stateExitMethods.ContainsKey(hash)) { stateExitMethods[hash] = new HashSet<Action>(); } stateExitMethods[hash].Add(method); } // 注册状态进入方法 public void RegisterStateEnterMethod(string path, Action method) { int hash = Animator.StringToHash(path); hashToAnimString[hash] = path; InsertStateEnterMethod(hash, method); } // 注册状态更新方法 public void RegisterStateUpdateMethod(string path, Action method) { int hash = Animator.StringToHash(path); hashToAnimString[hash] = path; InsertStateUpdateMethod(hash, method); } // 注册状态退出方法 public void RegisterStateExitMethod(string path, Action method) { int hash = Animator.StringToHash(path); hashToAnimString[hash] = path; InsertStateExitMethod(hash, method); } }
此后只需要按需注册特定的方法即可让Animator状态机按照要求运作。
利用属性实现自动注册方法
虽然Animator状态机可以从外部注册方法,但每次都要调用Register还是有些麻烦,如果使用C#的Attribute特性则能让这个注册过程更加简化。
为了达到这个目的,首先需要定义相应的属性
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class StateUpdateMethod : Attribute { public string state; public StateUpdateMethod(string state) { this.state = state; } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class StateEnterMethod : Attribute { public string state; public StateEnterMethod(string state) { this.state = state; } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class StateExitMethod : Attribute { public string state; public StateExitMethod(string state) { this.state = state; } }
定义了属性之后便可以在Animator组件所在对象上挂载的任意脚本中定义所需的方法。
[StateEnterMethod("Base.TestState")] public void TestEnter() { // 进入方法 } [StateUpdateMethod("Base.TestState")] public void TestEnter() { // 更新方法 } [StateExitMethod("Base.TestState")] public void TestEnter() { // 退出方法 }
为了能自动搜索这些由属性修饰的方法并注册到控制器中,需要添加如下一些代码
// 自动搜索和注册属性修饰的方法 private void DiscoverStateMethods() { hashToAnimString = new Dictionary<int, string>(); var components = gameObject.GetComponents<MonoBehaviour>(); List<StateMethod> enterStateMethods = new List<StateMethod>(); List<StateMethod> updateStateMethods = new List<StateMethod>(); List<StateMethod> exitStateMethods = new List<StateMethod>(); foreach (var component in components) { if (component == null) continue; Type type = component.GetType(); MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.InvokeMethod); foreach (var method in methods) { object[] attributes; attributes = method.GetCustomAttributes(typeof(StateUpdateMethod), true); foreach (StateUpdateMethod attribute in attributes) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) { StateMethod sm = CreateStateMethod(attribute.state, method, component); InsertStateUpdateMethod(sm.stateHash, sm.method); } } attributes = method.GetCustomAttributes(typeof(StateEnterMethod), true); foreach (StateEnterMethod attribute in attributes) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) { StateMethod sm = CreateStateMethod(attribute.state, method, component); InsertStateEnterMethod(sm.stateHash, sm.method); } } attributes = method.GetCustomAttributes(typeof(StateExitMethod), true); foreach (StateExitMethod attribute in attributes) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) { StateMethod sm = CreateStateMethod(attribute.state, method, component); InsertStateExitMethod(sm.stateHash, sm.method); } } } } } protected virtual StateMethod CreateStateMethod(string state, MethodInfo method, MonoBehaviour component) { int stateHash = Animator.StringToHash(state); hashToAnimString[stateHash] = state; StateMethod stateMethod = new StateMethod(); stateMethod.stateHash = stateHash; stateMethod.method = () => { method.Invoke(component, null); }; return stateMethod; }
随后只需要在Awake方法以及OnValidate方法中调用DiscoverStateMethods即可,如此一来无论是在编辑器还是实际运行中都会自动按照属性修饰搜索方法并注册
整理代码后如下
public class AnimatorStateMachine : MonoBehaviour { public bool isDebug = false; public bool autoUpdate; public Animator Animator { get { return _animator; } } protected Dictionary<string, HashSet<Action<string>>> dispatchTable = new Dictionary<string, HashSet<Action<string>>>(); protected Animator _animator; protected Dictionary<int, HashSet<Action>> stateUpdateMethods = new Dictionary<int, HashSet<Action>>(); protected Dictionary<int, HashSet<Action>> stateEnterMethods = new Dictionary<int, HashSet<Action>>(); protected Dictionary<int, HashSet<Action>> stateExitMethods = new Dictionary<int, HashSet<Action>>(); protected Dictionary<int, string> hashToAnimString; protected int[] _lastStateLayers; private void Awake() { _animator = GetComponent<Animator>(); _lastStateLayers = new int[_animator.layerCount]; DiscoverStateMethods(); } private void Update() { if (autoUpdate) { StateMachineUpdate(); } } private void OnValidate() { DiscoverStateMethods(); } protected virtual void StateMachineUpdate() { for (int layer = 0; layer < _lastStateLayers.Length; layer++) { int _lastState = _lastStateLayers[layer]; int stateId = _animator.GetCurrentAnimatorStateInfo(layer).fullPathHash; if (_lastState != stateId) { if (stateExitMethods.ContainsKey(_lastState)) { foreach (Action action in stateExitMethods[_lastState]) { action.Invoke(); } } if (stateEnterMethods.ContainsKey(stateId)) { foreach (Action action in stateEnterMethods[stateId]) { action.Invoke(); } } } if (stateUpdateMethods.ContainsKey(stateId)) { foreach (Action action in stateUpdateMethods[stateId]) { action.Invoke(); } } _lastStateLayers[layer] = stateId; } } private void DiscoverStateMethods() { hashToAnimString = new Dictionary<int, string>(); var components = gameObject.GetComponents<MonoBehaviour>(); List<StateMethod> enterStateMethods = new List<StateMethod>(); List<StateMethod> updateStateMethods = new List<StateMethod>(); List<StateMethod> exitStateMethods = new List<StateMethod>(); foreach (var component in components) { if (component == null) continue; Type type = component.GetType(); MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.InvokeMethod); foreach (var method in methods) { object[] attributes; attributes = method.GetCustomAttributes(typeof(StateUpdateMethod), true); foreach (StateUpdateMethod attribute in attributes) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) { StateMethod sm = CreateStateMethod(attribute.state, method, component); InsertStateUpdateMethod(sm.stateHash, sm.method); } } attributes = method.GetCustomAttributes(typeof(StateEnterMethod), true); foreach (StateEnterMethod attribute in attributes) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) { StateMethod sm = CreateStateMethod(attribute.state, method, component); InsertStateEnterMethod(sm.stateHash, sm.method); } } attributes = method.GetCustomAttributes(typeof(StateExitMethod), true); foreach (StateExitMethod attribute in attributes) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) { StateMethod sm = CreateStateMethod(attribute.state, method, component); InsertStateExitMethod(sm.stateHash, sm.method); } } } } } protected virtual StateMethod CreateStateMethod(string state, MethodInfo method, MonoBehaviour component) { int stateHash = Animator.StringToHash(state); hashToAnimString[stateHash] = state; StateMethod stateMethod = new StateMethod(); stateMethod.stateHash = stateHash; stateMethod.method = () => { method.Invoke(component, null); }; return stateMethod; } protected void InsertStateEnterMethod(int hash, Action method) { if (!stateEnterMethods.ContainsKey(hash)) { stateEnterMethods[hash] = new HashSet<Action>(); } stateEnterMethods[hash].Add(method); } protected void InsertStateUpdateMethod(int hash, Action method) { if (!stateUpdateMethods.ContainsKey(hash)) { stateUpdateMethods[hash] = new HashSet<Action>(); } stateUpdateMethods[hash].Add(method); } protected void InsertStateExitMethod(int hash, Action method) { if (!stateExitMethods.ContainsKey(hash)) { stateExitMethods[hash] = new HashSet<Action>(); } stateExitMethods[hash].Add(method); } public void RegisterStateEnterMethod(string path, Action method) { int hash = Animator.StringToHash(path); hashToAnimString[hash] = path; InsertStateEnterMethod(hash, method); } public void RegisterStateUpdateMethod(string path, Action method) { int hash = Animator.StringToHash(path); hashToAnimString[hash] = path; InsertStateUpdateMethod(hash, method); } public void RegisterStateExitMethod(string path, Action method) { int hash = Animator.StringToHash(path); hashToAnimString[hash] = path; InsertStateExitMethod(hash, method); } public void RegisterReceiver(string name, Action<string> callback) { if (!string.IsNullOrEmpty(name)) { if (!dispatchTable.ContainsKey(name)) { dispatchTable[name] = new HashSet<Action<string>>(); } dispatchTable[name].Add(callback); } else { Debug.LogWarning("Cannot register Animation Event Receiver with empty name!"); } } public void UnregisterReceiver(Action<string> callback) { if (!string.IsNullOrEmpty(name)) { foreach (string name in dispatchTable.Keys) { dispatchTable[name].Remove(callback); } } } public void DispatchAnimationEvent(string name) { if (isDebug) { Debug.Log("Animator Event: " + name); } if (!string.IsNullOrEmpty(name) && dispatchTable.ContainsKey(name)) { HashSet<Action<string>> callbacks = dispatchTable[name]; foreach (Action<string> func in callbacks) { func.Invoke(name); } } } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class StateUpdateMethod : Attribute { public string state; public StateUpdateMethod(string state) { this.state = state; } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class StateEnterMethod : Attribute { public string state; public StateEnterMethod(string state) { this.state = state; } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class StateExitMethod : Attribute { public string state; public StateExitMethod(string state) { this.state = state; } } public class StateMethod { public int stateHash; public Action method; }
将该脚本挂载到Animator所在对象上并获取引用即可开始注册或者编写控制方法了。
两者整合
以上提到的两种封装可以并入一个类中,更加简洁和方便,简单的拷贝即可完成这一工作。
此外还可以为它添加更多能力,比如直接暴露一些设置Animator中数据或者触发器的方法接口。
封装后的控制器相比于直接使用Animator会方便一些。
暂无关于此日志的评论。