// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using System; using System.Runtime.CompilerServices; using System.Text; using UnityEngine; 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; /************************************************************************************************************************/ /// The event name used for s. /// /// This is a so that even if the same name happens /// to be used elsewhere, it would be treated as a different name. /// The reason for this is explained in . /// public static readonly StringReference EndEventName = StringReference.Unique("EndEvent"); /************************************************************************************************************************/ /// Does nothing. /// This delegate can be 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 readonly override string ToString() { var text = StringBuilderPool.Instance.Acquire(); text.Append($"{nameof(AnimancerEvent)}("); AppendDetails(text); text.Append(')'); return text.ReleaseToString(); } /************************************************************************************************************************/ /// Appends the details of this event to the `text`. public readonly void AppendDetails(StringBuilder text) { text.Append("NormalizedTime: ") .Append(normalizedTime) .Append(", Callback: ") .AppendDelegate(callback); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Invocation /************************************************************************************************************************/ /// The details of the event currently being triggered. /// Cleared after the event is invoked. // Having the underlying field here can cause type initialization errors due to circular dependencies. public static Invocation Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Invocation.Current; } /************************************************************************************************************************/ /// /// A cached delegate which calls /// on the . /// public static readonly Action InvokeBoundCallback = InvokeCurrentBoundCallback; /// /// Calls on the . /// private static void InvokeCurrentBoundCallback() => Current.InvokeBoundCallback(); /************************************************************************************************************************/ /// The custom parameter of the event currently being triggered. /// Cleared after the event is finished. public static object CurrentParameter { get; private set; } /// Calls on the . public static T GetCurrentParameter() => ConvertableUtilities.ConvertOrThrow(CurrentParameter); /// Returns a new delegate which invokes the `callback` using . /// /// If is , /// consider using instead of this. /// /// The `callback` is null. public static Action Parametize(Action callback) { #if UNITY_ASSERTIONS if (callback == null) throw new ArgumentNullException( nameof(callback), $"Can't {nameof(Parametize)} a null callback."); #endif return () => callback(GetCurrentParameter()); } /// Returns a new delegate which invokes the `callback` using the . /// The `callback` is null. public static Action Parametize(Action callback) { #if UNITY_ASSERTIONS if (callback == null) throw new ArgumentNullException( nameof(callback), $"Can't {nameof(Parametize)} a null callback."); #endif return () => callback(CurrentParameter?.ToString()); } /************************************************************************************************************************/ /// [Assert-Only] /// Logs an error if the `callback` doesn't contain a /// so that adding to it with can use that parameter. /// [System.Diagnostics.Conditional(Strings.Assertions)] public static void AssertContainsParameter(Action callback) { if (!ContainsParameterInvoke(callback)) Debug.LogWarning( $"Adding parametized callback will do nothing because the existing callback" + $" doesn't contain a {typeof(T).GetNameCS()} parameter." + $"\n• Existing Callback: {callback.ToStringDetailed()}"); } /// Does the `callback` contain a ? private static bool ContainsParameterInvoke(Action callback) { if (callback == null) return false; if (IsParameterInvoke(callback)) return true; var invocations = AnimancerReflection.GetInvocationList(callback); if (invocations.Length == 1 && ReferenceEquals(invocations[0], callback)) return false; for (int i = 0; i < invocations.Length; i++) { var invocation = invocations[i]; if (IsParameterInvoke(invocation)) return true; } return false; } /// Is the `callback` a call to ? private static bool IsParameterInvoke(Delegate callback) => callback.Target is IParameter parameter && callback.Method.Name == nameof(IInvokable.Invoke) && typeof(T).IsAssignableFrom(parameter.Value.GetType()); /************************************************************************************************************************/ /// /// Adds this event to the /// which will call later in the current frame. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void DelayInvoke( StringReference eventName, AnimancerState state) => Invoker.Add(new(this, eventName, state)); /************************************************************************************************************************/ /// [Assert-Conditional] /// This method should be called when an animation is played. /// It asserts that either no event is currently being triggered /// or that the event is being triggered inside `playing`. /// Otherwise, it logs . /// [System.Diagnostics.Conditional(Strings.Assertions)] public static void AssertEventPlayMismatch(AnimancerGraph playing) { #if UNITY_ASSERTIONS if (Current.State == null || Current.State.Graph == playing || OptionalWarning.EventPlayMismatch.IsDisabled()) return; OptionalWarning.EventPlayMismatch.Log( $"An Animancer Event triggered by '{Current.State}' on '{Current.State.Graph}'" + $" was used to play an animation on a different character ('{playing}')." + $"\n\nThis most commonly happens when a Transition is shared by multiple characters" + $" and they all register their own callbacks to its events which leads to" + $" those events being triggered by the wrong character." + $" See the Shared Events page for more information: " + Strings.DocsURLs.SharedEventSequences + $"\n\n{Current}", playing.Component); #endif } /************************************************************************************************************************/ /// /// Returns either the /// or the /// of the state (whichever is higher). /// public static float GetFadeOutDuration() => GetFadeOutDuration(Current.State, AnimancerGraph.DefaultFadeDuration); /// /// Returns either the `minDuration` or the /// of the state (whichever is higher). /// public static float GetFadeOutDuration(float minDuration) => GetFadeOutDuration(Current.State, 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 readonly bool Equals(AnimancerEvent other) => callback == other.callback && normalizedTime.IsEqualOrBothNaN(other.normalizedTime); /// public readonly override bool Equals(object obj) => obj is AnimancerEvent animancerEvent && Equals(animancerEvent); /// public readonly override int GetHashCode() => AnimancerUtilities.Hash(-78069441, normalizedTime.GetHashCode(), callback.SafeGetHashCode()); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }