// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Text; using UnityEngine; using Object = UnityEngine.Object; namespace Animancer { /// /// A delegate paired with a to determine when to invoke it. /// /// /// Documentation: Animancer Events /// /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent /// public partial struct AnimancerEvent : IEquatable { /************************************************************************************************************************/ #region Event /************************************************************************************************************************/ /// The at which to invoke the . public float normalizedTime; /// The delegate to invoke when the passes. public Action callback; /************************************************************************************************************************/ /// The largest possible float value less than 1. /// /// This value is useful for placing events at the end of a looping animation since they do not allow the /// to be greater than or equal to 1. /// public const float AlmostOne = 0.99999994f; /************************************************************************************************************************/ /// Does nothing. /// This delegate is used for events which would otherwise have a null . public static readonly Action DummyCallback = Dummy; /// Does nothing. /// Used by . private static void Dummy() { } /// Is the `callback` null or the ? public static bool IsNullOrDummy(Action callback) => callback == null || callback == DummyCallback; /************************************************************************************************************************/ /// Creates a new . public AnimancerEvent(float normalizedTime, Action callback) { this.normalizedTime = normalizedTime; this.callback = callback; } /************************************************************************************************************************/ /// Returns a string describing the details of this event. public override string ToString() { var text = ObjectPool.AcquireStringBuilder(); text.Append($"{nameof(AnimancerEvent)}("); AppendDetails(text); text.Append(')'); return text.ReleaseToString(); } /************************************************************************************************************************/ /// Appends the details of this event to the `text`. public void AppendDetails(StringBuilder text) { text.Append("NormalizedTime: ") .Append(normalizedTime) .Append(", Callback: "); if (callback == null) { text.Append("null"); } else if (callback.Target == null) { text.Append(callback.Method.DeclaringType.FullName) .Append('.') .Append(callback.Method.Name); } else { text.Append("(Target: '") .Append(callback.Target) .Append("', Method: ") .Append(callback.Method.DeclaringType.FullName) .Append('.') .Append(callback.Method.Name) .Append(')'); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Invocation /************************************************************************************************************************/ /// The currently triggering an event via . public static AnimancerState CurrentState => _CurrentState; private static AnimancerState _CurrentState; /************************************************************************************************************************/ /// The currently being triggered via . public static ref readonly AnimancerEvent CurrentEvent => ref _CurrentEvent; private static AnimancerEvent _CurrentEvent; /************************************************************************************************************************/ /// /// Sets the and then invokes the . /// /// This method catches and logs any exception thrown by the . /// The is null. public void Invoke(AnimancerState state) { #if UNITY_ASSERTIONS if (IsNullOrDummy(callback)) OptionalWarning.UselessEvent.Log( $"An {nameof(AnimancerEvent)} that does nothing was invoked." + " Most likely it was not configured correctly." + " Unused events should be removed to avoid wasting performance checking and invoking them.", state?.Root?.Component); #endif var previousState = _CurrentState; var previousEvent = _CurrentEvent; _CurrentState = state; _CurrentEvent = this; try { callback(); } catch (Exception exception) { Debug.LogException(exception, state?.Root?.Component as Object); } _CurrentState = previousState; _CurrentEvent = previousEvent; } /************************************************************************************************************************/ /// /// Returns either the or the /// of the (whichever is higher). /// public static float GetFadeOutDuration() => GetFadeOutDuration(CurrentState, AnimancerPlayable.DefaultFadeDuration); /// /// Returns either the `minDuration` or the of the /// (whichever is higher). /// public static float GetFadeOutDuration(float minDuration) => GetFadeOutDuration(CurrentState, minDuration); /// /// Returns either the `minDuration` or the of the /// `state` (whichever is higher). /// public static float GetFadeOutDuration(AnimancerState state, float minDuration) { if (state == null) return minDuration; var time = state.Time; var speed = state.EffectiveSpeed; if (speed == 0) return minDuration; float remainingDuration; if (state.IsLooping) { var previousTime = time - speed * Time.deltaTime; var inverseLength = 1f / state.Length; // If we just passed the end of the animation, the remaining duration would technically be the full // duration of the animation, so we most likely want to use the minimum duration instead. if (Math.Floor(time * inverseLength) != Math.Floor(previousTime * inverseLength)) return minDuration; } if (speed > 0) { remainingDuration = (state.Length - time) / speed; } else { remainingDuration = time / -speed; } return Math.Max(minDuration, remainingDuration); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Operators /************************************************************************************************************************/ /// Are the and equal? public static bool operator ==(AnimancerEvent a, AnimancerEvent b) => a.Equals(b); /// Are the and not equal? public static bool operator !=(AnimancerEvent a, AnimancerEvent b) => !a.Equals(b); /************************************************************************************************************************/ /// [] /// Are the and of this event equal to `other`? /// public bool Equals(AnimancerEvent other) => callback == other.callback && (normalizedTime == other.normalizedTime || (float.IsNaN(normalizedTime) && float.IsNaN(other.normalizedTime))); /// public override bool Equals(object obj) => obj is AnimancerEvent animancerEvent && Equals(animancerEvent); /// public override int GetHashCode() { const int Multiplyer = -1521134295; var hashCode = -78069441; hashCode = hashCode * Multiplyer + normalizedTime.GetHashCode(); if (callback != null) hashCode = hashCode * Multiplyer + callback.GetHashCode(); return hashCode; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }