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