動(dòng)畫事件
近來想在PlayMaker上Action上加一個(gè)播放動(dòng)畫并等待完成的功能桩匪,于是乎落入了井底深坑怕篷。
雖然處處設(shè)坎历筝,但是也還是有兩個(gè)可行的解決方案(注意這里可行的意思是只能在PlayMaker的Action上進(jìn)行編輯操作,而不是通過加組件這種增加操作復(fù)雜性來解決問題):
動(dòng)態(tài)添加AnimationEvent做回調(diào)(同步事件)
模擬當(dāng)前動(dòng)畫進(jìn)度做回調(diào)(非同步事件)
同步回調(diào) AnimationEvent
先來說說事件這個(gè)廊谓,實(shí)現(xiàn)相對簡單梳猪,因?yàn)槭录莻蓪nimationClip而添加,所以這里實(shí)現(xiàn)大概又分以下幾層:
給AnimationClip添加與移除事件蒸痹。
實(shí)現(xiàn)通過Animator的Play與CrossFade來取到當(dāng)前AnimationClip春弥。
通過Mono實(shí)現(xiàn)回調(diào)事件與動(dòng)畫事件的結(jié)合。
把以上方法封裝成擴(kuò)展方法(一句代碼搞定的那種)叠荠。
先看看最終的使用形態(tài):
//Play
animator.Play(stateName, layer, normalizedTime, Callback);
//CrossFade
animator.CrossFade(stateName, layer, transitionDuration, Callback);
//等待當(dāng)前動(dòng)畫播放完成
animator.AddFinishEventToCurrentState(Callback, layer);
public void Callback()
{
Debug.Log("我播完了");
}
開始實(shí)現(xiàn):
- 給AnimationClip添加與移除事件匿沛。
static private Dictionary<AnimationClip, AnimationEvent> dictClipToEndEvent = new Dictionary<AnimationClip, AnimationEvent>();
/// <summary>
/// 因?yàn)閁nity的Clip的共用性質(zhì),一旦加入了一個(gè)之后榛鼎,所有動(dòng)畫都加入了
/// 所以會(huì)使用到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);
//同名方法無需重新設(shè)置
else return;
}
AnimationEvent ev = new AnimationEvent();
ev.time = clip.length;
dictClipToEndEvent.Add(clip, ev);
//在clip.AddEvent前必須設(shè)置好functionName否則不生效
ev.functionName = functionName;
//無需報(bào)錯(cuò)
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);
}
}
- 實(shí)現(xiàn)通過Animator的Play與CrossFade來取到當(dāng)前AnimationClip。
/// <summary>
/// 注意是Clip的名字不是 StateName
/// 且此方法為運(yùn)行中支持借帘,不會(huì)影響到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使用此方法之前蜘渣,請保證參數(shù)為Animator里面的動(dòng)畫名而非狀態(tài)名");
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;
}
- 通過Mono實(shí)現(xiàn)回調(diào)事件與動(dòng)畫事件的結(jié)合。
/// <summary>
/// 此腳本在Clip的最后一幀上添加AnimationEvent肺然,以做到回調(diào)
/// </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; //實(shí)際添加了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蔫缸、把以上方法封裝成擴(kuò)展方法(一句代碼搞定的那種)。
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;
}
}
模擬回調(diào)(非同步事件)
這個(gè)整體解決思路就是每幀去判斷當(dāng)前動(dòng)畫是否播放际起。在這里就不再碼代碼了拾碌,感興趣的可以嘗試自己實(shí)現(xiàn)。
結(jié)語
代碼粗糙需慎重使用街望,希望起到拋磚引玉的效果校翔,也希望有大神可以提出其它的解決方案。