You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1220 lines
51 KiB
C#
1220 lines
51 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// A <see cref="PlayableBehaviour"/> which can be used as a substitute for the
|
|
/// <see cref="RuntimeAnimatorController"/> normally used to control an <see cref="Animator"/>.
|
|
/// <para></para>
|
|
/// This class can be used as a custom yield instruction to wait until all animations finish playing.
|
|
/// </summary>
|
|
public sealed partial class AnimancerPlayable : PlayableBehaviour,
|
|
IEnumerator, IPlayableWrapper, IAnimationClipCollection
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Fields and Properties
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// The fade duration for any of the CrossFade methods to use if the caller doesn't specify.
|
|
/// </summary>
|
|
public const float DefaultFadeDuration = 0.25f;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal] The <see cref="PlayableGraph"/> containing this <see cref="AnimancerPlayable"/>.</summary>
|
|
internal PlayableGraph _Graph;
|
|
|
|
/// <summary>[Internal] The <see cref="Playable"/> connected to the <see cref="PlayableGraph"/> output.</summary>
|
|
internal Playable _RootPlayable;
|
|
|
|
/// <summary>[Internal] The <see cref="Playable"/> which layers connect to.</summary>
|
|
internal Playable _LayerMixer;
|
|
|
|
/// <summary>[Internal] The <see cref="Playable"/> which layers connect to.</summary>
|
|
Playable IPlayableWrapper.Playable { get { return _LayerMixer; } }
|
|
|
|
/// <summary>An <see cref="AnimancerPlayable"/> is the root of the graph so it has no parent.</summary>
|
|
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.
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Pro-Only] The layers which each manage their own set of animations.</summary>
|
|
public LayerList Layers { get; private set; }
|
|
|
|
/// <summary>The states managed by this playable.</summary>
|
|
public StateDictionary States { get; private set; }
|
|
|
|
/// <summary>All of the nodes that need to be updated.</summary>
|
|
private Key.KeyedList<AnimancerNode> _DirtyNodes;
|
|
|
|
/// <summary>All of the objects that need to be updated early.</summary>
|
|
private Key.KeyedList<IUpdatable> _Updatables;
|
|
|
|
/// <summary>The <see cref="PlayableBehaviour"/> that calls <see cref="IUpdatable.LateUpdate"/>.</summary>
|
|
private LateUpdate _LateUpdate;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The component that is playing this <see cref="AnimancerPlayable"/>.</summary>
|
|
public IAnimancerComponent Component { get; private set; }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// The number of times the <see cref="StateDictionary.Current"/> 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.
|
|
/// </summary>
|
|
public int CommandCount { get { return Layers[0].CommandCount; } }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Determines what time source is used to update the <see cref="PlayableGraph"/>.</summary>
|
|
public DirectorUpdateMode UpdateMode
|
|
{
|
|
get { return _Graph.GetTimeUpdateMode(); }
|
|
set { _Graph.SetTimeUpdateMode(value); }
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// How fast the <see cref="AnimancerState.Time"/> of all animations is advancing every frame.
|
|
/// <para></para>
|
|
/// 1 is the normal speed.
|
|
/// <para></para>
|
|
/// A negative value will play the animations backwards.
|
|
/// <para></para>
|
|
/// Setting this value to 0 would pause all animations, but calling <see cref="PauseGraph"/> is more efficient.
|
|
/// <para></para>
|
|
/// Animancer Lite does not allow this value to be changed in a runtime build.
|
|
/// </summary>
|
|
///
|
|
/// <example>
|
|
/// <code>
|
|
/// 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.
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public float Speed
|
|
{
|
|
get { return (float)_LayerMixer.GetSpeed(); }
|
|
set { _LayerMixer.SetSpeed(value); }
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#region KeepChildrenConnected
|
|
/************************************************************************************************************************/
|
|
|
|
private bool _KeepChildrenConnected;
|
|
|
|
/// <summary>
|
|
/// Indicates whether child playables should stay connected to the graph at all times.
|
|
/// <para></para>
|
|
/// 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.
|
|
/// </summary>
|
|
public bool KeepChildrenConnected
|
|
{
|
|
get { return _KeepChildrenConnected; }
|
|
set
|
|
{
|
|
if (_KeepChildrenConnected == value)
|
|
return;
|
|
|
|
_KeepChildrenConnected = value;
|
|
Layers.SetWeightlessChildrenConnected(value);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Graph Management
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Since <see cref="ScriptPlayable{T}.Create(PlayableGraph, int)"/> 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.
|
|
/// </summary>
|
|
private static readonly AnimancerPlayable Template = new AnimancerPlayable();
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Creates a new <see cref="PlayableGraph"/> containing an <see cref="AnimancerPlayable"/>.
|
|
/// <para></para>
|
|
/// The caller is responsible for calling <see cref="Destroy()"/> on the returned object, except in Edit Mode
|
|
/// where it will be called automatically.
|
|
/// <para></para>
|
|
/// Consider calling <see cref="SetNextGraphName"/> before this method to give it a name.
|
|
/// </summary>
|
|
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<AnimancerPlayable>.Create(graph, Template, 2)
|
|
.GetBehaviour();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal] Called by Unity as it creates this <see cref="AnimancerPlayable"/>.</summary>
|
|
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<IUpdatable>();
|
|
_DirtyNodes = new Key.KeyedList<AnimancerNode>();
|
|
_LateUpdate = LateUpdate.Create(this);
|
|
|
|
#if UNITY_EDITOR
|
|
RegisterInstance();
|
|
#endif
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
#if UNITY_EDITOR && UNITY_2018_1_OR_NEWER
|
|
private static string _NextGraphName;
|
|
#endif
|
|
|
|
/// <summary>[Editor-Conditional]
|
|
/// Sets the display name for the next <see cref="Create"/> call to give its <see cref="PlayableGraph"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Having this method separate from <see cref="Create"/> allows the
|
|
/// <see cref="System.Diagnostics.ConditionalAttribute"/> to compile it out of runtime builds which would
|
|
/// otherwise require #ifs on the caller side.
|
|
/// </remarks>
|
|
[System.Diagnostics.Conditional(Strings.EditorOnly)]
|
|
public static void SetNextGraphName(string name)
|
|
{
|
|
#if UNITY_EDITOR && UNITY_2018_1_OR_NEWER
|
|
_NextGraphName = name;
|
|
#endif
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Plays this playable on the <see cref="IAnimancerComponent.Animator"/>.
|
|
/// </summary>
|
|
public void SetOutput(IAnimancerComponent animancer)
|
|
{
|
|
SetOutput(animancer.Animator, animancer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plays this playable on the specified `animator`.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>Indicates whether the <see cref="PlayableGraph"/> is currently playing.</summary>
|
|
public bool IsGraphPlaying
|
|
{
|
|
get { return _IsGraphPlaying; }
|
|
set
|
|
{
|
|
if (value)
|
|
UnpauseGraph();
|
|
else
|
|
PauseGraph();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes playing the <see cref="PlayableGraph"/> if <see cref="PauseGraph"/> was called previously.
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Freezes the <see cref="PlayableGraph"/> at its current state.
|
|
/// <para></para>
|
|
/// If you call this method, you are responsible for calling <see cref="UnpauseGraph"/> to resume playing.
|
|
/// </summary>
|
|
public void PauseGraph()
|
|
{
|
|
if (_IsGraphPlaying)
|
|
{
|
|
_Graph.Stop();
|
|
_IsGraphPlaying = false;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Evaluates all of the currently playing animations to apply their states to the animated objects.
|
|
/// </summary>
|
|
public void Evaluate()
|
|
{
|
|
_Graph.Evaluate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void Evaluate(float deltaTime)
|
|
{
|
|
_Graph.Evaluate(deltaTime);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns true as long as the <see cref="PlayableGraph"/> hasn't been destroyed (such as by <see cref="Destroy()"/>).
|
|
/// </summary>
|
|
public bool IsValid { get { return _Graph.IsValid(); } }
|
|
|
|
/// <summary>
|
|
/// Destroys the <see cref="PlayableGraph"/> and all its layers and states. This operation cannot be undone.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Appends a detailed descrption of all currently playing states and other registered states.</summary>
|
|
public string GetDescription(int maxChildDepth = 7)
|
|
{
|
|
var text = new StringBuilder();
|
|
AppendDescription(text, maxChildDepth);
|
|
return text.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends a detailed descrption of all currently playing states and other registered states.
|
|
/// </summary>
|
|
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
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Calls <see cref="IAnimancerComponent.GetKey"/> on the <see cref="Component"/>.</summary>
|
|
public object GetKey(AnimationClip clip)
|
|
{
|
|
return Component.GetKey(clip);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Stops all other animations, plays the `clip`, and returns its state.
|
|
/// <para></para>
|
|
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
|
/// To restart it from the beginning you can use <c>...Play(clip, layerIndex).Time = 0;</c>.
|
|
/// </summary>
|
|
public AnimancerState Play(AnimationClip clip)
|
|
{
|
|
return Play(States.GetOrCreate(clip));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops all other animations, plays the `state`, and returns it.
|
|
/// <para></para>
|
|
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
|
/// If you wish to force it back to the start, you can simply set the `state`s time to 0.
|
|
/// </summary>
|
|
public AnimancerState Play(AnimancerState state)
|
|
{
|
|
return state.Layer.Play(state);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a state for the `transition` if it didn't already exist, then calls
|
|
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
|
|
/// depending on the <see cref="ITransition.FadeDuration"/>.
|
|
/// </summary>
|
|
public AnimancerState Play(ITransition transition)
|
|
{
|
|
return Play(transition, transition.FadeDuration, transition.FadeMode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// <para></para>
|
|
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
|
/// 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.
|
|
/// </summary>
|
|
public AnimancerState Play(object key)
|
|
{
|
|
AnimancerState state;
|
|
if (States.TryGet(key, out state))
|
|
return Play(state);
|
|
else
|
|
return null;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Starts fading in the `clip` over the course of the `fadeDuration` while fading out all others in the same
|
|
/// layer. Returns its state.
|
|
/// <para></para>
|
|
/// 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.
|
|
/// <para></para>
|
|
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
|
|
/// and simply <see cref="AnimancerState.Play"/> the `state`.
|
|
/// <para></para>
|
|
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build.
|
|
/// </summary>
|
|
public AnimancerState Play(AnimationClip clip, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed)
|
|
{
|
|
return Play(States.GetOrCreate(clip), fadeDuration, mode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts fading in the `state` over the course of the `fadeDuration` while fading out all others in the same
|
|
/// layer. Returns the `state`.
|
|
/// <para></para>
|
|
/// 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.
|
|
/// <para></para>
|
|
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
|
|
/// and simply <see cref="AnimancerState.Play"/> the `state`.
|
|
/// <para></para>
|
|
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build.
|
|
/// </summary>
|
|
public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = FadeMode.FixedSpeed)
|
|
{
|
|
return state.Layer.Play(state, fadeDuration, mode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a state for the `transition` if it didn't already exist, then calls
|
|
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
|
|
/// depending on the <see cref="ITransition.FadeDuration"/>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// <para></para>
|
|
/// 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.
|
|
/// <para></para>
|
|
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
|
|
/// and simply <see cref="AnimancerState.Play"/> the `state`.
|
|
/// <para></para>
|
|
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in a runtime build.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Gets the state registered with the <see cref="IHasKey.Key"/>, stops and rewinds it to the start, then
|
|
/// returns it.
|
|
/// </summary>
|
|
public AnimancerState Stop(IHasKey hasKey)
|
|
{
|
|
return Stop(hasKey.Key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="AnimancerState.Stop"/> on the state registered with the `key` to stop it from playing and
|
|
/// rewind it to the start.
|
|
/// </summary>
|
|
public AnimancerState Stop(object key)
|
|
{
|
|
AnimancerState state;
|
|
if (States.TryGet(key, out state))
|
|
state.Stop();
|
|
|
|
return state;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="AnimancerState.Stop"/> on all animations to stop them from playing and rewind them to the
|
|
/// start.
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
if (Layers._Layers == null)
|
|
return;
|
|
|
|
var count = Layers.Count;
|
|
for (int i = 0; i < count; i++)
|
|
Layers._Layers[i].Stop();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns true if a state is registered with the <see cref="IHasKey.Key"/> and it is currently playing.
|
|
/// </summary>
|
|
public bool IsPlaying(IHasKey hasKey)
|
|
{
|
|
return IsPlaying(hasKey.Key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if a state is registered with the `key` and it is currently playing.
|
|
/// </summary>
|
|
public bool IsPlaying(object key)
|
|
{
|
|
AnimancerState state;
|
|
|
|
return
|
|
States.TryGet(key, out state) &&
|
|
state.IsPlaying;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if at least one animation is being played.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns true if the `clip` is currently being played by at least one state in the specified layer.
|
|
/// <para></para>
|
|
/// This method is inefficient because it searches through every state to find any that are playing the `clip`,
|
|
/// unlike <see cref="IsPlaying(object)"/> which only checks the state registered using the specified key.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Calculates the total <see cref="AnimancerNode.Weight"/> of all states in this playable.
|
|
/// </summary>
|
|
public float GetTotalWeight()
|
|
{
|
|
float weight = 0;
|
|
|
|
var count = Layers.Count;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
weight += Layers._Layers[i].GetTotalWeight();
|
|
}
|
|
|
|
return weight;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[<see cref="IAnimationClipCollection"/>]
|
|
/// Gathers all the animations in all layers.
|
|
/// </summary>
|
|
public void GatherAnimationClips(ICollection<AnimationClip> clips)
|
|
{
|
|
Layers.GatherAnimationClips(clips);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
// IEnumerator for yielding in a coroutine to wait until animations have stopped.
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Determines if any animations are still playing so this object can be used as a custom yield instruction.
|
|
/// </summary>
|
|
bool IEnumerator.MoveNext()
|
|
{
|
|
var count = Layers.Count;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
if (Layers._Layers[i].IsPlayingAndNotEnding())
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Returns null.</summary>
|
|
object IEnumerator.Current { get { return null; } }
|
|
|
|
/// <summary>Does nothing.</summary>
|
|
void IEnumerator.Reset() { }
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region End Events
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// The <see cref="AnimationEvent"/> called 'End' which is currently being triggered.
|
|
/// </summary>
|
|
public static AnimationEvent CurrentEndEvent { get; private set; }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Invokes the <see cref="AnimancerEvent.Sequence.OnEnd"/> 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).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// If the <see cref="AnimancerState.Clip"/> and <see cref="AnimancerNode.Weight"/> match the
|
|
/// <see cref="AnimationEvent"/>, this method invokes the <see cref="AnimancerEvent.Sequence.OnEnd"/> callback
|
|
/// and returns true.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// If the <see cref="CurrentEndEvent"/> has a float parameter above 0, this method returns that value.
|
|
/// Otherwise this method calls <see cref="AnimancerEvent.GetFadeOutDuration"/> so if you aren't using an
|
|
/// Animation Event with the function name "End" you can just call that method directly.
|
|
/// </summary>
|
|
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.
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Warning]
|
|
/// You should not use an <see cref="AnimancerState"/> as a key.
|
|
/// Just call <see cref="AnimancerState.Stop"/>.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>[Warning]
|
|
/// You should not use an <see cref="AnimancerState"/> as a key.
|
|
/// Just check <see cref="AnimancerState.IsPlaying"/>.
|
|
/// </summary>
|
|
[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
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Adds the `updatable` to the list of objects that need to be updated if it wasn't there already.
|
|
/// <para></para>
|
|
/// This method is safe to call at any time, even during an update.
|
|
/// <para></para>
|
|
/// The execution order of updatables is non-deterministic. Specifically, the most recently added will be
|
|
/// updated first and <see cref="CancelUpdate"/> will change the order by swapping the last one into the place
|
|
/// of the removed element.
|
|
/// </summary>
|
|
public void RequireUpdate(IUpdatable updatable)
|
|
{
|
|
_Updatables.AddNew(updatable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the `updatable` from the list of objects that need to be updated.
|
|
/// <para></para>
|
|
/// This method is safe to call at any time, even during an update.
|
|
/// <para></para>
|
|
/// 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.
|
|
/// </summary>
|
|
public void CancelUpdate(IUpdatable updatable)
|
|
{
|
|
var index = Key.IndexOf(updatable.Key);
|
|
if (index < 0)
|
|
return;
|
|
|
|
_Updatables.RemoveAtSwap(index);
|
|
|
|
if (_CurrentUpdatable < index && this == Current)
|
|
_CurrentUpdatable--;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void RequireUpdate(AnimancerNode node)
|
|
{
|
|
Validate.Root(node, this);
|
|
_DirtyNodes.AddNew(node);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The object currently executing <see cref="PrepareFrame"/>.</summary>
|
|
public static AnimancerPlayable Current { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The current (most recent) <see cref="FrameData.deltaTime"/>.
|
|
/// <para></para>
|
|
/// After <see cref="PrepareFrame"/>, this property will be left at its most recent value.
|
|
/// </summary>
|
|
public static float DeltaTime { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The current (most recent) <see cref="FrameData.frameId"/>.
|
|
/// <para></para>
|
|
/// <see cref="AnimancerState.Time"/> uses this value to determine whether it has accessed the playable's time
|
|
/// since it was last updated in order to cache its value.
|
|
/// </summary>
|
|
public uint FrameID { get; private set; }
|
|
|
|
/// <summary>The index of the <see cref="IUpdatable"/> currently being updated.</summary>
|
|
private static int _CurrentUpdatable = -1;
|
|
|
|
/// <summary>An error message for potential multithreading issues.</summary>
|
|
private const string UpdatableLoopStartError = "AnimancerPlayable._CurrentUpdatable != -1." +
|
|
" This may mean that multiple loops are iterating through the updatables simultaneously" +
|
|
" (likely on different threads).";
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal]
|
|
/// Called by the <see cref="PlayableGraph"/> before the rest of the <see cref="Playable"/>s are evaluated.
|
|
/// Calls <see cref="IUpdatable.EarlyUpdate"/> and <see cref="AnimancerNode.Update"/> on everything
|
|
/// that needs it.
|
|
/// </summary>
|
|
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
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// A <see cref="PlayableBehaviour"/> which connects to a later port than the main layer mixer so that its
|
|
/// <see cref="PrepareFrame"/> method gets called after all other playables are updated in order to call
|
|
/// <see cref="IUpdatable.LateUpdate"/> on the <see cref="_Updatables"/>.
|
|
/// </summary>
|
|
private sealed class LateUpdate : PlayableBehaviour
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>See <see cref="AnimancerPlayable.Template"/>.</summary>
|
|
private static readonly LateUpdate Template = new LateUpdate();
|
|
|
|
/// <summary>The <see cref="AnimancerPlayable"/> this behaviour is connected to.</summary>
|
|
private AnimancerPlayable _Root;
|
|
|
|
/// <summary>The underlying <see cref="Playable"/> of this behaviour.</summary>
|
|
private Playable _Playable;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Creates a new <see cref="LateUpdate"/> for the `root`.</summary>
|
|
public static LateUpdate Create(AnimancerPlayable root)
|
|
{
|
|
var instance = ScriptPlayable<LateUpdate>.Create(root._Graph, Template, 0)
|
|
.GetBehaviour();
|
|
instance._Root = root;
|
|
return instance;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Called by Unity as it creates this <see cref="AnimancerPlayable"/>.</summary>
|
|
public override void OnPlayableCreate(Playable playable)
|
|
{
|
|
_Playable = playable;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private bool _IsConnected;
|
|
|
|
/// <summary>
|
|
/// Indicates whether this behaviour is connected to the <see cref="PlayableGraph"/> and thus, whether it
|
|
/// will receive <see cref="PrepareFrame"/> calls.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Called by the <see cref="PlayableGraph"/> after the rest of the <see cref="Playable"/>s are evaluated.
|
|
/// Calls <see cref="IUpdatable.LateUpdate"/> on everything that needs it.
|
|
/// </summary>
|
|
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<AnimancerPlayable> _AllInstances;
|
|
|
|
/// <summary>[Editor-Only]
|
|
/// Registers this object in the list of things that need to be updated in edit-mode.
|
|
/// </summary>
|
|
private void RegisterInstance()
|
|
{
|
|
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
|
return;
|
|
|
|
if (_AllInstances == null)
|
|
{
|
|
_AllInstances = new List<AnimancerPlayable>();
|
|
|
|
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);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Determines whether this playable should stay alive or be destroyed.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Editor-Only]
|
|
/// Returns true if the `initial` mode was <see cref="AnimatorUpdateMode.AnimatePhysics"/> and the `current`
|
|
/// has changed to another mode or if the `initial` mode was something else and the `current` has changed to
|
|
/// <see cref="AnimatorUpdateMode.AnimatePhysics"/>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Editor-Only]
|
|
/// Draws the <see cref="_Updatables"/> and <see cref="_DirtyNodes"/> lists.
|
|
/// </summary>
|
|
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
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|