# 动画事件通知
实现通用的动画事件通知功能 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); | |
} | |
} | |
} | |
} |
# 使用示例


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); | |
} | |
} | |
} |