# 动画事件通知

实现通用的动画事件通知功能 AnimNotify

# 数据类

  • 动画通知时使用的数据类
namespace Common
{
    /// <summary>
    /// 一次动画通知所处的生命周期阶段
    /// </summary>
    public enum AnimNotifyPhase
    {
        /// <summary>
        /// 表示瞬时通知阶段
        /// </summary>
        Notify,
        /// <summary>
        /// 表示持续通知开始阶段
        /// </summary>
        StateBegin,
        /// <summary>
        /// 表示持续通知更新阶段
        /// </summary>
        StateTick,
        /// <summary>
        /// 表示持续通知结束阶段
        /// </summary>
        StateEnd
    }
}
namespace Common
{
    /// <summary>
    /// 决定动画通知何时交给接收器
    /// </summary>
    public enum AnimNotifyTickType
    {
        /// <summary>
        /// 动画状态完成本帧求值后在 LateUpdate 中按时间顺序发送
        /// </summary>
        Queued,
        /// <summary>
        /// 跨过通知时间时立即发送,命中窗口和投射物生成等关键玩法通知应使用此类型
        /// </summary>
        BranchingPoint
    }
}
using UnityEngine;
namespace Common
{
    /// <summary>
    /// 通知执行时临时创建的只读上下文
    /// </summary>
    public readonly struct AnimNotifyContext
    {
        /// <summary>
        /// 获取产生本次回调的无状态通知配置
        /// </summary>
        public AnimNotify Notify { get; }
        /// <summary>
        /// 获取本次回调的生命周期阶段
        /// </summary>
        public AnimNotifyPhase Phase { get; }
        /// <summary>
        /// 获取正在播放动画的 Animator
        /// </summary>
        public Animator Animator { get; }
        /// <summary>
        /// 获取回调时的动画状态信息
        /// </summary>
        public AnimatorStateInfo StateInfo { get; }
        /// <summary>
        /// 获取 Animator 层索引
        /// </summary>
        public int LayerIndex { get; }
        /// <summary>
        /// 获取回调发生时包含循环次数的归一化播放时间
        /// </summary>
        public float NormalizedTime { get; }
        /// <summary>
        /// 获取通知名称
        /// </summary>
        public string EventName => Notify?.EventName ?? string.Empty;
        /// <summary>
        /// 获取通知携带的数值强度
        /// </summary>
        public float EventMagnitude => Notify?.EventMagnitude ?? 0f;
        /// <summary>
        /// 获取本次通知是否为立即执行的 Branching Point
        /// </summary>
        public bool IsBranchingPoint => Notify?.TickType == AnimNotifyTickType.BranchingPoint;
        /// <summary>
        /// 初始化动画通知上下文
        /// </summary>
        internal AnimNotifyContext(
            AnimNotify notify,
            AnimNotifyPhase phase,
            Animator animator,
            AnimatorStateInfo stateInfo,
            int layerIndex,
            float normalizedTime)
        {
            Notify = notify;
            Phase = phase;
            Animator = animator;
            StateInfo = stateInfo;
            LayerIndex = layerIndex;
            NormalizedTime = normalizedTime;
        }
    }
}
using System;
namespace Common
{
    /// <summary>
    /// 标识某个 Behaviour 在指定 Animator 层上的播放状态
    /// </summary>
    internal readonly struct PlaybackKey : IEquatable<PlaybackKey>
    {
        /// <summary>
        /// 获取通知时间轴 Behaviour 的实例标识
        /// </summary>
        public int SourceId { get; }
        /// <summary>
        /// 获取 Animator 层索引
        /// </summary>
        public int LayerIndex { get; }
        /// <summary>
        /// 初始化播放状态键
        /// </summary>
        public PlaybackKey(int sourceId, int layerIndex)
        {
            SourceId = sourceId;
            LayerIndex = layerIndex;
        }
        /// <summary>
        /// 判断当前播放状态键是否与另一个键相等
        /// </summary>
        public bool Equals(PlaybackKey other)
        {
            return SourceId == other.SourceId && LayerIndex == other.LayerIndex;
        }
        /// <summary>
        /// 判断当前播放状态键是否与指定对象相等
        /// </summary>
        public override bool Equals(object obj)
        {
            return obj is PlaybackKey other && Equals(other);
        }
        /// <summary>
        /// 获取当前播放状态键的哈希码
        /// </summary>
        public override int GetHashCode()
        {
            return HashCode.Combine(SourceId, LayerIndex);
        }
    }
}
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
    /// <summary>
    /// 保存单个通知时间轴的运行时播放状态
    /// </summary>
    internal sealed class PlaybackState
    {
        /// <summary>
        /// 获取提供通知配置的 Behaviour
        /// </summary>
        public AnimNotifyStateMachineBehaviour Source { get; }
        /// <summary>
        /// 获取当前活动的持续通知集合
        /// </summary>
        public Dictionary<StateOccurrence, AnimNotifyState> ActiveStates { get; } = new();
        /// <summary>
        /// 获取或设置最近一次求值时的 Animator 状态信息
        /// </summary>
        public AnimatorStateInfo StateInfo { get; set; }
        /// <summary>
        /// 获取或设置上一次求值时的归一化播放时间
        /// </summary>
        public float PreviousNormalizedTime { get; set; }
        /// <summary>
        /// 获取或设置当前播放状态是否仍然有效
        /// </summary>
        public bool IsActive { get; set; } = true;
        /// <summary>
        /// 初始化通知时间轴的运行时播放状态
        /// </summary>
        public PlaybackState(
            AnimNotifyStateMachineBehaviour source,
            AnimatorStateInfo stateInfo,
            float previousNormalizedTime)
        {
            Source = source;
            StateInfo = stateInfo;
            PreviousNormalizedTime = previousNormalizedTime;
        }
    }
}
using System;
namespace Common
{
    /// <summary>
    /// 标识某个持续通知在指定动画循环中的一次活动实例
    /// </summary>
    internal readonly struct StateOccurrence : IEquatable<StateOccurrence>
    {
        /// <summary>
        /// 获取持续通知在配置列表中的索引
        /// </summary>
        public int NotifyIndex { get; }
        /// <summary>
        /// 获取持续通知所属的动画循环索引
        /// </summary>
        public int LoopIndex { get; }
        /// <summary>
        /// 初始化持续通知活动实例标识
        /// </summary>
        public StateOccurrence(int notifyIndex, int loopIndex)
        {
            NotifyIndex = notifyIndex;
            LoopIndex = loopIndex;
        }
        /// <summary>
        /// 判断当前活动实例标识是否与另一个标识相等
        /// </summary>
        public bool Equals(StateOccurrence other)
        {
            return NotifyIndex == other.NotifyIndex && LoopIndex == other.LoopIndex;
        }
        /// <summary>
        /// 判断当前活动实例标识是否与指定对象相等
        /// </summary>
        public override bool Equals(object obj)
        {
            return obj is StateOccurrence other && Equals(other);
        }
        /// <summary>
        /// 获取当前活动实例标识的哈希码
        /// </summary>
        public override int GetHashCode()
        {
            return HashCode.Combine(NotifyIndex, LoopIndex);
        }
    }
}
namespace Common
{
    /// <summary>
    /// 描述播放区间内等待发送的一次通知事件
    /// </summary>
    internal readonly struct ScheduledEvent
    {
        /// <summary>
        /// 获取通知在完整播放时间轴上的触发时间
        /// </summary>
        public float Time { get; }
        /// <summary>
        /// 获取通知生命周期阶段
        /// </summary>
        public AnimNotifyPhase Phase { get; }
        /// <summary>
        /// 获取通知配置
        /// </summary>
        public AnimNotify Notify { get; }
        /// <summary>
        /// 获取通知在配置列表中的索引
        /// </summary>
        public int NotifyIndex { get; }
        /// <summary>
        /// 获取通知所属的动画循环索引
        /// </summary>
        public int LoopIndex { get; }
        /// <summary>
        /// 初始化等待发送的通知事件
        /// </summary>
        public ScheduledEvent(
            float time,
            AnimNotifyPhase phase,
            AnimNotify notify,
            int notifyIndex,
            int loopIndex)
        {
            Time = time;
            Phase = phase;
            Notify = notify;
            NotifyIndex = notifyIndex;
            LoopIndex = loopIndex;
        }
        /// <summary>
        /// 按触发时间和生命周期阶段比较两个通知事件
        /// </summary>
        public static int Compare(ScheduledEvent left, ScheduledEvent right)
        {
            int timeComparison = left.Time.CompareTo(right.Time);
            if (timeComparison != 0)
            {
                return timeComparison;
            }
            int phaseComparison = GetPhaseOrder(left.Phase).CompareTo(GetPhaseOrder(right.Phase));
            return phaseComparison != 0 ? phaseComparison : left.NotifyIndex.CompareTo(right.NotifyIndex);
        }
        /// <summary>
        /// 获取通知生命周期阶段的排序优先级
        /// </summary>
        private static int GetPhaseOrder(AnimNotifyPhase phase)
        {
            return phase switch
            {
                AnimNotifyPhase.StateEnd => 0,
                AnimNotifyPhase.StateBegin => 1,
                AnimNotifyPhase.Notify => 2,
                _ => 3
            };
        }
    }
}

# 通知状态

  • AnimNotifyState 负责自己 Begin/End 对称的动画局部状态,而修改的状态必须由调用者的生命周期负责清理
using System;
using UnityEngine;
namespace Common
{
    /// <summary>
    /// 动画状态时间轴上的瞬时通知
    /// 只保存编辑期数据,播放游标等运行时状态由 AnimNotifyDispatcher 保存
    /// </summary>
    [Serializable]
    public class AnimNotify
    {
        [Tooltip("通知名称,接入 GAS 时应填写已注册的 GameplayTag 完整名称")]
        [SerializeField]
        private string eventName;
        [Tooltip("通知在动画状态内的归一化时间,0 表示开始,1 表示结束")]
        [Range(0f, 1f)]
        [SerializeField]
        private float normalizedTime;
        [Tooltip("随通知传递的数值强度")]
        [SerializeField]
        private float eventMagnitude = 1f;
        [Tooltip("关键玩法通知应使用 Branching Point,以便跨过触发点时立即执行")]
        [SerializeField]
        private AnimNotifyTickType tickType;
        /// <summary>
        /// 获取通知名称
        /// </summary>
        public string EventName => eventName ?? string.Empty;
        /// <summary>
        /// 获取动画状态内的归一化触发时间
        /// </summary>
        public float NormalizedTime => Mathf.Clamp01(normalizedTime);
        /// <summary>
        /// 获取通知携带的数值强度
        /// </summary>
        public float EventMagnitude => eventMagnitude;
        /// <summary>
        /// 获取通知的执行方式
        /// </summary>
        public AnimNotifyTickType TickType => tickType;
    }
}
using System;
using UnityEngine;
namespace Common
{
    /// <summary>
    /// 动画状态时间轴上的持续通知,依次产生 Begin、Tick 和 End 阶段
    /// </summary>
    [Serializable]
    public sealed class AnimNotifyState : AnimNotify
    {
        [Tooltip("通知窗口的归一化持续时间,窗口不会超过当前动画循环")]
        [Range(0f, 1f)]
        [SerializeField]
        private float duration = 0.1f;
        /// <summary>
        /// 获取归一化持续时间
        /// </summary>
        public float Duration => Mathf.Clamp(duration, 0f, 1f - NormalizedTime);
        /// <summary>
        /// 获取窗口结束的归一化时间
        /// </summary>
        public float EndNormalizedTime => NormalizedTime + Duration;
    }
}

# 动画状态机

  • 在状态机上配置动画通知参数
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
    /// <summary>
    /// 配置在 Animator State 上的通知时间轴
    /// 只保存通知数据,全部运行时播放状态都位于 AnimNotifyDispatcher
    /// </summary>
    public sealed class AnimNotifyStateMachineBehaviour : StateMachineBehaviour
    {
        [Tooltip("瞬时动画通知")]
        [SerializeField]
        private List<AnimNotify> notifies = new();
        [Tooltip("具有 Begin、Tick、End 生命周期的持续动画通知")]
        [SerializeField]
        private List<AnimNotifyState> notifyStates = new();
        /// <summary>
        /// 获取瞬时通知的只读列表
        /// </summary>
        public IReadOnlyList<AnimNotify> Notifies => notifies;
        /// <summary>
        /// 获取持续通知的只读列表
        /// </summary>
        public IReadOnlyList<AnimNotifyState> NotifyStates => notifyStates;
        /// <summary>
        /// 在进入状态时调用
        /// </summary>
        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            AnimNotifyDispatcher.GetOrAdd(animator).EnterState(this, stateInfo, layerIndex);
        }
        /// <summary>
        /// 在状态更新时调用
        /// </summary>
        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            AnimNotifyDispatcher.GetOrAdd(animator).EvaluateState(this, stateInfo, layerIndex);
        }
        /// <summary>
        /// 在退出状态时调用
        /// </summary>
        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (animator != null && animator.TryGetComponent(out AnimNotifyDispatcher dispatcher))
            {
                dispatcher.ExitState(this, stateInfo, layerIndex);
            }
        }
    }
}

