// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using System; using System.Runtime.CompilerServices; using System.Text; using UnityEngine; namespace Animancer { /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent partial struct AnimancerEvent { /// /// A system which triggers events in an /// based on a target . /// /// https://kybernetik.com.au/animancer/api/Animancer/Dispatcher public class Dispatcher : IHasDescription { /************************************************************************************************************************/ /// The target state. public readonly AnimancerState State; /// /// and /// . /// /// Should never be null. public Sequence Events { get; private set; } /// public bool HasOwnEvents { get; private set; } private float _PreviousNormalizedTime; private int _NextEventIndex = RecalculateEventIndex; private int _SequenceVersion = -1;// When version changes, next event index is invalid. private bool _WasPlayingForwards;// When direction changes, next event index is invalid. /// /// A special value for the /// which indicates that it needs to be recalculated. /// private const int RecalculateEventIndex = int.MinValue; /************************************************************************************************************************/ /// Creates a new . public Dispatcher(AnimancerState state) { State = state; _PreviousNormalizedTime = state.NormalizedTime; #if UNITY_ASSERTIONS OptionalWarning.UnsupportedEvents.Log(state.UnsupportedEventsMessage, state.Graph?.Component); #endif } /************************************************************************************************************************/ /// /// Setters for /// and . /// public void SetEvents(Sequence events, bool isOwned) { Events = events; _NextEventIndex = RecalculateEventIndex; HasOwnEvents = isOwned; } /************************************************************************************************************************/ /// Sets to false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void DismissEventOwnership() => HasOwnEvents = false; /************************************************************************************************************************/ /// . public bool InitializeEvents(out Sequence events) { if (HasOwnEvents) { events = Events; return false; } Events = events = new(Events); _NextEventIndex = RecalculateEventIndex; _SequenceVersion = Events.Version; HasOwnEvents = true; return true; } /************************************************************************************************************************/ /// [Internal] /// Notifies this dispatcher that the target's has changed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void OnSetTime() { // The Playable's time won't move in the same frame it was set, // so we'll just let the next frame grab its time. _PreviousNormalizedTime = float.NaN; } /************************************************************************************************************************/ /// public void UpdateEvents(bool raiseEvents) { State.GetEventDispatchInfo(out var length, out var normalizedTime, out var isLooping); // If we aren't raising events or don't have a previous time, just keep track of the time. if (!raiseEvents || float.IsNaN(_PreviousNormalizedTime)) { _PreviousNormalizedTime = normalizedTime; // Since we aren't paying attention to the events, // we also aren't paying attention to which index the time corresponds to. _NextEventIndex = RecalculateEventIndex; return; } // If the sequence is modified, we need to recalculate the next event index. var sequenceVersion = Events.Version; if (_SequenceVersion != sequenceVersion) { _SequenceVersion = sequenceVersion; _NextEventIndex = RecalculateEventIndex; } if (length > 0) { if (_PreviousNormalizedTime == normalizedTime) return; CheckGeneralEvents(normalizedTime, isLooping); CheckEndEvent(normalizedTime); _PreviousNormalizedTime = normalizedTime; } else// Length zero, negative, or NaN. { UpdateZeroLength(); } } /************************************************************************************************************************/ /// If the state has zero length, trigger its events every frame. private void UpdateZeroLength() { var speed = State.EffectiveSpeed; if (speed == 0) return; if (Events.Count > 0) { int playDirectionInt; if (speed < 0) { playDirectionInt = -1; if (_NextEventIndex == RecalculateEventIndex || _WasPlayingForwards) { _NextEventIndex = Events.Count - 1; _WasPlayingForwards = false; } } else { playDirectionInt = 1; if (_NextEventIndex == RecalculateEventIndex || !_WasPlayingForwards) { _NextEventIndex = 0; _WasPlayingForwards = true; } } if (!InvokeAllEvents(Events, 1, playDirectionInt)) return; } var endEvent = Events.EndEvent; if (endEvent.callback != null) endEvent.DelayInvoke(EndEventName, State); } /************************************************************************************************************************/ /// General events are triggered on the frame when their time passes. /// Looping animations trigger their events every loop. private void CheckGeneralEvents(float currentTime, bool isLooping) { var count = Events.Count; if (count == 0) { _NextEventIndex = 0; return; } ValidateNextEventIndex( isLooping, ref currentTime, out var playDirectionFloat, out var playDirectionInt); if (isLooping)// Looping. { var animancerEvent = Events[_NextEventIndex]; var eventTime = animancerEvent.normalizedTime * playDirectionFloat; var loopDelta = GetLoopDelta(_PreviousNormalizedTime, currentTime, eventTime); if (loopDelta == 0) return; // For each additional loop, invoke all events without needing to check their times. if (!InvokeAllEvents(Events, loopDelta - 1, playDirectionInt)) return; var loopStartIndex = _NextEventIndex; Invoke: animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State); if (!NextEventLooped(Events, playDirectionInt) || _NextEventIndex == loopStartIndex) return; animancerEvent = Events[_NextEventIndex]; eventTime = animancerEvent.normalizedTime * playDirectionFloat; if (loopDelta == GetLoopDelta(_PreviousNormalizedTime, currentTime, eventTime)) goto Invoke; } else// Non-Looping. { while ((uint)_NextEventIndex < (uint)count) { var animancerEvent = Events[_NextEventIndex]; var eventTime = animancerEvent.normalizedTime * playDirectionFloat; if (currentTime <= eventTime) return; animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State); _NextEventIndex += playDirectionInt; } } } /************************************************************************************************************************/ private void ValidateNextEventIndex( bool isLooping, ref float currentTime, out float playDirectionFloat, out int playDirectionInt) { if (currentTime < _PreviousNormalizedTime)// Playing Backwards. { var previousTime = _PreviousNormalizedTime; _PreviousNormalizedTime = -previousTime; currentTime = -currentTime; playDirectionFloat = -1; playDirectionInt = -1; if (_NextEventIndex == RecalculateEventIndex || _WasPlayingForwards) { _NextEventIndex = Events.Count - 1; _WasPlayingForwards = false; if (isLooping) previousTime = AnimancerUtilities.Wrap01(previousTime); while (Events[_NextEventIndex].normalizedTime > previousTime) { _NextEventIndex--; if (_NextEventIndex < 0) { if (isLooping) _NextEventIndex = Events.Count - 1; break; } } Events.AssertNormalizedTimes(State, isLooping); } } else// Playing Forwards. { playDirectionFloat = 1; playDirectionInt = 1; if (_NextEventIndex == RecalculateEventIndex || !_WasPlayingForwards) { _NextEventIndex = 0; _WasPlayingForwards = true; var previousTime = _PreviousNormalizedTime; if (isLooping) previousTime = AnimancerUtilities.Wrap01(previousTime); var max = Events.Count - 1; while (Events[_NextEventIndex].normalizedTime < previousTime) { _NextEventIndex++; if (_NextEventIndex > max) { if (isLooping) _NextEventIndex = 0; break; } } Events.AssertNormalizedTimes(State, isLooping); } } // This method could be slightly optimised for playback direction changes by using the current index // as the starting point instead of iterating from the edge of the sequence, but that would make it // significantly more complex for something that shouldn't happen very often and would only matter if // there are lots of events (in which case the optimisation would be tiny compared to the cost of // actually invoking all those events and running the rest of the application). } /************************************************************************************************************************/ /// /// Calculates the number of times an event at `eventTime` should be invoked when the /// goes from `previousTime` to `nextTime` on a looping animation. /// private static int GetLoopDelta(float previousTime, float nextTime, float eventTime) { previousTime -= eventTime; nextTime -= eventTime; var previousLoopCount = Mathf.FloorToInt(previousTime); var nextLoopCount = Mathf.FloorToInt(nextTime); var loopCount = nextLoopCount - previousLoopCount; // Previous time must be inclusive. // And next time must be exclusive. // So if the previous time is exactly on a looped increment of the event time, count one more. // And if the next time is exactly on a looped increment of the event time, count one less. if (previousTime == previousLoopCount) loopCount++; if (nextTime == nextLoopCount) loopCount--; return loopCount; } /************************************************************************************************************************/ private static int _MaximumFullLoopCount = 3; /// /// The maximum number of times a looping animation can trigger all of its events in a single frame. /// Default 3, Minimum 1. /// /// /// This limit should only ever be reached when a state has a very short length and high speed. /// public static int MaximumFullLoopCount { get => _MaximumFullLoopCount; set => _MaximumFullLoopCount = Math.Max(value, 1); } private bool InvokeAllEvents(Sequence events, int count, int playDirectionInt) { if (count > _MaximumFullLoopCount) count = _MaximumFullLoopCount; var loopStartIndex = _NextEventIndex; while (count-- > 0) { do { events[_NextEventIndex].DelayInvoke(events.GetName(_NextEventIndex), State); if (!NextEventLooped(events, playDirectionInt)) return false; } while (_NextEventIndex != loopStartIndex); } return true; } /************************************************************************************************************************/ private bool NextEventLooped(Sequence events, int playDirectionInt) { _NextEventIndex += playDirectionInt; var count = events.Count; if (_NextEventIndex >= count) _NextEventIndex = 0; else if (_NextEventIndex < 0) _NextEventIndex = count - 1; return true; } /************************************************************************************************************************/ /// End events are triggered every frame after their time passes. /// /// This ensures that assigning the event after the time has passed /// will still trigger it rather than leaving it playing indefinitely. /// private void CheckEndEvent(float normalizedTime) { var endEvent = Events.EndEvent; if (endEvent.callback == null) return; if (normalizedTime > _PreviousNormalizedTime)// Playing Forwards. { var eventTime = float.IsNaN(endEvent.normalizedTime) ? 1 : endEvent.normalizedTime; if (normalizedTime > eventTime) endEvent.DelayInvoke(EndEventName, State); } else// Playing Backwards. { var eventTime = float.IsNaN(endEvent.normalizedTime) ? 0 : endEvent.normalizedTime; if (normalizedTime < eventTime) endEvent.DelayInvoke(EndEventName, State); } } /************************************************************************************************************************/ /// Returns " (Target State)". public override string ToString() => State != null ? $"{nameof(Dispatcher)} ({State})" : $"{nameof(Dispatcher)} (No Target State)"; /************************************************************************************************************************/ /// public void AppendDescription(StringBuilder text, string separator = "\n") { text.AppendField(separator, "State", State.GetPath()); text.AppendField(separator, "IsLooping", State.IsLooping); text.AppendField(separator, "PreviousNormalizedTime", _PreviousNormalizedTime); text.AppendField(separator, "NextEventIndex", _NextEventIndex); text.AppendField(separator, "SequenceVersion", _SequenceVersion); text.AppendField(separator, "WasPlayingForwards", _WasPlayingForwards); } /************************************************************************************************************************/ } } }