美文网首页unitycsharpUnity
Unity 3D 打造自己的Mecanim Callback S

Unity 3D 打造自己的Mecanim Callback S

作者: 雨落随风 | 来源:发表于2018-05-14 15:10 被阅读558次

    依旧是基于链式编程思想,笔者封装了一个动画机回调系统,简单实用。在本文,笔者将采用图文并茂的方式简单的讲讲如何实现这个回调系统,文章结尾会提供链接下载该回调系统。

    需求:

    1. 指定动画机运行到指定层 触动指定帧上的指定事件链
    2. AnimationEvent 可以指定 多参数,组合参数。

    效果:

    code
    输出
    Code
    业务流程说明:

    1. 获取 Animator 引用;
    2. 上述引用找到扩展方法(此方法2个重载):SetTarget
    3. 链式编程,继续点出OnProcess 方法,写入回调逻辑(lambda或者指定签名的方法)
    4. 链式编程,继续点出SetParm方法,设置这个AnimationEvent用到的参数(同时多参数组合),一般来说,如果不配合事件系统向外分发该事件,可以不调用这个方法
    5. 触发注册了该事件的的动画片段(AnimationClip),第三步回调将被触发。

    实现:

    基本结构

    代码:

    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的几个触发方法封装在这个基类中,效果如下:


      Preview
      运用语法糖,可以指定参数赋值
    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. 最后,整理一个简单的帮助类,用来了解动画机的基本信息,效果如下:


      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

    链接: https://pan.baidu.com/s/1inN3kpav28rxkzjJ3usz0w 密码: gj4v

    相关文章

      网友评论

      本文标题:Unity 3D 打造自己的Mecanim Callback S

      本文链接:https://www.haomeiwen.com/subject/qknddftx.html