# 动画通知调度

  • 统一管理和分发通知
  • 计算动画的播放时间,并在指定时间触发事件
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
    /// <summary>
    /// 对 Animator State 的时间轴进行求值并分发通知
    /// 播放游标、活动 NotifyState 和队列都由 Animator 实例独占
    /// </summary>
    [DisallowMultipleComponent]
    [RequireComponent(typeof(Animator))]
    public sealed class AnimNotifyDispatcher : MonoBehaviour
    {
        /// <summary>
        /// 用于动画时间比较的浮点容差
        /// </summary>
        private const float TimeTolerance = 0.00001f;
        /// <summary>
        /// 单次求值允许处理的最大动画循环数量
        /// </summary>
        private const int MaxLoopsPerEvaluation = 64;
        /// <summary>
        /// 保存各动画状态时间轴的运行时播放数据
        /// </summary>
        private readonly Dictionary<PlaybackKey, PlaybackState> playbackStates = new();
        /// <summary>
        /// 保存等待在 LateUpdate 阶段发送的非关键通知
        /// </summary>
        private readonly List<AnimNotifyContext> queuedNotifications = new();
        /// <summary>
        /// 保存当前求值区间内跨过的通知事件
        /// </summary>
        private readonly List<ScheduledEvent> scheduledEvents = new();
        /// <summary>
        /// 保存当前组件所属的 Animator
        /// </summary>
        private Animator animator;
        /// <summary>
        /// 当瞬时通知或 NotifyState 生命周期阶段被触发时调用
        /// </summary>
        public event Action<AnimNotifyContext> Notification;
        /// <summary>
        /// 缓存当前组件所属的 Animator
        /// </summary>
        private void Awake()
        {
            animator = GetComponent<Animator>();
        }
        /// <summary>
        /// 在本帧动画状态完成求值后发送排队通知
        /// </summary>
        private void LateUpdate()
        {
            FlushQueuedNotifications();
        }
        /// <summary>
        /// 组件禁用时发送剩余通知并清理全部运行时状态
        /// </summary>
        private void OnDisable()
        {
            FlushQueuedNotifications();
            EndAllActiveStatesImmediately();
            playbackStates.Clear();
            queuedNotifications.Clear();
            scheduledEvents.Clear();
        }
        /// <summary>
        /// 获取 Animator 上的分发器,不存在时自动添加
        /// </summary>
        public static AnimNotifyDispatcher GetOrAdd(Animator targetAnimator)
        {
            if (targetAnimator == null)
            {
                throw new ArgumentNullException(nameof(targetAnimator));
            }
            if (!targetAnimator.TryGetComponent(out AnimNotifyDispatcher dispatcher))
            {
                dispatcher = targetAnimator.gameObject.AddComponent<AnimNotifyDispatcher>();
            }
            return dispatcher;
        }
        /// <summary>
        /// 立即发送当前排队的非关键通知
        /// </summary>
        public void FlushQueuedNotifications()
        {
            if (queuedNotifications.Count == 0)
            {
                return;
            }
            AnimNotifyContext[] snapshot = queuedNotifications.ToArray();
            queuedNotifications.Clear();
            foreach (AnimNotifyContext context in snapshot)
            {
                Notification?.Invoke(context);
            }
        }
        /// <summary>
        /// 初始化指定 Animator State 的通知播放状态
        /// </summary>
        internal void EnterState(AnimNotifyStateMachineBehaviour source, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (source == null)
            {
                return;
            }
            PlaybackKey key = new(source.GetInstanceID(), layerIndex);
            if (playbackStates.TryGetValue(key, out PlaybackState previous))
            {
                previous.IsActive = false;
                EndActiveStates(previous, stateInfo, layerIndex, true);
            }
            float currentTime = GetEvaluationTime(stateInfo);
            PlaybackState playback = new(source, stateInfo, currentTime);
            playbackStates[key] = playback;
            InitializeAtCurrentTime(playback, stateInfo, layerIndex, currentTime);
        }
        /// <summary>
        /// 根据当前播放时间求值指定 Animator State 的通知时间轴
        /// </summary>
        internal void EvaluateState(AnimNotifyStateMachineBehaviour source, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (source == null)
            {
                return;
            }
            PlaybackKey key = new(source.GetInstanceID(), layerIndex);
            if (!playbackStates.TryGetValue(key, out PlaybackState playback))
            {
                EnterState(source, stateInfo, layerIndex);
                return;
            }
            // 如果当前播放时间小于上一次求值时间,说明动画回退或跳转,直接结束所有活动持续通知并重新初始化
            float currentTime = GetEvaluationTime(stateInfo);
            if (currentTime + TimeTolerance < playback.PreviousNormalizedTime)
            {
                EndActiveStates(playback, stateInfo, layerIndex, true);
                if (!playback.IsActive)
                {
                    return;
                }
                playback.ActiveStates.Clear();
                playback.PreviousNormalizedTime = currentTime;
                playback.StateInfo = stateInfo;
                InitializeAtCurrentTime(playback, stateInfo, layerIndex, currentTime);
                return;
            }
            ScheduleCrossedEvents(playback, playback.PreviousNormalizedTime, currentTime, stateInfo.loop);
            DispatchScheduledEvents(playback, stateInfo, layerIndex, currentTime);
            if (!playback.IsActive)
            {
                return;
            }
            TickActiveStates(playback, stateInfo, layerIndex, currentTime);
            if (!playback.IsActive)
            {
                return;
            }
            playback.PreviousNormalizedTime = currentTime;
            playback.StateInfo = stateInfo;
        }
        /// <summary>
        /// 结束指定 Animator State 的通知播放状态
        /// </summary>
        internal void ExitState(AnimNotifyStateMachineBehaviour source, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (source == null)
            {
                return;
            }
            PlaybackKey key = new(source.GetInstanceID(), layerIndex);
            if (!playbackStates.TryGetValue(key, out PlaybackState playback))
            {
                return;
            }
            playback.IsActive = false;
            EndActiveStates(playback, stateInfo, layerIndex, false);
            playbackStates.Remove(key);
        }
        /// <summary>
        /// 获取用于通知求值的安全归一化播放时间
        /// </summary>
        private static float GetEvaluationTime(AnimatorStateInfo stateInfo)
        {
            float time = stateInfo.normalizedTime;
            if (float.IsNaN(time) || float.IsInfinity(time))
            {
                return 0f;
            }
            return stateInfo.loop ? Mathf.Max(0f, time) : Mathf.Clamp01(time);
        }
        /// <summary>
        /// 根据状态进入时的播放位置初始化瞬时通知和活动持续通知
        /// </summary>
        private void InitializeAtCurrentTime(PlaybackState playback, AnimatorStateInfo stateInfo, int layerIndex, float currentTime)
        {
            int loopIndex = Mathf.FloorToInt(currentTime);
            float localTime = currentTime - loopIndex;
            if (!stateInfo.loop && Mathf.Approximately(currentTime, 1f))
            {
                loopIndex = 0;
                localTime = 1f;
            }
            // 初始化瞬时通知
            IReadOnlyList<AnimNotify> notifies = playback.Source.Notifies;
            for (int index = 0; index < notifies.Count; index++)
            {
                AnimNotify notify = notifies[index];
                if (notify != null && Mathf.Abs(notify.NormalizedTime - localTime) <= TimeTolerance)
                {
                    Emit(new AnimNotifyContext(notify, AnimNotifyPhase.Notify, animator, stateInfo, layerIndex, currentTime));
                    if (!playback.IsActive)
                    {
                        return;
                    }
                }
            }
            // 初始化活动持续通知
            IReadOnlyList<AnimNotifyState> notifyStates = playback.Source.NotifyStates;
            for (int index = 0; index < notifyStates.Count; index++)
            {
                AnimNotifyState notifyState = notifyStates[index];
                if (notifyState == null || notifyState.Duration <= TimeTolerance)
                {
                    continue;
                }
                if (localTime + TimeTolerance < notifyState.NormalizedTime
                    || localTime >= notifyState.EndNormalizedTime - TimeTolerance)
                {
                    continue;
                }
                StateOccurrence occurrence = new(index, loopIndex);
                playback.ActiveStates.Add(occurrence, notifyState);
                Emit(new AnimNotifyContext(notifyState, AnimNotifyPhase.StateBegin, animator, stateInfo, layerIndex, currentTime));
                if (!playback.IsActive)
                {
                    return;
                }
            }
        }
        /// <summary>
        /// 收集指定播放区间内跨过的全部通知事件
        /// </summary>
        private void ScheduleCrossedEvents(PlaybackState playback, float previousTime, float currentTime, bool isLooping)
        {
            scheduledEvents.Clear();
            if (currentTime <= previousTime + TimeTolerance)
            {
                return;
            }
            int firstLoop = Mathf.Max(0, Mathf.FloorToInt(previousTime));
            int lastLoop = Mathf.Max(firstLoop, Mathf.FloorToInt(currentTime));
            if (!isLooping)
            {
                firstLoop = 0;
                lastLoop = 0;
            }
            if (lastLoop - firstLoop + 1 > MaxLoopsPerEvaluation)
            {
                firstLoop = lastLoop - MaxLoopsPerEvaluation + 1;
            }
            // 收集跨过的瞬时通知和持续通知的开始 / 结束事件
            for (int loopIndex = firstLoop; loopIndex <= lastLoop; loopIndex++)
            {
                IReadOnlyList<AnimNotify> notifies = playback.Source.Notifies;
                for (int index = 0; index < notifies.Count; index++)
                {
                    AnimNotify notify = notifies[index];
                    if (notify == null)
                    {
                        continue;
                    }
                    float eventTime = loopIndex + notify.NormalizedTime;
                    if (WasCrossed(eventTime, previousTime, currentTime))
                    {
                        scheduledEvents.Add(
                            new ScheduledEvent(eventTime, AnimNotifyPhase.Notify, notify, index, loopIndex));
                    }
                }
                // 收集跨过的持续通知的开始 / 结束事件
                IReadOnlyList<AnimNotifyState> notifyStates = playback.Source.NotifyStates;
                for (int index = 0; index < notifyStates.Count; index++)
                {
                    AnimNotifyState notifyState = notifyStates[index];
                    if (notifyState == null || notifyState.Duration <= TimeTolerance)
                    {
                        continue;
                    }
                    float beginTime = loopIndex + notifyState.NormalizedTime;
                    if (WasCrossed(beginTime, previousTime, currentTime))
                    {
                        scheduledEvents.Add(
                            new ScheduledEvent(beginTime, AnimNotifyPhase.StateBegin, notifyState, index, loopIndex));
                    }
                    float endTime = loopIndex + notifyState.EndNormalizedTime;
                    if (WasCrossed(endTime, previousTime, currentTime))
                    {
                        scheduledEvents.Add(
                            new ScheduledEvent(endTime, AnimNotifyPhase.StateEnd, notifyState, index, loopIndex));
                    }
                }
            }
            scheduledEvents.Sort(ScheduledEvent.Compare);
        }
        /// <summary>
        /// 判断指定通知时间是否位于本次播放求值区间内
        /// </summary>
        private static bool WasCrossed(float eventTime, float previousTime, float currentTime)
        {
            return eventTime > previousTime + TimeTolerance
                && eventTime <= currentTime + TimeTolerance;
        }
        /// <summary>
        /// 按时间顺序发送当前收集的通知事件
        /// </summary>
        private void DispatchScheduledEvents(PlaybackState playback, AnimatorStateInfo stateInfo, int layerIndex, float currentTime)
        {
            ScheduledEvent[] snapshot = scheduledEvents.ToArray();
            foreach (ScheduledEvent scheduledEvent in snapshot)
            {
                if (scheduledEvent.Phase == AnimNotifyPhase.StateBegin)
                {
                    StateOccurrence occurrence = new(scheduledEvent.NotifyIndex, scheduledEvent.LoopIndex);
                    if (playback.ActiveStates.ContainsKey(occurrence))
                    {
                        continue;
                    }
                    AnimNotifyState notifyState = (AnimNotifyState)scheduledEvent.Notify;
                    playback.ActiveStates.Add(occurrence, notifyState);
                }
                else if (scheduledEvent.Phase == AnimNotifyPhase.StateEnd)
                {
                    StateOccurrence occurrence = new(scheduledEvent.NotifyIndex, scheduledEvent.LoopIndex);
                    if (!playback.ActiveStates.Remove(occurrence))
                    {
                        continue;
                    }
                }
                Emit(new AnimNotifyContext(scheduledEvent.Notify, scheduledEvent.Phase, animator, stateInfo, layerIndex, currentTime));
                if (!playback.IsActive)
                {
                    return;
                }
            }
        }
        /// <summary>
        /// 更新当前仍处于活动窗口内的持续通知
        /// </summary>
        private void TickActiveStates(PlaybackState playback, AnimatorStateInfo stateInfo, int layerIndex, float currentTime)
        {
            if (playback.ActiveStates.Count == 0)
            {
                return;
            }
            AnimNotifyState[] snapshot = new AnimNotifyState[playback.ActiveStates.Count];
            playback.ActiveStates.Values.CopyTo(snapshot, 0);
            foreach (AnimNotifyState notifyState in snapshot)
            {
                Emit(new AnimNotifyContext(notifyState, AnimNotifyPhase.StateTick, animator, stateInfo, layerIndex, currentTime));
                if (!playback.IsActive)
                {
                    return;
                }
            }
        }
        /// <summary>
        /// 结束指定播放状态中的全部活动持续通知
        /// </summary>
        private void EndActiveStates(PlaybackState playback, AnimatorStateInfo stateInfo, int layerIndex, bool forceImmediate)
        {
            if (playback.ActiveStates.Count == 0)
            {
                return;
            }
            AnimNotifyState[] snapshot = new AnimNotifyState[playback.ActiveStates.Count];
            playback.ActiveStates.Values.CopyTo(snapshot, 0);
            playback.ActiveStates.Clear();
            float currentTime = GetEvaluationTime(stateInfo);
            foreach (AnimNotifyState notifyState in snapshot)
            {
                AnimNotifyContext context =
                    new(notifyState, AnimNotifyPhase.StateEnd, animator, stateInfo, layerIndex, currentTime);
                if (forceImmediate)
                {
                    DispatchImmediately(context);
                }
                else
                {
                    Emit(context);
                }
            }
        }
        /// <summary>
        /// 立即结束分发器中的全部活动持续通知
        /// </summary>
        private void EndAllActiveStatesImmediately()
        {
            List<KeyValuePair<PlaybackKey, PlaybackState>> snapshot = new(playbackStates);
            foreach (KeyValuePair<PlaybackKey, PlaybackState> pair in snapshot)
            {
                PlaybackState playback = pair.Value;
                playback.IsActive = false;
                EndActiveStates(playback, playback.StateInfo, pair.Key.LayerIndex, true);
            }
        }
        /// <summary>
        /// 根据通知执行方式立即发送或加入待发送队列
        /// </summary>
        private void Emit(AnimNotifyContext context)
        {
            if (context.Notify == null || string.IsNullOrWhiteSpace(context.EventName))
            {
                return;
            }
            if (context.IsBranchingPoint)
            {
                DispatchImmediately(context);
                return;
            }
            queuedNotifications.Add(context);
        }
        /// <summary>
        /// 立即发送有效的通知上下文
        /// </summary>
        private void DispatchImmediately(AnimNotifyContext context)
        {
            if (context.Notify != null && !string.IsNullOrWhiteSpace(context.EventName))
            {
                Notification?.Invoke(context);
            }
        }
    }
}

