745 lines
33 KiB
C#
745 lines
33 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
|
|
|
|
using System;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Animancer
|
|
{
|
|
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
|
|
partial class AnimancerState
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The <see cref="IUpdatable"/> that manages the events of this state.</summary>
|
|
/// <remarks>
|
|
/// This field is null by default, acquires its reference from an <see cref="ObjectPool"/> when accessed, and
|
|
/// if it contains no events at the end of an update it releases the reference back to the pool.
|
|
/// </remarks>
|
|
private EventDispatcher _EventDispatcher;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// A list of <see cref="AnimancerEvent"/>s that will occur while this state plays as well as one that
|
|
/// specifically defines when this state ends.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Accessing this property will acquire a spare <see cref="AnimancerEvent.Sequence"/> from the
|
|
/// <see cref="ObjectPool"/> if none was already assigned. You can use <see cref="HasEvents"/> to check
|
|
/// beforehand.
|
|
/// <para></para>
|
|
/// These events will automatically be cleared by <see cref="Play"/>, <see cref="Stop"/>, and
|
|
/// <see cref="OnStartFade"/> (unless <see cref="AutomaticallyClearEvents"/> is disabled).
|
|
/// <para></para>
|
|
/// <em>Animancer Lite does not allow the use of events in runtime builds, except for
|
|
/// <see cref="AnimancerEvent.Sequence.OnEnd"/>.</em>
|
|
/// <para></para>
|
|
/// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">Animancer Events</see>
|
|
/// </remarks>
|
|
public AnimancerEvent.Sequence Events
|
|
{
|
|
get
|
|
{
|
|
EventDispatcher.Acquire(this);
|
|
return _EventDispatcher.Events;
|
|
}
|
|
set
|
|
{
|
|
if (value != null)
|
|
{
|
|
EventDispatcher.Acquire(this);
|
|
_EventDispatcher.Events = value;
|
|
}
|
|
else if (_EventDispatcher != null)
|
|
{
|
|
_EventDispatcher.Events = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Does this state have an <see cref="AnimancerEvent.Sequence"/>?</summary>
|
|
/// <remarks>Accessing <see cref="Events"/> would automatically get one from the <see cref="ObjectPool"/>.</remarks>
|
|
public bool HasEvents => _EventDispatcher != null && _EventDispatcher.HasEvents;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Should the <see cref="Events"/> be cleared automatically whenever <see cref="Play"/>, <see cref="Stop"/>,
|
|
/// or <see cref="OnStartFade"/> are called? Default true.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Disabling this property is not usually recommended since it would allow events to continue being triggered
|
|
/// while a state is fading out. For example, if a <em>Flinch</em> animation interrupts an <em>Attack</em>, you
|
|
/// probably don't want the <em>Attack</em>'s <em>Hit</em> event to still get triggered while it's fading out.
|
|
/// <para></para>
|
|
/// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer#clear-automatically">
|
|
/// Clear Automatically</see>
|
|
/// </remarks>
|
|
public static bool AutomaticallyClearEvents { get; set; } = true;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
#if UNITY_ASSERTIONS
|
|
/// <summary>[Assert-Only]
|
|
/// Returns <c>null</c> if Animancer Events will work properly on this type of state, or a message explaining
|
|
/// why they might not work.
|
|
/// </summary>
|
|
protected virtual string UnsupportedEventsMessage => null;
|
|
#endif
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>An <see cref="IUpdatable"/> which triggers events in an <see cref="AnimancerEvent.Sequence"/>.</summary>
|
|
/// https://kybernetik.com.au/animancer/api/Animancer/EventDispatcher
|
|
///
|
|
public class EventDispatcher : Key, IUpdatable
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Pooling
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// If the `state` has no <see cref="EventDispatcher"/>, this method gets one from the
|
|
/// <see cref="ObjectPool"/>.
|
|
/// </summary>
|
|
internal static void Acquire(AnimancerState state)
|
|
{
|
|
ref var dispatcher = ref state._EventDispatcher;
|
|
if (dispatcher != null)
|
|
return;
|
|
|
|
ObjectPool.Acquire(out dispatcher);
|
|
|
|
#if UNITY_ASSERTIONS
|
|
dispatcher._LoggedEndEventInterrupt = false;
|
|
|
|
OptionalWarning.UnsupportedEvents.Log(state.UnsupportedEventsMessage, state.Root?.Component);
|
|
|
|
if (dispatcher._State != null)
|
|
Debug.LogError($"{dispatcher} already has a state even though it was in the list of spares.",
|
|
state.Root?.Component as Object);
|
|
|
|
if (dispatcher._Events != null)
|
|
Debug.LogError($"{dispatcher} has event sequence even though it was in the list of spares.",
|
|
state.Root?.Component as Object);
|
|
|
|
if (dispatcher._GotEventsFromPool)
|
|
Debug.LogError($"{dispatcher} is marked as having pooled events even though it has no events.",
|
|
state.Root?.Component as Object);
|
|
|
|
if (dispatcher._NextEventIndex != RecalculateEventIndex)
|
|
Debug.LogError($"{dispatcher} has a {nameof(_NextEventIndex)} even though it was pooled.",
|
|
state.Root?.Component as Object);
|
|
|
|
if (IsInList(dispatcher))
|
|
Debug.LogError($"{dispatcher} is currently in a Keyed List even though it was also in the list of spares.",
|
|
state.Root?.Component as Object);
|
|
#endif
|
|
|
|
dispatcher._IsLooping = state.IsLooping;
|
|
dispatcher._PreviousTime = state.NormalizedTime;
|
|
dispatcher._State = state;
|
|
state.Root?.RequirePostUpdate(dispatcher);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns this <see cref="EventDispatcher"/> to the <see cref="ObjectPool"/>.</summary>
|
|
private void Release()
|
|
{
|
|
if (_State == null)
|
|
return;
|
|
|
|
_State.Root?.CancelPostUpdate(this);
|
|
_State._EventDispatcher = null;
|
|
_State = null;
|
|
|
|
Events = null;
|
|
|
|
ObjectPool.Release(this);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// If the <see cref="AnimancerEvent.Sequence"/> was acquired from the <see cref="ObjectPool"/>, this
|
|
/// method clears it. Otherwise it simply discards the reference.
|
|
/// </summary>
|
|
internal static void TryClear(EventDispatcher events)
|
|
{
|
|
if (events != null)
|
|
events.Events = null;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
|
|
private AnimancerState _State;
|
|
private AnimancerEvent.Sequence _Events;
|
|
private bool _GotEventsFromPool;
|
|
private bool _IsLooping;
|
|
private float _PreviousTime;
|
|
private int _NextEventIndex = RecalculateEventIndex;
|
|
private int _SequenceVersion;
|
|
private bool _WasPlayingForwards;
|
|
|
|
/// <summary>
|
|
/// A special value used by the <see cref="_NextEventIndex"/> to indicate that it needs to be recalculated.
|
|
/// </summary>
|
|
private const int RecalculateEventIndex = int.MinValue;
|
|
|
|
/// <summary>
|
|
/// This system accounts for external modifications to the sequence, but modifying it while checking which
|
|
/// of its events to update is not allowed because it would be impossible to efficiently keep track of
|
|
/// which events have been checked/invoked and which still need to be checked.
|
|
/// </summary>
|
|
private const string SequenceVersionException =
|
|
nameof(AnimancerState) + "." + nameof(AnimancerState.Events) + " sequence was modified while iterating through it." +
|
|
" Events in a sequence must not modify that sequence.";
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Does this dispatcher have an <see cref="AnimancerEvent.Sequence"/>?</summary>
|
|
/// <remarks>Accessing <see cref="Events"/> would automatically get one from the <see cref="ObjectPool"/>.</remarks>
|
|
public bool HasEvents => _Events != null;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The events managed by this dispatcher.</summary>
|
|
/// <remarks>If <c>null</c>, a new sequence will be acquired from the <see cref="ObjectPool"/>.</remarks>
|
|
internal AnimancerEvent.Sequence Events
|
|
{
|
|
get
|
|
{
|
|
if (_Events == null)
|
|
{
|
|
ObjectPool.Acquire(out _Events);
|
|
_GotEventsFromPool = true;
|
|
|
|
#if UNITY_ASSERTIONS
|
|
if (!_Events.IsEmpty)
|
|
Debug.LogError(_Events + " is not in its default state even though it was in the list of spares.",
|
|
_State?.Root?.Component as Object);
|
|
#endif
|
|
}
|
|
|
|
return _Events;
|
|
}
|
|
set
|
|
{
|
|
if (_GotEventsFromPool)
|
|
{
|
|
_Events.Clear();
|
|
ObjectPool.Release(_Events);
|
|
_GotEventsFromPool = false;
|
|
}
|
|
|
|
_Events = value;
|
|
_NextEventIndex = RecalculateEventIndex;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
void IUpdatable.Update()
|
|
{
|
|
if (_Events == null || _Events.IsEmpty)
|
|
{
|
|
Release();
|
|
return;
|
|
}
|
|
|
|
var length = _State.Length;
|
|
if (length == 0)
|
|
{
|
|
UpdateZeroLength();
|
|
return;
|
|
}
|
|
|
|
var currentTime = _State.Time / length;
|
|
if (_PreviousTime == currentTime)
|
|
return;
|
|
|
|
// General events are triggered on the frame when their time passes.
|
|
// This happens either once or repeatedly depending on whether the animation is looping or not.
|
|
CheckGeneralEvents(currentTime);
|
|
if (_Events == null)
|
|
{
|
|
Release();
|
|
return;
|
|
}
|
|
|
|
// 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.
|
|
var endEvent = _Events.EndEvent;
|
|
if (endEvent.callback != null)
|
|
{
|
|
|
|
if (currentTime > _PreviousTime)// Playing Forwards.
|
|
{
|
|
var eventTime = float.IsNaN(endEvent.normalizedTime)
|
|
? 1
|
|
: endEvent.normalizedTime;
|
|
|
|
if (currentTime > eventTime)
|
|
{
|
|
ValidateBeforeEndEvent();
|
|
endEvent.Invoke(_State);
|
|
ValidateAfterEndEvent(endEvent.callback);
|
|
}
|
|
}
|
|
else// Playing Backwards.
|
|
{
|
|
var eventTime = float.IsNaN(endEvent.normalizedTime)
|
|
? 0
|
|
: endEvent.normalizedTime;
|
|
|
|
if (currentTime < eventTime)
|
|
{
|
|
ValidateBeforeEndEvent();
|
|
endEvent.Invoke(_State);
|
|
ValidateAfterEndEvent(endEvent.callback);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store the current time as the previous for the next frame unless OnTimeChanged was called.
|
|
if (_NextEventIndex != RecalculateEventIndex)
|
|
_PreviousTime = currentTime;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#region End Event Validation
|
|
/************************************************************************************************************************/
|
|
|
|
#if UNITY_ASSERTIONS
|
|
private bool _LoggedEndEventInterrupt;
|
|
|
|
private static AnimancerLayer _BeforeEndLayer;
|
|
private static int _BeforeEndCommandCount;
|
|
#endif
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Assert-Conditional]
|
|
/// Called after the <see cref="AnimancerEvent.Sequence.EndEvent"/> is triggered to log a warning if the
|
|
/// <see cref="_State"/> was not interrupted or the `callback` contains multiple calls to the same method.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It would be better if we could validate the callback when it is assigned to get a useful stack trace,
|
|
/// but that is unfortunately not possible since <see cref="AnimancerEvent.Sequence.EndEvent"/> needs to be
|
|
/// a field for efficiency.
|
|
/// </remarks>
|
|
[System.Diagnostics.Conditional(Strings.Assertions)]
|
|
private void ValidateBeforeEndEvent()
|
|
{
|
|
#if UNITY_ASSERTIONS
|
|
_BeforeEndLayer = _State.Layer;
|
|
_BeforeEndCommandCount = _BeforeEndLayer.CommandCount;
|
|
#endif
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Assert-Conditional]
|
|
/// Called after the <see cref="AnimancerEvent.Sequence.EndEvent"/> is triggered to log a warning if the
|
|
/// <see cref="_State"/> was not interrupted or the `callback` contains multiple calls to the same method.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It would be better if we could validate the callback when it is assigned to get a useful stack trace,
|
|
/// but that is unfortunately not possible since <see cref="AnimancerEvent.Sequence.EndEvent"/> needs to be
|
|
/// a field for efficiency.
|
|
/// </remarks>
|
|
[System.Diagnostics.Conditional(Strings.Assertions)]
|
|
private void ValidateAfterEndEvent(Action callback)
|
|
{
|
|
#if UNITY_ASSERTIONS
|
|
if (ShouldLogEndEventInterrupt(callback))
|
|
{
|
|
_LoggedEndEventInterrupt = true;
|
|
if (OptionalWarning.EndEventInterrupt.IsEnabled())
|
|
OptionalWarning.EndEventInterrupt.Log(
|
|
"An End Event did not actually end the animation:" +
|
|
$"\n• State: {_State}" +
|
|
$"\n• Callback: {callback.Method.DeclaringType.Name}.{callback.Method.Name}" +
|
|
"\n\nEnd Events are triggered every frame after their time has passed," +
|
|
" so if that is not desired behaviour then it might be necessary to explicitly set the" +
|
|
$" state.{nameof(AnimancerState.Events)}.{nameof(AnimancerEvent.Sequence.OnEnd)} = null" +
|
|
" or simply use a regular event instead.",
|
|
_State.Root?.Component);
|
|
}
|
|
|
|
if (OptionalWarning.DuplicateEvent.IsDisabled())
|
|
return;
|
|
|
|
if (!AnimancerUtilities.TryGetInvocationListNonAlloc(callback, out var delegates) ||
|
|
delegates == null)
|
|
return;
|
|
|
|
var count = delegates.Length;
|
|
for (int iA = 0; iA < count; iA++)
|
|
{
|
|
var a = delegates[iA];
|
|
for (int iB = iA + 1; iB < count; iB++)
|
|
{
|
|
var b = delegates[iB];
|
|
|
|
if (a == b)
|
|
{
|
|
OptionalWarning.DuplicateEvent.Log(
|
|
$"The {nameof(AnimancerEvent)}.{nameof(AnimancerEvent.Sequence)}.{nameof(AnimancerEvent.Sequence.OnEnd)}" +
|
|
" callback being invoked contains multiple identical delegates which may mean" +
|
|
" that they are being unintentionally added multiple times." +
|
|
$"\n• State: {_State}" +
|
|
$"\n• Method: {a.Method.Name}",
|
|
_State.Root?.Component);
|
|
}
|
|
else if (a?.Method == b?.Method)
|
|
{
|
|
OptionalWarning.DuplicateEvent.Log(
|
|
$"The {nameof(AnimancerEvent)}.{nameof(AnimancerEvent.Sequence)}.{nameof(AnimancerEvent.Sequence.OnEnd)}" +
|
|
" callback being invoked contains multiple delegates using the same method with different targets." +
|
|
" This often happens when a Transition is shared by multiple objects," +
|
|
" in which case it can be avoided by giving each object its own" +
|
|
$" {nameof(AnimancerEvent)}.{nameof(AnimancerEvent.Sequence)} as explained in the documentation:" +
|
|
$" {Strings.DocsURLs.SharedEventSequences}" +
|
|
$"\n• State: {_State}" +
|
|
$"\n• Method: {a.Method.Name}",
|
|
_State.Root?.Component);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
#if UNITY_ASSERTIONS
|
|
/// <summary>Should <see cref="OptionalWarning.EndEventInterrupt"/> be logged?</summary>
|
|
private bool ShouldLogEndEventInterrupt(Action callback)
|
|
{
|
|
if (_LoggedEndEventInterrupt ||
|
|
_Events == null ||
|
|
_Events.OnEnd != callback)
|
|
return false;
|
|
|
|
var layer = _State.Layer;
|
|
if (_BeforeEndLayer != layer ||
|
|
_BeforeEndCommandCount != layer.CommandCount ||
|
|
!_State.Root.IsGraphPlaying ||
|
|
!_State.IsPlaying)
|
|
return false;
|
|
|
|
var speed = _State.EffectiveSpeed;
|
|
if (speed > 0)
|
|
{
|
|
return _State.NormalizedTime > _State.NormalizedEndTime;
|
|
}
|
|
else if (speed < 0)
|
|
{
|
|
return _State.NormalizedTime < _State.NormalizedEndTime;
|
|
}
|
|
else return false;// Speed 0.
|
|
}
|
|
#endif
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Notifies this dispatcher that the target's <see cref="Time"/> has changed.</summary>
|
|
internal void OnTimeChanged()
|
|
{
|
|
_PreviousTime = _State.NormalizedTime;
|
|
_NextEventIndex = RecalculateEventIndex;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>If the state has zero length, trigger its end event every frame.</summary>
|
|
private void UpdateZeroLength()
|
|
{
|
|
var speed = _State.EffectiveSpeed;
|
|
if (speed == 0)
|
|
return;
|
|
|
|
if (_Events.Count > 0)
|
|
{
|
|
var sequenceVersion = _Events.Version;
|
|
|
|
int playDirectionInt;
|
|
if (speed < 0)
|
|
{
|
|
playDirectionInt = -1;
|
|
if (_NextEventIndex == RecalculateEventIndex ||
|
|
_SequenceVersion != sequenceVersion ||
|
|
_WasPlayingForwards)
|
|
{
|
|
_NextEventIndex = Events.Count - 1;
|
|
_SequenceVersion = sequenceVersion;
|
|
_WasPlayingForwards = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
playDirectionInt = 1;
|
|
if (_NextEventIndex == RecalculateEventIndex ||
|
|
_SequenceVersion != sequenceVersion ||
|
|
!_WasPlayingForwards)
|
|
{
|
|
_NextEventIndex = 0;
|
|
_SequenceVersion = sequenceVersion;
|
|
_WasPlayingForwards = true;
|
|
}
|
|
}
|
|
|
|
if (!InvokeAllEvents(1, playDirectionInt))
|
|
return;
|
|
}
|
|
|
|
var endEvent = _Events.EndEvent;
|
|
if (endEvent.callback != null)
|
|
endEvent.Invoke(_State);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void CheckGeneralEvents(float currentTime)
|
|
{
|
|
var count = _Events.Count;
|
|
if (count == 0)
|
|
{
|
|
_NextEventIndex = 0;
|
|
return;
|
|
}
|
|
|
|
ValidateNextEventIndex(ref currentTime, out var playDirectionFloat, out var playDirectionInt);
|
|
|
|
if (_IsLooping)// Looping.
|
|
{
|
|
var animancerEvent = _Events[_NextEventIndex];
|
|
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
|
|
|
|
var loopDelta = GetLoopDelta(_PreviousTime, currentTime, eventTime);
|
|
if (loopDelta == 0)
|
|
return;
|
|
|
|
// For each additional loop, invoke all events without needing to check their times.
|
|
if (!InvokeAllEvents(loopDelta - 1, playDirectionInt))
|
|
return;
|
|
|
|
var loopStartIndex = _NextEventIndex;
|
|
|
|
Invoke:
|
|
animancerEvent.Invoke(_State);
|
|
|
|
if (!NextEventLooped(playDirectionInt) ||
|
|
_NextEventIndex == loopStartIndex)
|
|
return;
|
|
|
|
animancerEvent = _Events[_NextEventIndex];
|
|
eventTime = animancerEvent.normalizedTime * playDirectionFloat;
|
|
if (loopDelta == GetLoopDelta(_PreviousTime, 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.Invoke(_State);
|
|
|
|
if (!NextEvent(playDirectionInt))
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void ValidateNextEventIndex(ref float currentTime,
|
|
out float playDirectionFloat, out int playDirectionInt)
|
|
{
|
|
var sequenceVersion = _Events.Version;
|
|
|
|
if (currentTime < _PreviousTime)// Playing Backwards.
|
|
{
|
|
var previousTime = _PreviousTime;
|
|
_PreviousTime = -previousTime;
|
|
currentTime = -currentTime;
|
|
playDirectionFloat = -1;
|
|
playDirectionInt = -1;
|
|
|
|
if (_NextEventIndex == RecalculateEventIndex ||
|
|
_SequenceVersion != sequenceVersion ||
|
|
_WasPlayingForwards)
|
|
{
|
|
_NextEventIndex = _Events.Count - 1;
|
|
_SequenceVersion = sequenceVersion;
|
|
_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 ||
|
|
_SequenceVersion != sequenceVersion ||
|
|
!_WasPlayingForwards)
|
|
{
|
|
_NextEventIndex = 0;
|
|
_SequenceVersion = sequenceVersion;
|
|
_WasPlayingForwards = true;
|
|
|
|
var previousTime = _PreviousTime;
|
|
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="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 bool InvokeAllEvents(int count, int playDirectionInt)
|
|
{
|
|
var loopStartIndex = _NextEventIndex;
|
|
while (count-- > 0)
|
|
{
|
|
do
|
|
{
|
|
_Events[_NextEventIndex].Invoke(_State);
|
|
|
|
if (!NextEventLooped(playDirectionInt))
|
|
return false;
|
|
}
|
|
while (_NextEventIndex != loopStartIndex);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private bool NextEvent(int playDirectionInt)
|
|
{
|
|
if (_NextEventIndex == RecalculateEventIndex)
|
|
return false;
|
|
|
|
if (_Events.Version != _SequenceVersion)
|
|
throw new InvalidOperationException(SequenceVersionException);
|
|
|
|
_NextEventIndex += playDirectionInt;
|
|
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private bool NextEventLooped(int playDirectionInt)
|
|
{
|
|
if (!NextEvent(playDirectionInt))
|
|
return false;
|
|
|
|
var count = _Events.Count;
|
|
if (_NextEventIndex >= count)
|
|
_NextEventIndex = 0;
|
|
else if (_NextEventIndex < 0)
|
|
_NextEventIndex = count - 1;
|
|
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns "<see cref="EventDispatcher"/> (Target State)".</summary>
|
|
public override string ToString()
|
|
{
|
|
return _State != null ?
|
|
$"{nameof(EventDispatcher)} ({_State})" :
|
|
$"{nameof(EventDispatcher)} (No Target State)";
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|