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