// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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;
#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,
ICloneable,
ICopyable
{
/************************************************************************************************************************/
#region Graph
/************************************************************************************************************************/
/// Sets the .
///
/// The has a different .
/// Setting the 's
/// will apply to its children recursively because they must always match.
///
public virtual void SetGraph(AnimancerGraph graph)
{
if (Graph == graph)
return;
RemoveFromOldGraph(graph);
Graph = graph;
AddToNewGraph();
FadeGroup?.ChangeGraph(graph);
}
private void RemoveFromOldGraph(AnimancerGraph newGraph)
{
if (Graph == null)
{
#if UNITY_ASSERTIONS
if (Parent != null && Parent.Graph != newGraph)
throw new InvalidOperationException(
"Unable to set the Graph of a state which has a Parent." +
" Setting the Parent's Graph will apply to its children recursively" +
" because they must always match.");
#endif
return;
}
Graph.States.Unregister(this);
if (Parent != null && Parent.Graph != newGraph)
{
Parent.OnRemoveChild(this);
Parent = null;
Index = -1;
}
_Time = TimeD;
DestroyPlayable();
}
private void AddToNewGraph()
{
if (Graph != null)
{
Graph.States.Register(this);
CreatePlayable();
}
for (int i = ChildCount - 1; i >= 0; i--)
GetChild(i)?.SetGraph(Graph);
if (Parent != null)
CopyIKFlags(Parent);
}
/************************************************************************************************************************/
/// Connects this state to the `parent` at its next available child index.
/// If the `parent` is null, this state will be disconnected from everything.
public void SetParent(AnimancerNode parent)
{
#if UNITY_ASSERTIONS
if (Parent == parent)
Debug.LogWarning(
$"{nameof(Parent)} is already set to {AnimancerUtilities.ToStringOrNull(parent)}.",
Graph?.Component as Object);
#endif
if (Parent != null)
{
Parent.OnRemoveChild(this);
Parent = null;
}
if (parent == null)
{
FadeGroup?.ChangeParent(this);
Index = -1;
return;
}
SetGraph(parent.Graph);
Parent = parent;
parent.OnAddChild(this);
CopyIKFlags(parent);
FadeGroup?.ChangeParent(this);
}
/// [Internal]
/// Directly sets the and
/// without triggering any other connection methods.
///
internal void SetParentInternal(AnimancerNode parent, int index = -1)
{
Parent = parent;
Index = index;
}
/************************************************************************************************************************/
// Layer.
/************************************************************************************************************************/
///
/// The index of the this state is connected to
/// (determined by the ).
///
/// -1 if this state isn't 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 => SetParent(value >= 0
? Graph.Layers[value]
: null);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Key and Clip
/************************************************************************************************************************/
internal object _Key;
///
/// The object used to identify this state in the graph dictionary.
/// Can be null.
///
public object Key
{
get => _Key;
set
{
if (Graph == null)
{
_Key = value;
}
else
{
Graph.States.Unregister(this);
_Key = value;
Graph.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
{
MarkAsUsed(this);
throw new NotSupportedException($"{GetType()} doesn't 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
{
MarkAsUsed(this);
throw new NotSupportedException($"{GetType()} doesn't support setting the {nameof(MainObject)}.");
}
}
#if UNITY_EDITOR
/// [Editor-Only] The base type which can be assigned to the .
public virtual Type MainObjectType
=> null;
#endif
/************************************************************************************************************************/
///
/// Sets the `currentObject` and calls .
/// If the `currentObject` was being used as the then it is changed as well.
///
/// The `newObject` is null.
protected bool ChangeMainObject(ref T currentObject, T newObject)
where T : Object
{
if (newObject == null)
{
MarkAsUsed(this);
throw new ArgumentNullException(nameof(newObject));
}
if (ReferenceEquals(currentObject, newObject))
return false;
if (ReferenceEquals(_Key, currentObject))
Key = newObject;
currentObject = newObject;
if (Graph != null)
RecreatePlayable();
return true;
}
/************************************************************************************************************************/
/// 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;
/************************************************************************************************************************/
/// Is the automatically advancing?
///
///
/// Example:
/// 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
{
SetIsPlaying(value);
UpdateIsActive();
}
}
///
/// Sets and applies it to the
/// without calling .
///
protected internal void SetIsPlaying(bool isPlaying)
{
if (_IsPlaying == isPlaying)
return;
_IsPlaying = isPlaying;
if (_Playable.IsValid())
{
if (_IsPlaying)
_Playable.Play();
else
_Playable.Pause();
}
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 .
protected sealed override void CreatePlayable()
{
base.CreatePlayable();
if (Parent != null && (IsActive || Parent.KeepChildrenConnected))
Graph._PlayableGraph.Connect(Parent.Playable, Playable, Index, Weight);
if (!_IsPlaying)
_Playable.Pause();
RawTime = _Time;
}
/************************************************************************************************************************/
/// Is this state playing and not fading out?
///
/// If true, this state will usually be the but that is not always
/// the case.
///
public bool IsCurrent
=> _IsPlaying
&& TargetWeight > 0;
/// Is this state not playing and at 0 ?
public bool IsStopped
=> !_IsPlaying
&& Weight == 0;
/************************************************************************************************************************/
///
/// Plays this state immediately, without any blending and without affecting any other states.
///
///
/// Unlike ,
/// this method only affects this state and won't stop any others that are playing.
///
/// Sets = true and = 1.
///
/// Doesn't change the so it will continue from its current value.
///
public void Play()
{
SetIsPlaying(true);
Weight = 1;
}
/************************************************************************************************************************/
///
protected internal override void StopWithoutWeight()
{
SetIsPlaying(false);
TimeD = 0;
UpdateIsActive();
}
/************************************************************************************************************************/
///
protected internal override void OnStartFade()
{
UpdateIsActive();
}
///
protected internal override void InternalClearFade()
{
base.InternalClearFade();
UpdateIsActive();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Active
/************************************************************************************************************************/
///
/// The index of this state in its parent (or -1 if inactive).
///
///
/// If this state's direct parent isn't a layer (such as a child of a mixer), this value simply uses 0 to
/// indicate active.
///
internal int _ActiveIndex = ActiveList.NotInList;
/************************************************************************************************************************/
/// Is this state currently updating or affecting the animation output?
///
/// This property is true when or the or
/// are above 0.
///
public bool IsActive
=> _ActiveIndex >= 0;
/// [Internal] Should be true based on the current details of this state?
internal bool ShouldBeActive
{
get => IsPlaying
|| Weight > 0
|| FadeGroup != null;
set => _ActiveIndex = value ? 0 : -1;
}
/// [Internal] If this method sets it to false and returns true.
internal bool TryDeactivate()
{
if (_ActiveIndex < 0)
return false;
_ActiveIndex = ActiveList.NotInList;
return true;
}
/// Called when might change.
internal void UpdateIsActive()
{
var shouldBeActive = ShouldBeActive;
if (IsActive == shouldBeActive)
return;
var parent = Parent;
if (parent != null)
parent.ApplyChildActive(this, shouldBeActive);
else
ShouldBeActive = ShouldBeActive;
}
/************************************************************************************************************************/
///
public sealed override void SetWeight(float value)
{
base.SetWeight(value);
UpdateIsActive();
}
/************************************************************************************************************************/
/// [Internal] An based on .
internal readonly struct Indexer : IIndexer
{
/************************************************************************************************************************/
/// The .
public readonly AnimancerGraph Graph;
/// The of the .
public readonly Playable ParentPlayable;
/************************************************************************************************************************/
/// Creates a new .
public Indexer(AnimancerGraph graph, Playable parentPlayable)
{
Graph = graph;
ParentPlayable = parentPlayable;
}
/************************************************************************************************************************/
///
public readonly int GetIndex(AnimancerState state)
=> state._ActiveIndex;
/************************************************************************************************************************/
///
public readonly void SetIndex(AnimancerState state, int index)
{
if (!Graph.KeepChildrenConnected && state._ActiveIndex < 0)
{
Validate.AssertPlayable(state);
Graph._PlayableGraph.Connect(ParentPlayable, state._Playable, state.Index, state.Weight);
}
state._ActiveIndex = index;
}
/************************************************************************************************************************/
///
public readonly void ClearIndex(AnimancerState state)
{
if (!Graph.KeepChildrenConnected)
Graph._PlayableGraph.Disconnect(ParentPlayable, state.Index);
state._ActiveIndex = ActiveList.NotInList;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// [Internal]
/// An of s
/// which tracks .
///
internal class ActiveList : IndexedList
{
/// The default for newly created lists.
/// Default value is 4.
public static new int DefaultCapacity { get; set; } = 4;
/// Creates a new with the .
public ActiveList(Indexer accessor)
: base(DefaultCapacity, accessor)
{ }
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Timing
/************************************************************************************************************************/
// Time.
/************************************************************************************************************************/
///
/// The current time of the , retrieved by whenever the
/// is different from the .
///
private double _Time;
///
/// 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 continues increasing after the animation passes the end of its
/// , regardless of whether it or not.
///
/// The underlying can be accessed via .
///
/// Setting this value will skip Events and Root Motion between the old and new time.
/// Use instead if you don't want that behaviour.
///
/// Animancer Lite doesn't allow this value to be changed in runtime builds (except resetting it to 0).
///
/// Example:
/// void TimeExample(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 graph = Graph;
if (graph == null)
return _Time;
var frameID = graph.FrameID;
if (_TimeFrameID != frameID)
{
_TimeFrameID = frameID;
_Time = RawTime;
}
return _Time;
}
set
{
#if UNITY_ASSERTIONS
if (!value.IsFinite())
{
MarkAsUsed(this);
throw new ArgumentOutOfRangeException(
nameof(value),
value,
$"{nameof(Time)} {Strings.MustBeFinite}");
}
#endif
_Time = value;
var graph = Graph;
if (graph != null)
{
_TimeFrameID = graph.FrameID;
RawTime = value;
}
_EventDispatcher?.OnSetTime();
}
}
/************************************************************************************************************************/
///
/// The internal implementation of which directly gets and sets the underlying value.
///
///
/// This property should generally not be accessed directly.
///
/// Setting this value will skip Events and Root Motion between the old and new time.
/// Use instead if you don't want that behaviour.
///
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 continues increasing after the animation passes the end of its
/// , regardless of whether it 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.
///
/// Setting this value will skip Events and Root Motion between the old and new time.
/// Use instead if you don't want that behaviour.
///
/// Animancer Lite doesn't allow this value to be changed in runtime builds (except resetting it to 0).
///
/// Example:
/// void TimeExample(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 doesn't skip Events or Root Motion between the old and new time.
///
///
/// The Events and Root Motion will be applied during the next animation update.
/// If you want to apply them immediately you can call .
///
/// Events are triggered where old time <= event time < new time.
///
/// Avoid calling this method more than once per frame because doing so will cause
/// Animation Events and Root Motion to be skipped due to an unfortunate design
/// decision in the Playables API. Animancer Events would still be triggered,
/// but only between the old time and the last new time you set
/// (any other values would be ignored).
///
public void MoveTime(float time, bool normalized)
=> MoveTime((double)time, normalized);
///
/// Sets the or , but unlike those properties
/// this method doesn't skip Events or Root Motion between the old and new time.
///
///
/// The Events and Root Motion will be applied during the next animation update.
/// If you want to apply them immediately you can call .
///
/// Avoid calling this method more than once per frame because doing so will cause
/// Animation Events and Root Motion to be skipped due to an unfortunate design
/// decision in the Playables API. Animancer Events would still be triggered,
/// but only between the old time and the last new time you set
/// (any other values would be ignored).
///
public virtual void MoveTime(double time, bool normalized)
{
#if UNITY_ASSERTIONS
if (!time.IsFinite())
{
MarkAsUsed(this);
throw new ArgumentOutOfRangeException(nameof(time), time,
$"{nameof(Time)} {Strings.MustBeFinite}");
}
#endif
var graph = Graph;
if (graph != null)
_TimeFrameID = graph.FrameID;
if (normalized)
time *= Length;
_Time = time;
_Playable.SetTime(time);
}
/************************************************************************************************************************/
// Duration.
/************************************************************************************************************************/
///
/// The after which the
/// callback will be invoked every frame.
///
///
/// This is a wrapper around
/// so that if the value hasn't been set ()
/// it can be determined based on the :
/// positive speed ends at 1 and negative speed ends at 0.
///
public float NormalizedEndTime
{
get
{
var events = SharedEvents;
if (events != null)
{
var time = events.NormalizedEndTime;
if (!float.IsNaN(time))
return time;
}
return AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(EffectiveSpeed);
}
}
/************************************************************************************************************************/
///
/// 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 doesn't allow this value to be changed in runtime builds.
///
/// Example:
/// 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 (speed == 0)
return float.PositiveInfinity;
var events = SharedEvents;
if (events != null)
{
var endTime = 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;
var events = SharedEvents;
if (events != null)
{
var endTime = 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 doesn't allow this value to be changed in runtime builds.
///
/// Example:
/// 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?
///
/// Note that always continues increasing regardless of this value.
/// See the comments on for more information.
///
public virtual bool IsLooping => false;
/************************************************************************************************************************/
///
/// Gets the details used to trigger s on this state:
/// , , and .
///
public virtual void GetEventDispatchInfo(
out float length,
out float normalizedTime,
out bool isLooping)
{
length = Length;
normalizedTime = length != 0
? Time / length
: 0;
isLooping = IsLooping;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// 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;
}
FadeGroup = null;
Index = -1;
_EventDispatcher = null;
var graph = Graph;
if (graph != null)
{
graph.States.Unregister(this);
// This is slightly faster than _Playable.Destroy().
if (_Playable.IsValid() && graph._PlayableGraph.IsValid())
graph._PlayableGraph.DestroyPlayable(_Playable);
}
}
/************************************************************************************************************************/
///
public abstract AnimancerState Clone(CloneContext context);
/************************************************************************************************************************/
///
public sealed override void CopyFrom(AnimancerNode copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
///
public virtual void CopyFrom(AnimancerState copyFrom, CloneContext context)
{
CopyFirstGraphAndKeyFrom(copyFrom, context);
TimeD = copyFrom.TimeD;
IsPlaying = copyFrom.IsPlaying;
base.CopyFrom(copyFrom, context);
CopyEvents(copyFrom, context);
UpdateIsActive();
}
/************************************************************************************************************************/
/// Sets the and .
private void CopyFirstGraphAndKeyFrom(AnimancerState copyFrom, CloneContext context)
{
if (Graph != null)
return;
Graph = context.GetCloneOrOriginal(copyFrom.Graph);
// If a clone is registered for the key, use it.
// Otherwise, if the key is a state and we're cloning into a different graph, clone the key state.
// Otherwise, just use the same key.
_Key = copyFrom.Key is AnimancerState stateKey && stateKey.Graph != Graph
? context.GetOrCreateCloneOrOriginal(stateKey)
: context.GetCloneOrOriginal(copyFrom.Key);
// Each key can only be used once per graph,
// so we can only use it if it's different or we have a different graph.
if (_Key == copyFrom.Key && Graph == copyFrom.Graph)
_Key = null;
AddToNewGraph();
}
/************************************************************************************************************************/
/// [] 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;
var events = SharedEvents;
if (events != null)
{
endTime = events.NormalizedEndTime;
if (float.IsNaN(endTime))
endTime = Length;
else
endTime *= Length;
}
else endTime = Length;
return Time <= endTime;
}
else if (speed < 0)
{
float endTime;
var events = SharedEvents;
if (events != null)
{
endTime = events.NormalizedEndTime;
if (float.IsNaN(endTime))
endTime = 0;
else
endTime *= Length;
}
else endTime = 0;
return Time >= endTime;
}
else return true;
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
private string _CachedToString;
#endif
///
/// 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 (NameCache.TryToString(DebugName, out var cachedName))
return cachedName;
if (_CachedToString != null)
return _CachedToString;
#endif
string name;
var type = GetType().Name;
var mainObject = MainObject;
if (mainObject != null)
{
#if UNITY_ASSERTIONS
name = mainObject.GetCachedName();
#else
name = mainObject.name;
#endif
name = $"{name} ({type})";
}
else
{
name = type;
}
#if UNITY_ASSERTIONS
_CachedToString = name;
#endif
return name;
}
/************************************************************************************************************************/
#region Descriptions
/************************************************************************************************************************/
///
protected override void AppendDetails(StringBuilder text, string separator)
{
text.AppendField(separator, nameof(Key), _Key);
text.AppendField(separator, "ActiveIndex", _ActiveIndex);
var mainObject = MainObject;
if (mainObject != _Key as Object)
text.AppendField(separator, nameof(MainObject), mainObject);
#if UNITY_EDITOR
if (mainObject != null)
text.AppendField(separator, "AssetPath", AssetDatabase.GetAssetPath(mainObject));
#endif
base.AppendDetails(text, separator);
text.AppendField(separator, nameof(IsPlaying), IsPlaying);
try
{
text.AppendField(separator, nameof(Time), TimeD)
.Append("s / ")
.Append(Length)
.Append("s = ")
.Append((NormalizedTime * 100).ToString("0.00"))
.Append('%');
text.AppendField(separator, nameof(IsLooping), IsLooping);
}
catch (Exception exception)
{
text.Append(separator).Append(exception);
}
text.AppendField(separator, nameof(Events), SharedEvents?.DeepToString(false));
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}