// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
namespace Animancer
{
///
/// The main component through which other scripts can interact with . It allows you to play
/// animations on an without using a .
///
///
/// This class can be used as a custom yield instruction to wait until all animations finish playing.
///
/// This class is mostly just a wrapper that connects an to an
/// .
///
/// Documentation: Component Types
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerComponent
///
[AddComponentMenu(Strings.MenuPrefix + "Animancer Component")]
[HelpURL(Strings.DocsURLs.APIDocumentation + "/" + nameof(AnimancerComponent))]
[DefaultExecutionOrder(DefaultExecutionOrder)]
public class AnimancerComponent : MonoBehaviour,
IAnimancerComponent, IEnumerator, IAnimationClipSource, IAnimationClipCollection
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
/// Initialize before anything else tries to use this component.
public const int DefaultExecutionOrder = -5000;
/************************************************************************************************************************/
[SerializeField, Tooltip("The Animator component which this script controls")]
private Animator _Animator;
/// []
/// The component which this script controls.
///
public Animator Animator
{
get => _Animator;
set
{
_Animator = value;
if (IsPlayableInitialized)
{
_Playable.DestroyOutput();
_Playable.CreateOutput(value, this);
}
}
}
#if UNITY_EDITOR
/// [Editor-Only] The name of the serialized backing field for the property.
string IAnimancerComponent.AnimatorFieldName => nameof(_Animator);
#endif
/************************************************************************************************************************/
private AnimancerPlayable _Playable;
///
/// The internal system which manages the playing animations.
/// Accessing this property will automatically initialize it.
///
public AnimancerPlayable Playable
{
get
{
InitializePlayable();
return _Playable;
}
}
/// Indicates whether the has been initialized.
public bool IsPlayableInitialized
=> _Playable != null && _Playable.IsValid;
/************************************************************************************************************************/
/// The states managed by this component.
public AnimancerPlayable.StateDictionary States
=> Playable.States;
/// The layers which each manage their own set of animations.
public AnimancerPlayable.LayerList Layers
=> Playable.Layers;
/// Returns the .
public static implicit operator AnimancerPlayable(AnimancerComponent animancer)
=> animancer.Playable;
/// Returns layer 0.
public static implicit operator AnimancerLayer(AnimancerComponent animancer)
=> animancer.Playable.Layers[0];
/************************************************************************************************************************/
[SerializeField, Tooltip("Determines what happens when this component is disabled" +
" or its " + nameof(GameObject) + " becomes inactive (i.e. in OnDisable):" +
"\n• " + nameof(DisableAction.Stop) + " all animations" +
"\n• " + nameof(DisableAction.Pause) + " all animations" +
"\n• " + nameof(DisableAction.Continue) + " playing" +
"\n• " + nameof(DisableAction.Reset) + " to the original values" +
"\n• " + nameof(DisableAction.Destroy) + " all layers and states")]
private DisableAction _ActionOnDisable;
#if UNITY_EDITOR
/// [Editor-Only]
/// The name of the serialized backing field for the property.
///
string IAnimancerComponent.ActionOnDisableFieldName
=> nameof(_ActionOnDisable);
#endif
/// []
/// Determines what happens when this component is disabled or its becomes inactive
/// (i.e. in ).
///
/// The default value is .
public ref DisableAction ActionOnDisable
=> ref _ActionOnDisable;
///
bool IAnimancerComponent.ResetOnDisable
=> _ActionOnDisable == DisableAction.Reset;
///
/// An action to perform when disabling an . See .
///
public enum DisableAction
{
///
/// Stop all animations and rewind them, but leave all animated values as they are (unlike
/// ).
///
/// Calls and .
Stop,
/// Pause all animations in their current state so they can resume later.
/// Calls .
Pause,
/// Keep playing while inactive.
Continue,
///
/// Stop all animations, rewind them, and force the object back into its original state (often called the
/// bind pose).
///
///
/// The must be either above the in
/// the Inspector or on a child object so that so that this gets called first.
///
/// Calls , , and .
///
Reset,
///
/// Destroy the and all its layers and states. This means that any layers or
/// states referenced by other scripts will no longer be valid so they will need to be recreated if you
/// want to use this object again.
///
/// Calls .
Destroy,
}
/************************************************************************************************************************/
#region Update Mode
/************************************************************************************************************************/
///
/// Determines when animations are updated and which time source is used. This property is mainly a wrapper
/// around the .
///
/// Note that changing to or from at runtime has no effect.
/// No is assigned.
public AnimatorUpdateMode UpdateMode
{
get => _Animator.updateMode;
set
{
_Animator.updateMode = value;
if (!IsPlayableInitialized)
return;
// UnscaledTime on the Animator is actually identical to Normal when using the Playables API so we need
// to set the graph's DirectorUpdateMode to determine how it gets its delta time.
_Playable.UpdateMode = value == AnimatorUpdateMode.UnscaledTime ?
DirectorUpdateMode.UnscaledGameTime :
DirectorUpdateMode.GameTime;
#if UNITY_EDITOR
if (InitialUpdateMode == null)
{
InitialUpdateMode = value;
}
else if (UnityEditor.EditorApplication.isPlaying)
{
if (AnimancerPlayable.HasChangedToOrFromAnimatePhysics(InitialUpdateMode, value))
Debug.LogWarning($"Changing the {nameof(Animator)}.{nameof(Animator.updateMode)} to or from " +
#if UNITY_2023_1_OR_NEWER
$"{nameof(AnimatorUpdateMode.Fixed)}" +
#else
$"{nameof(AnimatorUpdateMode.AnimatePhysics)}" +
#endif
$" at runtime will have no effect." +
" You must set it in the Unity Editor or on startup.", this);
}
#endif
}
}
/************************************************************************************************************************/
#if UNITY_EDITOR
///
public AnimatorUpdateMode? InitialUpdateMode { get; private set; }
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
#if UNITY_EDITOR
/// [Editor-Only]
/// Destroys the if it was initialized and searches for an on
/// this object, or it's children or parents.
///
protected virtual void Reset()
{
OnDestroy();
gameObject.GetComponentInParentOrChildren(ref _Animator);
}
#endif
/************************************************************************************************************************/
/// Ensures that the is playing.
protected virtual void OnEnable()
{
if (IsPlayableInitialized)
_Playable.UnpauseGraph();
}
/// Acts according to the .
protected virtual void OnDisable()
{
if (!IsPlayableInitialized)
return;
switch (_ActionOnDisable)
{
case DisableAction.Stop:
Stop();
_Playable.PauseGraph();
break;
case DisableAction.Pause:
_Playable.PauseGraph();
break;
case DisableAction.Continue:
break;
case DisableAction.Reset:
Debug.Assert(_Animator.isActiveAndEnabled,
$"{nameof(DisableAction)}.{nameof(DisableAction.Reset)} failed because the {nameof(Animator)} is not enabled." +
$" This most likely means you are disabling the {nameof(GameObject)} and the {nameof(Animator)} is above the" +
$" {nameof(AnimancerComponent)} in the Inspector so it got disabled right before this method was called." +
$" See the Inspector of {this} to fix the issue" +
$" or use {nameof(DisableAction)}.{nameof(DisableAction.Stop)}" +
$" and call {nameof(Animator)}.{nameof(Animator.Rebind)} manually" +
$" before disabling the {nameof(GameObject)}.",
this);
Stop();
_Animator.Rebind();
_Playable.PauseGraph();
break;
case DisableAction.Destroy:
_Playable.DestroyGraph();
_Playable = null;
break;
default:
throw new ArgumentOutOfRangeException(nameof(ActionOnDisable));
}
}
/************************************************************************************************************************/
/// Creates and initializes the if it wasn't already initialized.
public void InitializePlayable()
{
if (IsPlayableInitialized)
return;
TryGetAnimator();
AnimancerPlayable.SetNextGraphName(name + " (Animancer)");
_Playable = AnimancerPlayable.Create();
_Playable.CreateOutput(_Animator, this);
OnInitializePlayable();
}
/************************************************************************************************************************/
/// Sets the and connects it to the .
///
/// The is already initialized.
/// You must call before re-initializing it.
///
public void InitializePlayable(AnimancerPlayable playable)
{
if (IsPlayableInitialized)
throw new InvalidOperationException($"The {nameof(AnimancerPlayable)} is already initialized." +
$" Either call this method before anything else uses it or call" +
$" animancerComponent.{nameof(Playable)}.{nameof(AnimancerPlayable.DestroyGraph)}" +
$" before re-initializing it.");
TryGetAnimator();
_Playable = playable;
_Playable.CreateOutput(_Animator, this);
OnInitializePlayable();
}
/************************************************************************************************************************/
/// Called right after the is initialized.
protected virtual void OnInitializePlayable()
{
#if UNITY_ASSERTIONS
ValidatePlayableInitialization();
#endif
}
/************************************************************************************************************************/
///
/// Tries to ensure that an is present using
/// if necessary.
///
public bool TryGetAnimator()
=> _Animator != null
|| TryGetComponent(out _Animator);
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// [Assert-Only] Validates various conditions relating to initialization.
private void ValidatePlayableInitialization()
{
#if UNITY_EDITOR
if (_Animator != null)
InitialUpdateMode = UpdateMode;
if (OptionalWarning.CreateGraphDuringGuiEvent.IsEnabled())
{
var currentEvent = Event.current;
if (currentEvent != null)
{
if (currentEvent.type == EventType.Layout ||
currentEvent.type == EventType.Repaint)
{
OptionalWarning.CreateGraphDuringGuiEvent.Log(
$"An {nameof(AnimancerPlayable)} is being initialized during a {currentEvent.type} event" +
$" which is likely undesirable.", this);
}
}
}
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
#endif
{
if (!gameObject.activeInHierarchy)
OptionalWarning.CreateGraphWhileDisabled.Log(
$"An {nameof(AnimancerPlayable)} is being created for '{this}'" +
$" which is attached to an inactive {nameof(GameObject)}." +
$" If that object is never activated then Unity will not call {nameof(OnDestroy)}" +
$" so {nameof(AnimancerPlayable)}.{nameof(AnimancerPlayable.DestroyGraph)}" +
$" will need to be called manually.", this);
}
if (_Animator != null)
{
if (!_Animator.enabled)
OptionalWarning.AnimatorDisabled.Log(Strings.AnimatorDisabledMessage, this);
if (_Animator.isHuman &&
_Animator.runtimeAnimatorController != null)
OptionalWarning.NativeControllerHumanoid.Log($"An Animator Controller is assigned to the" +
$" {nameof(Animator)} component but the Rig is Humanoid so it can't be blended with Animancer." +
$" See the documentation for more information: {Strings.DocsURLs.AnimatorControllersNative}", this);
}
}
#endif
/************************************************************************************************************************/
/// Ensures that the is properly cleaned up.
protected virtual void OnDestroy()
{
if (IsPlayableInitialized)
{
_Playable.DestroyGraph();
_Playable = null;
}
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/// [Editor-Only]
/// Ensures that the is destroyed in Edit Mode, but not in Play Mode since we want
/// to let Unity complain if that happens.
///
~AnimancerComponent()
{
if (_Playable != null)
{
UnityEditor.EditorApplication.delayCall += () =>
{
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
OnDestroy();
};
}
}
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Play Management
/************************************************************************************************************************/
/// Returns the `clip` itself.
///
/// This method is used to determine the dictionary key to use for an animation when none is specified by the
/// caller, such as in .
///
public virtual object GetKey(AnimationClip clip)
=> clip;
/************************************************************************************************************************/
// Play Immediately.
/************************************************************************************************************************/
/// Stops all other animations on the same layer, plays the `clip`, and returns its state.
///
/// The animation will continue playing from its current .
/// To restart it from the beginning you can use ...Play(clip).Time = 0;.
///
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
///
public AnimancerState Play(AnimationClip clip)
=> Playable.Play(States.GetOrCreate(clip));
/// Stops all other animations on the same layer, plays the `state`, and returns it.
///
/// The animation will continue playing from its current .
/// To restart it from the beginning you can use ...Play(state).Time = 0;.
///
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
///
public AnimancerState Play(AnimancerState state)
=> Playable.Play(state);
/************************************************************************************************************************/
// Cross Fade.
/************************************************************************************************************************/
///
/// Starts fading in the `clip` while fading out all other states in the same layer over the course of the
/// `fadeDuration`. Returns its state.
///
///
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
/// method will allow it to complete the existing fade rather than starting a slower one.
///
/// If the layer currently has 0 , this method will fade in the layer itself
/// and simply the `state`.
///
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.
///
public AnimancerState Play(AnimationClip clip, float fadeDuration, FadeMode mode = default)
=> Playable.Play(States.GetOrCreate(clip), fadeDuration, mode);
///
/// Starts fading in the `state` while fading out all others in the same layer over the course of the
/// `fadeDuration`. Returns the `state`.
///
///
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
/// method will allow it to complete the existing fade rather than starting a slower one.
///
/// If the layer currently has 0 , this method will fade in the layer itself
/// and simply the `state`.
///
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.
///
public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = default)
=> Playable.Play(state, fadeDuration, mode);
/************************************************************************************************************************/
// Transition.
/************************************************************************************************************************/
///
/// Creates a state for the `transition` if it didn't already exist, then calls
/// or
/// depending on .
///
///
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
///
public AnimancerState Play(ITransition transition)
=> Playable.Play(transition);
///
/// Creates a state for the `transition` if it didn't already exist, then calls
/// or
/// depending on .
///
///
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
///
public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = default)
=> Playable.Play(transition, fadeDuration, mode);
/************************************************************************************************************************/
// Try Play.
/************************************************************************************************************************/
///
/// Stops all other animations on the same layer, plays the animation registered with the `key`, and returns
/// that state. Or if no state is registered with that `key`, this method does nothing and returns null.
///
///
/// The animation will continue playing from its current .
/// If you wish to force it back to the start, you can simply set the returned state's time to 0.
///
/// This method is safe to call repeatedly without checking whether the animation was already playing.
///
/// The `key` is null.
public AnimancerState TryPlay(object key)
=> Playable.TryPlay(key);
///
/// Starts fading in the animation registered with the `key` while fading out all others in the same layer
/// over the course of the `fadeDuration`. Or if no state is registered with that `key`, this method does
/// nothing and returns null.
///
///
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
/// method will allow it to complete the existing fade rather than starting a slower one.
///
/// If the layer currently has 0 , this method will fade in the layer itself
/// and simply the `state`.
///
/// This method is safe to call repeatedly without checking whether the animation was already playing.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.
///
/// The `key` is null.
public AnimancerState TryPlay(object key, float fadeDuration, FadeMode mode = default)
=> Playable.TryPlay(key, fadeDuration, mode);
/************************************************************************************************************************/
///
/// Gets the state associated with the `clip`, stops and rewinds it to the start, then returns it.
///
public AnimancerState Stop(AnimationClip clip)
=> Stop(GetKey(clip));
///
/// Gets the state registered with the , stops and rewinds it to the start, then
/// returns it.
///
public AnimancerState Stop(IHasKey hasKey)
=> _Playable?.Stop(hasKey);
///
/// Gets the state associated with the `key`, stops and rewinds it to the start, then returns it.
///
public AnimancerState Stop(object key)
=> _Playable?.Stop(key);
/// Stops all animations and rewinds them to the start.
public void Stop()
{
if (IsPlayableInitialized)
_Playable.Stop();
}
/************************************************************************************************************************/
///
/// Returns true if a state is registered for the `clip` and it is currently playing.
///
/// The actual dictionary key is determined using .
///
public bool IsPlaying(AnimationClip clip)
=> IsPlaying(GetKey(clip));
///
/// Returns true if a state is registered with the and it is currently playing.
///
public bool IsPlaying(IHasKey hasKey)
=> IsPlayableInitialized
&& _Playable.IsPlaying(hasKey);
///
/// Returns true if a state is registered with the `key` and it is currently playing.
///
public bool IsPlaying(object key)
=> IsPlayableInitialized
&& _Playable.IsPlaying(key);
///
/// Returns true if at least one animation is being played.
///
public bool IsPlaying()
=> IsPlayableInitialized
&& _Playable.IsPlaying();
/************************************************************************************************************************/
///
/// Returns true if the `clip` is currently being played by at least one state.
///
/// This method is inefficient because it searches through every state to find any that are playing the `clip`,
/// unlike which only checks the state registered using the `clip`s key.
///
public bool IsPlayingClip(AnimationClip clip)
=> IsPlayableInitialized
&& _Playable.IsPlayingClip(clip);
/************************************************************************************************************************/
///
/// Immediately applies the current states of all animations to the animated objects.
///
public void Evaluate()
=> Playable.Evaluate();
///
/// Advances time by the specified value (in seconds) and immediately applies the current states of all
/// animations to the animated objects.
///
public void Evaluate(float deltaTime)
=> Playable.Evaluate(deltaTime);
/************************************************************************************************************************/
#region Key Error Methods
#if UNITY_EDITOR
/************************************************************************************************************************/
// These are overloads of other methods that take a System.Object key to ensure the user doesn't try to use an
// AnimancerState as a key, since the whole point of a key is to identify a state in the first place.
/************************************************************************************************************************/
/// [Warning]
/// You should not use an as a key.
/// Just call .
///
[Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Stop().", true)]
public AnimancerState Stop(AnimancerState key)
{
key.Stop();
return key;
}
/// [Warning]
/// You should not use an as a key.
/// Just check .
///
[Obsolete("You should not use an AnimancerState as a key. Just check AnimancerState.IsPlaying.", true)]
public bool IsPlaying(AnimancerState key)
=> key.IsPlaying;
/************************************************************************************************************************/
#endif
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
// IEnumerator for yielding in a coroutine to wait until all animations have stopped.
/************************************************************************************************************************/
///
/// Determines if any animations are still playing so this object can be used as a custom yield instruction.
///
bool IEnumerator.MoveNext()
{
if (!IsPlayableInitialized)
return false;
return ((IEnumerator)_Playable).MoveNext();
}
/// Returns null.
object IEnumerator.Current => null;
/// Does nothing.
void IEnumerator.Reset() { }
/************************************************************************************************************************/
/// []
/// Calls .
///
public void GetAnimationClips(List clips)
{
var set = ObjectPool.AcquireSet();
set.UnionWith(clips);
GatherAnimationClips(set);
clips.Clear();
clips.AddRange(set);
ObjectPool.Release(set);
}
/************************************************************************************************************************/
/// []
/// Gathers all the animations in the .
///
/// In the Unity Editor this method also gathers animations from other components on parent and child objects.
///
public virtual void GatherAnimationClips(ICollection clips)
{
if (IsPlayableInitialized)
_Playable.GatherAnimationClips(clips);
#if UNITY_EDITOR
Editor.AnimationGatherer.GatherFromGameObject(gameObject, clips);
if (_Animator != null && _Animator.gameObject != gameObject)
Editor.AnimationGatherer.GatherFromGameObject(_Animator.gameObject, clips);
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}