| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 | // 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    {        /// <summary>        /// A system which triggers events in an <see cref="Sequence"/>        /// based on a target <see cref="State"/>.        /// </summary>        /// https://kybernetik.com.au/animancer/api/Animancer/Dispatcher        public class Dispatcher : IHasDescription        {            /************************************************************************************************************************/            /// <summary>The target state.</summary>            public readonly AnimancerState State;            /// <summary>            /// <see cref="AnimancerState.OwnedEvents"/> and            /// <see cref="AnimancerState.SharedEvents"/>.            /// </summary>            /// <remarks>Should never be null.</remarks>            public Sequence Events { get; private set; }            /// <summary><see cref="AnimancerState.HasOwnedEvents"/></summary>            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.            /// <summary>            /// A special value for the <see cref="_NextEventIndex"/>            /// which indicates that it needs to be recalculated.            /// </summary>            private const int RecalculateEventIndex = int.MinValue;            /************************************************************************************************************************/            /// <summary>Creates a new <see cref="Dispatcher"/>.</summary>            public Dispatcher(AnimancerState state)            {                State = state;                _PreviousNormalizedTime = state.NormalizedTime;#if UNITY_ASSERTIONS                OptionalWarning.UnsupportedEvents.Log(state.UnsupportedEventsMessage, state.Graph?.Component);#endif            }            /************************************************************************************************************************/            /// <summary>            /// Setters for <see cref="AnimancerState.OwnedEvents"/>            /// and <see cref="AnimancerState.SharedEvents"/>.            /// </summary>            public void SetEvents(Sequence events, bool isOwned)            {                Events = events;                _NextEventIndex = RecalculateEventIndex;                HasOwnEvents = isOwned;            }            /************************************************************************************************************************/            /// <summary>Sets <see cref="HasOwnEvents"/> to <c>false</c>.</summary>            [MethodImpl(MethodImplOptions.AggressiveInlining)]            public void DismissEventOwnership()                => HasOwnEvents = false;            /************************************************************************************************************************/            /// <summary><see cref="AnimancerState.Events(object, out Sequence)"/>.</summary>            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;            }            /************************************************************************************************************************/            /// <summary>[Internal]            /// Notifies this dispatcher that the target's <see cref="Time"/> has changed.            /// </summary>            [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;            }            /************************************************************************************************************************/            /// <inheritdoc/>            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();                }            }            /************************************************************************************************************************/            /// <summary>If the state has zero length, trigger its events every frame.</summary>            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);            }            /************************************************************************************************************************/            /// <summary>General events are triggered on the frame when their time passes.</summary>            /// <remarks>Looping animations trigger their events every loop.</remarks>            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).            }            /************************************************************************************************************************/            /// <summary>            /// Calculates the number of times an event at `eventTime` should be invoked when the            /// <see cref="AnimancerState.NormalizedTime"/> goes from `previousTime` to `nextTime` on a looping animation.            /// </summary>            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;            /// <summary>            /// The maximum number of times a looping animation can trigger all of its events in a single frame.            /// Default 3, Minimum 1.            /// </summary>            /// <remarks>            /// This limit should only ever be reached when a state has a very short length and high speed.            /// </remarks>            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;            }            /************************************************************************************************************************/            /// <summary>End events are triggered every frame after their time passes.</summary>            /// <remarks>            /// This ensures that assigning the event after the time has passed            /// will still trigger it rather than leaving it playing indefinitely.            /// </remarks>            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);                }            }            /************************************************************************************************************************/            /// <summary>Returns "<see cref="Dispatcher"/> (Target State)".</summary>            public override string ToString()                    => State != null                    ? $"{nameof(Dispatcher)} ({State})"                    : $"{nameof(Dispatcher)} (No Target State)";            /************************************************************************************************************************/            /// <inheritdoc/>            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);            }            /************************************************************************************************************************/        }    }}
 |