// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using Object = UnityEngine.Object; namespace Animancer { /// [Pro-Only] /// An which plays a . /// /// /// This state can be controlled very similarly to an /// via its property. /// /// Documentation: /// /// Animator Controllers /// /// https://kybernetik.com.au/animancer/api/Animancer/ControllerState /// public partial class ControllerState : AnimancerState, ICopyable, IParametizedState, IUpdatable { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ private RuntimeAnimatorController _Controller; /// The which this state plays. public RuntimeAnimatorController Controller { get => _Controller; set => ChangeMainObject(ref _Controller, value); } /// The which this state plays. public override Object MainObject { get => Controller; set => Controller = (RuntimeAnimatorController)value; } #if UNITY_EDITOR /// public override Type MainObjectType => typeof(RuntimeAnimatorController); #endif /************************************************************************************************************************/ private new AnimatorControllerPlayable _Playable; /// The internal system which plays the . public new AnimatorControllerPlayable Playable { get { Validate.AssertPlayable(this); return _Playable; } } /************************************************************************************************************************/ /// Determines what a layer does when is called. public enum ActionOnStop { /// Reset the layer to the first state it was in. DefaultState, /// Rewind the current state's time to 0. RewindTime, /// Allow the current state to stay at its current time. Continue, } /// Determines what each layer does when is called. /// /// If empty, all layers will reset to their . /// /// If this array is smaller than the , /// any additional layers will use the last value in this array. /// public ActionOnStop[] ActionsOnStop { get; set; } /// /// The of the default state on each layer, /// used to reset to those states when /// is called for layers using . /// /// Gathered automatically by . public int[] DefaultStateHashes { get; set; } /************************************************************************************************************************/ #if UNITY_ASSERTIONS /************************************************************************************************************************/ /// [Assert-Only] Animancer Events work badly on s. protected internal override string UnsupportedEventsMessage => "Animancer Events on " + nameof(ControllerState) + "s will probably not work as expected." + " The events will be associated with the entire Animator Controller and be triggered by any of the" + " states inside it. If you want to use events in an Animator Controller you will likely need to use" + " Unity's regular Animation Event system."; /************************************************************************************************************************/ #endif /************************************************************************************************************************/ /// [Assert-Conditional] Asserts that the `value` is valid for a parameter. /// The `value` is NaN or Infinity. [System.Diagnostics.Conditional(Strings.Assertions)] public void AssertParameterValue(float value, [CallerMemberName] string parameterName = null) { if (!value.IsFinite()) { MarkAsUsed(this); throw new ArgumentOutOfRangeException(parameterName, Strings.MustBeFinite); } } /************************************************************************************************************************/ /// IK cannot be dynamically enabled on a . public override void CopyIKFlags(AnimancerNodeBase copyFrom) { } /************************************************************************************************************************/ /// IK cannot be dynamically enabled on a . public override bool ApplyAnimatorIK { get => false; set { #if UNITY_ASSERTIONS if (value) OptionalWarning.UnsupportedIK.Log( $"IK cannot be dynamically enabled on a {nameof(ControllerState)}." + " You must instead enable it on the desired layer inside the Animator Controller.", _Controller); #endif } } /************************************************************************************************************************/ /// IK cannot be dynamically enabled on a . public override bool ApplyFootIK { get => false; set { #if UNITY_ASSERTIONS if (value) OptionalWarning.UnsupportedIK.Log( $"IK cannot be dynamically enabled on a {nameof(ControllerState)}." + " You must instead enable it on the desired state inside the Animator Controller.", _Controller); #endif } } /************************************************************************************************************************/ /// Returns the hash of a parameter being wrapped by this state. /// This state doesn't wrap any parameters. public virtual int GetParameterHash(int index) { MarkAsUsed(this); throw new NotSupportedException(); } /************************************************************************************************************************/ /// public virtual void GetParameters(List parameters) { } /// public virtual void SetParameters(List parameters) { } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Public API /************************************************************************************************************************/ /// Creates a new to play the `controller`. public ControllerState(RuntimeAnimatorController controller) { _Controller = controller != null ? controller : throw new ArgumentNullException(nameof(controller)); } /// Creates a new to play the `controller`. public ControllerState(RuntimeAnimatorController controller, params ActionOnStop[] actionsOnStop) : this(controller) { ActionsOnStop = actionsOnStop; } /************************************************************************************************************************/ /// Creates and assigns the managed by this state. protected override void CreatePlayable(out Playable playable) { playable = _Playable = AnimatorControllerPlayable.Create(Graph._PlayableGraph, _Controller); GatherDefaultStates(); DeserializeParameterBindings(); } /************************************************************************************************************************/ /// /// Stores the values of all parameters and calls , /// then restores the parameter values. /// public override void RecreatePlayable() { if (!_Playable.IsValid()) { CreatePlayable(); return; } var parameterCount = _Playable.GetParameterCount(); var values = new object[parameterCount]; for (int i = 0; i < parameterCount; i++) { values[i] = AnimancerUtilities.GetParameterValue(_Playable, _Playable.GetParameter(i)); } base.RecreatePlayable(); for (int i = 0; i < parameterCount; i++) { AnimancerUtilities.SetParameterValue(_Playable, _Playable.GetParameter(i), values[i]); } } /************************************************************************************************************************/ /// /// Returns the current state on the specified `layer`, /// or the next state if it is currently in a transition. /// public AnimatorStateInfo GetStateInfo(int layerIndex) { if (!_Playable.IsValid()) return default; Validate.AssertPlayable(this); return _Playable.IsInTransition(layerIndex) ? _Playable.GetNextAnimatorStateInfo(layerIndex) : _Playable.GetCurrentAnimatorStateInfo(layerIndex); } /************************************************************************************************************************/ /// /// The * of layer 0. /// public override double RawTime { get { var info = GetStateInfo(0); return info.normalizedTime * info.length; } set { Validate.AssertPlayable(this); _Playable.PlayInFixedTime(0, 0, (float)value); // Setting the time requires it to be playing. // This will leave it at the specified time. if (!IsPlaying) { _Playable.Play(); Graph.RequirePostUpdate(this); } } } /************************************************************************************************************************/ /// int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList; /************************************************************************************************************************/ /// Pauses the if necessary after was set. void IUpdatable.Update() { if (!IsPlaying) _Playable.Pause(); AnimancerGraph.Current.CancelPostUpdate(this); } /************************************************************************************************************************/ /// public override void SetGraph(AnimancerGraph graph) { if (Graph == graph) return; Graph?.CancelPostUpdate(this); base.SetGraph(graph); } /************************************************************************************************************************/ /// The current of layer 0. public override float Length => GetStateInfo(0).length; /************************************************************************************************************************/ /// The current of layer 0. public override bool IsLooping => GetStateInfo(0).loop; /************************************************************************************************************************/ /// public override void GetEventDispatchInfo( out float length, out float normalizedTime, out bool isLooping) { var state = GetStateInfo(0); length = state.length; normalizedTime = state.normalizedTime; isLooping = state.loop; } /************************************************************************************************************************/ /// Gathers the from the current states on each layer. /// This is called by . public void GatherDefaultStates() { Validate.AssertPlayable(this); var layerCount = _Playable.GetLayerCount(); if (DefaultStateHashes == null || DefaultStateHashes.Length != layerCount) DefaultStateHashes = new int[layerCount]; while (--layerCount >= 0) DefaultStateHashes[layerCount] = _Playable.GetCurrentAnimatorStateInfo(layerCount).shortNameHash; } /************************************************************************************************************************/ /// /// Stops the animation and makes it inactive immediately so it no longer affects the output. /// Also calls . /// protected internal override void StopWithoutWeight() { // Don't call base.StopWithoutWeight(); because it sets Time = 0; // which uses PlayInFixedTime and interferes with resetting to the default states. SetIsPlaying(false); UpdateIsActive(); ApplyActionsOnStop(); _SmoothingVelocities?.Clear(); } /// Applies the to their corresponding layers. /// is null. public void ApplyActionsOnStop() { Validate.AssertPlayable(this); var layerCount = Math.Min(DefaultStateHashes.Length, _Playable.GetLayerCount()); if (ActionsOnStop == null || ActionsOnStop.Length == 0) { for (int i = layerCount - 1; i >= 0; i--) _Playable.Play(DefaultStateHashes[i], i, 0); } else { for (int i = layerCount - 1; i >= 0; i--) { var index = i < ActionsOnStop.Length ? i : ActionsOnStop.Length - 1; switch (ActionsOnStop[index]) { case ActionOnStop.DefaultState: _Playable.Play(DefaultStateHashes[i], i, 0); break; case ActionOnStop.RewindTime: _Playable.Play(0, i, 0); break; case ActionOnStop.Continue: break; } } } } /************************************************************************************************************************/ /// public override void GatherAnimationClips(ICollection clips) { if (_Controller != null) clips.Gather(_Controller.animationClips); } /************************************************************************************************************************/ /// public override void Destroy() { _Controller = null; DisposeParameterBindings(); base.Destroy(); } /************************************************************************************************************************/ /// public override AnimancerState Clone(CloneContext context) { var clone = new ControllerState(_Controller); clone.CopyFrom(this, context); return clone; } /************************************************************************************************************************/ /// public sealed override void CopyFrom(AnimancerState copyFrom, CloneContext context) => this.CopyFromBase(copyFrom, context); /// public virtual void CopyFrom(ControllerState copyFrom, CloneContext context) { ActionsOnStop = copyFrom.ActionsOnStop; if (copyFrom.Graph != null && Graph != null) { var layerCount = copyFrom._Playable.GetLayerCount(); for (int i = 0; i < layerCount; i++) { var info = copyFrom._Playable.GetCurrentAnimatorStateInfo(i); _Playable.Play(info.shortNameHash, i, info.normalizedTime); } var parameterCount = copyFrom._Playable.GetParameterCount(); for (int i = 0; i < parameterCount; i++) { AnimancerUtilities.CopyParameterValue( copyFrom._Playable, _Playable, copyFrom._Playable.GetParameter(i)); } } CopySmoothingVelocitiesFrom(copyFrom); base.CopyFrom(copyFrom, context); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Animator Controller Wrappers /************************************************************************************************************************/ #region Cross Fade /************************************************************************************************************************/ /// /// The default constant for fade duration parameters which causes it to use the /// instead. /// public const float DefaultFadeDuration = -1; /************************************************************************************************************************/ /// /// Returns the `fadeDuration` if it is zero or positive. /// Otherwise returns the . /// public static float GetFadeDuration(float fadeDuration) => fadeDuration >= 0 ? fadeDuration : AnimancerGraph.DefaultFadeDuration; /************************************************************************************************************************/ /// Starts a transition from the current state to the specified state using normalized times. /// If `fadeDuration` is negative, it uses the . public void CrossFade( int stateNameHash, float fadeDuration = DefaultFadeDuration, int layer = -1, float normalizedTime = float.NegativeInfinity) => Playable.CrossFade(stateNameHash, GetFadeDuration(fadeDuration), layer, normalizedTime); /************************************************************************************************************************/ /// Starts a transition from the current state to the specified state using normalized times. /// If `fadeDuration` is negative, it uses the . public void CrossFade( string stateName, float fadeDuration = DefaultFadeDuration, int layer = -1, float normalizedTime = float.NegativeInfinity) => Playable.CrossFade(stateName, GetFadeDuration(fadeDuration), layer, normalizedTime); /************************************************************************************************************************/ /// Starts a transition from the current state to the specified state using times in seconds. /// If `fadeDuration` is negative, it uses the . public void CrossFadeInFixedTime( int stateNameHash, float fadeDuration = DefaultFadeDuration, int layer = -1, float fixedTime = 0) => Playable.CrossFadeInFixedTime(stateNameHash, GetFadeDuration(fadeDuration), layer, fixedTime); /************************************************************************************************************************/ /// Starts a transition from the current state to the specified state using times in seconds. /// If `fadeDuration` is negative, it uses the . public void CrossFadeInFixedTime( string stateName, float fadeDuration = DefaultFadeDuration, int layer = -1, float fixedTime = 0) => Playable.CrossFadeInFixedTime(stateName, GetFadeDuration(fadeDuration), layer, fixedTime); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Play /************************************************************************************************************************/ /// Plays the specified state immediately, starting from a particular normalized time. public void Play( int stateNameHash, int layer = -1, float normalizedTime = float.NegativeInfinity) => Playable.Play(stateNameHash, layer, normalizedTime); /************************************************************************************************************************/ /// Plays the specified state immediately, starting from a particular normalized time. public void Play( string stateName, int layer = -1, float normalizedTime = float.NegativeInfinity) => Playable.Play(stateName, layer, normalizedTime); /************************************************************************************************************************/ /// Plays the specified state immediately, starting from a particular time (in seconds). public void PlayInFixedTime( int stateNameHash, int layer = -1, float fixedTime = 0) => Playable.PlayInFixedTime(stateNameHash, layer, fixedTime); /************************************************************************************************************************/ /// Plays the specified state immediately, starting from a particular time (in seconds). public void PlayInFixedTime( string stateName, int layer = -1, float fixedTime = 0) => Playable.PlayInFixedTime(stateName, layer, fixedTime); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Parameters /************************************************************************************************************************/ /// Gets the value of the specified boolean parameter. public bool GetBool(int id) => Playable.GetBool(id); /// Gets the value of the specified boolean parameter. public bool GetBool(string name) => Playable.GetBool(name); /// Sets the value of the specified boolean parameter. public void SetBool(int id, bool value) => Playable.SetBool(id, value); /// Sets the value of the specified boolean parameter. public void SetBool(string name, bool value) => Playable.SetBool(name, value); /// Gets the value of the specified float parameter. public float GetFloat(int id) => Playable.GetFloat(id); /// Gets the value of the specified float parameter. public float GetFloat(string name) => Playable.GetFloat(name); /// Sets the value of the specified float parameter. public void SetFloat(int id, float value) => Playable.SetFloat(id, value); /// Sets the value of the specified float parameter. public void SetFloat(string name, float value) => Playable.SetFloat(name, value); /// Gets the value of the specified integer parameter. public int GetInteger(int id) => Playable.GetInteger(id); /// Gets the value of the specified integer parameter. public int GetInteger(string name) => Playable.GetInteger(name); /// Sets the value of the specified integer parameter. public void SetInteger(int id, int value) => Playable.SetInteger(id, value); /// Sets the value of the specified integer parameter. public void SetInteger(string name, int value) => Playable.SetInteger(name, value); /// Sets the specified trigger parameter to true. public void SetTrigger(int id) => Playable.SetTrigger(id); /// Sets the specified trigger parameter to true. /// public void SetTrigger(string name) => Playable.SetTrigger(name); /// Resets the specified trigger parameter to false. /// public void ResetTrigger(int id) => Playable.ResetTrigger(id); /// Resets the specified trigger parameter to false. /// public void ResetTrigger(string name) => Playable.ResetTrigger(name); /// Indicates whether the specified parameter is controlled by an . public bool IsParameterControlledByCurve(int id) => Playable.IsParameterControlledByCurve(id); /// Indicates whether the specified parameter is controlled by an . public bool IsParameterControlledByCurve(string name) => Playable.IsParameterControlledByCurve(name); /// Gets the details of one of the 's parameters. public AnimatorControllerParameter GetParameter(int index) => Playable.GetParameter(index); /// Gets the number of parameters in the . public int GetParameterCount() => Playable.GetParameterCount(); /************************************************************************************************************************/ /// The number of parameters in the . public int parameterCount => Playable.GetParameterCount(); /************************************************************************************************************************/ private AnimatorControllerParameter[] _Parameters; /// The parameters in the . /// /// This property allocates a new array when first accessed. To avoid that, you can use /// and instead. /// public AnimatorControllerParameter[] parameters { get { if (_Parameters == null) { var count = GetParameterCount(); _Parameters = new AnimatorControllerParameter[count]; for (int i = 0; i < count; i++) _Parameters[i] = GetParameter(i); } return _Parameters; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Smoothed Set Float /************************************************************************************************************************/ private Dictionary _SmoothingVelocities; /************************************************************************************************************************/ /// Sets the value of the specified float parameter with smoothing. /// Consider using a instead. public float SetFloat( string name, float value, float dampTime, float deltaTime, float maxSpeed = float.PositiveInfinity) => SetFloat(Animator.StringToHash(name), value, dampTime, deltaTime, maxSpeed); /// Sets the value of the specified float parameter with smoothing. /// Consider using a instead. public float SetFloat( int id, float value, float dampTime, float deltaTime, float maxSpeed = float.PositiveInfinity) { _SmoothingVelocities ??= new(); _SmoothingVelocities.TryGetValue(id, out var velocity); value = Mathf.SmoothDamp(GetFloat(id), value, ref velocity, dampTime, maxSpeed, deltaTime); SetFloat(id, value); _SmoothingVelocities[id] = velocity; return value; } /************************************************************************************************************************/ /// Copies the smoothing velocities. private void CopySmoothingVelocitiesFrom(ControllerState copyFrom) { if (copyFrom._SmoothingVelocities != null) { if (_SmoothingVelocities == null) _SmoothingVelocities = new(); else _SmoothingVelocities.Clear(); foreach (var item in copyFrom._SmoothingVelocities) _SmoothingVelocities[item.Key] = item.Value; } else { _SmoothingVelocities?.Clear(); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Misc /************************************************************************************************************************/ // Layers. /************************************************************************************************************************/ /// Gets the weight of the layer at the specified index. public float GetLayerWeight(int layerIndex) => Playable.GetLayerWeight(layerIndex); /// Sets the weight of the layer at the specified index. public void SetLayerWeight(int layerIndex, float weight) => Playable.SetLayerWeight(layerIndex, weight); /// Gets the number of layers in the . public int GetLayerCount() => Playable.GetLayerCount(); /// The number of layers in the . public int layerCount => Playable.GetLayerCount(); /// Gets the index of the layer with the specified name. public int GetLayerIndex(string layerName) => Playable.GetLayerIndex(layerName); /// Gets the name of the layer with the specified index. public string GetLayerName(int layerIndex) => Playable.GetLayerName(layerIndex); /************************************************************************************************************************/ // States. /************************************************************************************************************************/ /// Returns information about the current state. public AnimatorStateInfo GetCurrentAnimatorStateInfo(int layerIndex = 0) => Playable.GetCurrentAnimatorStateInfo(layerIndex); /// Returns information about the next state being transitioned towards. public AnimatorStateInfo GetNextAnimatorStateInfo(int layerIndex = 0) => Playable.GetNextAnimatorStateInfo(layerIndex); /// Indicates whether the specified layer contains the specified state. public bool HasState(int layerIndex, int stateID) => Playable.HasState(layerIndex, stateID); /************************************************************************************************************************/ // Transitions. /************************************************************************************************************************/ /// Indicates whether the specified layer is currently executing a transition. public bool IsInTransition(int layerIndex = 0) => Playable.IsInTransition(layerIndex); /// Gets information about the current transition. public AnimatorTransitionInfo GetAnimatorTransitionInfo(int layerIndex = 0) => Playable.GetAnimatorTransitionInfo(layerIndex); /************************************************************************************************************************/ // Clips. /************************************************************************************************************************/ /// Gets information about the s currently being played. public AnimatorClipInfo[] GetCurrentAnimatorClipInfo(int layerIndex = 0) => Playable.GetCurrentAnimatorClipInfo(layerIndex); /// Gets information about the s currently being played. public void GetCurrentAnimatorClipInfo(int layerIndex, List clips) => Playable.GetCurrentAnimatorClipInfo(layerIndex, clips); /// Gets the number of s currently being played. /// public int GetCurrentAnimatorClipInfoCount(int layerIndex = 0) => Playable.GetCurrentAnimatorClipInfoCount(layerIndex); /// Gets information about the s currently being transitioned towards. public AnimatorClipInfo[] GetNextAnimatorClipInfo(int layerIndex = 0) => Playable.GetNextAnimatorClipInfo(layerIndex); /// Gets information about the s currently being transitioned towards. public void GetNextAnimatorClipInfo(int layerIndex, List clips) => Playable.GetNextAnimatorClipInfo(layerIndex, clips); /// Gets the number of s currently being transitioned towards. public int GetNextAnimatorClipInfoCount(int layerIndex = 0) => Playable.GetNextAnimatorClipInfoCount(layerIndex); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }