You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

225 lines
9.8 KiB

// Animancer // https://kybernetik.com.au/animancer // Copyright 2021 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A component which uses Animation Events with the Function Name "End" to invoke the
/// <see cref="AnimancerEvent.Sequence.endEvent"/> of the <see cref="AnimancerState"/> that triggered the event.
/// </summary>
/// <remarks>
/// This component must always be attached to the same <see cref="GameObject"/> as the <see cref="Animator"/> in
/// order to receive Animation Events from it.
/// <para></para>
/// Note that Unity will allocate some garbage every time it triggers an Animation Event with an
/// <see cref="AnimationEvent"/> parameter.
/// <para></para>
/// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/events/animation#end-animation-events">End Animation Events</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/EndEventReceiver
///
[AddComponentMenu(Strings.MenuPrefix + "End Event Receiver")]
[HelpURL(Strings.DocsURLs.APIDocumentation + "/" + nameof(EndEventReceiver))]
public class EndEventReceiver : MonoBehaviour
{
/************************************************************************************************************************/
[SerializeField]
private AnimancerComponent _Animancer;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="AnimancerComponent"/> on which the <see cref="AnimancerEvent.Sequence.endEvent"/> will be
/// triggered.
/// </summary>
public ref AnimancerComponent Animancer => ref _Animancer;
/************************************************************************************************************************/
/// <summary>
/// The <see cref="AnimationEvent"/> called 'End' which is currently being triggered.
/// </summary>
public static AnimationEvent CurrentEvent { get; private set; }
/************************************************************************************************************************/
/// <summary>Calls <see cref="End(AnimancerComponent, AnimationEvent)"/>.</summary>
/// <remarks>Called by Animation Events with the Function Name "End".</remarks>
private void End(AnimationEvent animationEvent)
{
End(_Animancer, animationEvent);
}
/// <summary>
/// Tries to invoke the <see cref="AnimancerEvent.Sequence.endEvent"/> of the <see cref="AnimancerState"/> that
/// triggered the `animationEvent`.
/// </summary>
/// <remarks>
/// Note that Unity will allocate some garbage every time it triggers an Animation Event with an
/// <see cref="AnimationEvent"/> parameter.
/// </remarks>
public static bool End(AnimancerComponent animancer, AnimationEvent animationEvent)
{
if (!animancer.IsPlayableInitialized)
{
// This could only happen if another Animator triggers the event on this object somehow.
Debug.LogWarning($"{nameof(AnimationEvent)} '{nameof(End)}' was triggered by {animationEvent.animatorClipInfo.clip}" +
$", but the {nameof(AnimancerComponent)}.{nameof(AnimancerComponent.Playable)} hasn't been initialized.",
animancer);
return false;
}
var layers = animancer.Layers;
var count = layers.Count;
// Try targeting the current state on each layer first.
for (int i = 0; i < count; i++)
{
if (TryInvokeOnEndEventRecursive(layers[i].CurrentState, animationEvent))
return true;
}
// Otherwise try every state.
for (int i = 0; i < count; i++)
{
if (TryInvokeOnEndEventRecursive(layers[i], animationEvent))
return true;
}
if (animationEvent.messageOptions == SendMessageOptions.RequireReceiver)
{
Debug.LogWarning($"{nameof(AnimationEvent)} '{nameof(End)}' was triggered by {animationEvent.animatorClipInfo.clip}" +
$", but no state was found with that {nameof(AnimancerState.Key)}.",
animancer);
}
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Invokes the <see cref="AnimancerEvent.Sequence.OnEnd"/> callback of the state that is playing the animation
/// which triggered the event. Returns true if such a state exists (even if it doesn't have a callback).
/// </summary>
private static bool OnEndEventReceived(AnimancerPlayable animancer, AnimationEvent animationEvent)
{
var layers = animancer.Layers;
var count = layers.Count;
// Try targeting the current state on each layer first.
for (int i = 0; i < count; i++)
{
if (TryInvokeOnEndEventRecursive(layers[i].CurrentState, animationEvent))
return true;
}
// Otherwise try every state.
for (int i = 0; i < count; i++)
{
if (TryInvokeOnEndEventRecursive(layers[i], animationEvent))
return true;
}
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Invokes the <see cref="AnimancerEvent.Sequence.OnEnd"/> callback of the state that is playing the animation
/// which triggered the event. Returns true if such a state exists (even if it doesn't have a callback).
/// </summary>
private static bool TryInvokeOnEndEventRecursive(AnimancerNode node, AnimationEvent animationEvent)
{
var childCount = node.ChildCount;
for (int i = 0; i < childCount; i++)
{
var child = node.GetChild(i);
if (child != null &&
(TryInvokeOnEndEvent(child, animationEvent) ||
TryInvokeOnEndEventRecursive(child, animationEvent)))
return true;
}
return false;
}
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="AnimancerState.Clip"/> and <see cref="AnimancerNode.Weight"/> match the
/// <see cref="AnimationEvent"/>, this method invokes the <see cref="AnimancerEvent.Sequence.OnEnd"/> callback
/// and returns true.
/// </summary>
private static bool TryInvokeOnEndEvent(AnimancerState state, AnimationEvent animationEvent)
{
if (state.Weight != animationEvent.animatorClipInfo.weight ||
state.Clip != animationEvent.animatorClipInfo.clip ||
!state.HasEvents)
return false;
var endEvent = state.Events.endEvent;
if (endEvent.callback != null)
{
Debug.Assert(CurrentEvent == null, $"Recursive call to {nameof(TryInvokeOnEndEvent)} detected");
try
{
CurrentEvent = animationEvent;
endEvent.Invoke(state);
}
finally
{
CurrentEvent = null;
}
}
return true;
}
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="CurrentEvent"/> has a float parameter above 0, this method returns that value.
/// Otherwise this method calls <see cref="AnimancerEvent.GetFadeOutDuration"/> so if you aren't using an
/// Animation Event with the function name "End" you can just call that method directly.
/// </summary>
public static float GetFadeOutDuration(float minDuration)
{
if (CurrentEvent != null && CurrentEvent.floatParameter > 0)
return CurrentEvent.floatParameter;
return AnimancerEvent.GetFadeOutDuration(minDuration);
}
/// <summary>
/// If the <see cref="CurrentEvent"/> has a float parameter above 0, this method returns that value.
/// Otherwise this method calls <see cref="AnimancerEvent.GetFadeOutDuration"/> so if you aren't using an
/// Animation Event with the function name "End" you can just call that method directly.
/// </summary>
/// <remarks>
/// Calls <see cref="GetFadeOutDuration(float)"/> using the
/// <see cref="AnimancerPlayable.DefaultFadeDuration"/>.
/// </remarks>
public static float GetFadeOutDuration()
=> GetFadeOutDuration(AnimancerPlayable.DefaultFadeDuration);
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only]
/// Called when this component is first added in Edit Mode.
/// Tries to automatically find the <see cref="Animancer"/> component using
/// <see cref="AnimancerUtilities.GetComponentInParentOrChildren{T}(GameObject, ref T)"/>.
/// </summary>
protected virtual void Reset()
{
gameObject.GetComponentInParentOrChildren(ref _Animancer);
}
#endif
/************************************************************************************************************************/
}
}