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