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