// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using Object = UnityEngine.Object; namespace Animancer { /// /// A which can be used as a substitute for the /// normally used to control an . /// /// /// /// This class can be used as a custom yield instruction to wait until all animations finish playing. /// /// The most common way to access this class is via . /// /// Documentation: Playing Animations /// /// /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerPlayable /// public partial class AnimancerPlayable : PlayableBehaviour, IEnumerator, IPlayableWrapper, IAnimationClipCollection { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ private static float _DefaultFadeDuration = 0.25f; /************************************************************************************************************************/ #if UNITY_EDITOR /// [Editor-Only] /// The namespace that should be used for a class which sets the . /// public const string DefaultFadeDurationNamespace = nameof(Animancer); /// [Editor-Only] /// The name that should be used for a class which sets the . /// public const string DefaultFadeDurationClass = nameof(DefaultFadeDuration); /// [Editor-Only] /// Initializes the (see its example for more information). /// /// /// This method takes about 2 milliseconds if a class exists, or 0 if it /// doesn't (less than 0.5 rounded off according to a ). /// /// The can't simply be stored in the /// because it needs to be initialized before Unity is able to load /// s. /// static AnimancerPlayable() { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); // Iterate backwards since it's more likely to be towards the end. for (int iAssembly = assemblies.Length - 1; iAssembly >= 0; iAssembly--) { var type = assemblies[iAssembly].GetType(DefaultFadeDurationNamespace + "." + DefaultFadeDurationClass); if (type != null) { var methods = type.GetMethods(Editor.AnimancerEditorUtilities.StaticBindings); for (int iMethod = 0; iMethod < methods.Length; iMethod++) { var method = methods[iMethod]; if (method.IsDefined(typeof(RuntimeInitializeOnLoadMethodAttribute), false)) { method.Invoke(null, null); return; } } } } } #endif /************************************************************************************************************************/ /// The fade duration to use if not specified. Default is 0.25. /// The value is negative or infinity. /// Animancer Lite doesn't allow this value to be changed in runtime builds (except to 0). /// /// based games often have no use for fading so you could set this value to 0 using the /// following script so that you don't need to manually set the of all /// your transitions. /// /// To set this value automatically on startup, put the following class into any script: /// /// namespace Animancer /// { /// internal static class DefaultFadeDuration /// { /// [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] /// private static void Initialize() => AnimancerPlayable.DefaultFadeDuration = 0; /// } /// } /// /// Using that specific namespace () and class name /// () allows Animancer to find and run it immediately in the Unity /// Editor so that newly created transition fields can start with the correct value (using a /// [UnityEditor.InitializeOnLoadMethod] attribute would run it too late). /// public static float DefaultFadeDuration { get => _DefaultFadeDuration; set { AnimancerUtilities.Assert(value >= 0 && value < float.PositiveInfinity, $"{nameof(AnimancerPlayable)}.{nameof(DefaultFadeDuration)} must not be negative or infinity."); _DefaultFadeDuration = value; } } /************************************************************************************************************************/ /// [Internal] The containing this . internal PlayableGraph _Graph; /// [Pro-Only] The containing this . public PlayableGraph Graph => _Graph; /// [Internal] The containing this . internal Playable _RootPlayable; /// [Internal] The which layers connect to. internal Playable _LayerMixer; /************************************************************************************************************************/ /// [Internal] The which layers connect to. Playable IPlayableWrapper.Playable => _LayerMixer; /// [Internal] An is the root of the graph so it has no parent. IPlayableWrapper IPlayableWrapper.Parent => null; /// [Internal] The current blend weight of this node which determines how much it affects the final output. float IPlayableWrapper.Weight => 1; /// [Internal] The . int IPlayableWrapper.ChildCount => Layers.Count; /// [Internal] Returns the layer at the specified `index`. AnimancerNode IPlayableWrapper.GetChild(int index) => Layers[index]; /************************************************************************************************************************/ // These collections can't be readonly because when Unity clones the Template it copies the memory without running the // field initializers on the new clone so everything would be referencing the same collections. /************************************************************************************************************************/ /// The s which each manage their own set of animations. /// /// Documentation: Layers /// public LayerList Layers { get; private set; } /// The s managed by this playable. /// /// Documentation: States /// public StateDictionary States { get; private set; } /// All of the nodes that need to be updated. private Key.KeyedList _PreUpdatables; /// All of the objects that need to be updated early. private Key.KeyedList _PostUpdatables; /// A that updates the . private PostUpdate _PostUpdate; /************************************************************************************************************************/ /// The component that is playing this . public IAnimancerComponent Component { get; private set; } /************************************************************************************************************************/ /// /// The number of times the has changed on layer 0. By storing this /// value and later comparing the stored value to the current value, you can determine whether the state has /// been changed since then, even it has changed back to the same state. /// public int CommandCount => Layers[0].CommandCount; /************************************************************************************************************************/ /// Determines what time source is used to update the . public DirectorUpdateMode UpdateMode { get => _Graph.GetTimeUpdateMode(); set => _Graph.SetTimeUpdateMode(value); } /************************************************************************************************************************/ private float _Speed = 1; /// How fast the of all animations is advancing every frame. /// /// /// 1 is the normal speed. /// /// A negative value will play the animations backwards. /// /// Setting this value to 0 would pause all animations, but calling is more efficient. /// /// Animancer Lite does not allow this value to be changed in runtime builds. /// /// /// /// void SetSpeed(AnimancerComponent animancer) /// { /// animancer.Playable.Speed = 1;// Normal speed. /// animancer.Playable.Speed = 2;// Double speed. /// animancer.Playable.Speed = 0.5f;// Half speed. /// animancer.Playable.Speed = -1;// Normal speed playing backwards. /// } /// public float Speed { get => _Speed; set => _LayerMixer.SetSpeed(_Speed = value); } /************************************************************************************************************************/ private bool _KeepChildrenConnected; /// /// Should playables stay connected to the graph at all times? /// Otherwise they will be disconnected when their is 0. /// /// /// /// Humanoid Rigs default this value to false so that playables will be disconnected from the graph /// while they are at 0 weight which stops it from evaluating them every frame. /// /// Generic Rigs default this value to true because they do not always animate the same standard set of /// values so every connection change has a higher performance cost than with Humanoid Rigs which is generally /// more significant than the gains for having fewer playables connected at a time. /// /// The default is set by . /// /// /// /// [SerializeField] /// private AnimancerComponent _Animancer; /// /// public void Initialize() /// { /// _Animancer.Playable.KeepChildrenConnected = true; /// } /// public bool KeepChildrenConnected { get => _KeepChildrenConnected; set { if (_KeepChildrenConnected == value) return; _KeepChildrenConnected = value; if (value) { _PostUpdate.IsConnected = true; for (int i = Layers.Count - 1; i >= 0; i--) Layers.GetLayer(i).ConnectAllChildrenToGraph(); } else { for (int i = Layers.Count - 1; i >= 0; i--) Layers.GetLayer(i).DisconnectWeightlessChildrenFromGraph(); } } } /************************************************************************************************************************/ private bool _SkipFirstFade; /// /// Normally the first animation on the Base Layer should not fade in because there is nothing fading out. But /// sometimes that is undesirable, such as if the is assigned /// since Animancer can blend with that. /// /// /// Setting this value to false ensures that the has at least two /// inputs because it ignores the of the layer when there is only one. /// public bool SkipFirstFade { get => _SkipFirstFade; set { _SkipFirstFade = value; if (!value && Layers.Count < 2) { Layers.Count = 1; _LayerMixer.SetInputCount(2); } } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Initialization /************************************************************************************************************************/ /// /// Creates a new containing an . /// /// The caller is responsible for calling on the returned object, except in Edit Mode /// where it will be called automatically. /// /// Consider calling before this method to give it a name. /// public static AnimancerPlayable Create() { #if UNITY_EDITOR var name = _NextGraphName; _NextGraphName = null; var graph = name != null ? PlayableGraph.Create(name) : PlayableGraph.Create(); #else var graph = PlayableGraph.Create(); #endif return Create(graph); } /************************************************************************************************************************/ /// /// Since needs to clone an existing instance, we /// keep a static template to avoid allocating an extra garbage one every time. This is why the fields are /// assigned in rather than being readonly with field initializers. /// private static readonly AnimancerPlayable Template = new AnimancerPlayable(); /// Creates an in an existing . public static AnimancerPlayable Create(PlayableGraph graph) => Create(graph, Template); /************************************************************************************************************************/ /// Creates an in an existing . /// /// When inheriting from , it is recommended to give your class a field like the /// following to use as the `template` for this method: /// /// private static readonly MyAnimancerPlayable Template = new MyAnimancerPlayable(); /// protected static T Create(PlayableGraph graph, T template) where T : AnimancerPlayable, new() => ScriptPlayable.Create(graph, template, 2) .GetBehaviour(); /************************************************************************************************************************/ /// [Internal] Called by Unity when it creates this . public override void OnPlayableCreate(Playable playable) { _RootPlayable = playable; _Graph = playable.GetGraph(); _PostUpdatables = new Key.KeyedList(); _PreUpdatables = new Key.KeyedList(); _PostUpdate = PostUpdate.Create(this); Layers = new LayerList(this, out _LayerMixer); States = new StateDictionary(this); playable.SetInputWeight(0, 1); #if UNITY_EDITOR RegisterInstance(); #endif } /************************************************************************************************************************/ #if UNITY_EDITOR private static string _NextGraphName; #endif /// [Editor-Conditional] /// Sets the display name for the next call to give its . /// /// /// Having this method separate from allows the /// to compile it out of runtime builds which would /// otherwise require #ifs on the caller side. /// [System.Diagnostics.Conditional(Strings.UnityEditor)] public static void SetNextGraphName(string name) { #if UNITY_EDITOR _NextGraphName = name; #endif } /************************************************************************************************************************/ #if UNITY_EDITOR /// [Editor-Only] Returns "AnimancerPlayable (Graph Name)". public override string ToString() => $"{nameof(AnimancerPlayable)} ({(_Graph.IsValid() ? _Graph.GetEditorName() : "Graph Not Initialized")})"; #endif /************************************************************************************************************************/ /// /// Outputs the connected to the and returns true /// if it was found. Otherwise returns false. /// public bool TryGetOutput(out PlayableOutput output) { var outputCount = _Graph.GetOutputCount(); for (int i = 0; i < outputCount; i++) { output = _Graph.GetOutput(i); if (output.GetSourcePlayable().Equals(_RootPlayable)) return true; } output = default; return false; } /************************************************************************************************************************/ /// /// Plays this playable on the and sets the /// . /// public void CreateOutput(IAnimancerComponent animancer) => CreateOutput(animancer.Animator, animancer); /// Plays this playable on the specified `animator` and sets the . public void CreateOutput(Animator animator, IAnimancerComponent animancer) { #if UNITY_ASSERTIONS if (animator == null) throw new ArgumentNullException(nameof(animator), $"An {nameof(Animator)} component is required to play animations."); #if UNITY_EDITOR if (UnityEditor.EditorUtility.IsPersistent(animator)) throw new ArgumentException( $"The specified {nameof(Animator)} component is a prefab which means it cannot play animations.", nameof(animator)); #endif if (animancer != null) { Debug.Assert(animancer.IsPlayableInitialized && animancer.Playable == this, $"{nameof(CreateOutput)} was called on an {nameof(AnimancerPlayable)} which does not match the" + $" {nameof(IAnimancerComponent)}.{nameof(IAnimancerComponent.Playable)}."); Debug.Assert(animator == animancer.Animator, $"{nameof(CreateOutput)} was called with an {nameof(Animator)} which does not match the" + $" {nameof(IAnimancerComponent)}.{nameof(IAnimancerComponent.Animator)}."); } if (TryGetOutput(out var output)) { Debug.LogWarning( $"A {nameof(PlayableGraph)} output is already connected to the {nameof(AnimancerPlayable)}." + $" The old output should be destroyed using `animancerComponent.Playable.DestroyOutput();`" + $" before calling {nameof(CreateOutput)}.", animator); } #endif Component = animancer; var isHumanoid = animator.isHuman; // Generic Rigs get better performance by keeping children connected but Humanoids don't. KeepChildrenConnected = !isHumanoid; // Generic Rigs can blend with an underlying Animator Controller but Humanoids can't. SkipFirstFade = isHumanoid || animator.runtimeAnimatorController == null; #pragma warning disable CS0618 // Type or member is obsolete. // Unity 2022 marked this method as [Obsolete] even though it's the only way to use Animate Physics mode. AnimationPlayableUtilities.Play(animator, _RootPlayable, _Graph); #pragma warning restore CS0618 // Type or member is obsolete. _IsGraphPlaying = true; } /************************************************************************************************************************/ /// [Pro-Only] /// Inserts a `playable` after the root of the so that it can modify the final output. /// /// It can be removed using . public void InsertOutputPlayable(Playable playable) { var output = _Graph.GetOutput(0); _Graph.Connect(output.GetSourcePlayable(), 0, playable, 0); playable.SetInputWeight(0, 1); output.SetSourcePlayable(playable); } /// [Pro-Only] /// Inserts an animation job after the root of the so that it can modify the final output. /// /// /// It can can be removed by passing the returned value into . /// public AnimationScriptPlayable InsertOutputJob(T data) where T : struct, IAnimationJob { var playable = AnimationScriptPlayable.Create(_Graph, data, 1); var output = _Graph.GetOutput(0); _Graph.Connect(output.GetSourcePlayable(), 0, playable, 0); playable.SetInputWeight(0, 1); output.SetSourcePlayable(playable); return playable; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Cleanup /************************************************************************************************************************/ /// Is this currently usable (not destroyed)? public bool IsValid => _Graph.IsValid(); /************************************************************************************************************************/ /// Destroys the . This operation cannot be undone. public void DestroyGraph() { if (_Graph.IsValid()) _Graph.Destroy(); } /************************************************************************************************************************/ /// /// Destroys the connected to this and returns /// true if it was found. Otherwise returns false. /// public bool DestroyOutput() { if (TryGetOutput(out var output)) { _Graph.DestroyOutput(output); return true; } else return false; } /************************************************************************************************************************/ /// Cleans up the resources managed by this . public override void OnPlayableDestroy(Playable playable) { var previous = Current; Current = this; DisposeAll(); GC.SuppressFinalize(this); // No need to destroy every layer and state individually because destroying the graph will do so anyway. Layers = null; States = null; Current = previous; } /************************************************************************************************************************/ private List _Disposables; /// A list of objects that need to be disposed when this is destroyed. /// This list is primarily used to dispose native arrays used in Animation Jobs. public List Disposables => _Disposables ?? (_Disposables = new List()); /************************************************************************************************************************/ /// Calls on all the . ~AnimancerPlayable() => DisposeAll(); /// Calls on all the . private void DisposeAll() { if (_Disposables == null) return; var i = _Disposables.Count; DisposeNext: try { while (--i >= 0) { _Disposables[i].Dispose(); } _Disposables.Clear(); _Disposables = null; } catch (Exception exception) { Debug.LogException(exception, Component as Object); goto DisposeNext; } } /************************************************************************************************************************/ #region Inverse Kinematics // These fields are stored here but accessed via the LayerList. /************************************************************************************************************************/ private bool _ApplyAnimatorIK; /// public bool ApplyAnimatorIK { get => _ApplyAnimatorIK; set { _ApplyAnimatorIK = value; for (int i = Layers.Count - 1; i >= 0; i--) Layers.GetLayer(i).ApplyAnimatorIK = value; } } /************************************************************************************************************************/ private bool _ApplyFootIK; /// public bool ApplyFootIK { get => _ApplyFootIK; set { _ApplyFootIK = value; for (int i = Layers.Count - 1; i >= 0; i--) Layers.GetLayer(i).ApplyFootIK = value; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Playing /************************************************************************************************************************/ /// Calls on the . /// If the is null, this method returns the `clip` itself. public object GetKey(AnimationClip clip) => Component != null ? Component.GetKey(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) => 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) => GetLocalLayer(state).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 `state` 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) => 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) { return GetLocalLayer(state).Play(state, fadeDuration, mode); } /************************************************************************************************************************/ // Transition. /************************************************************************************************************************/ /// /// Creates a state for the `transition` if it didn't already exist, then calls /// or /// depending on the . /// /// /// This method is safe to call repeatedly without checking whether the `transition` was already playing. /// public AnimancerState Play(ITransition transition) => Play(transition, transition.FadeDuration, transition.FadeMode); /// /// Creates a state for the `transition` if it didn't already exist, then calls /// or /// depending on the . /// /// /// 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) { var state = States.GetOrCreate(transition); state = Play(state, fadeDuration, mode); transition.Apply(state); return state; } /************************************************************************************************************************/ // 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) => States.TryGet(key, out var state) ? Play(state) : null; /// /// 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) => States.TryGet(key, out var state) ? Play(state, fadeDuration, mode) : null; /************************************************************************************************************************/ /// /// Returns the if the is this. /// Otherwise returns the first layer in this graph. /// private AnimancerLayer GetLocalLayer(AnimancerState state) { if (state.Root == this) { var layer = state.Layer; if (layer != null) return layer; } return Layers[0]; } /************************************************************************************************************************/ /// /// Gets the state registered with the , stops and rewinds it to the start, then /// returns it. /// public AnimancerState Stop(IHasKey hasKey) => Stop(hasKey.Key); /// /// Calls on the state registered with the `key` to stop it from playing and /// rewind it to the start. /// public AnimancerState Stop(object key) { if (States.TryGet(key, out var state)) state.Stop(); return state; } /// /// Calls on all animations to stop them from playing and rewind them to the /// start. /// public void Stop() { for (int i = Layers.Count - 1; i >= 0; i--) Layers.GetLayer(i).Stop(); } /************************************************************************************************************************/ /// Is a state registered with the and currently playing? public bool IsPlaying(IHasKey hasKey) => IsPlaying(hasKey.Key); /// Is a state registered with the `key` and currently playing? public bool IsPlaying(object key) => States.TryGet(key, out var state) && state.IsPlaying; /// Is least one animation being played? public bool IsPlaying() { if (!_IsGraphPlaying) return false; for (int i = Layers.Count - 1; i >= 0; i--) { if (Layers.GetLayer(i).IsAnyStatePlaying()) return true; } return false; } /************************************************************************************************************************/ /// /// Returns true if the `clip` is currently being played by at least one state in the specified layer. /// /// 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 specified key. /// public bool IsPlayingClip(AnimationClip clip) { if (!_IsGraphPlaying) return false; for (int i = Layers.Count - 1; i >= 0; i--) if (Layers.GetLayer(i).IsPlayingClip(clip)) return true; return false; } /************************************************************************************************************************/ /// Calculates the total of all states in all layers. public float GetTotalWeight() { float weight = 0; for (int i = Layers.Count - 1; i >= 0; i--) weight += Layers.GetLayer(i).GetTotalWeight(); return weight; } /************************************************************************************************************************/ /// [] Gathers all the animations in all layers. public void GatherAnimationClips(ICollection clips) => Layers.GatherAnimationClips(clips); /************************************************************************************************************************/ // IEnumerator for yielding in a coroutine to wait until animations have stopped. /************************************************************************************************************************/ /// Are any animations playing? /// This allows this object to be used as a custom yield instruction. bool IEnumerator.MoveNext() { for (int i = Layers.Count - 1; i >= 0; i--) if (Layers.GetLayer(i).IsPlayingAndNotEnding()) return true; return false; } /// Returns null. object IEnumerator.Current => null; /// Does nothing. void IEnumerator.Reset() { } /************************************************************************************************************************/ #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 Evaluation /************************************************************************************************************************/ private bool _IsGraphPlaying = true; /// Indicates whether the is currently playing. public bool IsGraphPlaying { get => _IsGraphPlaying; set { if (value) UnpauseGraph(); else PauseGraph(); } } /// /// Resumes playing the if was called previously. /// public void UnpauseGraph() { if (!_IsGraphPlaying) { _Graph.Play(); _IsGraphPlaying = true; #if UNITY_EDITOR // In Edit Mode, unpausing the graph does not work properly unless we force it to change. if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) Evaluate(Time.maximumDeltaTime); #endif } } /// /// Freezes the at its current state. /// /// If you call this method, you are responsible for calling to resume playing. /// public void PauseGraph() { if (_IsGraphPlaying) { _Graph.Stop(); _IsGraphPlaying = false; } } /************************************************************************************************************************/ /// /// Evaluates all of the currently playing animations to apply their states to the animated objects. /// public void Evaluate() => _Graph.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) => _Graph.Evaluate(deltaTime); /************************************************************************************************************************/ /// Returns a detailed descrption of all currently playing states and other registered states. public string GetDescription() { var text = ObjectPool.AcquireStringBuilder(); AppendDescription(text); return text.ReleaseToString(); } /// Appends a detailed descrption of all currently playing states and other registered states. public void AppendDescription(StringBuilder text) { text.Append($"{nameof(AnimancerPlayable)} (") .Append(Component) .Append(") Layer Count: ") .Append(Layers.Count); const string separator = "\n "; AnimancerNode.AppendIKDetails(text, separator, this); var count = Layers.Count; for (int i = 0; i < count; i++) { text.Append(separator); Layers[i].AppendDescription(text, separator); } text.AppendLine(); AppendInternalDetails(text, Strings.Indent, Strings.Indent + Strings.Indent); } /// Appends all registered s and s. public void AppendInternalDetails(StringBuilder text, string sectionPrefix, string itemPrefix) { AppendAll(text, sectionPrefix, itemPrefix, _PreUpdatables, "Pre Updatables"); text.AppendLine(); AppendAll(text, sectionPrefix, itemPrefix, _PostUpdatables, "Post Updatables"); text.AppendLine(); AppendAll(text, sectionPrefix, itemPrefix, _Disposables, "Disposables"); } private static void AppendAll(StringBuilder text, string sectionPrefix, string itemPrefix, ICollection collection, string name) { var count = collection != null ? collection.Count : 0; text.Append(sectionPrefix).Append(name).Append(": ").Append(count); if (collection != null) { foreach (var item in collection) { text.AppendLine().Append(itemPrefix).Append(item); } } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Update /************************************************************************************************************************/ /// [Pro-Only] /// Adds the `updatable` to the list that need to be updated before the playables if it was not there already. /// /// /// This method is safe to call at any time, even during an update. /// /// The execution order is non-deterministic. Specifically, the most recently added will be updated first and /// will change the order by swapping the last one into the place of the removed /// object. /// public void RequirePreUpdate(IUpdatable updatable) { #if UNITY_ASSERTIONS if (updatable is AnimancerNode node) { Validate.AssertPlayable(node); Validate.AssertRoot(node, this); } #endif _PreUpdatables.AddNew(updatable); } /************************************************************************************************************************/ /// [Pro-Only] /// Adds the `updatable` to the list that need to be updated after the playables if it was not there already. /// /// /// This method is safe to call at any time, even during an update. /// /// The execution order is non-deterministic. Specifically, the most recently added will be updated first and /// will change the order by swapping the last one into the place of the removed /// object. /// public void RequirePostUpdate(IUpdatable updatable) { #if UNITY_ASSERTIONS if (updatable is AnimancerNode node) { Validate.AssertPlayable(node); Validate.AssertRoot(node, this); } #endif _PostUpdatables.AddNew(updatable); } /************************************************************************************************************************/ /// Removes the `updatable` from the `updatables`. /// /// This method is safe to call at any time, even during an update. /// /// The last element is swapped into the place of the one being removed so that the rest of them do not need to /// be moved down one place to fill the gap. This is more efficient, but means that the update order can change. /// private void CancelUpdate(Key.KeyedList updatables, IUpdatable updatable) { var index = updatables.IndexOf(updatable); if (index < 0) return; updatables.RemoveAtSwap(index); if (_CurrentUpdatable < index && updatables == _CurrentUpdatables) _CurrentUpdatable--; } /// Removes the `updatable` from the list of objects that need to be updated before the playables. /// /// This method is safe to call at any time, even during an update. /// /// The last element is swapped into the place of the one being removed so that the rest of them do not need to /// be moved down one place to fill the gap. This is more efficient, but means that the update order can change. /// public void CancelPreUpdate(IUpdatable updatable) => CancelUpdate(_PreUpdatables, updatable); /// Removes the `updatable` from the list of objects that need to be updated after the playebles. /// /// This method is safe to call at any time, even during an update. /// /// The last element is swapped into the place of the one being removed so that the rest of them do not need to /// be moved down one place to fill the gap. This is more efficient, but means that the update order can change. /// public void CancelPostUpdate(IUpdatable updatable) => CancelUpdate(_PostUpdatables, updatable); /************************************************************************************************************************/ /// The number of objects that have been registered by . public int PreUpdatableCount => _PreUpdatables.Count; /// The number of objects that have been registered by . public int PostUpdatableCount => _PostUpdatables.Count; /************************************************************************************************************************/ /// Returns the object registered by at the specified `index`. public IUpdatable GetPreUpdatable(int index) => _PreUpdatables[index]; /// Returns the object registered by at the specified `index`. public IUpdatable GetPostUpdatable(int index) => _PostUpdatables[index]; /************************************************************************************************************************/ /// The object currently executing . public static AnimancerPlayable Current { get; private set; } /// The current . /// After , this property will be left at its most recent value. public static float DeltaTime { get; private set; } /// The current . /// /// After , this property will be left at its most recent value. /// /// uses this value to determine whether it has accessed the playable's time /// since it was last updated in order to cache its value. /// public ulong FrameID { get; private set; } /// The list s currently being updated. private static Key.KeyedList _CurrentUpdatables; /// The index of the currently being updated. private static int _CurrentUpdatable = -1; /************************************************************************************************************************/ /// [Internal] /// Calls on everything registered using . /// /// /// Called by the before the rest of the s are evaluated. /// public override void PrepareFrame(Playable playable, FrameData info) { #if UNITY_ASSERTIONS if (OptionalWarning.AnimatorSpeed.IsEnabled() && Component != null) { var animator = Component.Animator; if (animator != null && animator.speed != 1 && animator.runtimeAnimatorController == null) { animator.speed = 1; OptionalWarning.AnimatorSpeed.Log( $"{nameof(Animator)}.{nameof(Animator.speed)} does not affect {nameof(Animancer)}." + $" Use {nameof(AnimancerPlayable)}.{nameof(Speed)} instead.", animator); } } #endif UpdateAll(_PreUpdatables, info.deltaTime * info.effectiveParentSpeed); if (!_KeepChildrenConnected) _PostUpdate.IsConnected = _PostUpdatables.Count != 0; // Any time before or during this method will still have all Playables at their time from last frame, so we // don't want them to think their time is dirty until we are done. FrameID = info.frameId; } /************************************************************************************************************************/ /// Calls on each of the updatables`. private void UpdateAll(Key.KeyedList updatables, float deltaTime) { var previous = Current; Current = this; var previousUpdatables = _CurrentUpdatables; _CurrentUpdatables = updatables; DeltaTime = deltaTime; var previousUpdatable = _CurrentUpdatable; _CurrentUpdatable = updatables.Count; ContinueNodeLoop: try { while (--_CurrentUpdatable >= 0) { updatables[_CurrentUpdatable].Update(); } } catch (Exception exception) { Debug.LogException(exception, Component as Object); goto ContinueNodeLoop; } _CurrentUpdatable = previousUpdatable; _CurrentUpdatables = previousUpdatables; Current = previous; } /************************************************************************************************************************/ #region Post Update /************************************************************************************************************************/ /// Indicates whether the internal is currently executing. public static bool IsRunningPostUpdate(AnimancerPlayable animancer) => _CurrentUpdatables == animancer._PostUpdatables; /************************************************************************************************************************/ /// /// A which connects to a later port than the main layer mixer so that its /// method gets called after all other playables are updated in order to call /// on the . /// private class PostUpdate : PlayableBehaviour { /************************************************************************************************************************/ /// See . private static readonly PostUpdate Template = new PostUpdate(); /// The this behaviour is connected to. private AnimancerPlayable _Root; /// The underlying of this behaviour. private Playable _Playable; /************************************************************************************************************************/ /// Creates a new for the `root`. public static PostUpdate Create(AnimancerPlayable root) { var instance = ScriptPlayable.Create(root._Graph, Template, 0) .GetBehaviour(); instance._Root = root; return instance; } /************************************************************************************************************************/ /// [Internal] Called by Unity when it creates this . public override void OnPlayableCreate(Playable playable) => _Playable = playable; /************************************************************************************************************************/ private bool _IsConnected; /// /// Indicates whether this behaviour is connected to the and thus, whether it /// will receive calls. /// public bool IsConnected { get => _IsConnected; set { if (value) { if (!_IsConnected) { _IsConnected = true; _Root._Graph.Connect(_Playable, 0, _Root._RootPlayable, 1); } } else { if (_IsConnected) { _IsConnected = false; _Root._Graph.Disconnect(_Root._RootPlayable, 1); } } } } /************************************************************************************************************************/ /// [Internal] /// Calls on everything registered using . /// /// /// Called by the after the rest of the s are evaluated. /// public override void PrepareFrame(Playable playable, FrameData info) { _Root.UpdateAll(_Root._PostUpdatables, info.deltaTime * info.effectiveParentSpeed); // Ideally we would be able to update the dirty nodes here instead of in the early update so that they // can respond immediately to the effects of the post update. // However, doing that with KeepChildrenConnected == false causes problems where states that aren't // connected early (before they update) don't affect the output even though weight changes do apply. So // in the first frame when cross fading to a new animation it will lower the weight of the previous // state a bit without the corresponding increase to the new animation's weight having any effect, // giving a total weight less than 1 and thus an incorrect output. } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Editor #if UNITY_EDITOR /************************************************************************************************************************/ private static List _AllInstances; /// [Editor-Only] /// Registers this object in the list of things that need to be cleaned up in Edit Mode. /// private void RegisterInstance() { if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) return; if (_AllInstances == null) { _AllInstances = new List(); UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += () => { for (int i = _AllInstances.Count - 1; i >= 0; i--) { var playable = _AllInstances[i]; if (playable.IsValid) playable.DestroyGraph(); } _AllInstances.Clear(); }; } else// Clear out any old instances. { for (int i = _AllInstances.Count - 1; i >= 0; i--) { var playable = _AllInstances[i]; if (!playable.ShouldStayAlive()) { if (playable.IsValid) playable.DestroyGraph(); _AllInstances.RemoveAt(i); } } } _AllInstances.Add(this); } /************************************************************************************************************************/ /// Should this playable should stay alive instead of being destroyed? private bool ShouldStayAlive() { if (!IsValid) return false; if (Component == null) return true; if (Component is Object obj && obj == null) return false; if (Component.Animator == null) return false; return true; } /************************************************************************************************************************/ /// [Editor-Only] /// Returns true if the `initial` mode was and the `current` /// has changed to another mode or if the `initial` mode was something else and the `current` has changed to /// . /// public static bool HasChangedToOrFromAnimatePhysics(AnimatorUpdateMode? initial, AnimatorUpdateMode current) { if (initial == null) return false; #if UNITY_2023_1_OR_NEWER var wasAnimatePhysics = initial.Value == AnimatorUpdateMode.Fixed; var isAnimatePhysics = current == AnimatorUpdateMode.Fixed; #else var wasAnimatePhysics = initial.Value == AnimatorUpdateMode.AnimatePhysics; var isAnimatePhysics = current == AnimatorUpdateMode.AnimatePhysics; #endif return wasAnimatePhysics != isAnimatePhysics; } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ } }