// Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Playables; namespace Animancer { /// <summary> /// The main component through which other scripts can interact with <see cref="Animancer"/>. It allows you to play /// animations on an <see cref="UnityEngine.Animator"/> without using a <see cref="RuntimeAnimatorController"/>. /// </summary> /// <remarks> /// This class can be used as a custom yield instruction to wait until all animations finish playing. /// <para></para> /// This class is mostly just a wrapper that connects an <see cref="AnimancerPlayable"/> to an /// <see cref="UnityEngine.Animator"/>. /// <para></para> /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/playing/component-types">Component Types</see> /// </remarks> /// 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 /************************************************************************************************************************/ /// <summary>Initialize before anything else tries to use this component.</summary> public const int DefaultExecutionOrder = -5000; /************************************************************************************************************************/ [SerializeField, Tooltip("The Animator component which this script controls")] private Animator _Animator; /// <summary>[<see cref="SerializeField"/>] /// The <see cref="UnityEngine.Animator"/> component which this script controls. /// </summary> public Animator Animator { get => _Animator; set { _Animator = value; if (IsPlayableInitialized) { _Playable.DestroyOutput(); _Playable.CreateOutput(value, this); } } } #if UNITY_EDITOR /// <summary>[Editor-Only] The name of the serialized backing field for the <see cref="Animator"/> property.</summary> string IAnimancerComponent.AnimatorFieldName => nameof(_Animator); #endif /************************************************************************************************************************/ private AnimancerPlayable _Playable; /// <summary> /// The internal system which manages the playing animations. /// Accessing this property will automatically initialize it. /// </summary> public AnimancerPlayable Playable { get { InitializePlayable(); return _Playable; } } /// <summary>Indicates whether the <see cref="Playable"/> has been initialized.</summary> public bool IsPlayableInitialized => _Playable != null && _Playable.IsValid; /************************************************************************************************************************/ /// <summary>The states managed by this component.</summary> public AnimancerPlayable.StateDictionary States => Playable.States; /// <summary>The layers which each manage their own set of animations.</summary> public AnimancerPlayable.LayerList Layers => Playable.Layers; /// <summary>Returns the <see cref="Playable"/>.</summary> public static implicit operator AnimancerPlayable(AnimancerComponent animancer) => animancer.Playable; /// <summary>Returns layer 0.</summary> 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 " + nameof(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 /// <summary>[Editor-Only] The name of the serialized backing field for the <see cref="ActionOnDisable"/> property.</summary> string IAnimancerComponent.ActionOnDisableFieldName => nameof(_ActionOnDisable); #endif /// <summary>[<see cref="SerializeField"/>] /// Determines what happens when this component is disabled or its <see cref="GameObject"/> becomes inactive /// (i.e. in <see cref="OnDisable"/>). /// </summary> /// <remarks>The default value is <see cref="DisableAction.Stop"/>.</remarks> public ref DisableAction ActionOnDisable => ref _ActionOnDisable; /// <inheritdoc/> bool IAnimancerComponent.ResetOnDisable => _ActionOnDisable == DisableAction.Reset; /// <summary> /// An action to perform when disabling an <see cref="AnimancerComponent"/>. See <see cref="ActionOnDisable"/>. /// </summary> public enum DisableAction { /// <summary> /// Stop all animations and rewind them, but leave all animated values as they are (unlike /// <see cref="Reset"/>). /// </summary> /// <remarks>Calls <see cref="Stop()"/> and <see cref="AnimancerPlayable.PauseGraph"/>.</remarks> Stop, /// <summary>Pause all animations in their current state so they can resume later.</summary> /// <remarks>Calls <see cref="AnimancerPlayable.PauseGraph"/>.</remarks> Pause, /// <summary>Keep playing while inactive.</summary> Continue, /// <summary> /// Stop all animations, rewind them, and force the object back into its original state (often called the /// bind pose). /// </summary> /// <remarks> /// The <see cref="AnimancerComponent"/> must be either above the <see cref="UnityEngine.Animator"/> in /// the Inspector or on a child object so that so that this <see cref="OnDisable"/> gets called first. /// <para></para> /// Calls <see cref="Stop()"/>, <see cref="Animator.Rebind"/>, and <see cref="AnimancerPlayable.PauseGraph"/>. /// </remarks> Reset, /// <summary> /// Destroy the <see cref="PlayableGraph"/> 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. /// </summary> /// <remarks>Calls <see cref="AnimancerPlayable.DestroyGraph()"/>.</remarks> Destroy, } /************************************************************************************************************************/ #region Update Mode /************************************************************************************************************************/ /// <summary> /// Determines when animations are updated and which time source is used. This property is mainly a wrapper /// around the <see cref="Animator.updateMode"/>. /// </summary> /// <remarks>Note that changing to or from <see cref="AnimatorUpdateMode.AnimatePhysics"/> at runtime has no effect.</remarks> /// <exception cref="NullReferenceException">No <see cref="Animator"/> is assigned.</exception> 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 {nameof(AnimatorUpdateMode.AnimatePhysics)} at runtime will have no effect." + " You must set it in the Unity Editor or on startup.", this); } #endif } } /************************************************************************************************************************/ #if UNITY_EDITOR /// <inheritdoc/> public AnimatorUpdateMode? InitialUpdateMode { get; private set; } #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Initialization /************************************************************************************************************************/ #if UNITY_EDITOR /// <summary>[Editor-Only] /// Destroys the <see cref="Playable"/> if it was initialized and searches for an <see cref="Animator"/> on /// this object, or it's children or parents. /// </summary> protected virtual void Reset() { OnDestroy(); gameObject.GetComponentInParentOrChildren(ref _Animator); } #endif /************************************************************************************************************************/ /// <summary>Ensures that the <see cref="PlayableGraph"/> is playing.</summary> protected virtual void OnEnable() { if (IsPlayableInitialized) _Playable.UnpauseGraph(); } /// <summary>Acts according to the <see cref="ActionOnDisable"/>.</summary> 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)); } } /************************************************************************************************************************/ /// <summary>Creates a new <see cref="AnimancerPlayable"/> if it doesn't already exist.</summary> public void InitializePlayable() { if (IsPlayableInitialized) return; if (_Animator == null) _Animator = GetComponent<Animator>(); #if UNITY_ASSERTIONS ValidatePlayableInitialization(); #endif AnimancerPlayable.SetNextGraphName(name + " (Animancer)"); _Playable = AnimancerPlayable.Create(); _Playable.CreateOutput(_Animator, this); #if UNITY_EDITOR if (_Animator != null) InitialUpdateMode = UpdateMode; #endif } /************************************************************************************************************************/ /// <summary>Creates a new <see cref="AnimancerPlayable"/> in the specified `graph`.</summary> /// <exception cref="InvalidOperationException"> /// The <see cref="AnimancerPlayable"/> is already initialized. /// You must call <see cref="AnimancerPlayable.DestroyGraph"/> before re-initializing it. /// </exception> public void InitializePlayable(PlayableGraph graph) { 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."); if (_Animator == null) _Animator = GetComponent<Animator>(); #if UNITY_ASSERTIONS ValidatePlayableInitialization(); #endif _Playable = AnimancerPlayable.Create(graph); _Playable.CreateOutput(_Animator, this); #if UNITY_EDITOR if (_Animator != null) InitialUpdateMode = UpdateMode; #endif } /************************************************************************************************************************/ #if UNITY_ASSERTIONS /// <summary>Validates various conditions relating to <see cref="AnimancerPlayable"/> initialization.</summary> private void ValidatePlayableInitialization() { #if UNITY_EDITOR if (OptionalWarning.CreateGraphDuringGuiEvent.IsEnabled()) { var currentEvent = Event.current; if (currentEvent != null && (currentEvent.type == EventType.Layout || currentEvent.type == EventType.Repaint)) OptionalWarning.CreateGraphDuringGuiEvent.Log( $"An {nameof(AnimancerPlayable)} is being created 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 && _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 /************************************************************************************************************************/ /// <summary>Ensures that the <see cref="Playable"/> is properly cleaned up.</summary> protected virtual void OnDestroy() { if (IsPlayableInitialized) { _Playable.DestroyGraph(); _Playable = null; } } /************************************************************************************************************************/ #if UNITY_EDITOR /// <summary>[Editor-Only] /// Ensures that the <see cref="AnimancerPlayable"/> is destroyed in Edit Mode, but not in Play Mode since we want /// to let Unity complain if that happens. /// </summary> ~AnimancerComponent() { if (_Playable != null) { UnityEditor.EditorApplication.delayCall += () => { if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) OnDestroy(); }; } } #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Play Management /************************************************************************************************************************/ /// <summary>Returns the `clip` itself.</summary> /// <remarks> /// This method is used to determine the dictionary key to use for an animation when none is specified by the /// caller, such as in <see cref="Play(AnimationClip)"/>. /// </remarks> public virtual object GetKey(AnimationClip clip) => clip; /************************************************************************************************************************/ // Play Immediately. /************************************************************************************************************************/ /// <summary>Stops all other animations on the same layer, plays the `clip`, and returns its state.</summary> /// <remarks> /// The animation will continue playing from its current <see cref="AnimancerState.Time"/>. /// To restart it from the beginning you can use <c>...Play(clip).Time = 0;</c>. /// <para></para> /// This method is safe to call repeatedly without checking whether the `clip` was already playing. /// </remarks> public AnimancerState Play(AnimationClip clip) => Playable.Play(States.GetOrCreate(clip)); /// <summary>Stops all other animations on the same layer, plays the `state`, and returns it.</summary> /// <remarks> /// The animation will continue playing from its current <see cref="AnimancerState.Time"/>. /// To restart it from the beginning you can use <c>...Play(state).Time = 0;</c>. /// <para></para> /// This method is safe to call repeatedly without checking whether the `state` was already playing. /// </remarks> public AnimancerState Play(AnimancerState state) => Playable.Play(state); /************************************************************************************************************************/ // Cross Fade. /************************************************************************************************************************/ /// <summary> /// Starts fading in the `clip` while fading out all other states in the same layer over the course of the /// `fadeDuration`. Returns its state. /// </summary> /// <remarks> /// 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. /// <para></para> /// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself /// and simply <see cref="AnimancerState.Play"/> the `state`. /// <para></para> /// This method is safe to call repeatedly without checking whether the `clip` was already playing. /// <para></para> /// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em> /// </remarks> public AnimancerState Play(AnimationClip clip, float fadeDuration, FadeMode mode = default) => Playable.Play(States.GetOrCreate(clip), fadeDuration, mode); /// <summary> /// Starts fading in the `state` while fading out all others in the same layer over the course of the /// `fadeDuration`. Returns the `state`. /// </summary> /// <remarks> /// 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. /// <para></para> /// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself /// and simply <see cref="AnimancerState.Play"/> the `state`. /// <para></para> /// This method is safe to call repeatedly without checking whether the `state` was already playing. /// <para></para> /// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em> /// </remarks> public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = default) => Playable.Play(state, fadeDuration, mode); /************************************************************************************************************************/ // Transition. /************************************************************************************************************************/ /// <summary> /// Creates a state for the `transition` if it didn't already exist, then calls /// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/> /// depending on <see cref="ITransition.CrossFadeFromStart"/>. /// </summary> /// <remarks> /// This method is safe to call repeatedly without checking whether the `transition` was already playing. /// </remarks> public AnimancerState Play(ITransition transition) => Playable.Play(transition); /// <summary> /// Creates a state for the `transition` if it didn't already exist, then calls /// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/> /// depending on <see cref="ITransition.CrossFadeFromStart"/>. /// </summary> /// <remarks> /// This method is safe to call repeatedly without checking whether the `transition` was already playing. /// </remarks> public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = default) => Playable.Play(transition, fadeDuration, mode); /************************************************************************************************************************/ // Try Play. /************************************************************************************************************************/ /// <summary> /// 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. /// </summary> /// <remarks> /// The animation will continue playing from its current <see cref="AnimancerState.Time"/>. /// If you wish to force it back to the start, you can simply set the returned state's time to 0. /// <para></para> /// This method is safe to call repeatedly without checking whether the animation was already playing. /// </remarks> /// <exception cref="ArgumentNullException">The `key` is null.</exception> public AnimancerState TryPlay(object key) => Playable.TryPlay(key); /// <summary> /// 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. /// </summary> /// <remarks> /// 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. /// <para></para> /// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself /// and simply <see cref="AnimancerState.Play"/> the `state`. /// <para></para> /// This method is safe to call repeatedly without checking whether the animation was already playing. /// <para></para> /// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em> /// </remarks> /// <exception cref="ArgumentNullException">The `key` is null.</exception> public AnimancerState TryPlay(object key, float fadeDuration, FadeMode mode = default) => Playable.TryPlay(key, fadeDuration, mode); /************************************************************************************************************************/ /// <summary> /// Gets the state associated with the `clip`, stops and rewinds it to the start, then returns it. /// </summary> public AnimancerState Stop(AnimationClip clip) => Stop(GetKey(clip)); /// <summary> /// Gets the state registered with the <see cref="IHasKey.Key"/>, stops and rewinds it to the start, then /// returns it. /// </summary> public AnimancerState Stop(IHasKey hasKey) => _Playable?.Stop(hasKey); /// <summary> /// Gets the state associated with the `key`, stops and rewinds it to the start, then returns it. /// </summary> public AnimancerState Stop(object key) => _Playable?.Stop(key); /// <summary> /// Stops all animations and rewinds them to the start. /// </summary> public void Stop() { if (_Playable != null) _Playable.Stop(); } /************************************************************************************************************************/ /// <summary> /// Returns true if a state is registered for the `clip` and it is currently playing. /// <para></para> /// The actual dictionary key is determined using <see cref="GetKey"/>. /// </summary> public bool IsPlaying(AnimationClip clip) => IsPlaying(GetKey(clip)); /// <summary> /// Returns true if a state is registered with the <see cref="IHasKey.Key"/> and it is currently playing. /// </summary> public bool IsPlaying(IHasKey hasKey) => _Playable != null && _Playable.IsPlaying(hasKey); /// <summary> /// Returns true if a state is registered with the `key` and it is currently playing. /// </summary> public bool IsPlaying(object key) => _Playable != null && _Playable.IsPlaying(key); /// <summary> /// Returns true if at least one animation is being played. /// </summary> public bool IsPlaying() => _Playable != null && _Playable.IsPlaying(); /************************************************************************************************************************/ /// <summary> /// Returns true if the `clip` is currently being played by at least one state. /// <para></para> /// This method is inefficient because it searches through every state to find any that are playing the `clip`, /// unlike <see cref="IsPlaying(AnimationClip)"/> which only checks the state registered using the `clip`s key. /// </summary> public bool IsPlayingClip(AnimationClip clip) => _Playable != null && _Playable.IsPlayingClip(clip); /************************************************************************************************************************/ /// <summary> /// Evaluates all of the currently playing animations to apply their states to the animated objects. /// </summary> public void Evaluate() => Playable.Evaluate(); /// <summary> /// 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. /// </summary> 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. /************************************************************************************************************************/ /// <summary>[Warning] /// You should not use an <see cref="AnimancerState"/> as a key. /// Just call <see cref="AnimancerState.Stop"/>. /// </summary> [Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Stop().", true)] public AnimancerState Stop(AnimancerState key) { key.Stop(); return key; } /// <summary>[Warning] /// You should not use an <see cref="AnimancerState"/> as a key. /// Just check <see cref="AnimancerState.IsPlaying"/>. /// </summary> [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. /************************************************************************************************************************/ /// <summary> /// Determines if any animations are still playing so this object can be used as a custom yield instruction. /// </summary> bool IEnumerator.MoveNext() { if (!IsPlayableInitialized) return false; return ((IEnumerator)_Playable).MoveNext(); } /// <summary>Returns null.</summary> object IEnumerator.Current => null; /// <summary>Does nothing.</summary> void IEnumerator.Reset() { } /************************************************************************************************************************/ /// <summary>[<see cref="IAnimationClipSource"/>] /// Calls <see cref="GatherAnimationClips(ICollection{AnimationClip})"/>. /// </summary> public void GetAnimationClips(List<AnimationClip> clips) { var set = ObjectPool.AcquireSet<AnimationClip>(); set.UnionWith(clips); GatherAnimationClips(set); clips.Clear(); clips.AddRange(set); ObjectPool.Release(set); } /************************************************************************************************************************/ /// <summary>[<see cref="IAnimationClipCollection"/>] /// Gathers all the animations in the <see cref="Playable"/>. /// <para></para> /// In the Unity Editor this method also gathers animations from other components on parent and child objects. /// </summary> public virtual void GatherAnimationClips(ICollection<AnimationClip> 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 /************************************************************************************************************************/ } }