479 lines
20 KiB
C#
479 lines
20 KiB
C#
// 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);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
}
|
|
|