// Animancer // Copyright 2020 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
/// .
///
[AddComponentMenu(Strings.MenuPrefix + "Animancer Component")]
[HelpURL(Strings.APIDocumentationURL + "/AnimancerComponent")]
[DefaultExecutionOrder(-5000)]// Initialise before anything else tries to use this component.
public class AnimancerComponent : MonoBehaviour,
IAnimancerComponent, IEnumerable, IEnumerator, IAnimationClipSource, IAnimationClipCollection
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField, Tooltip("The Animator component which this script controls")]
private Animator _Animator;
/// []
/// The component which this script controls.
///
public Animator Animator
{
get { return _Animator; }
set
{
#if UNITY_EDITOR
Editor.AnimancerEditorUtilities.SetIsInspectorExpanded(_Animator, true);
Editor.AnimancerEditorUtilities.SetIsInspectorExpanded(value, false);
#endif
// It doesn't seem to be possible to stop the old Animator from playing the graph.
_Animator = value;
if (IsPlayableInitialised)
_Playable.SetOutput(value, this);
}
}
#if UNITY_EDITOR
/// [Editor-Only] The name of the serialized backing field for the property.
string IAnimancerComponent.AnimatorFieldName { get { return "_Animator"; } }
#endif
/************************************************************************************************************************/
private AnimancerPlayable _Playable;
///
/// The internal system which manages the playing animations.
/// Accessing this property will automatically initialise it.
///
public AnimancerPlayable Playable
{
get
{
InitialisePlayable();
return _Playable;
}
}
/// Indicates whether the has been initialised.
public bool IsPlayableInitialised { get { return _Playable != null && _Playable.IsValid; } }
/************************************************************************************************************************/
/// The states managed by this component.
public AnimancerPlayable.StateDictionary States { get { return Playable.States; } }
/// The layers which each manage their own set of animations.
public AnimancerPlayable.LayerList Layers { get { return Playable.Layers; } }
/// Returns layer 0.
public static implicit operator AnimancerLayer(AnimancerComponent animancer)
{
return animancer.Playable.Layers[0];
}
/************************************************************************************************************************/
[SerializeField, Tooltip("Determines what happens when this component is disabled" +
" or its GameObject becomes inactive (i.e. in OnDisable):" +
"\n- Stop all animations" +
"\n- Pause all animations" +
"\n- Continue playing" +
"\n- Reset to the original values" +
"\n- Destroy all layers and states")]
private DisableAction _ActionOnDisable;
/// []
/// Determines what happens when this component is disabled or its becomes inactive
/// (i.e. in ).
///
/// The default value is .
///
public DisableAction ActionOnDisable
{
get { return _ActionOnDisable; }
set { _ActionOnDisable = value; }
}
/// Determines whether the object will be reset to its original values when disabled.
bool IAnimancerComponent.ResetOnDisable { get { return _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).
///
/// WARNING: this must occur before the receives its OnDisable
/// call, meaning the must be above it in the Inspector or on a child
/// object so that 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.
///
/// Thrown if no is assigned.
public AnimatorUpdateMode UpdateMode
{
get { return _Animator.updateMode; }
set
{
_Animator.updateMode = value;
if (!IsPlayableInitialised)
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 Animator.updateMode to or from AnimatePhysics at runtime will have no effect." +
" You must set it in the Unity Editor or on startup.");
}
#endif
}
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// [Editor-Only]
/// The what was first used when this script initialised.
/// This is used to give a warning when changing to or from at
/// runtime since it won't work correctly.
///
public AnimatorUpdateMode? InitialUpdateMode { get; private set; }
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Animation Events
/************************************************************************************************************************/
// These methods are above their regular overloads so Animation Events find them first (because the others can't be used).
/************************************************************************************************************************/
/// Calls .
/// This method is called by Animation Events.
private void Play(AnimationEvent animationEvent)
{
var clip = (AnimationClip)animationEvent.objectReferenceParameter;
var layerIndex = animationEvent.intParameter;
if (layerIndex < 0)
Play(clip);
else
Layers[layerIndex].Play(clip);
}
///
/// Calls and sets the = 0.
///
/// This method is called by Animation Events.
private void PlayFromStart(AnimationEvent animationEvent)
{
var clip = (AnimationClip)animationEvent.objectReferenceParameter;
var layerIndex = animationEvent.intParameter;
if (layerIndex < 0)
Play(clip).Time = 0;
else
Layers[layerIndex].Play(clip).Time = 0;
}
/// Calls .
/// This method is called by Animation Events.
private void CrossFade(AnimationEvent animationEvent)
{
var clip = (AnimationClip)animationEvent.objectReferenceParameter;
var fadeDuration = animationEvent.floatParameter;
if (fadeDuration <= 0)
fadeDuration = AnimancerPlayable.DefaultFadeDuration;
var layerIndex = animationEvent.intParameter;
if (layerIndex < 0)
Play(clip, fadeDuration);
else
Layers[layerIndex].Play(clip, fadeDuration);
}
/// Calls .
/// This method is called by Animation Events.
private void CrossFadeFromStart(AnimationEvent animationEvent)
{
var clip = (AnimationClip)animationEvent.objectReferenceParameter;
var fadeDuration = animationEvent.floatParameter;
if (fadeDuration <= 0)
fadeDuration = AnimancerPlayable.DefaultFadeDuration;
var layerIndex = animationEvent.intParameter;
if (layerIndex < 0)
Play(clip, fadeDuration, FadeMode.FromStart);
else
Layers[layerIndex].Play(clip, fadeDuration, FadeMode.FromStart);
}
/// Calls .
/// This method is called by Animation Events.
private void Transition(AnimationEvent animationEvent)
{
var transition = (ITransition)animationEvent.objectReferenceParameter;
var layerIndex = animationEvent.intParameter;
if (layerIndex < 0)
Play(transition);
else
Layers[layerIndex].Play(transition);
}
/************************************************************************************************************************/
///
/// Invokes the event of the
/// if it is playing the which triggered the event.
///
/// Logs a warning if no state is registered for that animation.
///
/// This method is called by Animation Events.
private void End(AnimationEvent animationEvent)
{
if (_Playable == null)
{
// This could only happen if another Animator triggers the event on this object somehow.
Debug.LogWarning("AnimationEvent 'End' was triggered by " + animationEvent.animatorClipInfo.clip +
", but the AnimancerComponent.Playable hasn't been initialised.",
this);
return;
}
if (_Playable.OnEndEventReceived(animationEvent))
return;
if (animationEvent.messageOptions == SendMessageOptions.RequireReceiver)
{
Debug.LogWarning("AnimationEvent 'End' was triggered by " + animationEvent.animatorClipInfo.clip +
", but no state was found with that key.",
this);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialisation
/************************************************************************************************************************/
#if UNITY_EDITOR
/// [Editor-Only]
/// Called by the Unity Editor when this component is first added (in Edit Mode) and whenever the Reset command
/// is executed from its context menu.
///
/// Destroys the playable if one has been initialised.
/// Searches for an on this object, or it's children or parents.
/// Removes the if it finds one.
///
/// This method also prevents you from adding multiple copies of this component to a single object. Doing so
/// will destroy the new one immediately and change the old one's type to match the new one, allowing you to
/// change the type without losing the values of any serialized fields they share.
///
protected virtual void Reset()
{
OnDestroy();
_Animator = Editor.AnimancerEditorUtilities.GetComponentInHierarchy(gameObject);
if (_Animator != null)
{
_Animator.runtimeAnimatorController = null;
Editor.AnimancerEditorUtilities.SetIsInspectorExpanded(_Animator, false);
// Collapse the Animator property because the custom Inspector uses that to control whether the
// Animator's Inspector is expanded.
using (var serializedObject = new UnityEditor.SerializedObject(this))
{
var property = serializedObject.FindProperty("_Animator");
property.isExpanded = false;
serializedObject.ApplyModifiedProperties();
}
}
AnimancerUtilities.IfMultiComponentThenChangeType(this);
}
#endif
/************************************************************************************************************************/
///
/// Called by Unity when this component becomes enabled and active.
///
/// Ensures that the is playing.
///
protected virtual void OnEnable()
{
if (IsPlayableInitialised)
_Playable.UnpauseGraph();
}
///
/// Called by Unity when this component becomes disabled or inactive. Acts according to the
/// .
///
protected virtual void OnDisable()
{
if (!IsPlayableInitialised)
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,
"DisableAction.Reset failed because the Animator is not enabled." +
" This most likely means you are disabling the GameObject and the Animator is above the" +
" 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 DisableAction.Stop and call Animator.Rebind" +
" manually before disabling the GameObject.",
this);
Stop();
_Animator.Rebind();
_Playable.PauseGraph();
break;
case DisableAction.Destroy:
_Playable.Destroy();
_Playable = null;
break;
default:
throw new ArgumentOutOfRangeException("ActionOnDisable");
}
}
/************************************************************************************************************************/
/// Creates a new if it doesn't already exist.
private void InitialisePlayable()
{
if (IsPlayableInitialised)
return;
#if UNITY_EDITOR
var currentEvent = Event.current;
if (currentEvent != null && (currentEvent.type == EventType.Layout || currentEvent.type == EventType.Repaint))
Debug.LogWarning("Creating an AnimancerPlayable during a " + currentEvent.type + " event is likely undesirable.");
#endif
if (_Animator == null)
_Animator = GetComponent();
AnimancerPlayable.SetNextGraphName(name + ".Animancer");
_Playable = AnimancerPlayable.Create();
_Playable.SetOutput(_Animator, this);
#if UNITY_EDITOR
if (_Animator != null)
InitialUpdateMode = UpdateMode;
#endif
}
/************************************************************************************************************************/
///
/// Called by Unity when this component is destroyed.
/// Ensures that the is properly cleaned up.
///
protected virtual void OnDestroy()
{
if (IsPlayableInitialised)
{
_Playable.Destroy();
_Playable = null;
}
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/// [Editor-Only]
/// Ensures that the is destroyed.
///
~AnimancerComponent()
{
if (_Playable != null)
Editor.AnimancerEditorUtilities.EditModeDelayCall(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 user, such as in . It can be overridden by
/// child classes to use something else as the key.
///
public virtual object GetKey(AnimationClip clip)
{
return clip;
}
/************************************************************************************************************************/
///
/// Stops all other animations, 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, layerIndex).Time = 0;.
///
public AnimancerState Play(AnimationClip clip)
{
return Play(States.GetOrCreate(clip));
}
///
/// Stops all other animations, 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;.
///
public AnimancerState Play(AnimancerState state)
{
return Playable.Play(state);
}
///
/// Creates a state for the `transition` if it didn't already exist, then calls
/// or
/// depending on .
///
public AnimancerState Play(ITransition transition)
{
return Playable.Play(transition);
}
///
/// Stops all other animations, plays the animation registered with the `key`, and returns that
/// state. If no state is registered with the `key`, this method does nothing and returns null.
///
/// The animation will continue playing from its current .
/// To restart it from the beginning you can use ...Play(key).Time = 0;.
///
public AnimancerState Play(object key)
{
return Playable.Play(key);
}
/************************************************************************************************************************/
///
/// Starts fading in the `clip` over the course of the `fadeDuration` while fading out all others in the same
/// layer. 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 `clip`.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build.
///
public AnimancerState Play(AnimationClip clip, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed)
{
return Play(States.GetOrCreate(clip), fadeDuration, mode);
}
///
/// Starts fading in the `state` over the course of the `fadeDuration` while fading out all others in the same
/// layer. 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`.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build.
///
public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed)
{
return Playable.Play(state, fadeDuration, mode);
}
///
/// Creates a state for the `transition` if it didn't already exist, then calls
/// or
/// depending on .
///
public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed)
{
return Playable.Play(transition, fadeDuration, mode);
}
///
/// Starts fading in the animation registered with the `key` over the course of the `fadeDuration` while fading
/// out all others in the same layer. Returns the animation's state (or null if none was registered).
///
/// 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.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build.
///
public AnimancerState Play(object key, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed)
{
return Playable.Play(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)
{
return Stop(GetKey(clip));
}
///
/// Gets the state registered with the , stops and rewinds it to the start, then
/// returns it.
///
public AnimancerState Stop(IHasKey hasKey)
{
if (_Playable != null)
return _Playable.Stop(hasKey);
else
return null;
}
///
/// Gets the state associated with the `key`, stops and rewinds it to the start, then returns it.
///
public AnimancerState Stop(object key)
{
if (_Playable != null)
return _Playable.Stop(key);
else
return null;
}
///
/// Stops all animations and rewinds them to the start.
///
public void Stop()
{
if (_Playable != null)
_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)
{
return IsPlaying(GetKey(clip));
}
///
/// Returns true if a state is registered with the and it is currently playing.
///
public bool IsPlaying(IHasKey hasKey)
{
if (_Playable != null)
return _Playable.IsPlaying(hasKey);
else
return false;
}
///
/// Returns true if a state is registered with the `key` and it is currently playing.
///
public bool IsPlaying(object key)
{
if (_Playable != null)
return _Playable.IsPlaying(key);
else
return false;
}
///
/// Returns true if at least one animation is being played.
///
public bool IsPlaying()
{
if (_Playable != null)
return _Playable.IsPlaying();
else
return false;
}
/************************************************************************************************************************/
///
/// 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)
{
if (_Playable != null)
return _Playable.IsPlayingClip(clip);
else
return false;
}
/************************************************************************************************************************/
///
/// Evaluates all of the currently playing animations to apply their states to the animated objects.
///
public void Evaluate()
{
Playable.Evaluate();
}
///
/// Advances all currently playing animations by the specified amount of time (in seconds) and evaluates the
/// graph to apply their states 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)
{
return key.IsPlaying;
}
/************************************************************************************************************************/
#endif
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
// IEnumerable for 'foreach' statements.
/************************************************************************************************************************/
///
/// Returns an enumerator that will iterate through all states in each layer (not states inside mixers).
///
public IEnumerator GetEnumerator()
{
if (!IsPlayableInitialised)
yield break;
foreach (var state in _Playable.Layers.GetAllStateEnumerable())
yield return state;
}
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
/************************************************************************************************************************/
// 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 (!IsPlayableInitialised)
return false;
return ((IEnumerator)_Playable).MoveNext();
}
/// Returns null.
object IEnumerator.Current { get { return null; } }
#pragma warning disable UNT0006 // Incorrect message signature.
/// Does nothing.
void IEnumerator.Reset() { }
#pragma warning restore UNT0006 // Incorrect message signature.
/************************************************************************************************************************/
/// []
/// 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 (IsPlayableInitialised)
_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
/************************************************************************************************************************/
}
}