动画事件
近来想在PlayMaker上Action上加一个播放动画并等待完成的功能,于是乎落入了井底深坑。
虽然处处设坎,但是也还是有两个可行的解决方案(注意这里可行的意思是只能在PlayMaker的Action上进行编辑操作,而不是通过加组件这种增加操作复杂性来解决问题):
- 动态添加AnimationEvent做回调(同步事件)
- 模拟当前动画进度做回调(非同步事件)
同步回调 AnimationEvent
先来说说事件这个,实现相对简单,因为事件是侦对AnimationClip而添加,所以这里实现大概又分以下几层:
- 给AnimationClip添加与移除事件。
- 实现通过Animator的Play与CrossFade来取到当前AnimationClip。
- 通过Mono实现回调事件与动画事件的结合。
- 把以上方法封装成扩展方法(一句代码搞定的那种)。
先看看最终的使用形态:
1 2 3 4 5 6 7 8 9 10 11 |
//Play animator.Play(stateName, layer, normalizedTime, Callback); //CrossFade animator.CrossFade(stateName, layer, transitionDuration, Callback); //等待当前动画播放完成 animator.AddFinishEventToCurrentState(Callback, layer); public void Callback() { Debug.Log("我播完了"); } |
开始实现:
1、给AnimationClip添加与移除事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
static private Dictionary<AnimationClip, AnimationEvent> dictClipToEndEvent = new Dictionary<AnimationClip, AnimationEvent>(); /// <summary> /// 因为Unity的Clip的共用性质,一旦加入了一个之后,所有动画都加入了 /// 所以会使用到DontRequireReceiver /// </summary> /// <param name="clip"></param> /// <param name="functionName"></param> static public void AddFinishEventToAnimationClip(this AnimationClip clip, string functionName) { if (clip != null) { if (dictClipToEndEvent.ContainsKey(clip)) { //不同名的方法先移除,后添加 if (functionName != dictClipToEndEvent[clip].functionName) RemoveFinishEvent(clip); //同名方法无需重新设置 else return; } AnimationEvent ev = new AnimationEvent(); ev.time = clip.length; dictClipToEndEvent.Add(clip, ev); //在clip.AddEvent前必须设置好functionName否则不生效 ev.functionName = functionName; //无需报错 ev.messageOptions = SendMessageOptions.DontRequireReceiver; clip.AddEvent(ev); } } static public void RemoveFinishEvent(this AnimationClip clip) { if (dictClipToEndEvent.ContainsKey(clip)) { if (clip.events.Length == 1) clip.events = null; else { List<AnimationEvent> events = new List<AnimationEvent>(clip.events); var e = dictClipToEndEvent[clip]; if (events.Contains(e)) events.Remove(e); clip.events = events.ToArray(); } dictClipToEndEvent.Remove(clip); } } |
2、实现通过Animator的Play与CrossFade来取到当前AnimationClip。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <summary> /// 注意是Clip的名字不是 StateName /// 且此方法为运行中支持,不会影响到Clip的anim文件 /// </summary> /// <param name="animator"></param> /// <param name="clipName"></param> /// <returns></returns> static public AnimationClip GetRuntimeClip(this Animator animator, string clipName) { AnimationClip[] clips = animator.runtimeAnimatorController.animationClips; if (clips != null && clips.Length > 0) for (int i = 0; i < clips.Length; i++) { if (string.Equals(clips[i].name, clipName)) return clips[i]; } Debug.LogError("Don't find animation name :" + clipName + "\n使用此方法之前,请保证参数为Animator里面的动画名而非状态名"); return null; } static public AnimationClip GetRuntimeCurrentClip(this Animator animator, int layer = 0) { var clipSource = GetCurrentClip(animator, layer); if (clipSource != null) return animator.GetRuntimeClip(clipSource.name); return null; } static public AnimationClip GetCurrentClip(this Animator animator, int layer = 0) { var clipInfos = animator.GetCurrentAnimatorClipInfo(layer); if (clipInfos == null) return null; if (clipInfos.Length > 0) { var clipSource = clipInfos[0].clip; return clipSource; } return null; } |
3、通过Mono实现回调事件与动画事件的结合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
/// <summary> /// 此脚本在Clip的最后一帧上添加AnimationEvent,以做到回调 /// </summary> public class AnimatorClipEventListener : MonoBehaviour { private Action onFinishState; private Animator animator; private bool IsInit = false; private int layer = 0; private string stateName = ""; private AnimationClip clip; //实际添加了Clip事件的片段 private AnimatorStateInfo CurStateInfo => animator.GetCurrentAnimatorStateInfo(layer); // Start is called before the first frame update private void Start() { } private void OnDestroy() { if (clip) clip.RemoveFinishEvent(); } private bool IsInTransition() => animator.IsInTransition(layer); private void Init(Animator anim, Action act, int layer = 0, string stateName = "") { animator = anim; onFinishState = act; IsInit = true; this.layer = layer; this.stateName = stateName; } private void AddEvent() { clip = animator.GetRuntimeCurrentClip(layer); if (clip != null) clip.AddFinishEventToAnimationClip(nameof(_MzCallback)); else { Debug.Log("Add event error", animator); _MzCallback(); } } private bool CheckState() { if (!animator.HasState(stateName, layer)) { _MzCallback(); Debug.Log("Don't find stateName with " + stateName, animator); return false; } return true; } public void ListenCurrent(Animator animator, Action action, int layer = 0) { Init(animator, action, layer); AddEvent(); } public void Play(Animator animator, string stateName, Action action, int layer = 0) { Init(animator, action, layer, stateName); if (!CheckState()) return; animator.Play(stateName, layer); StartCoroutine(DoInit()); } public void CrossFade(Animator animator, string stateName, float fadeTime, Action cb, int layer = 0) { Init(animator, cb, layer, stateName); if (!CheckState()) return; animator.CrossFade(stateName, fadeTime, layer); StartCoroutine(DoInit()); } private IEnumerator DoInit() { while (IsInTransition() || !CurStateInfo.IsName(stateName)) yield return new WaitForEndOfFrame(); yield return new WaitForEndOfFrame(); AddEvent(); } public void _MzCallback() { onFinishState?.Invoke(); onFinishState = null; Destroy(this); } } |
4、把以上方法封装成扩展方法(一句代码搞定的那种)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public static class _AnimatorClipFinishEventListener { static private AnimatorClipEventListener GetListener(Animator animator) { AnimatorClipEventListener listener = animator.gameObject.AddComponent<AnimatorClipEventListener>(); return listener; } static public AnimatorClipEventListener Play(this Animator animator, string stateName, int layer, float norTime, Action cb) { AnimatorClipEventListener cbMono = GetListener(animator); cbMono.Play(animator, stateName, cb, layer); return cbMono; } static public AnimatorClipEventListener CrossFade(this Animator animator, string stateName, float fadeTime, Action cb, int layer = 0) { AnimatorClipEventListener cbMono = GetListener(animator); cbMono.CrossFade(animator, stateName, fadeTime, cb, layer); return cbMono; } static public AnimatorClipEventListener MzAddFinishEventToCurrentState(this Animator animator, Action cb, int layer = 0) { AnimatorClipEventListener cbMono = GetListener(animator); cbMono.ListenCurrent(animator, cb, layer); return cbMono; } static public AnimatorClipEventListener AddFinishEvent(this Animator animator, string clipName, Action cb) { AnimatorClipEventListener cbMono = GetListener(animator); cbMono.AddEvent(animator, clipName, cb); return cbMono; } } |
模拟回调
未完待续
结语
加油,骚年。