// 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
/************************************************************************************************************************/
}
}