Net.Like.Xue.Tokyo/Assets/Plugins/Animancer/Internal/Core/AnimancerState.cs

1076 lines
43 KiB
C#

// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
using Animancer.Editor;
#endif
namespace Animancer
{
/// <summary>
/// Base class for all states in an <see cref="AnimancerPlayable"/> graph which manages one or more
/// <see cref="Playable"/>s.
/// </summary>
///
/// <remarks>
/// This class can be used as a custom yield instruction to wait until the animation either stops playing or
/// reaches its end.
/// <para></para>
/// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">States</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
///
public abstract partial class AnimancerState : AnimancerNode,
IAnimationClipCollection,
ICopyable<AnimancerState>
{
/************************************************************************************************************************/
#region Graph
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerPlayable"/> at the root of the graph.</summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="Parent"/> has a different <see cref="AnimancerNode.Root"/>.
/// Setting the <see cref="Parent"/>'s <see cref="AnimancerNode.Root"/> will apply to its children recursively
/// because they must always match.
/// </exception>
public void SetRoot(AnimancerPlayable root)
{
if (Root == root)
return;
// Remove from the old root.
if (Root != null)
{
Root.CancelPreUpdate(this);
Root.States.Unregister(this);
if (_EventDispatcher != null)
Root.CancelPostUpdate(_EventDispatcher);
if (_Parent != null && _Parent.Root != root)
{
_Parent.OnRemoveChild(this);
_Parent = null;
Index = -1;
}
DestroyPlayable();
}
#if UNITY_ASSERTIONS
else
{
if (_Parent != null && _Parent.Root != root)
throw new InvalidOperationException(
"Unable to set the Root of a state which has a Parent." +
" Setting the Parent's Root will apply to its children recursively" +
" because they must always match.");
}
#endif
// Set the root.
Root = root;
// Add to the new root.
if (root != null)
{
root.States.Register(this);
if (_EventDispatcher != null)
root.RequirePostUpdate(_EventDispatcher);
CreatePlayable();
}
for (int i = ChildCount - 1; i >= 0; i--)
GetChild(i)?.SetRoot(root);
if (_Parent != null)
CopyIKFlags(_Parent);
}
/************************************************************************************************************************/
private AnimancerNode _Parent;
/// <summary>The object which receives the output of the <see cref="Playable"/>.</summary>
public sealed override IPlayableWrapper Parent => _Parent;
/// <summary>Connects this state to the `parent` state at the specified `index`.</summary>
/// <remarks>
/// If the `parent` is null, this state will be disconnected from everything.
/// <para></para>
/// Use <see cref="AnimancerLayer.AddChild(AnimancerState)"/> instead of this method to connect to a layer.
/// </remarks>
public void SetParent(AnimancerNode parent, int index)
{
if (_Parent != null)
{
_Parent.OnRemoveChild(this);
_Parent = null;
}
if (parent == null)
{
Index = -1;
return;
}
SetRoot(parent.Root);
Index = index;
_Parent = parent;
parent.OnAddChild(this);
CopyIKFlags(parent);
}
/// <summary>[Internal] Directly sets the <see cref="Parent"/> without triggering any other connection methods.</summary>
internal void SetParentInternal(AnimancerNode parent, int index = -1)
{
_Parent = parent;
Index = index;
}
/************************************************************************************************************************/
// Layer.
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerLayer Layer => _Parent?.Layer;
/// <summary>
/// The index of the <see cref="AnimancerLayer"/> this state is connected to (determined by the
/// <see cref="Parent"/>). Returns <c>-1</c> if this state is not connected to a layer.
/// </summary>
public int LayerIndex
{
get
{
if (_Parent == null)
return -1;
var layer = _Parent.Layer;
if (layer == null)
return -1;
return layer.Index;
}
set
{
Root.Layers[value].AddChild(this);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Key and Clip
/************************************************************************************************************************/
internal object _Key;
/// <summary>
/// The object used to identify this state in the root <see cref="AnimancerPlayable.States"/> dictionary.
/// Can be null.
/// </summary>
public object Key
{
get => _Key;
set
{
if (Root == null)
{
_Key = value;
}
else
{
Root.States.Unregister(this);
_Key = value;
Root.States.Register(this);
}
}
}
/************************************************************************************************************************/
/// <summary>The <see cref="AnimationClip"/> which this state plays (if any).</summary>
/// <exception cref="NotSupportedException">This state type doesn't have a clip and you try to set it.</exception>
public virtual AnimationClip Clip
{
get => null;
set => throw new NotSupportedException($"{GetType()} does not support setting the {nameof(Clip)}.");
}
/// <summary>The main object to show in the Inspector for this state (if any).</summary>
/// <exception cref="NotSupportedException">This state type doesn't have a main object and you try to set it.</exception>
/// <exception cref="InvalidCastException">This state can't use the assigned value.</exception>
public virtual Object MainObject
{
get => null;
set => throw new NotSupportedException($"{GetType()} does not support setting the {nameof(MainObject)}.");
}
/************************************************************************************************************************/
/// <summary>
/// Sets the `currentObject` and calls <see cref="AnimancerNode.RecreatePlayable"/>. If the `currentObject` was
/// being used as the <see cref="Key"/> then it is changed as well.
/// </summary>
/// <exception cref="ArgumentNullException">The `newObject` is null.</exception>
protected void ChangeMainObject<T>(ref T currentObject, T newObject) where T : Object
{
if (newObject == null)
throw new ArgumentNullException(nameof(newObject));
if (ReferenceEquals(currentObject, newObject))
return;
if (ReferenceEquals(_Key, currentObject))
Key = newObject;
currentObject = newObject;
RecreatePlayable();
}
/************************************************************************************************************************/
/// <summary>The average velocity of the root motion caused by this state.</summary>
public virtual Vector3 AverageVelocity => default;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Playing
/************************************************************************************************************************/
/// <summary>Is the <see cref="Time"/> automatically advancing?</summary>
private bool _IsPlaying;
/// <summary>Has <see cref="_IsPlaying"/> changed since it was last applied to the <see cref="Playable"/>.</summary>
/// <remarks>
/// Playables start playing by default so we start dirty to pause it during the first update (unless
/// <see cref="IsPlaying"/> is set to true before that).
/// </remarks>
private bool _IsPlayingDirty = true;
/************************************************************************************************************************/
/// <summary>Is the <see cref="Time"/> automatically advancing?</summary>
///
/// <example><code>
/// void IsPlayingExample(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.States.GetOrCreate(clip);
///
/// if (state.IsPlaying)
/// Debug.Log(clip + " is playing");
/// else
/// Debug.Log(clip + " is paused");
///
/// state.IsPlaying = false;// Pause the animation.
///
/// state.IsPlaying = true;// Unpause the animation.
/// }
/// </code></example>
public bool IsPlaying
{
get => _IsPlaying;
set
{
if (_IsPlaying == value)
return;
_IsPlaying = value;
// If it was already dirty then we just returned to the previous state so it is no longer dirty.
if (_IsPlayingDirty)
{
_IsPlayingDirty = false;
// We may still need to be updated for other reasons (such as Weight),
// but if not then we will be removed from the update list next update.
}
else// Otherwise we are now dirty so we need to be updated.
{
_IsPlayingDirty = true;
RequireUpdate();
}
OnSetIsPlaying();
}
}
/// <summary>Called when the value of <see cref="IsPlaying"/> is changed.</summary>
protected virtual void OnSetIsPlaying() { }
/// <summary>Creates and assigns the <see cref="Playable"/> managed by this state.</summary>
/// <remarks>This method also applies the <see cref="AnimancerNode.Speed"/> and <see cref="IsPlaying"/>.</remarks>
public sealed override void CreatePlayable()
{
base.CreatePlayable();
if (_MustSetTime)
{
_MustSetTime = false;
RawTime = _Time;
}
if (!_IsPlaying)
_Playable.Pause();
_IsPlayingDirty = false;
}
/************************************************************************************************************************/
/// <summary>
/// Returns true if this state is playing and is at or fading towards a non-zero
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
public bool IsActive => _IsPlaying && TargetWeight > 0;
/// <summary>
/// Returns true if this state isn't playing and is at 0 <see cref="AnimancerNode.Weight"/>.
/// </summary>
public bool IsStopped => !_IsPlaying && Weight == 0;
/************************************************************************************************************************/
/// <summary>
/// Plays this state immediately, without any blending.
/// <para></para>
/// Unlike <see cref="AnimancerPlayable.Play(AnimancerState)"/>, this method only affects this state and won't
/// stop any others that are playing.
/// </summary>
/// <remarks>
/// Sets <see cref="IsPlaying"/> = true, <see cref="AnimancerNode.Weight"/> = 1, and clears the
/// <see cref="Events"/> (unless <see cref="AutomaticallyClearEvents"/> is disabled).
/// <para></para>
/// Doesn't change the <see cref="Time"/> so it will continue from its current value.
/// </remarks>
public void Play()
{
IsPlaying = true;
Weight = 1;
if (AutomaticallyClearEvents)
EventDispatcher.TryClear(_EventDispatcher);
}
/************************************************************************************************************************/
/// <summary>Stops the animation and makes it inactive immediately so it no longer affects the output.</summary>
/// <remarks>
/// Sets <see cref="AnimancerNode.Weight"/> = 0, <see cref="IsPlaying"/> = false, <see cref="Time"/> = 0, and
/// clears the <see cref="Events"/> (unless <see cref="AutomaticallyClearEvents"/> is disabled).
/// <para></para>
/// To freeze the animation in place without ending it, you only need to set <see cref="IsPlaying"/> = false
/// instead. Or to freeze all animations, you can call <see cref="AnimancerPlayable.PauseGraph"/>.
/// </remarks>
public override void Stop()
{
base.Stop();
IsPlaying = false;
TimeD = 0;
if (AutomaticallyClearEvents)
EventDispatcher.TryClear(_EventDispatcher);
}
/************************************************************************************************************************/
/// <summary>
/// Called by <see cref="AnimancerNode.StartFade(float, float)"/>.
/// Clears the <see cref="Events"/> (unless <see cref="AutomaticallyClearEvents"/> is disabled).
/// </summary>
protected internal override void OnStartFade()
{
if (AutomaticallyClearEvents)
EventDispatcher.TryClear(_EventDispatcher);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Timing
/************************************************************************************************************************/
// Time.
/************************************************************************************************************************/
/// <summary>
/// The current time of the <see cref="Playable"/>, retrieved by <see cref="Time"/> whenever the
/// <see cref="_TimeFrameID"/> is different from the <see cref="AnimancerPlayable.FrameID"/>.
/// </summary>
private double _Time;
/// <summary>
/// Indicates whether the <see cref="_Time"/> needs to be assigned to the <see cref="Playable"/> next update.
/// </summary>
/// <remarks>
/// <see cref="EventDispatcher"/> executes after all other playables, at which point changes can still be made to
/// their time but not their weight which means that if we set the time immediately then it can be out of sync
/// with the weight. For example, if an animation ends and you play another, the first animation would be
/// stopped and rewinded to the start but would still be at full weight so it would show its first frame before
/// the new animation actually takes effect (even if the previous animation was not looping).
/// <para></para>
/// So instead, we simply delay setting the actual playable time until the next update so that time and weight
/// are always in sync.
/// </remarks>
private bool _MustSetTime;
/// <summary>
/// The <see cref="AnimancerPlayable.FrameID"/> from when the <see cref="Time"/> was last retrieved from the
/// <see cref="Playable"/>.
/// </summary>
private ulong _TimeFrameID;
/************************************************************************************************************************/
/// <summary>The number of seconds that have passed since the start of this animation.</summary>
///
/// <remarks>
/// This value will continue increasing after the animation passes the end of its <see cref="Length"/> while
/// the animated object either freezes in place or starts again from the beginning according to whether it is
/// looping or not.
/// <para></para>
/// Events and root motion between the old and new time will be skipped when setting this value. Use
/// <see cref="MoveTime(float, bool)"/> instead if you don't want that behaviour.
/// <para></para>
/// This property internally uses <see cref="RawTime"/> whenever the value is out of date or gets changed.
/// <para></para>
/// <em>Animancer Lite does not allow this value to be changed in runtime builds (except resetting it to 0).</em>
/// </remarks>
///
/// <example><code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// // Skip 0.5 seconds into the animation:
/// state.Time = 0.5f;
///
/// // Skip 50% of the way through the animation (0.5 in a range of 0 to 1):
/// state.NormalizedTime = 0.5f;
///
/// // Skip to the end of the animation and play backwards.
/// state.NormalizedTime = 1;
/// state.Speed = -1;
/// }
/// </code></example>
public float Time
{
get => (float)TimeD;
set => TimeD = value;
}
/// <summary>The underlying <see cref="double"/> value of <see cref="Time"/>.</summary>
public double TimeD
{
get
{
var root = Root;
if (root == null || _MustSetTime)
return _Time;
var frameID = root.FrameID;
if (_TimeFrameID != frameID)
{
_TimeFrameID = frameID;
_Time = RawTime;
}
return _Time;
}
set
{
#if UNITY_ASSERTIONS
if (!value.IsFinite())
throw new ArgumentOutOfRangeException(nameof(value), value,
$"{nameof(Time)} {Strings.MustBeFinite}");
#endif
_Time = value;
var root = Root;
if (root == null)
{
_MustSetTime = true;
}
else
{
_TimeFrameID = root.FrameID;
// Don't allow the time to be changed during a post update because it would take effect this frame
// but Weight changes wouldn't so the Time and Weight would be out of sync. For example, if an
// event plays a state, the old state would be stopped back at Time 0 but its Weight would not yet
// be 0 so it would show its first frame before the new animation takes effect.
if (AnimancerPlayable.IsRunningPostUpdate(root))
{
_MustSetTime = true;
root.RequirePreUpdate(this);
}
else
{
RawTime = value;
}
}
_EventDispatcher?.OnTimeChanged();
}
}
/************************************************************************************************************************/
/// <summary>
/// The internal implementation of <see cref="Time"/> which directly gets and sets the underlying value.
/// </summary>
/// <remarks>
/// Setting this value actually calls <see cref="PlayableExtensions.SetTime"/> twice to ensure that animation
/// events aren't triggered incorrectly. Calling it only once would trigger any animation events between the
/// previous time and the new time. So if an animation plays to the end and you set the time back to 0 (such as
/// by calling <see cref="Stop"/> or playing a different animation), the next time that animation played it
/// would immediately trigger all of its events, then play through and trigger them normally as well.
/// </remarks>
public virtual double RawTime
{
get
{
Validate.AssertPlayable(this);
return _Playable.GetTime();
}
set
{
Validate.AssertPlayable(this);
var time = value;
_Playable.SetTime(time);
_Playable.SetTime(time);
}
}
/************************************************************************************************************************/
/// <summary>
/// The <see cref="Time"/> of this state as a portion of the animation's <see cref="Length"/>, meaning the
/// value goes from 0 to 1 as it plays from start to end, regardless of how long that actually takes.
/// </summary>
///
/// <remarks>
/// This value will continue increasing after the animation passes the end of its <see cref="Length"/> while
/// the animated object either freezes in place or starts again from the beginning according to whether it is
/// looping or not.
/// <para></para>
/// The fractional part of the value (<c>NormalizedTime % 1</c>) is the percentage (0-1) of progress in the
/// current loop while the integer part (<c>(int)NormalizedTime</c>) is the number of times the animation has
/// been looped.
/// <para></para>
/// Events and root motion between the old and new time will be skipped when setting this value. Use
/// <see cref="MoveTime(float, bool)"/> instead if you don't want that behaviour.
/// <para></para>
/// <em>Animancer Lite does not allow this value to be changed in runtime builds (except resetting it to 0).</em>
/// </remarks>
///
/// <example><code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// // Skip 0.5 seconds into the animation:
/// state.Time = 0.5f;
///
/// // Skip 50% of the way through the animation (0.5 in a range of 0 to 1):
/// state.NormalizedTime = 0.5f;
///
/// // Skip to the end of the animation and play backwards.
/// state.NormalizedTime = 1;
/// state.Speed = -1;
/// }
/// </code></example>
public float NormalizedTime
{
get => (float)NormalizedTimeD;
set => NormalizedTimeD = value;
}
/// <summary>The underlying <see cref="double"/> value of <see cref="NormalizedTime"/>.</summary>
public double NormalizedTimeD
{
get
{
var length = Length;
if (length != 0)
return TimeD / Length;
else
return 0;
}
set => TimeD = value * Length;
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="Time"/> or <see cref="NormalizedTime"/>, but unlike those properties this method
/// applies any Root Motion and Animation Events (but not Animancer Events) between the old and new time.
/// </summary>
public void MoveTime(float time, bool normalized)
=> MoveTime((double)time, normalized);
/// <summary>
/// Sets the <see cref="Time"/> or <see cref="NormalizedTime"/>, but unlike those properties this method
/// applies any Root Motion and Animation Events (but not Animancer Events) between the old and new time.
/// </summary>
public virtual void MoveTime(double time, bool normalized)
{
#if UNITY_ASSERTIONS
if (!time.IsFinite())
throw new ArgumentOutOfRangeException(nameof(time), time,
$"{nameof(Time)} {Strings.MustBeFinite}");
#endif
var root = Root;
if (root != null)
_TimeFrameID = root.FrameID;
if (normalized)
time *= Length;
_Time = time;
_Playable.SetTime(time);
}
/************************************************************************************************************************/
/// <summary>Prevents the <see cref="RawTime"/> from being applied.</summary>
protected void CancelSetTime() => _MustSetTime = false;
/************************************************************************************************************************/
// Duration.
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// The <see cref="NormalizedTime"/> after which the <see cref="AnimancerEvent.Sequence.OnEnd"/> callback will
/// be invoked every frame.
/// </summary>
/// <remarks>
/// This is a wrapper around <see cref="AnimancerEvent.Sequence.NormalizedEndTime"/> so that if the value has
/// not been set (<see cref="float.NaN"/>) it can be determined based on the
/// <see cref="AnimancerNode.EffectiveSpeed"/>: positive speed ends at 1 and negative speed ends at 0.
/// <para></para>
/// <em>Animancer Lite does not allow this value to be changed in runtime builds.</em>
/// </remarks>
public float NormalizedEndTime
{
get
{
if (_EventDispatcher != null)
{
var time = _EventDispatcher.Events.NormalizedEndTime;
if (!float.IsNaN(time))
return time;
}
return AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(EffectiveSpeed);
}
set => Events.NormalizedEndTime = value;
}
/************************************************************************************************************************/
/// <summary>
/// The number of seconds the animation will take to play fully at its current
/// <see cref="AnimancerNode.EffectiveSpeed"/>.
/// </summary>
///
/// <remarks>
/// For the time remaining from now until it reaches the end, use <see cref="RemainingDuration"/> instead.
/// <para></para>
/// Setting this value modifies the <see cref="AnimancerNode.EffectiveSpeed"/>, not the <see cref="Length"/>.
/// <para></para>
/// <em>Animancer Lite does not allow this value to be changed in runtime builds.</em>
/// </remarks>
///
/// <example><code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// state.Duration = 1;// Play fully in 1 second.
/// state.Duration = 2;// Play fully in 2 seconds.
/// state.Duration = 0.5f;// Play fully in half a second.
/// state.Duration = -1;// Play backwards fully in 1 second.
/// state.NormalizedTime = 1; state.Duration = -1;// Play backwards from the end in 1 second.
/// }
/// </code></example>
public float Duration
{
get
{
var speed = EffectiveSpeed;
if (_EventDispatcher != null)
{
var endTime = _EventDispatcher.Events.NormalizedEndTime;
if (!float.IsNaN(endTime))
{
if (speed > 0)
return Length * endTime / speed;
else
return Length * (1 - endTime) / -speed;
}
}
return Length / Math.Abs(speed);
}
set
{
var length = Length;
if (_EventDispatcher != null)
{
var endTime = _EventDispatcher.Events.NormalizedEndTime;
if (!float.IsNaN(endTime))
{
if (EffectiveSpeed > 0)
length *= endTime;
else
length *= 1 - endTime;
}
}
EffectiveSpeed = length / value;
}
}
/************************************************************************************************************************/
/// <summary>
/// The number of seconds this state will take to go from its current <see cref="NormalizedTime"/> to the
/// <see cref="NormalizedEndTime"/> at its current <see cref="AnimancerNode.EffectiveSpeed"/>.
/// </summary>
///
/// <remarks>
/// For the time it would take to play fully from the start, use the <see cref="Duration"/> instead.
/// <para></para>
/// Setting this value modifies the <see cref="AnimancerNode.EffectiveSpeed"/>, not the <see cref="Length"/>.
/// <para></para>
/// <em>Animancer Lite does not allow this value to be changed in runtime builds.</em>
/// </remarks>
///
/// <example><code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// state.RemainingDuration = 1;// Play from the current time to the end in 1 second.
/// state.RemainingDuration = 2;// Play from the current time to the end in 2 seconds.
/// state.RemainingDuration = 0.5f;// Play from the current time to the end in half a second.
/// state.RemainingDuration = -1;// Play from the current time away from the end.
/// }
/// </code></example>
public float RemainingDuration
{
get => (Length * NormalizedEndTime - Time) / EffectiveSpeed;
set => EffectiveSpeed = (Length * NormalizedEndTime - Time) / value;
}
/************************************************************************************************************************/
// Length.
/************************************************************************************************************************/
/// <summary>The total time this state would take to play in seconds when <see cref="AnimancerNode.Speed"/> = 1.</summary>
public abstract float Length { get; }
/// <summary>Will this state loop back to the start when it reaches the end?</summary>
public virtual bool IsLooping => false;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>
/// Updates the <see cref="AnimancerNode.Weight"/> for fading, applies it to this state's port on the parent
/// mixer, and plays or pauses the <see cref="Playable"/> if its state is dirty.
/// </summary>
/// <remarks>
/// If the <see cref="Parent"/>'s <see cref="AnimancerNode.KeepChildrenConnected"/> is set to false, this
/// method will also connect/disconnect this node from the <see cref="Parent"/> in the playable graph.
/// </remarks>
protected internal override void Update(out bool needsMoreUpdates)
{
base.Update(out needsMoreUpdates);
if (_IsPlayingDirty)
{
_IsPlayingDirty = false;
if (_IsPlaying)
_Playable.Play();
else
_Playable.Pause();
}
if (_MustSetTime)
{
_MustSetTime = false;
RawTime = _Time;
}
}
/************************************************************************************************************************/
/// <summary>Destroys the <see cref="Playable"/> and cleans up this state.</summary>
/// <remarks>
/// This method is NOT called automatically, so when implementing a custom state type you must use
/// <see cref="AnimancerPlayable.Disposables"/> if you need to guarantee that things will get cleaned up.
/// </remarks>
public virtual void Destroy()
{
if (_Parent != null)
{
_Parent.OnRemoveChild(this);
_Parent = null;
}
Index = -1;
EventDispatcher.TryClear(_EventDispatcher);
var root = Root;
if (root != null)
{
root.States.Unregister(this);
// For some reason this is slightly faster than _Playable.Destroy().
if (_Playable.IsValid())
root._Graph.DestroyPlayable(_Playable);
}
}
/************************************************************************************************************************/
/// <summary>Creates a copy of this state with the same <see cref="AnimancerNode.Root"/>.</summary>
public AnimancerState Clone()
=> Clone(Root);
/// <summary>Creates a copy of this state with the specified <see cref="AnimancerNode.Root"/>.</summary>
public abstract AnimancerState Clone(AnimancerPlayable root);
/// <summary>Sets the <see cref="AnimancerNode.Root"/>.</summary>
/// <remarks>
/// This method skips several steps of <see cref="SetRoot"/> and is intended to only be called on states
/// immediately after their creation.
/// </remarks>
protected void SetNewCloneRoot(AnimancerPlayable root)
{
if (root == null)
return;
Root = root;
CreatePlayable();
}
/// <inheritdoc/>
void ICopyable<AnimancerState>.CopyFrom(AnimancerState copyFrom)
{
Events = copyFrom.HasEvents ? copyFrom.Events : null;
TimeD = copyFrom.TimeD;
((ICopyable<AnimancerNode>)this).CopyFrom(copyFrom);
}
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>] Gathers all the animations in this state.</summary>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
{
clips.Gather(Clip);
for (int i = ChildCount - 1; i >= 0; i--)
GetChild(i).GatherAnimationClips(clips);
}
/************************************************************************************************************************/
/// <summary>
/// Returns true if the animation is playing and has not yet passed the
/// <see cref="AnimancerEvent.Sequence.EndEvent"/>.
/// </summary>
/// <remarks>
/// This method is called by <see cref="IEnumerator.MoveNext"/> so this object can be used as a custom yield
/// instruction to wait until it finishes.
/// </remarks>
public override bool IsPlayingAndNotEnding()
{
if (!IsPlaying || !_Playable.IsValid())
return false;
var speed = EffectiveSpeed;
if (speed > 0)
{
float endTime;
if (_EventDispatcher != null)
{
endTime = _EventDispatcher.Events.NormalizedEndTime;
if (float.IsNaN(endTime))
endTime = Length;
else
endTime *= Length;
}
else endTime = Length;
return Time <= endTime;
}
else if (speed < 0)
{
float endTime;
if (_EventDispatcher != null)
{
endTime = _EventDispatcher.Events.NormalizedEndTime;
if (float.IsNaN(endTime))
endTime = 0;
else
endTime *= Length;
}
else endTime = 0;
return Time >= endTime;
}
else return true;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="AnimancerNode.DebugName"/> if one is set, otherwise a string describing the type of this
/// state and the name of the <see cref="MainObject"/>.
/// </summary>
public override string ToString()
{
#if UNITY_ASSERTIONS
if (!string.IsNullOrEmpty(DebugName))
return DebugName;
#endif
var type = GetType().Name;
var mainObject = MainObject;
if (mainObject != null)
return $"{mainObject.name} ({type})";
else
return type;
}
/************************************************************************************************************************/
#region Descriptions
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] Returns a custom drawer for this state.</summary>
protected internal virtual IAnimancerNodeDrawer CreateDrawer()
=> new AnimancerStateDrawer<AnimancerState>(this);
#endif
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AppendDetails(StringBuilder text, string separator)
{
text.Append(separator).Append($"{nameof(Key)}: ").Append(AnimancerUtilities.ToStringOrNull(_Key));
var mainObject = MainObject;
if (mainObject != _Key as Object)
text.Append(separator).Append($"{nameof(MainObject)}: ").Append(AnimancerUtilities.ToStringOrNull(mainObject));
#if UNITY_EDITOR
if (mainObject != null)
text.Append(separator).Append("AssetPath: ").Append(AssetDatabase.GetAssetPath(mainObject));
#endif
base.AppendDetails(text, separator);
text.Append(separator).Append($"{nameof(IsPlaying)}: ").Append(IsPlaying);
try
{
text.Append(separator).Append($"{nameof(Time)} (Normalized): ").Append(Time);
text.Append(" (").Append(NormalizedTime).Append(')');
text.Append(separator).Append($"{nameof(Length)}: ").Append(Length);
text.Append(separator).Append($"{nameof(IsLooping)}: ").Append(IsLooping);
}
catch (Exception exception)
{
text.Append(separator).Append(exception);
}
text.Append(separator).Append($"{nameof(Events)}: ");
if (_EventDispatcher != null && _EventDispatcher.Events != null)
text.Append(_EventDispatcher.Events.DeepToString(false));
else
text.Append("null");
}
/************************************************************************************************************************/
/// <summary>Returns the hierarchy path of this state through its <see cref="Parent"/>s.</summary>
public string GetPath()
{
if (_Parent == null)
return null;
var path = ObjectPool.AcquireStringBuilder();
AppendPath(path, _Parent);
AppendPortAndType(path);
return path.ReleaseToString();
}
/// <summary>Appends the hierarchy path of this state through its <see cref="Parent"/>s.</summary>
private static void AppendPath(StringBuilder path, AnimancerNode parent)
{
var parentState = parent as AnimancerState;
if (parentState != null && parentState._Parent != null)
{
AppendPath(path, parentState._Parent);
}
else
{
path.Append("Layers[")
.Append(parent.Layer.Index)
.Append("].States");
return;
}
var state = parent as AnimancerState;
if (state != null)
{
state.AppendPortAndType(path);
}
else
{
path.Append(" -> ")
.Append(parent.GetType());
}
}
/// <summary>Appends "[Index] -> GetType().Name".</summary>
private void AppendPortAndType(StringBuilder path)
{
path.Append('[')
.Append(Index)
.Append("] -> ")
.Append(GetType().Name);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}