123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
- using System;
- using System.Runtime.CompilerServices;
- using System.Text;
- using UnityEngine;
- namespace Animancer
- {
- /// <summary>
- /// A <see cref="callback"/> delegate paired with a <see cref="normalizedTime"/> to determine when to invoke it.
- /// </summary>
- /// <remarks>
- /// <strong>Documentation:</strong>
- /// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
- /// Animancer Events</see>
- /// </remarks>
- /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
- ///
- public partial struct AnimancerEvent : IEquatable<AnimancerEvent>
- {
- /************************************************************************************************************************/
- #region Event
- /************************************************************************************************************************/
- /// <summary>The <see cref="AnimancerState.NormalizedTime"/> at which to invoke the <see cref="callback"/>.</summary>
- public float normalizedTime;
- /// <summary>The delegate to invoke when the <see cref="normalizedTime"/> passes.</summary>
- public Action callback;
- /************************************************************************************************************************/
- /// <summary>The largest possible float value less than 1.</summary>
- /// <remarks>
- /// This value is useful for placing events at the end of a looping animation since they do not allow the
- /// <see cref="normalizedTime"/> to be greater than or equal to 1.
- /// </remarks>
- public const float
- AlmostOne = 0.99999994f;
- /************************************************************************************************************************/
- /// <summary>The event name used for <see cref="Sequence.EndEvent"/>s.</summary>
- /// <remarks>
- /// This is a <see cref="StringReference.Unique"/> 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 <see cref="NamedEventDictionary.AssertNotEndEvent"/>.
- /// </remarks>
- public static readonly StringReference
- EndEventName = StringReference.Unique("EndEvent");
- /************************************************************************************************************************/
- /// <summary>Does nothing.</summary>
- /// <remarks>This delegate can be used for events which would otherwise have a <c>null</c> <see cref="callback"/>.</remarks>
- public static readonly Action
- DummyCallback = Dummy;
- /// <summary>Does nothing.</summary>
- /// <remarks>Used by <see cref="DummyCallback"/>.</remarks>
- private static void Dummy() { }
- /// <summary>Is the `callback` <c>null</c> or the <see cref="DummyCallback"/>?</summary>
- public static bool IsNullOrDummy(Action callback)
- => callback == null
- || callback == DummyCallback;
- /************************************************************************************************************************/
- /// <summary>Creates a new <see cref="AnimancerEvent"/>.</summary>
- public AnimancerEvent(float normalizedTime, Action callback)
- {
- this.normalizedTime = normalizedTime;
- this.callback = callback;
- }
- /************************************************************************************************************************/
- /// <summary>Returns a string describing the details of this event.</summary>
- public readonly override string ToString()
- {
- var text = StringBuilderPool.Instance.Acquire();
- text.Append($"{nameof(AnimancerEvent)}(");
- AppendDetails(text);
- text.Append(')');
- return text.ReleaseToString();
- }
- /************************************************************************************************************************/
- /// <summary>Appends the details of this event to the `text`.</summary>
- public readonly void AppendDetails(StringBuilder text)
- {
- text.Append("NormalizedTime: ")
- .Append(normalizedTime)
- .Append(", Callback: ")
- .AppendDelegate(callback);
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Invocation
- /************************************************************************************************************************/
- /// <summary>The details of the event currently being triggered.</summary>
- /// <remarks>Cleared after the event is invoked.</remarks>
- // Having the underlying field here can cause type initialization errors due to circular dependencies.
- public static Invocation Current
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => Invocation.Current;
- }
- /************************************************************************************************************************/
- /// <summary>
- /// A cached delegate which calls <see cref="Invocation.InvokeBoundCallback"/>
- /// on the <see cref="Current"/>.
- /// </summary>
- public static readonly Action
- InvokeBoundCallback = InvokeCurrentBoundCallback;
- /// <summary>
- /// Calls <see cref="Invocation.InvokeBoundCallback"/> on the <see cref="Current"/>.
- /// </summary>
- private static void InvokeCurrentBoundCallback()
- => Current.InvokeBoundCallback();
- /************************************************************************************************************************/
- /// <summary>The custom parameter of the event currently being triggered.</summary>
- /// <remarks>Cleared after the event is finished.</remarks>
- public static object CurrentParameter { get; private set; }
- /// <summary>Calls <see cref="ConvertableUtilities.ConvertOrThrow"/> on the <see cref="CurrentParameter"/>.</summary>
- public static T GetCurrentParameter<T>()
- => ConvertableUtilities.ConvertOrThrow<T>(CurrentParameter);
- /// <summary>Returns a new delegate which invokes the `callback` using <see cref="GetCurrentParameter{T}"/>.</summary>
- /// <remarks>
- /// If <typeparamref name="T"/> is <see cref="string"/>,
- /// consider using <see cref="Parametize(Action{string})"/> instead of this.
- /// </remarks>
- /// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
- public static Action Parametize<T>(Action<T> callback)
- {
- #if UNITY_ASSERTIONS
- if (callback == null)
- throw new ArgumentNullException(
- nameof(callback),
- $"Can't {nameof(Parametize)} a null callback.");
- #endif
- return () => callback(GetCurrentParameter<T>());
- }
- /// <summary>Returns a new delegate which invokes the `callback` using the <see cref="CurrentParameter"/>.</summary>
- /// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
- public static Action Parametize(Action<string> callback)
- {
- #if UNITY_ASSERTIONS
- if (callback == null)
- throw new ArgumentNullException(
- nameof(callback),
- $"Can't {nameof(Parametize)} a null callback.");
- #endif
- return () => callback(CurrentParameter?.ToString());
- }
- /************************************************************************************************************************/
- /// <summary>[Assert-Only]
- /// Logs an error if the `callback` doesn't contain a <see cref="Parameter{T}.Invoke"/>
- /// so that adding to it with <see cref="Parametize{T}(Action{T})"/> can use that parameter.
- /// </summary>
- [System.Diagnostics.Conditional(Strings.Assertions)]
- public static void AssertContainsParameter<T>(Action callback)
- {
- if (!ContainsParameterInvoke<T>(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()}");
- }
- /// <summary>Does the `callback` contain a <see cref="Parameter{T}.Invoke"/>?</summary>
- private static bool ContainsParameterInvoke<T>(Action callback)
- {
- if (callback == null)
- return false;
- if (IsParameterInvoke<T>(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<T>(invocation))
- return true;
- }
- return false;
- }
- /// <summary>Is the `callback` a call to <see cref="Parameter{T}.Invoke"/>?</summary>
- private static bool IsParameterInvoke<T>(Delegate callback)
- => callback.Target is IParameter parameter
- && callback.Method.Name == nameof(IInvokable.Invoke)
- && typeof(T).IsAssignableFrom(parameter.Value.GetType());
- /************************************************************************************************************************/
- /// <summary>
- /// Adds this event to the <see cref="Invoker"/>
- /// which will call <see cref="Invocation.Invoke"/> later in the current frame.
- /// </summary>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public readonly void DelayInvoke(
- StringReference eventName,
- AnimancerState state)
- => Invoker.Add(new(this, eventName, state));
- /************************************************************************************************************************/
- /// <summary>[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 <see cref="OptionalWarning.EventPlayMismatch"/>.
- /// </summary>
- [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
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Returns either the <see cref="AnimancerGraph.DefaultFadeDuration"/>
- /// or the <see cref="AnimancerState.RemainingDuration"/>
- /// of the <see cref="Current"/> state (whichever is higher).
- /// </summary>
- public static float GetFadeOutDuration()
- => GetFadeOutDuration(Current.State, AnimancerGraph.DefaultFadeDuration);
- /// <summary>
- /// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
- /// of the <see cref="Current"/> state (whichever is higher).
- /// </summary>
- public static float GetFadeOutDuration(float minDuration)
- => GetFadeOutDuration(Current.State, minDuration);
- /// <summary>
- /// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
- /// of the `state` (whichever is higher).
- /// </summary>
- 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
- /************************************************************************************************************************/
- /// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> equal?</summary>
- public static bool operator ==(AnimancerEvent a, AnimancerEvent b)
- => a.Equals(b);
- /// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> not equal?</summary>
- public static bool operator !=(AnimancerEvent a, AnimancerEvent b)
- => !a.Equals(b);
- /************************************************************************************************************************/
- /// <summary>[<see cref="IEquatable{AnimancerEvent}"/>]
- /// Are the <see cref="normalizedTime"/> and <see cref="callback"/> of this event equal to `other`?
- /// </summary>
- public readonly bool Equals(AnimancerEvent other)
- => callback == other.callback
- && normalizedTime.IsEqualOrBothNaN(other.normalizedTime);
- /// <inheritdoc/>
- public readonly override bool Equals(object obj)
- => obj is AnimancerEvent animancerEvent
- && Equals(animancerEvent);
- /// <inheritdoc/>
- public readonly override int GetHashCode()
- => AnimancerUtilities.Hash(-78069441,
- normalizedTime.GetHashCode(),
- callback.SafeGetHashCode());
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- }
- }
|