// Animancer // https://kybernetik.com.au/animancer // Copyright 2021 Kybernetik // using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEditor; using UnityEditor.Animations; #endif 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 class ControllerState : AnimancerState { /************************************************************************************************************************/ /// 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 AnimatorControllerPlayable Playable { get { Validate.AssertPlayable(this); return _Playable; } } private new AnimatorControllerPlayable _Playable; /************************************************************************************************************************/ private bool _KeepStateOnStop; /// /// If false, will reset all layers to their default state. Default False. /// /// The will only be gathered the first time this property is set to false or /// is called manually. /// public bool KeepStateOnStop { get => _KeepStateOnStop; set { _KeepStateOnStop = value; if (!value && DefaultStateHashes == null && _Playable.IsValid()) GatherDefaultStates(); } } /// /// The of the default state on each layer, used to reset to /// those states when is called if is true. /// 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 /************************************************************************************************************************/ /// IK cannot be dynamically enabled on a . public override void CopyIKFlags(AnimancerNode node) { } /************************************************************************************************************************/ /// 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 } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Public API /************************************************************************************************************************/ /// Creates a new to play the `controller`. public ControllerState(RuntimeAnimatorController controller, bool keepStateOnStop = false) { if (controller == null) throw new ArgumentNullException(nameof(controller)); _Controller = controller; _KeepStateOnStop = keepStateOnStop; } /************************************************************************************************************************/ /// Creates and assigns the managed by this state. protected override void CreatePlayable(out Playable playable) { playable = _Playable = AnimatorControllerPlayable.Create(Root._Graph, _Controller); if (!_KeepStateOnStop) 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]); } } /************************************************************************************************************************/ /// /// The current state on layer 0, or the next state if it is currently in a transition. /// public AnimatorStateInfo StateInfo { get { Validate.AssertPlayable(this); return _Playable.IsInTransition(0) ? _Playable.GetNextAnimatorStateInfo(0) : _Playable.GetCurrentAnimatorStateInfo(0); } } /************************************************************************************************************************/ /// /// The * of the /// . /// protected override float RawTime { get { var info = StateInfo; return info.normalizedTime * info.length; } set { Validate.AssertPlayable(this); _Playable.PlayInFixedTime(0, 0, value); } } /************************************************************************************************************************/ /// The current (on layer 0). public override float Length => StateInfo.length; /************************************************************************************************************************/ /// Indicates whether the current state on layer 0 will loop back to the start when it reaches the end. public override bool IsLooping => StateInfo.loop; /************************************************************************************************************************/ /// Gathers the from the current states. 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; } /// /// Calls the base and if is false it also /// calls . /// public override void Stop() { if (_KeepStateOnStop) { base.Stop(); } else { ResetToDefaultStates(); // 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; Events = null; } } /// /// Resets all layers to their default state. /// /// is null. /// /// The size of is larger than the number of layers in the /// . /// public void ResetToDefaultStates() { Validate.AssertPlayable(this); for (int i = DefaultStateHashes.Length - 1; i >= 0; i--) _Playable.Play(DefaultStateHashes[i], i, 0); // 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(); } /************************************************************************************************************************/ #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); /// 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(); /// 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); /************************************************************************************************************************/ #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(); /// 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 /************************************************************************************************************************/ #region Parameter IDs /************************************************************************************************************************/ /// A wrapper for the name and hash of an . public readonly struct ParameterID { /************************************************************************************************************************/ /// The name of this parameter. public readonly string Name; /// The name hash of this parameter. public readonly int Hash; /************************************************************************************************************************/ /// /// Creates a new with the specified and uses /// to calculate the . /// public ParameterID(string name) { Name = name; Hash = Animator.StringToHash(name); } /// /// Creates a new with the specified and leaves the /// null. /// public ParameterID(int hash) { Name = null; Hash = hash; } /// Creates a new with the specified and . /// This constructor does not verify that the `hash` actually corresponds to the `name`. public ParameterID(string name, int hash) { Name = name; Hash = hash; } /************************************************************************************************************************/ /// /// Creates a new with the specified and uses /// to calculate the . /// public static implicit operator ParameterID(string name) => new ParameterID(name); /// /// Creates a new with the specified and leaves the /// null. /// public static implicit operator ParameterID(int hash) => new ParameterID(hash); /************************************************************************************************************************/ /// Returns the . public static implicit operator int(ParameterID parameter) => parameter.Hash; /************************************************************************************************************************/ #if UNITY_EDITOR private static Dictionary> _ControllerToParameterHashAndType; #endif /// [Editor-Conditional] /// Throws if the `controller` doesn't have a parameter with the specified /// and `type`. /// /// [System.Diagnostics.Conditional(Strings.UnityEditor)] public void ValidateHasParameter(RuntimeAnimatorController controller, AnimatorControllerParameterType type) { #if UNITY_EDITOR Editor.AnimancerEditorUtilities.InitializeCleanDictionary(ref _ControllerToParameterHashAndType); // Get the parameter details. if (!_ControllerToParameterHashAndType.TryGetValue(controller, out var parameterDetails)) { var editorController = (AnimatorController)controller; var parameters = editorController.parameters; var count = parameters.Length; // Animator Controllers loaded from Asset Bundles only contain their RuntimeAnimatorController data // but not the editor AnimatorController data which we need to perform this validation. if (count == 0 && editorController.layers.Length == 0)// Double check that the editor data is actually empty. { _ControllerToParameterHashAndType.Add(controller, null); return; } parameterDetails = new Dictionary(); for (int i = 0; i < count; i++) { var parameter = parameters[i]; parameterDetails.Add(parameter.nameHash, parameter.type); } _ControllerToParameterHashAndType.Add(controller, parameterDetails); } if (parameterDetails == null) return; // Check that there is a parameter with the correct hash and type. if (!parameterDetails.TryGetValue(Hash, out var parameterType)) { throw new ArgumentException($"{controller} has no {type} parameter matching {this}"); } if (type != parameterType) { throw new ArgumentException($"{controller} has a parameter matching {this}, but it is not a {type}"); } #endif } /************************************************************************************************************************/ /// Returns a string containing the and . public override string ToString() { return $"{nameof(ControllerState)}.{nameof(ParameterID)}" + $"({nameof(Name)}: '{Name}'" + $", {nameof(Hash)}: {Hash})"; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Inspector /************************************************************************************************************************/ /// 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(); /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Returns a for this state. protected internal override Editor.IAnimancerNodeDrawer CreateDrawer() => new Drawer(this); /************************************************************************************************************************/ /// public sealed class Drawer : Editor.ParametizedAnimancerStateDrawer { /************************************************************************************************************************/ /// Creates a new to manage the Inspector GUI for the `state`. public Drawer(ControllerState state) : base(state) { } /************************************************************************************************************************/ /// protected override void DoDetailsGUI() { GatherParameters(); base.DoDetailsGUI(); } /************************************************************************************************************************/ private readonly List Parameters = new List(); /// Fills the list with the current parameter details. private void GatherParameters() { Parameters.Clear(); var count = Target.ParameterCount; if (count == 0) return; for (int i = 0; i < count; i++) { var hash = Target.GetParameterHash(i); Parameters.Add(GetParameter(hash)); } } /************************************************************************************************************************/ private AnimatorControllerParameter GetParameter(int hash) { Validate.AssertPlayable(Target); var parameterCount = Target._Playable.GetParameterCount(); for (int i = 0; i < parameterCount; i++) { var parameter = Target._Playable.GetParameter(i); if (parameter.nameHash == hash) return parameter; } return null; } /************************************************************************************************************************/ /// public override int ParameterCount => Parameters.Count; /// public override string GetParameterName(int index) => Parameters[index].name; /// public override AnimatorControllerParameterType GetParameterType(int index) => Parameters[index].type; /// public override object GetParameterValue(int index) { Validate.AssertPlayable(Target); return AnimancerUtilities.GetParameterValue(Target._Playable, Parameters[index]); } /// public override void SetParameterValue(int index, object value) { Validate.AssertPlayable(Target); AnimancerUtilities.SetParameterValue(Target._Playable, Parameters[index], value); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }