// Animancer // Copyright 2020 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; 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. /// public sealed partial class AnimancerPlayable : PlayableBehaviour, IEnumerator, IPlayableWrapper, IAnimationClipCollection { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ /// /// The fade duration for any of the CrossFade methods to use if the caller doesn't specify. /// public const float DefaultFadeDuration = 0.25f; /************************************************************************************************************************/ /// [Internal] The containing this . internal PlayableGraph _Graph; /// [Internal] The connected to the output. internal Playable _RootPlayable; /// [Internal] The which layers connect to. internal Playable _LayerMixer; /// [Internal] The which layers connect to. Playable IPlayableWrapper.Playable { get { return _LayerMixer; } } /// An is the root of the graph so it has no parent. IPlayableWrapper IPlayableWrapper.Parent { get { return null; } } /************************************************************************************************************************/ // These collections can not be readonly because when Unity clones the Template it copies the memory without running the // field initialisers on the new clone so everything would be referencing the same collections. /************************************************************************************************************************/ /// [Pro-Only] The layers which each manage their own set of animations. public LayerList Layers { get; private set; } /// The states managed by this playable. public StateDictionary States { get; private set; } /// All of the nodes that need to be updated. private Key.KeyedList _DirtyNodes; /// All of the objects that need to be updated early. private Key.KeyedList _Updatables; /// The that calls . private LateUpdate _LateUpdate; /************************************************************************************************************************/ /// The component that is playing this . public IAnimancerComponent Component { get; private set; } /************************************************************************************************************************/ /// /// The number of times the has changed. 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 { get { return Layers[0].CommandCount; } } /************************************************************************************************************************/ /// Determines what time source is used to update the . public DirectorUpdateMode UpdateMode { get { return _Graph.GetTimeUpdateMode(); } set { _Graph.SetTimeUpdateMode(value); } } /************************************************************************************************************************/ /// /// 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 a runtime build. /// /// /// /// /// 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 { return (float)_LayerMixer.GetSpeed(); } set { _LayerMixer.SetSpeed(value); } } /************************************************************************************************************************/ #region KeepChildrenConnected /************************************************************************************************************************/ private bool _KeepChildrenConnected; /// /// Indicates whether child playables should stay connected to the graph at all times. /// /// By default, this value is false so that playables will be disconnected from the graph while they are at 0 /// weight which stops it from evaluating them every frame and is generally more efficient. /// public bool KeepChildrenConnected { get { return _KeepChildrenConnected; } set { if (_KeepChildrenConnected == value) return; _KeepChildrenConnected = value; Layers.SetWeightlessChildrenConnected(value); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Graph Management /************************************************************************************************************************/ /// /// 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 OnPlayableCreate rather than being readonly with field initialisers. /// private static readonly AnimancerPlayable Template = new AnimancerPlayable(); /************************************************************************************************************************/ /// /// 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 && UNITY_2018_1_OR_NEWER var graph = _NextGraphName != null ? PlayableGraph.Create(_NextGraphName) : PlayableGraph.Create(); _NextGraphName = null; #else var graph = PlayableGraph.Create(); #endif return ScriptPlayable.Create(graph, Template, 2) .GetBehaviour(); } /************************************************************************************************************************/ /// [Internal] Called by Unity as it creates this . public override void OnPlayableCreate(Playable playable) { _RootPlayable = playable; _Graph = playable.GetGraph(); Layers = new LayerList(this, out _LayerMixer); States = new StateDictionary(this); _Updatables = new Key.KeyedList(); _DirtyNodes = new Key.KeyedList(); _LateUpdate = LateUpdate.Create(this); #if UNITY_EDITOR RegisterInstance(); #endif } /************************************************************************************************************************/ #if UNITY_EDITOR && UNITY_2018_1_OR_NEWER 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.EditorOnly)] public static void SetNextGraphName(string name) { #if UNITY_EDITOR && UNITY_2018_1_OR_NEWER _NextGraphName = name; #endif } /************************************************************************************************************************/ /// /// Plays this playable on the . /// public void SetOutput(IAnimancerComponent animancer) { SetOutput(animancer.Animator, animancer); } /// /// Plays this playable on the specified `animator`. /// public void SetOutput(Animator animator, IAnimancerComponent animancer) { #if UNITY_EDITOR // Do nothing if the target is a prefab. if (UnityEditor.EditorUtility.IsPersistent(animator)) return; #endif #if UNITY_ASSERTIONS if (animancer != null) { Debug.Assert(animancer.IsPlayableInitialised && animancer.Playable == this, "SetOutput was called on an AnimancerPlayable which does not match the IAnimancerComponent.Playable."); Debug.Assert(animator == animancer.Animator, "SetOutput was called with an Animator which does not match the IAnimancerComponent.Animator."); } #endif Component = animancer; var output = _Graph.GetOutput(0); if (output.IsOutputValid()) _Graph.DestroyOutput(output); if (animator != null) { AnimationPlayableUtilities.Play(animator, _RootPlayable, _Graph); _IsGraphPlaying = true; } } /************************************************************************************************************************/ private bool _IsGraphPlaying = true; /// Indicates whether the is currently playing. public bool IsGraphPlaying { get { return _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 true as long as the hasn't been destroyed (such as by ). /// public bool IsValid { get { return _Graph.IsValid(); } } /// /// Destroys the and all its layers and states. This operation cannot be undone. /// public void Destroy() { GC.SuppressFinalize(this); // Destroy all active updatables. Debug.Assert(_CurrentUpdatable == -1, UpdatableLoopStartError); _CurrentUpdatable = _Updatables.Count; ContinueLoop: try { while (--_CurrentUpdatable >= 0) { _Updatables[_CurrentUpdatable].OnDestroy(); } _Updatables.Clear(); } catch (Exception ex) { Debug.LogException(ex); goto ContinueLoop; } // No need to destroy every layer and state individually because destroying the graph will do so anyway. Layers = null; States = null; if (_Graph.IsValid()) _Graph.Destroy(); } /************************************************************************************************************************/ /// Appends a detailed descrption of all currently playing states and other registered states. public string GetDescription(int maxChildDepth = 7) { var text = new StringBuilder(); AppendDescription(text, maxChildDepth); return text.ToString(); } /// /// Appends a detailed descrption of all currently playing states and other registered states. /// public void AppendDescription(StringBuilder text, int maxChildDepth = 7) { text.Append("AnimancerPlayable (").Append(Component) .Append(") Layer Count: ").Append(Layers.Count); var count = Layers.Count; for (int i = 0; i < count; i++) Layers[i].AppendDescription(text, maxChildDepth, "\n "); text.AppendLine(); count = _Updatables.Count; text.Append(" Updatables: ").Append(count); for (int j = 0; j < count; j++) { text.AppendLine(); text.Append(" "); text.Append(_Updatables[j].ToString()); } text.AppendLine(); count = _DirtyNodes.Count; text.Append(" Dirty Nodes: ").Append(count); for (int j = 0; j < count; j++) { text.AppendLine(); text.Append(" "); text.Append(_DirtyNodes[j].ToString()); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Play Management /************************************************************************************************************************/ /// Calls on the . public object GetKey(AnimationClip clip) { return Component.GetKey(clip); } /************************************************************************************************************************/ /// /// Stops all other animations, plays the `clip`, and returns its state. /// /// The animation will continue playing from its current . /// To restart it from the beginning you can use ...Play(clip, layerIndex).Time = 0;. /// public AnimancerState Play(AnimationClip clip) { return Play(States.GetOrCreate(clip)); } /// /// Stops all other animations, plays the `state`, and returns it. /// /// The animation will continue playing from its current . /// If you wish to force it back to the start, you can simply set the `state`s time to 0. /// public AnimancerState Play(AnimancerState state) { return state.Layer.Play(state); } /// /// Creates a state for the `transition` if it didn't already exist, then calls /// or /// depending on the . /// public AnimancerState Play(ITransition transition) { return Play(transition, transition.FadeDuration, transition.FadeMode); } /// /// Stops all other animations, plays the animation registered with the `key`, and returns that /// state. If no state is registered with the `key`, this method does nothing and returns null. /// /// The animation will continue playing from its current . /// If you wish to force it back to the start, you can simply set the returned state's time to 0. /// on the returned state. /// public AnimancerState Play(object key) { AnimancerState state; if (States.TryGet(key, out state)) return Play(state); else return null; } /************************************************************************************************************************/ /// /// Starts fading in the `clip` over the course of the `fadeDuration` while fading out all others in the same /// layer. Returns its state. /// /// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this /// method will allow it to complete the existing fade rather than starting a slower one. /// /// If the layer currently has 0 , this method will fade in the layer itself /// and simply the `state`. /// /// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build. /// public AnimancerState Play(AnimationClip clip, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed) { return Play(States.GetOrCreate(clip), fadeDuration, mode); } /// /// Starts fading in the `state` over the course of the `fadeDuration` while fading out all others in the same /// layer. Returns the `state`. /// /// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this /// method will allow it to complete the existing fade rather than starting a slower one. /// /// If the layer currently has 0 , this method will fade in the layer itself /// and simply the `state`. /// /// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build. /// public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed) { return state.Layer.Play(state, fadeDuration, mode); } /// /// Creates a state for the `transition` if it didn't already exist, then calls /// or /// depending on the . /// public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed) { var state = States.GetOrCreate(transition); state = Play(state, fadeDuration, mode); transition.Apply(state); return state; } /// /// Starts fading in the animation registered with the `key` over the course of the `fadeDuration` while fading /// out all others in the same layer. Returns the animation's state (or null if none was registered). /// /// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this /// method will allow it to complete the existing fade rather than starting a slower one. /// /// If the layer currently has 0 , this method will fade in the layer itself /// and simply the `state`. /// /// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build. /// public AnimancerState Play(object key, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed) { AnimancerState state; if (States.TryGet(key, out state)) return Play(state, fadeDuration, mode); else return null; } /************************************************************************************************************************/ /// /// Gets the state registered with the , stops and rewinds it to the start, then /// returns it. /// public AnimancerState Stop(IHasKey hasKey) { return 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) { AnimancerState state; if (States.TryGet(key, out state)) state.Stop(); return state; } /// /// Calls on all animations to stop them from playing and rewind them to the /// start. /// public void Stop() { if (Layers._Layers == null) return; var count = Layers.Count; for (int i = 0; i < count; i++) Layers._Layers[i].Stop(); } /************************************************************************************************************************/ /// /// Returns true if a state is registered with the and it is currently playing. /// public bool IsPlaying(IHasKey hasKey) { return IsPlaying(hasKey.Key); } /// /// Returns true if a state is registered with the `key` and it is currently playing. /// public bool IsPlaying(object key) { AnimancerState state; return States.TryGet(key, out state) && state.IsPlaying; } /// /// Returns true if at least one animation is being played. /// public bool IsPlaying() { if (!_IsGraphPlaying) return false; var count = Layers.Count; for (int i = 0; i < count; i++) { if (Layers._Layers[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; var count = Layers.Count; while (--count >= 0) { if (Layers._Layers[count].IsPlayingClip(clip)) return true; } return false; } /************************************************************************************************************************/ /// /// Calculates the total of all states in this playable. /// public float GetTotalWeight() { float weight = 0; var count = Layers.Count; for (int i = 0; i < count; i++) { weight += Layers._Layers[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. /************************************************************************************************************************/ /// /// Determines if any animations are still playing so this object can be used as a custom yield instruction. /// bool IEnumerator.MoveNext() { var count = Layers.Count; for (int i = 0; i < count; i++) { if (Layers._Layers[i].IsPlayingAndNotEnding()) return true; } return false; } /// Returns null. object IEnumerator.Current { get { return null; } } /// Does nothing. void IEnumerator.Reset() { } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region End Events /************************************************************************************************************************/ /// /// The called 'End' which is currently being triggered. /// public static AnimationEvent CurrentEndEvent { get; private set; } /************************************************************************************************************************/ /// /// Invokes the callback of the state that is playing the animation /// which triggered the event. Returns true if such a state exists (even if it doesn't have a callback). /// public bool OnEndEventReceived(AnimationEvent animationEvent) { // This method could be changed to invoke all events with the correct clip and weight by collecting all the // events into a list and invoking them at the end. var count = Layers.Count; for (int i = 0; i < count; i++) { if (TryInvokeOnEndEvent(animationEvent, Layers._Layers[i].CurrentState)) return true; } for (int i = 0; i < count; i++) { if (Layers._Layers[i].TryInvokeOnEndEvent(animationEvent)) return true; } return false; } /************************************************************************************************************************/ /// /// If the and match the /// , this method invokes the callback /// and returns true. /// internal static bool TryInvokeOnEndEvent(AnimationEvent animationEvent, AnimancerState state) { if (state.Weight != animationEvent.animatorClipInfo.weight || state.Clip != animationEvent.animatorClipInfo.clip || !state.HasEvents) return false; var endEvent = state.Events.endEvent; if (endEvent.callback != null) { Debug.Assert(CurrentEndEvent == null, "Recursive call to TryInvokeOnEndEvent detected"); try { CurrentEndEvent = animationEvent; endEvent.Invoke(state); } finally { CurrentEndEvent = null; } } return true; } /************************************************************************************************************************/ /// /// If the has a float parameter above 0, this method returns that value. /// Otherwise this method calls so if you aren't using an /// Animation Event with the function name "End" you can just call that method directly. /// public static float GetFadeOutDuration(float minDuration = DefaultFadeDuration) { if (CurrentEndEvent != null && CurrentEndEvent.floatParameter > 0) return CurrentEndEvent.floatParameter; return AnimancerEvent.GetFadeOutDuration(minDuration); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #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 . /// [System.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 . /// [System.Obsolete("You should not use an AnimancerState as a key. Just check AnimancerState.IsPlaying.", true)] public bool IsPlaying(AnimancerState key) { return key.IsPlaying; } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ #region Update /************************************************************************************************************************/ /// /// Adds the `updatable` to the list of objects that need to be updated if it wasn't there already. /// /// This method is safe to call at any time, even during an update. /// /// The execution order of updatables 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 element. /// public void RequireUpdate(IUpdatable updatable) { _Updatables.AddNew(updatable); } /// /// Removes the `updatable` from the list of objects that need to be updated. /// /// 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, by means that the update order can change. /// public void CancelUpdate(IUpdatable updatable) { var index = Key.IndexOf(updatable.Key); if (index < 0) return; _Updatables.RemoveAtSwap(index); if (_CurrentUpdatable < index && this == Current) _CurrentUpdatable--; } /************************************************************************************************************************/ /// /// Adds the `node` to the list that need to be updated if it wasn't there already. /// This method is safe to call at any time, even during an update. /// public void RequireUpdate(AnimancerNode node) { Validate.Root(node, this); _DirtyNodes.AddNew(node); } /************************************************************************************************************************/ /// The object currently executing . public static AnimancerPlayable Current { get; private set; } /// /// The current (most recent) . /// /// After , this property will be left at its most recent value. /// public static float DeltaTime { get; private set; } /// /// The current (most recent) . /// /// 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 uint FrameID { get; private set; } /// The index of the currently being updated. private static int _CurrentUpdatable = -1; /// An error message for potential multithreading issues. private const string UpdatableLoopStartError = "AnimancerPlayable._CurrentUpdatable != -1." + " This may mean that multiple loops are iterating through the updatables simultaneously" + " (likely on different threads)."; /************************************************************************************************************************/ /// [Internal] /// Called by the before the rest of the s are evaluated. /// Calls and on everything /// that needs it. /// public override void PrepareFrame(Playable playable, FrameData info) { Current = this; DeltaTime = info.deltaTime; // These loops could potentially be swapped. The only thing EarlyUpdate currently does is cache the time of // states for events to compare with the time after updating. Debug.Assert(_CurrentUpdatable == -1, UpdatableLoopStartError); _CurrentUpdatable = _Updatables.Count; ContinueLoop: try { while (--_CurrentUpdatable >= 0) { _Updatables[_CurrentUpdatable].EarlyUpdate(); } } catch (Exception ex) { Debug.LogException(ex); goto ContinueLoop; } var count = _DirtyNodes.Count; while (--count >= 0) { bool needsMoreUpdates; _DirtyNodes[count].Update(out needsMoreUpdates); if (!needsMoreUpdates) _DirtyNodes.RemoveAtSwap(count); } _LateUpdate.IsConnected = _Updatables.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 = (uint)info.frameId; Current = null; } /************************************************************************************************************************/ #region Late Update /************************************************************************************************************************/ /// /// 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 sealed class LateUpdate : PlayableBehaviour { /************************************************************************************************************************/ /// See . private static readonly LateUpdate Template = new LateUpdate(); /// 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 LateUpdate Create(AnimancerPlayable root) { var instance = ScriptPlayable.Create(root._Graph, Template, 0) .GetBehaviour(); instance._Root = root; return instance; } /************************************************************************************************************************/ /// Called by Unity as 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 { return _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); } } } } /************************************************************************************************************************/ /// /// Called by the after the rest of the s are evaluated. /// Calls on everything that needs it. /// public override void PrepareFrame(Playable playable, FrameData info) { Debug.Assert(_CurrentUpdatable == -1, UpdatableLoopStartError); var updatables = _Root._Updatables; _CurrentUpdatable = updatables.Count; ContinueLoop: try { while (--_CurrentUpdatable >= 0) { updatables[_CurrentUpdatable].LateUpdate(); } } catch (Exception ex) { Debug.LogException(ex); goto ContinueLoop; } // 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 late update. // However, doing that with KeepChildrenConnected == false (the default for efficiency) 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 updated in edit-mode. /// private void RegisterInstance() { if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) return; if (_AllInstances == null) { _AllInstances = new List(); var previousFrameTime = UnityEditor.EditorApplication.timeSinceStartup; UnityEditor.EditorApplication.update += () => { var time = UnityEditor.EditorApplication.timeSinceStartup; #if !UNITY_2018_3_OR_NEWER var deltaTime = (float)(time - previousFrameTime); #endif previousFrameTime = time; for (int i = _AllInstances.Count - 1; i >= 0; i--) { var playable = _AllInstances[i]; if (playable.ShouldStayAlive()) { #if !UNITY_2018_3_OR_NEWER // Unity 2018.3+ automatically updates playables in Edit Mode. if (playable._IsGraphPlaying) playable.Evaluate(deltaTime); #endif } else { if (playable != null && playable.IsValid) playable.Destroy(); _AllInstances.RemoveAt(i); } } }; UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += () => { for (int i = _AllInstances.Count - 1; i >= 0; i--) { var playable = _AllInstances[i]; if (playable.IsValid) playable.Destroy(); } _AllInstances.Clear(); }; } _AllInstances.Add(this); } /************************************************************************************************************************/ /// /// Determines whether this playable should stay alive or be destroyed. /// private bool ShouldStayAlive() { if (!IsValid) return false; if (Component == null) return true; var obj = Component as Object; if (!ReferenceEquals(obj, null) && 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; var wasAnimatePhysics = initial.Value == AnimatorUpdateMode.AnimatePhysics; var isAnimatePhysics = current == AnimatorUpdateMode.AnimatePhysics; return wasAnimatePhysics != isAnimatePhysics; } /************************************************************************************************************************/ /// [Editor-Only] /// Draws the and lists. /// internal void DoUpdateListGUI() { Editor.AnimancerGUI.BeginVerticalBox(GUI.skin.box); GUILayout.Label("Updatables " + _Updatables.Count); for (int i = 0; i < _Updatables.Count; i++) { GUILayout.Label(_Updatables[i].ToString()); } GUILayout.Label("Dirty Nodes " + _DirtyNodes.Count); for (int i = 0; i < _DirtyNodes.Count; i++) { GUILayout.Label(_DirtyNodes[i].ToString()); } Editor.AnimancerGUI.EndVerticalBox(GUI.skin.box); if (Editor.AnimancerGUI.TryUseClickEventInLastRect(1)) { var menu = new UnityEditor.GenericMenu(); Editor.AnimancerLayerDrawer.ShowUpdatingNodes.AddToggleFunction(menu); menu.ShowAsContext(); } } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ } }