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