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