// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using Object = UnityEngine.Object; using System.Runtime.CompilerServices; namespace Animancer { /// [Pro-Only] An which plays a . /// /// You can control this state very similarly to an via its property. /// /// Documentation: Animator Controllers /// /// https://kybernetik.com.au/animancer/api/Animancer/ControllerState /// public partial class ControllerState : AnimancerState, ICopyable { /************************************************************************************************************************/ /// An that creates a . public interface ITransition : ITransition { } /************************************************************************************************************************/ #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; } /// The internal system which plays the . public new AnimatorControllerPlayable Playable { get { Validate.AssertPlayable(this); return _Playable; } } private new AnimatorControllerPlayable _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, } private ActionOnStop[] _ActionsOnStop; /// 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 => _ActionsOnStop; set { _ActionsOnStop = value; if (_Playable.IsValid()) GatherDefaultStates(); } } /// /// The of the default state on each layer, used to reset to /// those states when is called for layers using /// . /// /// Gathered using . public int[] DefaultStateHashes { get; set; } /************************************************************************************************************************/ #if UNITY_ASSERTIONS /************************************************************************************************************************/ /// [Assert-Only] Animancer Events doesn't work properly on s. protected 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."; /// [Assert-Only] /// does nothing on s. /// protected override string UnsupportedSpeedMessage => nameof(PlayableExtensions) + "." + nameof(PlayableExtensions.SetSpeed) + " does nothing on " + nameof(ControllerState) + "s so there is no way to directly control their speed." + " The Animator Controller Speed page explains a possible workaround for this issue:" + " https://kybernetik.com.au/animancer/docs/bugs/animator-controller-speed"; /************************************************************************************************************************/ #endif /************************************************************************************************************************/ /// [Assert-Conditional] Asserts that the `value` is valid. /// The `value` is NaN or Infinity. [System.Diagnostics.Conditional(Strings.Assertions)] public void AssertParameterValue(float value, [CallerMemberName] string parameterName = null) { if (!value.IsFinite()) throw new ArgumentOutOfRangeException(parameterName, Strings.MustBeFinite); } /************************************************************************************************************************/ /// IK cannot be dynamically enabled on a . public override void CopyIKFlags(AnimancerNode 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 } } /************************************************************************************************************************/ /// The number of parameters being wrapped by this state. public virtual int ParameterCount => 0; /// Returns the hash of a parameter being wrapped by this state. /// This state doesn't wrap any parameters. public virtual int GetParameterHash(int index) => throw new NotSupportedException(); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Public API /************************************************************************************************************************/ /// Creates a new to play the `controller`. public ControllerState(RuntimeAnimatorController controller) { if (controller == null) throw new ArgumentNullException(nameof(controller)); _Controller = 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(Root._Graph, _Controller); GatherDefaultStates(); } /************************************************************************************************************************/ /// /// Stores the values of all parameters, 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) { 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); if (!IsPlaying) { _Playable.Play(); DelayedPause.Register(this); } } } /************************************************************************************************************************/ /// The current of layer 0. public override float Length => GetStateInfo(0).length; /************************************************************************************************************************/ /// Indicates whether the current state on layer 0 will loop back to the start when it reaches the end. public override bool IsLooping => GetStateInfo(0).loop; /************************************************************************************************************************/ /// Gathers the from the current states on each layer. 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 . /// public override void Stop() { // Don't call base.Stop(); because it sets Time = 0; which uses PlayInFixedTime and interferes with // resetting to the default states. Weight = 0; IsPlaying = false; if (AutomaticallyClearEvents) Events = null; ApplyActionsOnStop(); if (_SmoothingVelocities != null) _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; } } } // Allowing the RawTime to be applied prevents the default state from being played because // Animator Controllers don't properly respond to multiple Play calls in the same frame. CancelSetTime(); } /************************************************************************************************************************/ /// public override void GatherAnimationClips(ICollection clips) { if (_Controller != null) clips.Gather(_Controller.animationClips); } /************************************************************************************************************************/ /// public override void Destroy() { _Controller = null; base.Destroy(); } /************************************************************************************************************************/ /// public override AnimancerState Clone(AnimancerPlayable root) { var clone = new ControllerState(_Controller); clone.SetNewCloneRoot(root); ((ICopyable)clone).CopyFrom(this); return clone; } /// void ICopyable.CopyFrom(ControllerState copyFrom) { _ActionsOnStop = copyFrom._ActionsOnStop; if (copyFrom.Root != null && Root != 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)); } } ((ICopyable)this).CopyFrom(copyFrom); } /************************************************************************************************************************/ #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 : AnimancerPlayable.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. 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. public float SetFloat(int id, float value, float dampTime, float deltaTime, float maxSpeed = float.PositiveInfinity) { if (_SmoothingVelocities == null) _SmoothingVelocities = new Dictionary(); _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; } /************************************************************************************************************************/ #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 /************************************************************************************************************************/ } }