You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
493 lines
20 KiB
C#
493 lines
20 KiB
C#
// Animancer // Copyright 2020 Kybernetik //
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Animancer
|
|
{
|
|
partial class AnimancerState
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// The <see cref="IUpdatable"/> that manages the events of this state.
|
|
/// <para></para>
|
|
/// 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.
|
|
/// </summary>
|
|
private EventUpdatable _EventUpdatable;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <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.
|
|
/// <para></para>
|
|
/// Animancer Lite does not allow the use of events in a runtime build, except for
|
|
/// <see cref="AnimancerEvent.Sequence.OnEnd"/>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Indicates whether this state currently has an <see cref="AnimancerEvent.Sequence"/> (since accessing the
|
|
/// <see cref="Events"/> would automatically get one from the <see cref="ObjectPool"/>).
|
|
/// </summary>
|
|
public bool HasEvents { get { return _EventUpdatable != null; } }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// The <see cref="ObjectPool{T}.Capacity"/> for <see cref="AnimancerEvent.Sequence"/> and
|
|
/// <see cref="EventUpdatable"/>.
|
|
/// </summary>
|
|
public static int EventPoolCapacity
|
|
{
|
|
get { return ObjectPool<EventUpdatable>.Capacity; }
|
|
set
|
|
{
|
|
ObjectPool<EventUpdatable>.Capacity = value;
|
|
ObjectPool<AnimancerEvent.Sequence>.Capacity = value;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// An <see cref="IUpdatable"/> which manages the triggering of events.
|
|
/// </summary>
|
|
private sealed class EventUpdatable : Key, IUpdatable
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Pooling
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// If the `state` has no <see cref="EventUpdatable"/>, this method gets one from the
|
|
/// <see cref="ObjectPool"/>.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns this <see cref="EventUpdatable"/> to the <see cref="ObjectPool"/>.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <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>
|
|
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;
|
|
|
|
/// <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 =
|
|
"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();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|