// 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
{
///
/// Base class for all states in an graph which manages one or more
/// s.
///
///
///
/// This class can be used as a custom yield instruction to wait until the animation either stops playing or
/// reaches its end.
///
/// Documentation: States
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
///
public abstract partial class AnimancerState : AnimancerNode,
IAnimationClipCollection,
ICopyable
{
/************************************************************************************************************************/
#region Graph
/************************************************************************************************************************/
/// The at the root of the graph.
///
/// The has a different .
/// Setting the 's will apply to its children recursively
/// because they must always match.
///
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;
/// The object which receives the output of the .
public sealed override IPlayableWrapper Parent => _Parent;
/// Connects this state to the `parent` state at the specified `index`.
///
/// If the `parent` is null, this state will be disconnected from everything.
///
/// Use instead of this method to connect to a layer.
///
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);
}
/// [Internal] Directly sets the without triggering any other connection methods.
internal void SetParentInternal(AnimancerNode parent, int index = -1)
{
_Parent = parent;
Index = index;
}
/************************************************************************************************************************/
// Layer.
/************************************************************************************************************************/
///
public override AnimancerLayer Layer => _Parent?.Layer;
///
/// The index of the this state is connected to (determined by the
/// ). Returns -1 if this state is not connected to a layer.
///
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;
///
/// The object used to identify this state in the root dictionary.
/// Can be null.
///
public object Key
{
get => _Key;
set
{
if (Root == null)
{
_Key = value;
}
else
{
Root.States.Unregister(this);
_Key = value;
Root.States.Register(this);
}
}
}
/************************************************************************************************************************/
/// The which this state plays (if any).
/// This state type doesn't have a clip and you try to set it.
public virtual AnimationClip Clip
{
get => null;
set => throw new NotSupportedException($"{GetType()} does not support setting the {nameof(Clip)}.");
}
/// The main object to show in the Inspector for this state (if any).
/// This state type doesn't have a main object and you try to set it.
/// This state can't use the assigned value.
public virtual Object MainObject
{
get => null;
set => throw new NotSupportedException($"{GetType()} does not support setting the {nameof(MainObject)}.");
}
/************************************************************************************************************************/
///
/// Sets the `currentObject` and calls . If the `currentObject` was
/// being used as the then it is changed as well.
///
/// The `newObject` is null.
protected void ChangeMainObject(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();
}
/************************************************************************************************************************/
/// The average velocity of the root motion caused by this state.
public virtual Vector3 AverageVelocity => default;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Playing
/************************************************************************************************************************/
/// Is the automatically advancing?
private bool _IsPlaying;
/// Has changed since it was last applied to the .
///
/// Playables start playing by default so we start dirty to pause it during the first update (unless
/// is set to true before that).
///
private bool _IsPlayingDirty = true;
/************************************************************************************************************************/
/// Is the automatically advancing?
///
///
/// 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.
/// }
///
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();
}
}
/// Called when the value of is changed.
protected virtual void OnSetIsPlaying() { }
/// Creates and assigns the managed by this state.
/// This method also applies the and .
public sealed override void CreatePlayable()
{
base.CreatePlayable();
if (_MustSetTime)
{
_MustSetTime = false;
RawTime = _Time;
}
if (!_IsPlaying)
_Playable.Pause();
_IsPlayingDirty = false;
}
/************************************************************************************************************************/
///
/// Returns true if this state is playing and is at or fading towards a non-zero
/// .
///
public bool IsActive => _IsPlaying && TargetWeight > 0;
///
/// Returns true if this state isn't playing and is at 0 .
///
public bool IsStopped => !_IsPlaying && Weight == 0;
/************************************************************************************************************************/
///
/// Plays this state immediately, without any blending.
///
/// Unlike , this method only affects this state and won't
/// stop any others that are playing.
///
///
/// Sets = true, = 1, and clears the
/// (unless is disabled).
///
/// Doesn't change the so it will continue from its current value.
///
public void Play()
{
IsPlaying = true;
Weight = 1;
if (AutomaticallyClearEvents)
EventDispatcher.TryClear(_EventDispatcher);
}
/************************************************************************************************************************/
/// Stops the animation and makes it inactive immediately so it no longer affects the output.
///
/// Sets = 0, = false, = 0, and
/// clears the (unless is disabled).
///
/// To freeze the animation in place without ending it, you only need to set = false
/// instead. Or to freeze all animations, you can call .
///
public override void Stop()
{
base.Stop();
IsPlaying = false;
TimeD = 0;
if (AutomaticallyClearEvents)
EventDispatcher.TryClear(_EventDispatcher);
}
/************************************************************************************************************************/
///
/// Called by .
/// Clears the (unless is disabled).
///
protected internal override void OnStartFade()
{
if (AutomaticallyClearEvents)
EventDispatcher.TryClear(_EventDispatcher);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Timing
/************************************************************************************************************************/
// Time.
/************************************************************************************************************************/
///
/// The current time of the , retrieved by whenever the
/// is different from the .
///
private double _Time;
///
/// Indicates whether the needs to be assigned to the next update.
///
///
/// 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).
///
/// So instead, we simply delay setting the actual playable time until the next update so that time and weight
/// are always in sync.
///
private bool _MustSetTime;
///
/// The from when the was last retrieved from the
/// .
///
private ulong _TimeFrameID;
/************************************************************************************************************************/
/// The number of seconds that have passed since the start of this animation.
///
///
/// This value will continue increasing after the animation passes the end of its while
/// the animated object either freezes in place or starts again from the beginning according to whether it is
/// looping or not.
///
/// Events and root motion between the old and new time will be skipped when setting this value. Use
/// instead if you don't want that behaviour.
///
/// This property internally uses whenever the value is out of date or gets changed.
///
/// Animancer Lite does not allow this value to be changed in runtime builds (except resetting it to 0).
///
///
///
/// 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;
/// }
///
public float Time
{
get => (float)TimeD;
set => TimeD = value;
}
/// The underlying value of .
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();
}
}
/************************************************************************************************************************/
///
/// The internal implementation of which directly gets and sets the underlying value.
///
///
/// Setting this value actually calls 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 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.
///
public virtual double RawTime
{
get
{
Validate.AssertPlayable(this);
return _Playable.GetTime();
}
set
{
Validate.AssertPlayable(this);
var time = value;
_Playable.SetTime(time);
_Playable.SetTime(time);
}
}
/************************************************************************************************************************/
///
/// The of this state as a portion of the animation's , meaning the
/// value goes from 0 to 1 as it plays from start to end, regardless of how long that actually takes.
///
///
///
/// This value will continue increasing after the animation passes the end of its while
/// the animated object either freezes in place or starts again from the beginning according to whether it is
/// looping or not.
///
/// The fractional part of the value (NormalizedTime % 1) is the percentage (0-1) of progress in the
/// current loop while the integer part ((int)NormalizedTime) is the number of times the animation has
/// been looped.
///
/// Events and root motion between the old and new time will be skipped when setting this value. Use
/// instead if you don't want that behaviour.
///
/// Animancer Lite does not allow this value to be changed in runtime builds (except resetting it to 0).
///
///
///
/// 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;
/// }
///
public float NormalizedTime
{
get => (float)NormalizedTimeD;
set => NormalizedTimeD = value;
}
/// The underlying value of .
public double NormalizedTimeD
{
get
{
var length = Length;
if (length != 0)
return TimeD / Length;
else
return 0;
}
set => TimeD = value * Length;
}
/************************************************************************************************************************/
///
/// Sets the or , but unlike those properties this method
/// applies any Root Motion and Animation Events (but not Animancer Events) between the old and new time.
///
public void MoveTime(float time, bool normalized)
=> MoveTime((double)time, normalized);
///
/// Sets the or , but unlike those properties this method
/// applies any Root Motion and Animation Events (but not Animancer Events) between the old and new time.
///
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);
}
/************************************************************************************************************************/
/// Prevents the from being applied.
protected void CancelSetTime() => _MustSetTime = false;
/************************************************************************************************************************/
// Duration.
/************************************************************************************************************************/
/// [Pro-Only]
/// The after which the callback will
/// be invoked every frame.
///
///
/// This is a wrapper around so that if the value has
/// not been set () it can be determined based on the
/// : positive speed ends at 1 and negative speed ends at 0.
///
/// Animancer Lite does not allow this value to be changed in runtime builds.
///
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;
}
/************************************************************************************************************************/
///
/// The number of seconds the animation will take to play fully at its current
/// .
///
///
///
/// For the time remaining from now until it reaches the end, use instead.
///
/// Setting this value modifies the , not the .
///
/// Animancer Lite does not allow this value to be changed in runtime builds.
///
///
///
/// 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.
/// }
///
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;
}
}
/************************************************************************************************************************/
///
/// The number of seconds this state will take to go from its current to the
/// at its current .
///
///
///
/// For the time it would take to play fully from the start, use the instead.
///
/// Setting this value modifies the , not the .
///
/// Animancer Lite does not allow this value to be changed in runtime builds.
///
///
///
/// 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.
/// }
///
public float RemainingDuration
{
get => (Length * NormalizedEndTime - Time) / EffectiveSpeed;
set => EffectiveSpeed = (Length * NormalizedEndTime - Time) / value;
}
/************************************************************************************************************************/
// Length.
/************************************************************************************************************************/
/// The total time this state would take to play in seconds when = 1.
public abstract float Length { get; }
/// Will this state loop back to the start when it reaches the end?
public virtual bool IsLooping => false;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
///
/// Updates the for fading, applies it to this state's port on the parent
/// mixer, and plays or pauses the if its state is dirty.
///
///
/// If the 's is set to false, this
/// method will also connect/disconnect this node from the in the playable graph.
///
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;
}
}
/************************************************************************************************************************/
/// Destroys the and cleans up this state.
///
/// This method is NOT called automatically, so when implementing a custom state type you must use
/// if you need to guarantee that things will get cleaned up.
///
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);
}
}
/************************************************************************************************************************/
/// Creates a copy of this state with the same .
public AnimancerState Clone()
=> Clone(Root);
/// Creates a copy of this state with the specified .
public abstract AnimancerState Clone(AnimancerPlayable root);
/// Sets the .
///
/// This method skips several steps of and is intended to only be called on states
/// immediately after their creation.
///
protected void SetNewCloneRoot(AnimancerPlayable root)
{
if (root == null)
return;
Root = root;
CreatePlayable();
}
///
void ICopyable.CopyFrom(AnimancerState copyFrom)
{
Events = copyFrom.HasEvents ? copyFrom.Events : null;
TimeD = copyFrom.TimeD;
((ICopyable)this).CopyFrom(copyFrom);
}
/************************************************************************************************************************/
/// [] Gathers all the animations in this state.
public virtual void GatherAnimationClips(ICollection clips)
{
clips.Gather(Clip);
for (int i = ChildCount - 1; i >= 0; i--)
GetChild(i).GatherAnimationClips(clips);
}
/************************************************************************************************************************/
///
/// Returns true if the animation is playing and has not yet passed the
/// .
///
///
/// This method is called by so this object can be used as a custom yield
/// instruction to wait until it finishes.
///
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;
}
/************************************************************************************************************************/
///
/// Returns the if one is set, otherwise a string describing the type of this
/// state and the name of the .
///
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
/// [Editor-Only] Returns a custom drawer for this state.
protected internal virtual IAnimancerNodeDrawer CreateDrawer()
=> new AnimancerStateDrawer(this);
#endif
/************************************************************************************************************************/
///
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");
}
/************************************************************************************************************************/
/// Returns the hierarchy path of this state through its s.
public string GetPath()
{
if (_Parent == null)
return null;
var path = ObjectPool.AcquireStringBuilder();
AppendPath(path, _Parent);
AppendPortAndType(path);
return path.ReleaseToString();
}
/// Appends the hierarchy path of this state through its s.
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());
}
}
/// Appends "[Index] -> GetType().Name".
private void AppendPortAndType(StringBuilder path)
{
path.Append('[')
.Append(Index)
.Append("] -> ")
.Append(GetType().Name);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}