// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using Animancer.TransitionLibraries; 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")] [AnimancerHelpUrl(typeof(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("Animancer works by using Unity's Playables API to control an Animator component." + "\n\nThe Animator's Controller field should be empty unless you intend to use it.")] private Animator _Animator; /// [] /// Animancer works by using Unity's Playables API to control an component. /// /// /// The should be empty unless you intend to use it. /// public Animator Animator { get => _Animator; set { _Animator = value; if (IsGraphInitialized) { _Graph.DestroyOutput(); _Graph.CreateOutput(value, this); } } } #if UNITY_EDITOR /// string IAnimancerComponent.AnimatorFieldName => nameof(_Animator); #endif /************************************************************************************************************************/ [SerializeField] [Tooltip(Strings.ProOnlyTag + "An optional Transition Library" + " which can modify the way Animancer transitions between animations.")] private TransitionLibraryAsset _Transitions; /// [] [Pro-Only] /// An optional /// which can modify the way Animancer transitions between animations. /// public TransitionLibraryAsset Transitions { get => _Transitions; set { _Transitions = value; if (IsGraphInitialized) _Graph.Transitions = value?.Library; } } /************************************************************************************************************************/ private AnimancerGraph _Graph; /// /// The internal system which manages the playing animations. /// Accessing this property will automatically initialize it. /// public AnimancerGraph Graph { get { InitializeGraph(); return _Graph; } } /// Has the been initialized? public bool IsGraphInitialized => _Graph != null && _Graph.IsValidOrDispose(); /************************************************************************************************************************/ /// The layers which each manage their own set of animations. public AnimancerLayerList Layers => Graph.Layers; /// The states managed by this component. public AnimancerStateDictionary States => Graph.States; /// Dynamic parameters which anything can get or set. public ParameterDictionary Parameters => Graph.Parameters; /// A dictionary of callbacks to be triggered by any event with a matching name. public NamedEventDictionary Events => Graph.Events; /************************************************************************************************************************/ /// Returns the . public static implicit operator AnimancerGraph(AnimancerComponent animancer) => animancer.Graph; /// Returns layer 0. public static implicit operator AnimancerLayer(AnimancerComponent animancer) => animancer.Graph.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) + "] and reset all animations" + "\n• [" + nameof(DisableAction.Pause) + "] to later resume from the current state" + "\n• [" + nameof(DisableAction.Continue) + "] playing while inactive" + "\n• [" + nameof(DisableAction.Reset) + "] to the original values" + "\n• [" + nameof(DisableAction.Destroy) + "] all layers and states" + "\n• If you're only destroying objects and not disabling them," + " using " + nameof(DisableAction.Continue) + " is the most efficient" + " because it avoids wasting performance stopping things that will be destroyed anyway.")] 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 . /// /// If you're only destroying objects and not disabling them, /// using is the most efficient /// because it avoids wasting performance stopping things that will be destroyed anyway. /// public ref DisableAction ActionOnDisable => ref _ActionOnDisable; /// bool IAnimancerComponent.ResetOnDisable => _ActionOnDisable == DisableAction.Reset; /// /// An action to perform when disabling an . /// See . /// public enum DisableAction { /// /// Stop and reset all animations, but leave all animated values as they are (unlike ). /// /// Calls and . Stop, /// Pause to later resume from the current state. /// 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 (!IsGraphInitialized) 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. _Graph.UpdateMode = value == AnimatorUpdateMode.UnscaledTime ? DirectorUpdateMode.UnscaledGameTime : DirectorUpdateMode.GameTime; #if UNITY_EDITOR if (InitialUpdateMode == null) { InitialUpdateMode = value; } else if (UnityEditor.EditorApplication.isPlaying) { if (Editor.AnimancerGraphCleanup.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 (IsGraphInitialized) { _Graph.UnpauseGraph(); #if UNITY_EDITOR AnimancerGraph.ClearInactiveInitializationStackTrace(this); #endif } } /// Acts according to the . protected virtual void OnDisable() { if (!IsGraphInitialized) return; switch (_ActionOnDisable) { case DisableAction.Stop: _Graph.Stop(); _Graph.PauseGraph(); break; case DisableAction.Pause: _Graph.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); _Graph.Stop(); _Animator.Rebind(); _Graph.PauseGraph(); break; case DisableAction.Destroy: _Graph.Destroy(); _Graph = null; break; default: throw new ArgumentOutOfRangeException(nameof(ActionOnDisable)); } } /************************************************************************************************************************/ /// Creates and initializes the if it wasn't already initialized. public void InitializeGraph() { if (IsGraphInitialized) return; TryGetAnimator(); AnimancerGraph.SetNextGraphName(name + " (Animancer)"); _Graph = new(_Transitions?.Library); _Graph.CreateOutput(_Animator, this); OnInitializeGraph(); } /************************************************************************************************************************/ /// Sets the and connects it to the . /// /// The is already initialized. /// You must call before re-initializing it. /// public void InitializePlayable(AnimancerGraph graph) { if (IsGraphInitialized) throw new InvalidOperationException( $"The {nameof(AnimancerGraph)} is already initialized." + $" Either call this method before anything else uses it or call" + $" animancerComponent.{nameof(Graph)}.{nameof(AnimancerGraph.Destroy)}" + $" before re-initializing it."); TryGetAnimator(); _Graph = graph; _Graph.Transitions = _Transitions?.Library; _Graph.CreateOutput(_Animator, this); OnInitializeGraph(); } /************************************************************************************************************************/ /// Called right after the is initialized. protected virtual void OnInitializeGraph() { #if UNITY_ASSERTIONS ValidateGraphInitialization(); #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 ValidateGraphInitialization() { #if UNITY_EDITOR if (_Animator != null) InitialUpdateMode = UpdateMode; #if UNITY_IMGUI if (OptionalWarning.CreateGraphDuringGuiEvent.IsEnabled()) { var currentEvent = Event.current; if (currentEvent != null) { var eventType = currentEvent.type; if (eventType == EventType.Layout || eventType == EventType.Repaint) { OptionalWarning.CreateGraphDuringGuiEvent.Log( $"An {nameof(AnimancerGraph)} is being initialized" + $" during a {eventType} event which is likely undesirable.", this); } } } #endif #endif 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 (IsGraphInitialized) { _Graph.Destroy(); _Graph = 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 (_Graph != 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) => Graph.Layers[0].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) => Graph.Layers[0].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) => Graph.Layers[0].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) => Graph.Layers[0].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) => Graph.Layers[0].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) => Graph.Layers[0].Play(transition, fadeDuration, mode); /************************************************************************************************************************/ // Try Play. /************************************************************************************************************************/ /// /// Stops all other animations on the base layer, /// plays the animation registered with the `key`, /// and returns the animation's 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 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) => Graph.Layers[0].TryPlay(key); /// /// Stops all other animations on the base layer, /// plays the animation registered with the `key`, /// and returns the animation's 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 simply set the returned state's time to 0. /// /// This method is safe to call repeatedly without checking whether the animation was already playing. /// public AnimancerState TryPlay( IHasKey hasKey) => TryPlay(hasKey.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) => Graph.Layers[0].TryPlay(key, fadeDuration, mode); /// /// Starts fading in the animation registered with the `key` /// while fading out all others in the same layer over the course of the `fadeDuration` /// and returns the animation's state. /// /// /// If no state is registered with the `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 allows it to continue 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. /// public AnimancerState TryPlay( IHasKey hasKey, float fadeDuration, FadeMode mode = default) => TryPlay(hasKey.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) => _Graph?.Stop(hasKey); /// /// Gets the state associated with the `key`, stops and rewinds it to the start, then returns it. /// public AnimancerState Stop(object key) => _Graph?.Stop(key); /// Stops all animations and rewinds them to the start. public void Stop() { if (IsGraphInitialized) _Graph.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) => IsGraphInitialized && _Graph.IsPlaying(hasKey); /// /// Returns true if a state is registered with the `key` and it is currently playing. /// public bool IsPlaying(object key) => IsGraphInitialized && _Graph.IsPlaying(key); /// /// Returns true if at least one animation is being played. /// public bool IsPlaying() => IsGraphInitialized && _Graph.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) => IsGraphInitialized && _Graph.IsPlayingClip(clip); /************************************************************************************************************************/ /// /// Immediately applies the current states of all animations to the animated objects. /// public void Evaluate() => Graph.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) => Graph.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. /************************************************************************************************************************/ /// Are any animations are still playing? /// This allows this object to be used as a custom yield instruction. bool IEnumerator.MoveNext() => IsGraphInitialized && ((IEnumerator)_Graph).MoveNext(); /// Returns null. object IEnumerator.Current => null; /// Does nothing. void IEnumerator.Reset() { } /************************************************************************************************************************/ /// [] /// Calls . /// public void GetAnimationClips(List clips) { var set = SetPool.Acquire(); set.UnionWith(clips); GatherAnimationClips(set); clips.Clear(); clips.AddRange(set); SetPool.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 (IsGraphInitialized) _Graph.GatherAnimationClips(clips); #if UNITY_EDITOR Editor.AnimationGatherer.GatherFromGameObject(gameObject, clips); if (_Animator != null && _Animator.gameObject != gameObject) Editor.AnimationGatherer.GatherFromGameObject(_Animator.gameObject, clips); #endif } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }