// Animancer // Copyright 2020 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
partial class AnimancerState
{
/************************************************************************************************************************/
///
/// The that manages the events of this state.
///
/// This field is null by default, acquires its reference from an when accessed, and
/// if it contains no events at the end of an update it releases the reference back to the pool.
///
private EventUpdatable _EventUpdatable;
/************************************************************************************************************************/
///
/// A list of s that will occur while this state plays as well as one that
/// specifically defines when this state ends.
///
/// Animancer Lite does not allow the use of events in a runtime build, except for
/// .
///
public AnimancerEvent.Sequence Events
{
get
{
EventUpdatable.Acquire(this);
return _EventUpdatable.Events;
}
set
{
if (value != null)
{
EventUpdatable.Acquire(this);
_EventUpdatable.Events = value;
}
else if (_EventUpdatable != null)
{
_EventUpdatable.Events = null;
}
}
}
/************************************************************************************************************************/
///
/// Indicates whether this state currently has an (since accessing the
/// would automatically get one from the ).
///
public bool HasEvents { get { return _EventUpdatable != null; } }
/************************************************************************************************************************/
///
/// The for and
/// .
///
public static int EventPoolCapacity
{
get { return ObjectPool.Capacity; }
set
{
ObjectPool.Capacity = value;
ObjectPool.Capacity = value;
}
}
/************************************************************************************************************************/
///
/// An which manages the triggering of events.
///
private sealed class EventUpdatable : Key, IUpdatable
{
/************************************************************************************************************************/
#region Pooling
/************************************************************************************************************************/
///
/// If the `state` has no , this method gets one from the
/// .
///
public static void Acquire(AnimancerState state)
{
var updatable = state._EventUpdatable;
if (updatable != null)
return;
ObjectPool.Acquire(out updatable);
#if UNITY_ASSERTIONS
if (updatable._State != null)
Debug.LogError(updatable + " already has a state even though it was in the list of spares.");
if (updatable._Events != null)
Debug.LogError(updatable + " has event sequence even though it was in the list of spares.");
if (updatable._GotEventsFromPool)
Debug.LogError(updatable + " is marked as having pooled events even though it has no events.");
if (updatable._NextEventIndex != RecalculateEventIndex)
Debug.LogError(updatable + " has a _NextEventIndex even though it was pooled.");
if (IsInList(updatable))
Debug.LogError(updatable + " is currently in a Keyed List even though it was in the list of spares.");
#endif
updatable._IsLooping = state.IsLooping;
updatable._State = state;
state._EventUpdatable = updatable;
state.Root.RequireUpdate(updatable);
}
/************************************************************************************************************************/
///
/// Returns this to the .
///
private void Release()
{
if (_State == null)
return;
_State.Root.CancelUpdate(this);
if (_GotEventsFromPool)
{
_Events.Clear();
ObjectPool.Release(_Events);
_GotEventsFromPool = false;
}
_Events = null;
_State._EventUpdatable = null;
_State = null;
_NextEventIndex = RecalculateEventIndex;
ObjectPool.Release(this);
}
/************************************************************************************************************************/
///
/// If the was acquired from the , this
/// method clears it. Otherwise it simply discards the reference.
///
public static void TryClear(EventUpdatable events)
{
if (events != null && events._Events != null)
{
events._NextEventIndex = RecalculateEventIndex;
if (events._GotEventsFromPool)
{
events._Events.Clear();
events._GotEventsFromPool = false;
}
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;
private const int RecalculateEventIndex = int.MinValue;
///
/// 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.
///
private const string SequenceVersionException =
"AnimancerState.Events sequence was modified while iterating through it." +
" Events in a sequence must not modify that sequence.";
/************************************************************************************************************************/
public 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.");
#endif
}
return _Events;
}
set
{
if (_GotEventsFromPool)
{
_GotEventsFromPool = false;
ObjectPool.Release(_Events);
}
_Events = value;
_NextEventIndex = RecalculateEventIndex;
}
}
/************************************************************************************************************************/
void IUpdatable.EarlyUpdate()
{
if (_Events == null || _Events.IsEmpty)
{
Release();
return;
}
_PreviousTime = _State.NormalizedTime;
}
/************************************************************************************************************************/
void IUpdatable.LateUpdate()
{
if (_Events == null || _Events.IsEmpty)
{
Release();
return;
}
var currentTime = _State.NormalizedTime;
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 onEnd = _Events.endEvent;
if (onEnd.callback != null)
{
if (currentTime > _PreviousTime)// Playing Forwards.
{
var eventTime = float.IsNaN(onEnd.normalizedTime) ?
1 : onEnd.normalizedTime;
if (currentTime > eventTime)
onEnd.Invoke(_State);
}
else// Playing Backwards.
{
var eventTime = float.IsNaN(onEnd.normalizedTime) ?
0 : onEnd.normalizedTime;
if (currentTime < eventTime)
onEnd.Invoke(_State);
}
}
}
/************************************************************************************************************************/
public void OnTimeChanged()
{
_NextEventIndex = RecalculateEventIndex;
}
/************************************************************************************************************************/
private void CheckGeneralEvents(float currentTime)
{
var count = _Events.Count;
if (count == 0)
return;
float playDirectionFloat;
int playDirectionInt;
ValidateNextEventIndex(ref currentTime, out playDirectionFloat, out 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)
break;
animancerEvent.Invoke(_State);
if (!NextEvent(playDirectionInt))
return;
}
}
}
/************************************************************************************************************************/
private void ValidateNextEventIndex(ref float currentTime,
out float playDirectionFloat, out int playDirectionInt)
{
if (currentTime > _PreviousTime)// Playing Forwards.
{
playDirectionFloat = 1;
playDirectionInt = 1;
if (_NextEventIndex == RecalculateEventIndex ||
_SequenceVersion != _Events.Version ||
!_WasPlayingForwards)
{
_NextEventIndex = 0;
_SequenceVersion = _Events.Version;
_WasPlayingForwards = true;
var previousTime = _PreviousTime;
if (_IsLooping)
previousTime = previousTime.Wrap01();
var max = _Events.Count - 1;
while (_NextEventIndex < max &&
_Events[_NextEventIndex].normalizedTime < previousTime)
_NextEventIndex++;
_Events.AssertNormalizedTimes(_IsLooping);
}
}
else// Playing Backwards.
{
var previousTime = _PreviousTime;
_PreviousTime = -previousTime;
currentTime = -currentTime;
playDirectionFloat = -1;
playDirectionInt = -1;
if (_NextEventIndex == RecalculateEventIndex ||
_SequenceVersion != _Events.Version ||
_WasPlayingForwards)
{
_NextEventIndex = _Events.Count - 1;
_SequenceVersion = _Events.Version;
_WasPlayingForwards = false;
if (_IsLooping)
previousTime = previousTime.Wrap01();
while (_NextEventIndex > 0 &&
_Events[_NextEventIndex].normalizedTime > previousTime)
_NextEventIndex--;
_Events.AssertNormalizedTimes(_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 should not 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).
}
/************************************************************************************************************************/
private static int GetLoopDelta(float previousTime, float nextTime, float eventTime)
{
previousTime -= eventTime;
var previousLoopCount = Mathf.FloorToInt(previousTime);
var nextLoopCount = Mathf.FloorToInt(nextTime - eventTime);
if (previousTime == previousLoopCount)
nextLoopCount++;
return nextLoopCount - previousLoopCount;
}
/************************************************************************************************************************/
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;
}
/************************************************************************************************************************/
void IUpdatable.OnDestroy()
{
Release();
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}