# 使用示例

img
img

using Common;
using UnityEngine;
namespace Default
{
    /// <summary>
    /// 演示如何接收动画通知
    /// </summary>
    public class TestAnim : MonoBehaviour
    {
        [Tooltip("用于播放动画并产生通知的 Animator,未指定时使用当前对象上的 Animator")]
        [SerializeField]
        private Animator animator;
        [Tooltip("是否输出持续通知每帧产生的 StateTick 日志")]
        [SerializeField]
        private bool logStateTick;
        /// <summary>
        /// 保存当前 Animator 上的动画通知分发器
        /// </summary>
        private AnimNotifyDispatcher dispatcher;
        /// <summary>
        /// 表示示例持续通知窗口当前是否处于活动状态
        /// </summary>
        private bool notifyStateActive;
        /// <summary>
        /// 初始化动画通知分发器
        /// </summary>
        private void Awake()
        {
            animator = GetComponent<Animator>();
            InitializeDispatcher();
        }
        /// <summary>
        /// 组件启用时订阅动画通知
        /// </summary>
        private void OnEnable()
        {
            InitializeDispatcher();
            if (dispatcher != null)
            {
                dispatcher.Notification -= OnAnimationNotification;
                dispatcher.Notification += OnAnimationNotification;
            }
        }
        /// <summary>
        /// 组件禁用时取消动画通知订阅
        /// </summary>
        private void OnDisable()
        {
            if (dispatcher != null)
            {
                dispatcher.Notification -= OnAnimationNotification;
            }
            notifyStateActive = false;
        }
        /// <summary>
        /// 获取 Animator 并确保其拥有动画通知分发器
        /// </summary>
        private void InitializeDispatcher()
        {
            if (animator == null)
            {
                animator = GetComponent<Animator>();
            }
            if (animator != null)
            {
                dispatcher = AnimNotifyDispatcher.GetOrAdd(animator);
            }
        }
        /// <summary>
        /// 根据通知名称和生命周期阶段处理动画通知
        /// </summary>
        private void OnAnimationNotification(AnimNotifyContext context)
        {
            if (context.Phase == AnimNotifyPhase.Notify)
            {
                HandleInstantNotify(context);
                return;
            }
            switch (context.Phase)
            {
                case AnimNotifyPhase.StateBegin:
                    BeginNotifyState(context);
                    break;
                case AnimNotifyPhase.StateTick:
                    TickNotifyState(context);
                    break;
                case AnimNotifyPhase.StateEnd:
                    EndNotifyState(context);
                    break;
            }
        }
        /// <summary>
        /// 处理示例瞬时通知并读取 EventMagnitude 和 Branching Point 信息
        /// </summary>
        private void HandleInstantNotify(AnimNotifyContext context)
        {
            Debug.Log($"收到瞬时动画通知 {context.EventName}," +
                $"Magnitude={context.EventMagnitude}," +
                $"BranchingPoint={context.IsBranchingPoint}",
                this);
            if (animator.parameters.Find(p => { return p.name == context.EventName; }) != null)
            {
                animator.SetBool(context.EventName, false);
            }
        }
        /// <summary>
        /// 处理示例持续通知的开始阶段
        /// </summary>
        private void BeginNotifyState(AnimNotifyContext context)
        {
            notifyStateActive = true;
            Debug.Log($"持续动画通知开始 {context.EventName}," +
                $"Magnitude={context.EventMagnitude}",
                this);
        }
        /// <summary>
        /// 处理示例持续通知的更新阶段
        /// </summary>
        private void TickNotifyState(AnimNotifyContext context)
        {
            if (!notifyStateActive || !logStateTick)
            {
                return;
            }
            Debug.Log($"持续动画通知更新 {context.EventName}," +
                $"NormalizedTime={context.NormalizedTime}",
                this);
        }
        /// <summary>
        /// 处理示例持续通知的结束阶段
        /// </summary>
        private void EndNotifyState(AnimNotifyContext context)
        {
            notifyStateActive = false;
            Debug.Log($"持续动画通知结束 {context.EventName}", this);
        }
    }
}