// 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
/************************************************************************************************************************/
}
}