Unity 3D 打造自己的Mecanim Callback System

weixin_34321977發表於2018-05-14

依舊是基於鏈式程式設計思想,筆者封裝了一個動畫機回撥系統,簡單實用。在本文,筆者將採用圖文並茂的方式簡單的講講如何實現這個回撥系統,文章結尾會提供連結下載該回撥系統。

需求:

  1. 指定動畫機執行到指定層 觸動指定幀上的指定事件鏈
  2. AnimationEvent 可以指定 多引數,組合引數。

效果:

3600713-a0a50e5fac3036d6.png
code

3600713-04325cfa7323a488.png
輸出

3600713-889b10c3e5d12a9d.png
Code
業務流程說明:

  1. 獲取 Animator 引用;
  2. 上述引用找到擴充套件方法(此方法2個過載):SetTarget
  3. 鏈式程式設計,繼續點出OnProcess 方法,寫入回撥邏輯(lambda或者指定簽名的方法)
  4. 鏈式程式設計,繼續點出SetParm方法,設定這個AnimationEvent用到的引數(同時多引數組合),一般來說,如果不配合事件系統向外分發該事件,可以不呼叫這個方法
  5. 觸發註冊了該事件的的動畫片段(AnimationClip),第三步回撥將被觸發。

實現:

3600713-e64db379ff447c36.png
基本結構

程式碼:

  1. 方法擴充套件實現的入口:
using UnityEngine;

namespace zFrame.Event
{
    public static class A_EventExtend
    {
        /// <summary>
        /// 指定需要繫結回撥的AnimationClip
        /// </summary>
        /// <param name="animator">動畫機</param>
        /// <param name="clipName">動畫片段</param>
        /// <returns>事件配置器</returns>
        public static A_EventConfig_A SetTarget(this Animator animator, string clipName)
        {
            A_EventInfo a_EventInfo = A_EventHandler.Handler.GenerAnimationInfo(animator, clipName);
            if (null != a_EventInfo)
            {
                if (null == animator.GetComponent<CallbackListener>())
                {
                    animator.gameObject.AddComponent<CallbackListener>();
                }
            }
            //獲得需要處理的動畫片段
            return new A_EventConfig_A(a_EventInfo);
        }
        public static A_EventConfig_B SetTarget(this Animator animator, string clipName, int frame)
        {
            A_EventInfo a_EventInfo = A_EventHandler.Handler.GenerAnimationInfo(animator, clipName);
            if (null != a_EventInfo)
            {
                if (null == animator.GetComponent<CallbackListener>())
                {
                    animator.gameObject.AddComponent<CallbackListener>();
                }
            }
            //獲得需要處理的動畫片段
            return new A_EventConfig_B(a_EventInfo, frame);
        }
    }
}
  1. 返回的事件配置物件不相同,因為需要物件點出不同的監聽方法
using System;
using UnityEngine;
namespace zFrame.Event
{
    /// <summary>Mecanim事件系統事件配置類_for start+completed callback </summary>
    public class A_EventConfig_A : BaseEventConfig
    {
        public A_EventConfig_A(A_EventInfo eventInfo, int frame = -1) : base(eventInfo, frame) { }
        /// <summary>
        /// 為Clip新增Onstart回撥事件
        /// </summary>
        /// <param name="onStart">回撥</param>
        /// <returns>引數配置器</returns>
        public A_EventConfig_A OnStart(Action<AnimationEvent> onStart)
        {
            if (a_Event == null) return null;
            ConfigProcess(0, onStart);
            return this;
        }
        /// <summary>
        /// 為Clip新增OnCompleted回撥事件
        /// </summary>
        /// <param name="OnCompleted">回撥</param>
        /// <returns>引數配置器</returns>
        public  A_EventConfig_A OnCompleted(Action<AnimationEvent> onCompleted)
        {
            if (a_Event == null) return null;
            ConfigProcess(a_Event.totalFrames,onCompleted);
            return this;
        }
    }
    /// <summary>Mecanim事件系統事件配置類_For Process callback </summary>
    public class A_EventConfig_B : BaseEventConfig
    {
        public A_EventConfig_B(A_EventInfo eventInfo, int frame) : base(eventInfo, frame) { }
        public A_EventConfig_B OnProcess(Action<AnimationEvent> onProcess)
        {
            if (a_Event == null) return null;
            ConfigProcess(_keyFrame, onProcess);
            return this;
        }
    }
}

SetTarget("name").OnStart()
SetTarget("name").OnCompleted()
SetTarget("name",10).OnProcess()

  1. 下一步,我們配置AnimationEvent引數,為了上面兩個不同的類都能點出這個SetParm()方法,我們將其寫在基類之中。
    同時,使用介面卡設計模式的思想,將Animator的幾個觸發方法封裝在這個基類中,效果如下:


    3600713-794ea1f1ccb61972.png
    Preview

    3600713-0aebac67cf277b3d.gif
    運用語法糖,可以指定引數賦值
using System;
using UnityEngine;

namespace zFrame.Event
{
    /// <summary>
    /// 引數配置類,不建議配置任何引數,除非配合事件系統使用
    /// </summary>
    public class BaseEventConfig
    {
        protected AnimationEvent _ClipEvent;
        protected int _keyFrame;
        protected A_EventInfo a_Event;
        protected Animator _animator;

        public BaseEventConfig(A_EventInfo eventInfo, int frame)
        {
            _keyFrame = frame;
            a_Event = eventInfo;
            _animator = eventInfo.animator;
        }

        /// <summary>設定組合引數</summary>
        /// <param name="intParm">int引數</param>
        /// <param name="floatParm">float引數</param>
        /// <param name="stringParm">string引數(必填)</param>
        /// <param name="objectParm">Object引數</param>
        /// <returns></returns>
        public Animator SetParms(string stringParm, int intParm = default(int), float floatParm = default(float), UnityEngine.Object objectParm = default(UnityEngine.Object))
        {
            if (null == a_Event){ return _animator;}
            AnimationEvent _ClipEvent;
            a_Event.frameEventPairs.TryGetValue(_keyFrame, out _ClipEvent);
            if (null == _ClipEvent){ return _animator; }
            _ClipEvent.intParameter = intParm;
            _ClipEvent.floatParameter = floatParm;
            _ClipEvent.stringParameter = stringParm;
            _ClipEvent.objectReferenceParameter = objectParm;
            ResignEvent();
            return a_Event.animator;
        }

        /// <summary>
        /// 引數被變更,需要重新繫結所有的事件
        /// </summary>
        private void ResignEvent()
        {
            a_Event.animationClip.events = default(AnimationEvent[]); //被逼的,AnimationEvent不是簡單的物件引用及其欄位修改的問題,只能從新插入事件
            foreach (AnimationEvent item in a_Event.frameEventPairs.Values)
            {
                a_Event.animationClip.AddEvent(item);
            }
            a_Event.animator.Rebind();
        }

        /// <summary>
        /// 為指定幀加入回撥鏈
        /// </summary>
        /// <param name="frame"></param>
        /// <param name="action"></param>
        protected void ConfigProcess(int frame, Action<AnimationEvent> action)
        {
            if (null == action) return;
            _keyFrame = frame;
            if (!a_Event.frameCallBackPairs.ContainsKey(_keyFrame))
            {
                a_Event.frameCallBackPairs.Add(_keyFrame, action);
            }
            else
            {
                Action<AnimationEvent> t_action = a_Event.frameCallBackPairs[_keyFrame];
                if (null == t_action)
                {
                    a_Event.frameCallBackPairs[_keyFrame] = action;
                }
                else
                {
                    Delegate[] delegates = t_action.GetInvocationList();
                    if (Array.IndexOf(delegates, action) == -1)
                    {
                        a_Event.frameCallBackPairs[_keyFrame] += action;
                    }
                    else
                    {
                        Debug.LogWarningFormat("AnimatorEventSystem[一般]:指定AnimationClip【{0}】已經訂閱了該事件【{1}】!\n 建議:請勿頻繁訂閱!", a_Event.animationClip.name,action.Method.Name);
                    }
                }
            }
            if (!a_Event.frameEventPairs.ContainsKey(_keyFrame))
            {
                A_EventHandler.Handler.GenerAnimationEvent(a_Event, _keyFrame);
            }
        }

        #region Adapter For Animator
        /// <summary>
        /// 設定動畫機bool引數
        /// </summary>
        /// <param name="name">引數名</param>
        /// <param name="value">引數值</param>
        /// <returns></returns>
        public Animator SetBool(string name, bool value)
        {
            _animator.SetBool(name, value);
            return _animator;
        }
        /// <summary>
        /// 設定動畫機bool引數
        /// </summary>
        /// <param name="name">引數id</param>
        /// <param name="value">引數值</param>
        /// <returns></returns>
        public Animator SetBool(int id, bool value)
        {
            _animator.SetBool(id, value);
            return _animator;
        }
        /// <summary>
        /// 設定動畫機float引數
        /// </summary>
        /// <param name="name">引數id</param>
        /// <param name="value">引數值</param>
        /// <returns></returns>
        public Animator SetFloat(int id, float value)
        {
            _animator.SetFloat(id, value);
            return _animator;
        }
        /// <summary>
        /// 設定動畫機float引數
        /// </summary>
        /// <param name="name">引數名</param>
        /// <param name="value">引數值</param>
        /// <returns></returns>
        public Animator SetFloat(string name, float value)
        {
            _animator.SetFloat(name, value);
            return _animator;
        }
        /// <summary>
        ///  設定動畫機float引數
        /// </summary>
        /// <param name="name">引數名</param>
        /// <param name="value">引數值</param>
        /// <param name="dampTime"></param>
        /// <param name="deltaTime"></param>
        /// <returns></returns>
        public Animator SetFloat(string name, float value, float dampTime, float deltaTime)
        {
            _animator.SetFloat(name, value, dampTime, deltaTime);
            return _animator;
        }
        /// <summary>
        /// 設定動畫機float引數
        /// </summary>
        /// <param name="name">引數id</param>
        /// <param name="value">引數值</param>
        /// <returns></returns>
        public Animator SetFloat(int id, float value, float dampTime, float deltaTime)
        {
            _animator.SetFloat(id, value, dampTime, deltaTime);
            return _animator;
        }
        /// <summary>
        /// 設定動畫機trigger引數
        /// </summary>
        /// <param name="name">引數id</param>
        /// <returns></returns>
        public Animator SetTrigger(int id)
        {
            _animator.SetTrigger(id);
            return _animator;
        }
        /// <summary>
        /// 設定動畫機trigger引數
        /// </summary>
        /// <param name="name">引數name</param>
        /// <returns></returns>
        public Animator SetTrigger(string name)
        {
            _animator.SetTrigger(name);
            return _animator;
        }
        #endregion
    }
}
  1. 可以預見,一個Clip上的事件資訊會很大,我們必須整一個類來儲存這些資訊。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 事件訂閱資訊資訊儲存類
/// </summary>
public class A_EventInfo  {

    /// <summary>訂閱事件所在的動畫機</summary>
    public Animator animator;       
    /// <summary>動畫機指定狀態的動畫片段</summary>
    public AnimationClip animationClip;
    /// <summary>動畫片段的總幀數</summary>
    public int totalFrames;
    /// <summary>幀以及對應的回撥鏈</summary>
    public Dictionary<int, Action<AnimationEvent>> frameCallBackPairs;
    /// <summary>幀以及對應的事件</summary>
    public Dictionary<int, AnimationEvent> frameEventPairs;

    public A_EventInfo(Animator anim, AnimationClip clip )
    {
        frameCallBackPairs = new Dictionary<int, Action<AnimationEvent>>();
        frameEventPairs = new Dictionary<int, AnimationEvent>();
        animator = anim;
        animationClip = clip;
        //向下取整(可以不需要),獲得該動畫片段的總幀數
        totalFrames = Mathf.CeilToInt(animationClip.frameRate* animationClip.length);
    }
    /// <summary>清除資料</summary>
    public void Clear()
    {
        animationClip.events = default(AnimationEvent[]);
        frameCallBackPairs=new Dictionary<int, Action<AnimationEvent>> ();
        frameEventPairs=new Dictionary<int, AnimationEvent> ();
        animationClip = null;
        animator = null;
    }
}

  1. 下面,完善一下打通整個業務邏輯的類
using System;
using System.Collections.Generic;
using UnityEngine;

namespace zFrame.Event
{
    public class A_EventHandler
    {
        #region 單例
        private static A_EventHandler _instance;
        /// <summary>獲得事件處理類例項</summary>
        public static A_EventHandler Handler
        {
            get
            {
                if (null == _instance)
                {
                    _instance = new A_EventHandler();
                }
                return _instance;
            }
        }
        #endregion
        /// <summary>動畫機及其事件資訊Pairs</summary>
        private List<A_EventInfo> eventContainer
        private const string func = "AnimatorEventCallBack";
        public A_EventHandler()
        {
            eventContainer = new List<A_EventInfo>();
            buffedEvent = new Dictionary<string, Action<AnimationEvent>>();
        }
        /// <summary>
        /// 為事件基礎資訊進行快取
        /// </summary>
        /// <param name="animator">動畫機</param>
        /// <param name="clipName">動畫片段名稱</param>
        /// <param name="frame">指定幀</param>
        public A_EventInfo GenerAnimationInfo(Animator animator, string clipName)
        {
            AnimationClip clip = GetAnimationClip(animator, clipName);
            if (null == clip)return null;
            A_EventInfo a_EventInfo = GetEventInfo(animator, clip);  //獲取指定事件資訊類
            return a_EventInfo;
        }

        /// <summary>
        /// 為指定動畫機片段插入回撥方法 
        /// </summary>
        /// <param name="eventInfo">回撥資訊類</param>
        /// <param name="frame">指定幀</param>
        /// <param name="func">方法名</param>
        public void GenerAnimationEvent(A_EventInfo eventInfo, int frame)
        {
            if (frame < 0 || frame > eventInfo.totalFrames)
            {
                Debug.LogErrorFormat("AnimatorEventSystem[緊急]:【{0}】所在的動畫機【{1}】片段幀數設定錯誤【{2}】!", eventInfo.animator.name, eventInfo.animationClip.name, frame);
                return;
            }
            float _time = frame / eventInfo.animationClip.frameRate;
            AnimationEvent[] events = eventInfo.animationClip.events;
            AnimationEvent varEvent = Array.Find(events, (v) => { return v.time == _time; });
            if (null != varEvent)
            {
                if (varEvent.functionName == func)
                {
                    Debug.LogWarningFormat("AnimatorEventSystem[一般]:【{0}】所在的動畫機【{1}】片段【{2}】幀已存在回撥方法,無需重複新增!", eventInfo.animator.name, eventInfo.animationClip.name, frame);
                    if (!eventInfo.frameEventPairs.ContainsKey(frame)) eventInfo.frameEventPairs.Add(frame, varEvent);
                    return;
                }
                else
                {
                    Debug.LogWarningFormat("AnimatorEventSystem[一般]:【{0}】所在的動畫機【{1}】片段【{2}】幀已存在回撥方法【{3}】,將自動覆蓋!", eventInfo.animator.name, eventInfo.animationClip.name, frame, varEvent.functionName);
                }
            }
            AnimationEvent a_event = new AnimationEvent //建立事件物件
            {
                functionName = func, //指定事件的函式名稱
                time = _time,  //對應動畫指定幀處觸發
                messageOptions = SendMessageOptions.DontRequireReceiver, //回撥未找到不提示
            };
            eventInfo.animationClip.AddEvent(a_event); //繫結事件
            eventInfo.frameEventPairs.Add(frame, a_event);
            eventInfo.animator.Rebind(); //重新繫結動畫器的所有動畫的屬性和網格資料。
        }

        /// <summary>資料重置,用於總管理類清理資料用</summary>
        public void Clear()
        {
            foreach (var item in eventContainer)
            {
                item.Clear();
            }
            eventContainer = new List<A_EventInfo>();
        }

        #region Helper Function
        /// <summary>
        /// 獲得指定的事件資訊類
        /// </summary>
        /// <param name="animator">動畫機</param>
        /// <param name="clip">動畫片段</param>
        /// <returns>事件資訊類</returns>
        private A_EventInfo GetEventInfo(Animator animator, AnimationClip clip)
        {
            A_EventInfo a_EventInfo = eventContainer.Find((v) => { return v.animator == animator && v.animationClip == clip; });
            if (null == a_EventInfo)
            {
                a_EventInfo = new A_EventInfo(animator, clip);
                eventContainer.Add(a_EventInfo);
            }
            return a_EventInfo;
        }

        /// <summary>
        /// 根據動畫片段名稱從指定動畫機獲得動畫片段
        /// </summary>
        /// <param name="animator">動畫機</param>
        /// <param name="name">動畫片段名稱</param>
        /// <returns></returns>
        public AnimationClip GetAnimationClip(Animator animator, string name)
        {
            #region 異常提示
            if (null == animator)
            {
                Debug.LogError("AnimatorEventSystem[緊急]:指定Animator不得為空!");
                return null;
            }
            RuntimeAnimatorController runtimeAnimatorController = animator.runtimeAnimatorController;
            if (null == runtimeAnimatorController)
            {
                Debug.LogError("AnimatorEventSystem[緊急]:指定【"+animator.name +"】Animator未掛載Controller!");
                return null;
            }
            AnimationClip[] clips = runtimeAnimatorController.animationClips;
            AnimationClip[] varclip = Array.FindAll(clips, (v) => { return v.name == name; });
            if (null == varclip || varclip.Length == 0)
            {
                Debug.LogError("AnimatorEventSystem[緊急]:指定【" + animator.name + "】Animator不存在名為【" + name + "】的動畫片段!");
                return null;
            }
            if (varclip.Length >= 2)
            {
                Debug.LogWarningFormat("AnimatorEventSystem[一般]:指定【{0}】Animator存在【{1}】個名為【{2}】的動畫片段!\n 建議:若非複用導致的重名,請務必修正!否則,事件將繫結在找的第一個Clip上。",animator.name, varclip.Length, name);
            }
            #endregion
            return varclip[0];
        }
        /// <summary>
        /// 根據給定資訊獲得委託
        /// </summary>
        /// <param name="animator"></param>
        /// <param name="clip"></param>
        /// <param name="frame"></param>
        /// <returns></returns>
        public Action<AnimationEvent> GetAction(Animator animator, AnimationClip clip, int frame)
        {
            Action<AnimationEvent> action = default(Action<AnimationEvent>);
            A_EventInfo a_EventInfo = eventContainer.Find((v) => { return v.animator == animator && v.animationClip == clip; });
            if (null != a_EventInfo)
            {
                a_EventInfo.frameCallBackPairs.TryGetValue(frame, out action);
            }
            return action;
        }
        #endregion 
    }
}

  1. 我們知道,動畫機的回撥方法需要方法所在的物件跟動畫機在同一個遊戲物件下,於是我們自動新增如下程式碼,實現回撥的執行。
using System;
using UnityEngine;

namespace zFrame.Event
{
    public class CallbackListener : MonoBehaviour
    {
        Animator animator;
        A_EventHandler eventHandler;
        void Start()
        {
            eventHandler = A_EventHandler.Handler; 
            animator = GetComponent<Animator>();
        }
        /// <summary>通用事件回撥</summary>
        /// <param name="ae">事件傳遞的引數資訊</param>
        private void AnimatorEventCallBack(AnimationEvent ae)
        {
            AnimationClip clip = ae.animatorClipInfo.clip;//動畫片段名稱
            int currentFrame = Mathf.CeilToInt(ae.animatorClipInfo.clip.frameRate* ae.time);  //動畫片段當前幀
            Action<AnimationEvent> action = eventHandler.GetAction(animator,clip,currentFrame);
            if (null!=action)
            {
                action(ae);
            }
        }
    }
}

如你所見,回撥方法給一個AnimationEvent引數是最優解,因為這樣我們可以拿到所有的使用者給定引數(int float string Object),當然包括動畫機返回的引數(ClipInfo,StateInfo)。
對於返回的這些資訊的拆分和使用,在開篇的截圖裡面可以瞧見。

  1. 最後,整理一個簡單的幫助類,用來了解動畫機的基本資訊,效果如下:


    3600713-2b44c83ef011ea86.gif
    Preview
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor.Animations;
#endif
using UnityEngine;
[ExecuteInEditMode]
public class AnimHelper : MonoBehaviour
{
    Animator animator;
    public List<StateInfo> clipsInfo = new List<StateInfo>();

    void Start()
    {
        animator = GetComponent<Animator>();
#if UNITY_EDITOR
        ShowClipInfo(animator);
#endif
    }

    private void ShowClipInfo(Animator animator)
    {
        AnimatorController controller = animator ? animator.runtimeAnimatorController as AnimatorController : null;
        if (null == controller)
        {
            Debug.LogError("[嚴重]:動畫機或動畫機控制器為空,請檢查!");
        }
        else
        {
            for (int i = 0; i < controller.layers.Length; i++)
            {
                ChildAnimatorState[] states = controller.layers[i].stateMachine.states;
                foreach (ChildAnimatorState item in states)
                {
                    StateInfo clipInfo = new StateInfo
                    {
                        stateName = string.Format("{0}.{1}",animator.GetLayerName(i), item.state.name),
                        layerIndex = i
                    };
                    if (item.state.motion.GetType() == typeof(AnimationClip))
                    {
                        clipInfo.clip = (AnimationClip)item.state.motion;
                    }
                    else
                    {
                        clipInfo.clip = null;
                        Debug.LogWarning("暫不支援BlendTree動畫片段預覽。");
                    }
                    foreach (AnimationEvent ev in clipInfo.clip.events)
                    {
                        clipInfo.funcs.Add(ev.functionName);
                    }
                    clipsInfo.Add(clipInfo);
                }
            }
        }
    }

    [System.Serializable]
    public class StateInfo
    {
        public string stateName;
        public AnimationClip clip;
        public int layerIndex;
        public List<string> funcs = new List<string>();
    }
}

標籤:Mecanim 、EventSystem、CallBack回撥 、Animator、 AnimationEvent,MecanimEventSystem

連結: https://pan.baidu.com/s/1inN3kpav28rxkzjJ3usz0w 密碼: gj4v

相關文章