// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using Animancer.TransitionLibraries; 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 { /// The root node which manages Animancer's . /// /// /// 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/AnimancerGraph /// public partial class AnimancerGraph : AnimancerNodeBase, IAnimationClipCollection, ICopyable, IEnumerator, IHasDescription { /************************************************************************************************************************/ #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 AnimancerGraph() { var typeName = $"{DefaultFadeDurationNamespace}.{DefaultFadeDurationClass}"; 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(typeName); if (type != null) { var methods = type.GetMethods(AnimancerReflection.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). /// /// Example: /// 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() => AnimancerGraph.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(AnimancerGraph)}.{nameof(DefaultFadeDuration)} must not be negative or infinity."); _DefaultFadeDuration = value; } } /************************************************************************************************************************/ /// [Internal] /// The containing this . /// internal PlayableGraph _PlayableGraph; /// [Pro-Only] /// The containing this . /// public PlayableGraph PlayableGraph => _PlayableGraph; /// Returns the . public static implicit operator PlayableGraph(AnimancerGraph animancer) => animancer.PlayableGraph; /************************************************************************************************************************/ /// [Internal] /// The of the pre-update . /// /// /// This is the final connected to the output of the . /// internal Playable _PreUpdatePlayable;// Internal for AnimancerLayerList. /************************************************************************************************************************/ /// public override AnimancerLayer Layer => null; /// public override int ChildCount => _Layers.Count; /// protected internal override AnimancerNode GetChildNode(int index) => _Layers[index]; /************************************************************************************************************************/ private AnimancerLayerList _Layers; /// The s which each manage their own set of animations. /// /// Documentation: /// /// Layers /// public AnimancerLayerList Layers { get => _Layers; set { #if UNITY_ASSERTIONS if (value.Graph != this) throw new ArgumentException( $"{nameof(AnimancerGraph)}.{nameof(AnimancerLayerList)}.{nameof(AnimancerLayerList.Graph)}" + $" mismatch: cannot use a list in an {nameof(AnimancerGraph)} that is not its" + $" {nameof(AnimancerLayerList.Graph)}"); #endif _Playable = value.Playable; if (_Layers != null && _Playable.IsValid()) { var count = _Layers.Count; for (int i = 0; i < count; i++) { var layer = _Playable.GetInput(i); if (layer.IsValid()) { _Playable.DisconnectInput(i); _PlayableGraph.Connect(_Playable, layer, i, _Layers[i].Weight); } } _PreUpdatePlayable.DisconnectInput(0); // Don't destroy the old Playable since it could still be reused later. } _Layers = value; _PlayableGraph.Connect(_PreUpdatePlayable, _Playable, 0, 1); } } /************************************************************************************************************************/ /// The s managed by this graph. /// /// Documentation: /// /// States /// public readonly AnimancerStateDictionary States; /************************************************************************************************************************/ private ParameterDictionary _Parameters; /// Dynamic parameters which anything can get or set. public ParameterDictionary Parameters => _Parameters ??= new(); /// Has the dictionary been initialized? public bool HasParameters => _Parameters != null; /************************************************************************************************************************/ /// [Internal] The backing field of . internal NamedEventDictionary _Events; /// A dictionary of callbacks to be triggered by any event with a matching name. public NamedEventDictionary Events => _Events ??= new(); /// Has the dictionary been initialized? public bool HasEvents => _Events != null; /************************************************************************************************************************/ /// [Pro-Only] The optional this graph can use. public TransitionLibrary Transitions { get; set; } /************************************************************************************************************************/ private readonly IUpdatable.List _PreUpdatables = new(); private readonly IUpdatable.List _PostUpdatables = new(); /// Objects to be updated before time advances. public IReadOnlyIndexedList PreUpdatables => _PreUpdatables; /// Objects to be updated after time advances. public IReadOnlyIndexedList PostUpdatables => _PostUpdatables; /************************************************************************************************************************/ /// The component that is playing this . public IAnimancerComponent Component { get; private set; } /************************************************************************************************************************/ /// Determines what time source is used to update the . public DirectorUpdateMode UpdateMode { get => _PlayableGraph.GetTimeUpdateMode(); set => _PlayableGraph.SetTimeUpdateMode(value); } /************************************************************************************************************************/ private bool _KeepChildrenConnected; /// /// Should playables stay connected to the graph at all times? /// Otherwise they will be disconnected when their is 0. /// /// /// /// This value defaults to false and can be set by . /// /// Example: /// [SerializeField] /// private AnimancerComponent _Animancer; /// /// public void Initialize() /// { /// _Animancer.Graph.SetKeepChildrenConnected(true); /// } /// public override bool KeepChildrenConnected => _KeepChildrenConnected; /// Sets . /// This method exists because the override can't add a setter. public void SetKeepChildrenConnected(bool value) { if (_KeepChildrenConnected == value) return; _KeepChildrenConnected = value; if (value) { for (int i = _Layers.Count - 1; i >= 0; i--) _Layers.GetLayer(i).ConnectAllStates(); } else { for (int i = _Layers.Count - 1; i >= 0; i--) _Layers.GetLayer(i).DisconnectInactiveStates(); } } /************************************************************************************************************************/ 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; Playable.SetInputCount(2); } } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Initialization /************************************************************************************************************************/ /// /// Creates an in an existing /// . /// public AnimancerGraph( PlayableGraph graph, TransitionLibrary transitions = null) { _PlayableGraph = graph; Transitions = transitions; Graph = this; _PreUpdatePlayable = UpdatableListPlayable.Create(this, 2, _PreUpdatables); var postUpdate = UpdatableListPlayable.Create(this, 0, _PostUpdatables); _PlayableGraph.Connect(postUpdate, 0, _PreUpdatePlayable, 1); States = new(this); #if UNITY_EDITOR Editor.AnimancerGraphCleanup.AddGraph(this); #endif } /// /// Creates an in a new /// . /// public AnimancerGraph(TransitionLibrary transitions = null) : this(CreateGraph(), transitions) { } /************************************************************************************************************************/ /// /// Creates a new empty /// and consumes the name set by if it was called. /// /// /// The caller is responsible for calling on the returned object, /// except in Edit Mode where it will be called automatically. /// public static PlayableGraph CreateGraph() { #if UNITY_EDITOR var name = _NextGraphName; _NextGraphName = null; return name != null ? PlayableGraph.Create(name) : PlayableGraph.Create(); #else return PlayableGraph.Create(); #endif } /************************************************************************************************************************/ #if UNITY_EDITOR private static string _NextGraphName; #endif /// [Editor-Conditional] /// Sets the display name for the next instance to give its . /// [System.Diagnostics.Conditional(Strings.UnityEditor)] public static void SetNextGraphName(string name) { #if UNITY_EDITOR _NextGraphName = name; #endif } /************************************************************************************************************************/ #if UNITY_EDITOR /// [Editor-Only] Returns "Component Name (Animancer)". public override string ToString() => !Component.IsNullOrDestroyed() ? $"{Component.gameObject.name} ({nameof(Animancer)})" : _PlayableGraph.IsValid() ? _PlayableGraph.GetEditorName() : $"Destroyed ({nameof(Animancer)})"; #endif /************************************************************************************************************************/ private PlayableOutput _Output; /// The connected to this . public PlayableOutput Output { get { if (!_Output.IsOutputValid()) _Output = _PlayableGraph.FindOutput(_PreUpdatePlayable); return _Output; } } /************************************************************************************************************************/ /// /// Plays this graph 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.IsGraphInitialized && animancer.Graph == this, $"{nameof(CreateOutput)} was called on an {nameof(AnimancerGraph)} which does not match the" + $" {nameof(IAnimancerComponent)}.{nameof(IAnimancerComponent.Graph)}."); Debug.Assert(animator == animancer.Animator, $"{nameof(CreateOutput)} was called with an {nameof(Animator)} which does not match the" + $" {nameof(IAnimancerComponent)}.{nameof(IAnimancerComponent.Animator)}."); #if UNITY_EDITOR CaptureInactiveInitializationStackTrace(animancer); #endif } if (Output.IsOutputValid()) { Debug.LogWarning( $"A {nameof(PlayableGraph)} output is already connected to the {nameof(AnimancerGraph)}." + $" The old output should be destroyed using `animancerComponent.Graph.DestroyOutput();`" + $" before calling {nameof(CreateOutput)}.", animator); } #endif Layers ??= new AnimancerLayerMixerList(this); Component = animancer; // Generic Rigs can blend with an underlying Animator Controller but Humanoids can't. SkipFirstFade = animator.isHuman || animator.runtimeAnimatorController == null; AnimancerEvent.Invoker.Initialize(animator.updateMode); #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, _PreUpdatePlayable, _PlayableGraph); #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 = Output; _PlayableGraph.Connect(playable, output.GetSourcePlayable(), 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(_PlayableGraph, data, 1); InsertOutputPlayable(playable); return playable; } /************************************************************************************************************************/ /// void ICopyable.CopyFrom(AnimancerGraph copyFrom, CloneContext context) => CopyFrom(copyFrom, context); /// Copies all layers and states from `copyFrom` into this graph. public void CopyFrom(AnimancerGraph copyFrom, bool includeInactiveStates = false) { var context = CloneContext.Pool.Instance.Acquire(); CopyFrom(copyFrom, context, includeInactiveStates); CloneContext.Pool.Instance.Release(context); } /// Copies all layers and states from `copyFrom` into this graph. public void CopyFrom(AnimancerGraph copyFrom, CloneContext context, bool includeInactiveStates = false) { if (copyFrom == this) return; var wouldCloneUpdatables = context.WillCloneUpdatables; context.WillCloneUpdatables = true; Speed = copyFrom.Speed; SetKeepChildrenConnected(copyFrom.KeepChildrenConnected); SkipFirstFade = copyFrom.SkipFirstFade; IsGraphPlaying = copyFrom.IsGraphPlaying; FrameID = copyFrom.FrameID; context[copyFrom] = this; // Register states in the context. foreach (var copyFromState in copyFrom.States) if (States.TryGet(copyFromState.Key, out var copyToState)) context[copyFromState] = copyToState; var layerCount = copyFrom._Layers.Count; // Register layers in the context. for (int i = 0; i < layerCount; i++) { var copyFromLayer = copyFrom._Layers[i]; var copyToLayer = _Layers[i]; context[copyFromLayer] = copyToLayer; } // Copy existing layers. for (int i = 0; i < layerCount; i++) { var copyFromLayer = copyFrom._Layers[i]; var copyToLayer = _Layers[i]; copyToLayer.CopyFrom(copyFromLayer, context); copyToLayer.CopyStatesFrom(copyFromLayer, context, includeInactiveStates); } // Stop any extra layers. for (int i = layerCount; i < _Layers.Count; i++) _Layers[i].Stop(); _PreUpdatables.CloneFrom(copyFrom._PreUpdatables, context); _PostUpdatables.CloneFrom(copyFrom._PostUpdatables, context); context.WillCloneUpdatables = wouldCloneUpdatables; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Cleanup /************************************************************************************************************************/ /// Is this currently usable (not destroyed)? /// Calls if the was already destroyed. public bool IsValidOrDispose() { if (_PlayableGraph.IsValid()) return true; DisposeAll(); return false; } /************************************************************************************************************************/ /// Destroys the and everything in it. This operation cannot be undone. /// If the is owned by another system, use instead. public void Destroy() { OnGraphDestroyed(); if (_PlayableGraph.IsValid()) { _PlayableGraph.Destroy(); #if UNITY_EDITOR Editor.AnimancerGraphCleanup.RemoveGraph(this); #endif } } /// /// Destroys this and everything inside it (layers, states, etc.) /// without destroying the . /// /// /// This can be useful if Animancer was initialized inside a owned by another /// system such as Unity's Animation Rigging package. Otherwise, use . /// public void DestroyPlayables() { OnGraphDestroyed(); if (_PlayableGraph.IsValid()) _PlayableGraph.DestroySubgraph(_PreUpdatePlayable); } /************************************************************************************************************************/ /// Destroys the if it exists and returns true if successful. public bool DestroyOutput() { var output = Output; if (!output.IsOutputValid()) return false; _PlayableGraph.DestroyOutput(output); return true; } /************************************************************************************************************************/ /// Calls in case wasn't called. ~AnimancerGraph() => OnGraphDestroyed(); /// Calls on all layers. private void OnGraphDestroyed() { if (_Layers != null) { var layerCount = _Layers.Count; for (int i = 0; i < layerCount; i++) _Layers[i].OnGraphDestroyed(); } DisposeAll(); } /************************************************************************************************************************/ 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 by Animation Jobs. public List Disposables => _Disposables ??= new(); /************************************************************************************************************************/ /// Calls on all and discards them. private void DisposeAll() { if (_Disposables == null || _Disposables.Count == 0) return; GC.SuppressFinalize(this); var previous = Current; Current = this; var i = _Disposables.Count; DisposeNext: try { while (--i >= 0) { _Disposables[i].Dispose(); } _Disposables.Clear(); } catch (Exception exception) { Debug.LogException(exception, Component as Object); goto DisposeNext; } Current = previous; } /************************************************************************************************************************/ #region Inverse Kinematics // These fields are stored here but accessed via the LayerList. /************************************************************************************************************************/ private bool _ApplyAnimatorIK; /// public override 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 override 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; /************************************************************************************************************************/ /// /// Gets the state registered with the , /// stops and rewinds it to the start, then returns it. /// /// Note that playing something new will automatically stop the old animation. 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. /// /// Note that playing something new will automatically stop the old animation. 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. /// /// Note that playing something new will automatically stop the old animation. 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; } /************************************************************************************************************************/ /// Is the `clip` currently being played by at least one state in the specified layer? /// /// This method is inefficient because it searches through every state, /// 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).GetTotalChildWeight(); 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) { _PlayableGraph.Play(); _IsGraphPlaying = true; #if UNITY_EDITOR // In Edit Mode, unpausing the graph doesn't work properly unless we force it to change. if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) Evaluate(0.00001f); #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) { _PlayableGraph.Stop(); _IsGraphPlaying = false; } } /************************************************************************************************************************/ /// /// Evaluates all of the currently playing animations to apply their states to the animated objects. /// public void Evaluate() { _PlayableGraph.Evaluate(); AnimancerEvent.Invoker.InvokeAllAndClear(); } /// /// 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) { _PlayableGraph.Evaluate(deltaTime); AnimancerEvent.Invoker.InvokeAllAndClear(); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Description /************************************************************************************************************************/ /// public void AppendDescription( StringBuilder text, string separator = "\n") { text.AppendField(null, nameof(AnimancerGraph), Component); separator += Strings.Indent; var layerDetailsSeparator = separator + Strings.Indent; text.AppendField(separator, "Layer Count", _Layers.Count); AnimancerNode.AppendIKDetails(text, separator, this); var count = _Layers.Count; for (int i = 0; i < count; i++) { text.Append(separator); _Layers[i].AppendDescription(text, layerDetailsSeparator); } States.AppendDescriptionOrOrphans(text, separator); text.AppendLine(); AppendInternalDetails(text, Strings.Indent, separator + Strings.Indent); AppendGraphDetails(text, separator, 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"); } /************************************************************************************************************************/ /// Appends everything in the `collection`. private static void AppendAll( StringBuilder text, string sectionPrefix, string itemPrefix, ICollection collection, string name) { var count = collection != null ? collection.Count : 0; text.AppendField(sectionPrefix, name, count); if (collection == null) return; var separator = $"{itemPrefix}{Strings.Indent}"; foreach (var item in collection) { text.Append(itemPrefix); if (item is AnimancerNode node) text.Append(node.GetPath()); else text.AppendDescription(item, separator); } } /************************************************************************************************************************/ private const string NoPlayable = "No Playable"; /// Appends the structure of the . public void AppendGraphDetails( StringBuilder text, string itemPrefix = "\n", string indent = Strings.Indent) { var indentedPrefix = itemPrefix + indent; var outputCount = _PlayableGraph.GetOutputCount(); for (int i = 0; i < outputCount; i++) { var output = _PlayableGraph.GetOutput(i); text.Append(itemPrefix) .Append(output.GetPlayableOutputType().Name); #if UNITY_EDITOR text.Append(" \"") .Append(UnityEditor.Playables.PlayableOutputEditorExtensions.GetEditorName(output)) .Append('"'); #endif text.Append(": "); var playable = output.GetSourcePlayable(); AppendGraphDetails(text, playable, indentedPrefix, indent); } } /// Appends the structure of the . private void AppendGraphDetails( StringBuilder text, Playable playable, string itemPrefix = "\n", string indent = Strings.Indent) { text.Append(itemPrefix); if (!playable.IsValid()) { text.Append(NoPlayable); return; } text.Append(playable.GetPlayableType().Name); var inputCount = playable.GetInputCount(); if (inputCount == 0) return; itemPrefix += indent; for (int i = 0; i < inputCount; i++) { var child = playable.GetInput(i); AppendGraphDetails(text, child, itemPrefix, indent); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Update /************************************************************************************************************************/ /// [Pro-Only] /// Adds the `updatable` to the list that need to be updated before the playables if it wasn't there already. /// /// /// The determines the update rate. /// /// 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.AssertGraph(node, this); } #endif _PreUpdatables.Add(updatable); } /************************************************************************************************************************/ /// [Pro-Only] /// Adds the `updatable` to the list that need to be updated after the playables if it wasn't there already. /// /// /// The determines the update rate. /// /// 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.AssertGraph(node, this); } #endif _PostUpdatables.Add(updatable); } /************************************************************************************************************************/ /// 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 don't 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) => _PreUpdatables.Remove(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 don't 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) => _PostUpdatables.Remove(updatable); /************************************************************************************************************************/ /// The graph currently being executed. /// /// During invocations, /// use AnimancerEvent.Current.State.Graph instead. /// public static AnimancerGraph Current { get; private set; } /// The current . /// After each update, this property will be left at its most recent value. public static float DeltaTime { get; private set; } /// The current . /// /// After each update, 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; } /************************************************************************************************************************/ /// [Internal] Calls on each of the `updatables`. internal void UpdateAll(IUpdatable.List updatables, float deltaTime, ulong frameID) { var previous = Current; Current = this; DeltaTime = deltaTime; updatables.UpdateAll(); if (FrameID != frameID)// Pre-Update. { // 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' a're done with the pre-update. FrameID = frameID; AssertPreUpdate(); } else// Post-Update. { for (int i = _Layers.Count - 1; i >= 0; i--) _Layers[i].UpdateEvents(); } Current = previous; } /************************************************************************************************************************/ /// [Assert-Conditional] Called during the pre-update to perform msome safety checks. [System.Diagnostics.Conditional(Strings.Assertions)] private void AssertPreUpdate() { #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)} doesn't affect {nameof(Animancer)}." + $" Use {nameof(AnimancerGraph)}.{nameof(Speed)} instead.", animator); } } #endif } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }