// Animancer // Copyright 2020 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 { /// <summary>[Pro-Only] /// An <see cref="AnimancerState"/> which plays a <see cref="RuntimeAnimatorController"/>. /// You can control this state very similarly to an <see cref="Animator"/> via its <see cref="Playable"/> field. /// </summary> public class ControllerState : AnimancerState { /************************************************************************************************************************/ #region Fields and Auto-Properties /************************************************************************************************************************/ private RuntimeAnimatorController _Controller; /// <summary>The <see cref="RuntimeAnimatorController"/> which this state plays.</summary> public RuntimeAnimatorController Controller { get { return _Controller; } set { if (ReferenceEquals(_Controller, value)) return; if (ReferenceEquals(_Key, value)) Key = value; if (_Playable.IsValid()) Root._Graph.DestroyPlayable(_Playable); CreatePlayable(value); SetWeightDirty(); } } /// <summary>The <see cref="RuntimeAnimatorController"/> which this state plays.</summary> public override Object MainObject { get { return Controller; } set { Controller = (RuntimeAnimatorController)value; } } /// <summary>The internal system which plays the <see cref="RuntimeAnimatorController"/>.</summary> public AnimatorControllerPlayable Playable { get; private set; } /************************************************************************************************************************/ /// <summary> /// If false, <see cref="Stop"/> will reset all layers to their default state. Default False. /// <para></para> /// If you set this value to false after the constructor, you must assign the <see cref="DefaultStateHashes"/> /// or call <see cref="GatherDefaultStates"/> yourself. /// </summary> public bool KeepStateOnStop { get; set; } /// <summary> /// The <see cref="AnimatorStateInfo.shortNameHash"/> of the default state on each layer, used to reset to /// those states when <see cref="Stop"/> is called. /// </summary> public int[] DefaultStateHashes { get; set; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Public API /************************************************************************************************************************/ /// <summary> /// Constructs a new <see cref="ControllerState"/> to play the `animatorController` without connecting /// it to the <see cref="PlayableGraph"/>. /// </summary> protected ControllerState(AnimancerPlayable root, RuntimeAnimatorController animatorController, bool keepStateOnStop = false) : base(root) { KeepStateOnStop = keepStateOnStop; CreatePlayable(animatorController); } /// <summary> /// Constructs a new <see cref="ControllerState"/> to play the `animatorController` and connects it to /// the `layer`. /// </summary> public ControllerState(AnimancerLayer layer, RuntimeAnimatorController animatorController, bool keepStateOnStop = false) : this(layer.Root, animatorController, keepStateOnStop) { layer.AddChild(this); } /// <summary> /// Constructs a new <see cref="ControllerState"/> to play the `animatorController` and /// connects it to the `parent` at the specified `index`. /// </summary> public ControllerState(AnimancerNode parent, int index, RuntimeAnimatorController animatorController, bool keepStateOnStop = false) : this(parent.Root, animatorController, keepStateOnStop) { SetParent(parent, index); } /************************************************************************************************************************/ private void CreatePlayable(RuntimeAnimatorController animatorController) { if (animatorController == null) throw new ArgumentNullException("animatorController"); _Controller = animatorController; Playable = AnimatorControllerPlayable.Create(Root._Graph, animatorController); _Playable = Playable; if (!KeepStateOnStop) GatherDefaultStates(); } /************************************************************************************************************************/ /// <summary> /// The current state on layer 0, or the next state if it is currently in a transition. /// </summary> public AnimatorStateInfo StateInfo { get { return Playable.IsInTransition(0) ? Playable.GetNextAnimatorStateInfo(0) : Playable.GetCurrentAnimatorStateInfo(0); } } /************************************************************************************************************************/ /// <summary> /// The <see cref="AnimatorStateInfo.normalizedTime"/> * <see cref="AnimatorStateInfo.length"/> of the /// <see cref="StateInfo"/> /// </summary> protected override float NewTime { get { var info = StateInfo; return info.normalizedTime * info.length; } set { Playable.PlayInFixedTime(0, 0, value); } } /************************************************************************************************************************/ /// <summary>The current <see cref="AnimatorStateInfo.length"/> (on layer 0).</summary> public override float Length { get { return StateInfo.length; } } /************************************************************************************************************************/ /// <summary> /// Indicates whether the current state on layer 0 will loop back to the start when it reaches the end. /// </summary> public override bool IsLooping { get { return StateInfo.loop; } } /************************************************************************************************************************/ /// <summary> /// Gathers the <see cref="DefaultStateHashes"/> from the current states. /// </summary> public void GatherDefaultStates() { var layerCount = Playable.GetLayerCount(); if (DefaultStateHashes == null || DefaultStateHashes.Length != layerCount) DefaultStateHashes = new int[layerCount]; while (--layerCount >= 0) DefaultStateHashes[layerCount] = Playable.GetCurrentAnimatorStateInfo(layerCount).shortNameHash; } /// <summary> /// Calls the base <see cref="AnimancerState.Stop"/> and if <see cref="KeepStateOnStop"/> is false it also /// calls <see cref="ResetToDefaultStates"/>. /// </summary> public override void Stop() { base.Stop(); if (!KeepStateOnStop) ResetToDefaultStates(); } /// <summary> /// Resets all layers to their default state. /// </summary> /// <exception cref="NullReferenceException">Thrown if <see cref="DefaultStateHashes"/> is null.</exception> /// <exception cref="IndexOutOfRangeException"> /// Thrown if the size of <see cref="DefaultStateHashes"/> is larger than the number of layers in the /// <see cref="Controller"/>. /// </exception> public void ResetToDefaultStates() { for (int i = 0; i < DefaultStateHashes.Length; i++) Playable.Play(DefaultStateHashes[i], i, 0); } /************************************************************************************************************************/ /// <summary> /// Returns a string describing the type of this state and the name of the <see cref="Controller"/>. /// </summary> public override string ToString() { if (_Controller != null) return string.Concat(base.ToString(), " (", _Controller.name, ")"); else return base.ToString() + " (null)"; } /************************************************************************************************************************/ /// <summary>[<see cref="IAnimationClipCollection"/>] /// Gathers all the animations in this state. /// </summary> public override void GatherAnimationClips(ICollection<AnimationClip> clips) { if (_Controller != null) clips.Gather(_Controller.animationClips); } /************************************************************************************************************************/ /// <summary>Destroys the <see cref="Playable"/>.</summary> public override void Destroy() { _Controller = null; base.Destroy(); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Parameters /************************************************************************************************************************/ /// <summary> /// A wrapper for the name and hash of an <see cref="AnimatorControllerParameter"/> to allow easy access. /// </summary> public struct Parameter { /************************************************************************************************************************/ private string _Name; private int _Hash; /************************************************************************************************************************/ /// <summary> /// The name of the wrapped parameter. This will be null if the <see cref="Hash"/> was assigned directly. /// </summary> public string Name { get { return _Name; } set { _Name = value; _Hash = Animator.StringToHash(value); } } /************************************************************************************************************************/ /// <summary> /// The name hash of the wrapped parameter. /// </summary> public int Hash { get { return _Hash; } set { _Name = null; _Hash = value; } } /************************************************************************************************************************/ /// <summary> /// Constructs a new <see cref="Parameter"/> with the specified <see cref="Name"/> and uses /// <see cref="Animator.StringToHash"/> to calculate the <see cref="Hash"/>. /// </summary> public Parameter(string name) { _Name = name; _Hash = Animator.StringToHash(name); } /// <summary> /// Constructs a new <see cref="Parameter"/> with the specified <see cref="Hash"/> and leaves the /// <see cref="Name"/> null. /// </summary> public Parameter(int hash) { _Name = null; _Hash = hash; } /************************************************************************************************************************/ /// <summary> /// Constructs a new <see cref="Parameter"/> with the specified <see cref="Name"/> and uses /// <see cref="Animator.StringToHash"/> to calculate the <see cref="Hash"/>. /// </summary> public static implicit operator Parameter(string name) { return new Parameter(name); } /// <summary> /// Constructs a new <see cref="Parameter"/> with the specified <see cref="Hash"/> and leaves the /// <see cref="Name"/> null. /// </summary> public static implicit operator Parameter(int hash) { return new Parameter(hash); } /************************************************************************************************************************/ /// <summary>Returns the <see cref="Hash"/>.</summary> public static implicit operator int(Parameter parameter) { return parameter._Hash; } /************************************************************************************************************************/ #if UNITY_EDITOR private static Dictionary<RuntimeAnimatorController, Dictionary<int, AnimatorControllerParameterType>> _ControllerToParameterHashAndType; #endif /// <summary>[Editor-Conditional] /// Throws if the `controller` doesn't have a parameter with the specified <see cref="Hash"/> /// and `type`. /// </summary> /// <exception cref="ArgumentException"/> [System.Diagnostics.Conditional(Strings.EditorOnly)] public void ValidateHasParameter(RuntimeAnimatorController controller, AnimatorControllerParameterType type) { #if UNITY_EDITOR if (_ControllerToParameterHashAndType == null) _ControllerToParameterHashAndType = new Dictionary<RuntimeAnimatorController, Dictionary<int, AnimatorControllerParameterType>>(); // Get the parameter details. Dictionary<int, AnimatorControllerParameterType> parameterDetails; if (!_ControllerToParameterHashAndType.TryGetValue(controller, out parameterDetails)) { parameterDetails = new Dictionary<int, AnimatorControllerParameterType>(); var animatorController = controller as AnimatorController; var parameters = animatorController.parameters; var count = parameters.Length; for (int i = 0; i < count; i++) { var parameter = parameters[i]; parameterDetails.Add(parameter.nameHash, parameter.type); } _ControllerToParameterHashAndType.Add(controller, parameterDetails); } // Check that there is a parameter with the correct hash and type. AnimatorControllerParameterType parameterType; if (!parameterDetails.TryGetValue(_Hash, out 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 } /************************************************************************************************************************/ /// <summary>Returns a string containing the <see cref="Name"/> and <see cref="Hash"/>.</summary> public override string ToString() { return string.Concat( "ControllerState.Parameter(Name:'", _Name, "', Hash:", _Hash.ToString(), ")"); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Inspector /************************************************************************************************************************/ /// <summary>The number of parameters being wrapped by this state.</summary> public virtual int ParameterCount { get { return 0; } } /// <summary>Returns the hash of a parameter being wrapped by this state.</summary> /// <exception cref="NotSupportedException">Thrown if this state doesn't wrap any parameters.</exception> public virtual int GetParameterHash(int index) { throw new NotSupportedException(); } /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// <summary>[Editor-Only] Returns a <see cref="Drawer"/> for this state.</summary> protected internal override Editor.IAnimancerNodeDrawer GetDrawer() { return new Drawer(this); } /************************************************************************************************************************/ /// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="ControllerState"/>.</summary> public sealed class Drawer : Editor.ParametizedAnimancerStateDrawer<ControllerState> { /************************************************************************************************************************/ /// <summary> /// Constructs a new <see cref="Drawer"/> to manage the Inspector GUI for the `state`. /// </summary> public Drawer(ControllerState state) : base(state) { } /************************************************************************************************************************/ /// <summary> Draws the details of the target state in the GUI.</summary> protected override void DoDetailsGUI(IAnimancerComponent owner) { GatherParameters(); base.DoDetailsGUI(owner); } /************************************************************************************************************************/ private readonly List<AnimatorControllerParameter> Parameters = new List<AnimatorControllerParameter>(); /// <summary> /// Fills the <see cref="Parameters"/> list with the current parameter details. /// </summary> 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) { 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; } /************************************************************************************************************************/ /// <summary>The number of parameters being managed by the target state.</summary> public override int ParameterCount { get { return Parameters.Count; } } /// <summary>Returns the name of a parameter being managed by the target state.</summary> /// <exception cref="NotSupportedException">Thrown if the target state doesn't manage any parameters.</exception> public override string GetParameterName(int index) { return Parameters[index].name; } /// <summary>Returns the type of a parameter being managed by the target state.</summary> /// <exception cref="NotSupportedException">Thrown if the target state doesn't manage any parameters.</exception> public override AnimatorControllerParameterType GetParameterType(int index) { return Parameters[index].type; } /// <summary>Returns the value of a parameter being managed by the target state.</summary> /// <exception cref="NotSupportedException">Thrown if the target state doesn't manage any parameters.</exception> public override object GetParameterValue(int index) { return Editor.AnimancerEditorUtilities.GetParameterValue(Target.Playable, Parameters[index]); } /// <summary>Sets the value of a parameter being managed by the target state.</summary> /// <exception cref="NotSupportedException">Thrown if the target state doesn't manage any parameters.</exception> public override void SetParameterValue(int index, object value) { Editor.AnimancerEditorUtilities.SetParameterValue(Target.Playable, Parameters[index], value); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Transition /************************************************************************************************************************/ /// <summary> /// Base class for serializable <see cref="ITransition"/>s which can create a particular type of /// <see cref="ControllerState"/> when passed into <see cref="AnimancerPlayable.Play(ITransition)"/>. /// </summary> /// <remarks> /// Unfortunately the tool used to generate this documentation does not currently support nested types with /// identical names, so only one <c>Transition</c> class will actually have a documentation page. /// <para></para> /// Even though it has the <see cref="SerializableAttribute"/>, this class won't actually get serialized /// by Unity because it's generic and abstract. Each child class still needs to include the attribute. /// </remarks> [Serializable] public abstract new class Transition<TState> : AnimancerState.Transition<TState>, IAnimationClipCollection where TState : ControllerState { /************************************************************************************************************************/ [SerializeField] private RuntimeAnimatorController _Controller; /// <summary>[<see cref="SerializeField"/>] /// The <see cref="ControllerState.Controller"/> that will be used for the created state. /// </summary> public RuntimeAnimatorController Controller { get { return _Controller; } set { _Controller = value; } } /************************************************************************************************************************/ [SerializeField, Tooltip("If false, stopping this state will reset all its layers to their default state. Default False.")] private bool _KeepStateOnStop; /// <summary>[<see cref="SerializeField"/>] /// If false, <see cref="Stop"/> will reset all layers to their default state. /// <para></para> /// If you set this value to false after the constructor, you must assign the <see cref="DefaultStateHashes"/> /// or call <see cref="GatherDefaultStates"/> yourself. /// </summary> public bool KeepStateOnStop { get { return _KeepStateOnStop; } set { _KeepStateOnStop = value; } } /************************************************************************************************************************/ /// <summary>[<see cref="ITransitionDetailed"/>] /// The maximum amount of time the animation is expected to take (in seconds). /// </summary> public override float MaximumDuration { get { if (_Controller == null) return 0; var duration = 0f; var clips = _Controller.animationClips; for (int i = 0; i < clips.Length; i++) { var length = clips[i].length; if (duration < length) duration = length; } return duration; } } /************************************************************************************************************************/ /// <summary>Returns the <see cref="Controller"/>.</summary> public static implicit operator RuntimeAnimatorController(Transition<TState> transition) { return transition != null ? transition.Controller : null; } /************************************************************************************************************************/ /// <summary>Adds all clips in the <see cref="Controller"/> to the collection.</summary> void IAnimationClipCollection.GatherAnimationClips(ICollection<AnimationClip> clips) { if (_Controller != null) clips.Gather(_Controller.animationClips); } /************************************************************************************************************************/ } /************************************************************************************************************************/ /// <summary> /// A serializable <see cref="ITransition"/> which can create a <see cref="ControllerState"/> when /// passed into <see cref="AnimancerPlayable.Play(ITransition)"/>. /// <para></para> /// This class can be implicitly cast to and from <see cref="RuntimeAnimatorController"/>. /// </summary> [Serializable] public class Transition : Transition<ControllerState> { /************************************************************************************************************************/ /// <summary> /// Creates and returns a new <see cref="ControllerState"/> connected to the `layer`. /// <para></para> /// This method also assigns it as the <see cref="AnimancerState.Transition{TState}.State"/>. /// </summary> public override ControllerState CreateState(AnimancerLayer layer) { return new ControllerState(layer, Controller, KeepStateOnStop); } /************************************************************************************************************************/ /// <summary>Constructs a new <see cref="Transition"/>.</summary> public Transition() { } /// <summary>Constructs a new <see cref="Transition"/> with the specified Animator Controller.</summary> public Transition(RuntimeAnimatorController controller) { Controller = controller; } /************************************************************************************************************************/ /// <summary>Constructs a new <see cref="Transition"/> with the specified Animator Controller.</summary> public static implicit operator Transition(RuntimeAnimatorController controller) { return new Transition(controller); } /************************************************************************************************************************/ #region Drawer #if UNITY_EDITOR /************************************************************************************************************************/ /// <summary> /// [Editor-Only] Draws the Inspector GUI for a <see cref="Transition{TState}"/> or /// <see cref="Transition"/>. /// </summary> [CustomPropertyDrawer(typeof(Transition<>), true)] [CustomPropertyDrawer(typeof(Transition), true)] public class Drawer : Editor.TransitionDrawer { /************************************************************************************************************************/ private readonly string[] Parameters; private readonly string[] ParameterPrefixes; /************************************************************************************************************************/ /// <summary>Constructs a new <see cref="Drawer"/> without any parameters.</summary> public Drawer() : this(null) { } /// <summary>Constructs a new <see cref="Drawer"/> and sets the <see cref="Parameters"/>.</summary> public Drawer(params string[] parameters) : base("_Controller") { Parameters = parameters; if (parameters == null) return; ParameterPrefixes = new string[parameters.Length]; for (int i = 0; i < ParameterPrefixes.Length; i++) { ParameterPrefixes[i] = "." + parameters[i]; } } /************************************************************************************************************************/ /// <summary> /// Draws the `property` GUI in relation to the `rootProperty` which was passed into /// <see cref="Editor.TransitionDrawer.OnGUI"/>. /// </summary> protected override void DoPropertyGUI(ref Rect area, SerializedProperty rootProperty, SerializedProperty property, GUIContent label) { if (ParameterPrefixes != null) { var controllerProperty = rootProperty.FindPropertyRelative(MainPropertyName); var controller = controllerProperty.objectReferenceValue as AnimatorController; if (controller != null) { var path = property.propertyPath; for (int i = 0; i < ParameterPrefixes.Length; i++) { if (path.EndsWith(ParameterPrefixes[i])) { area.height = Editor.AnimancerGUI.LineHeight; DoParameterGUI(area, controller, property); return; } } } } EditorGUI.BeginChangeCheck(); base.DoPropertyGUI(ref area, rootProperty, property, label); // When the controller changes, validate all parameters. if (EditorGUI.EndChangeCheck() && Parameters != null && property.propertyPath.EndsWith(MainPropertyPathSuffix)) { var controller = property.objectReferenceValue as AnimatorController; if (controller != null) { for (int i = 0; i < Parameters.Length; i++) { property = rootProperty.FindPropertyRelative(Parameters[i]); var parameterName = property.stringValue; if (!HasFloatParameter(controller, parameterName)) { parameterName = GetFirstFloatParameterName(controller); if (!string.IsNullOrEmpty(parameterName)) property.stringValue = parameterName; } } } } } /************************************************************************************************************************/ /// <summary> /// Draws a dropdown menu to select the name of a parameter in the `controller`. /// </summary> protected void DoParameterGUI(Rect area, AnimatorController controller, SerializedProperty property) { var parameterName = property.stringValue; var parameters = controller.parameters; var label = Editor.AnimancerGUI.TempContent(property); label = EditorGUI.BeginProperty(area, label, property); var xMax = area.xMax; area.width = EditorGUIUtility.labelWidth; EditorGUI.PrefixLabel(area, label); area.x += area.width; area.xMax = xMax; var color = GUI.color; if (!HasFloatParameter(controller, parameterName)) GUI.color = Editor.AnimancerGUI.ErrorFieldColor; var content = Editor.AnimancerGUI.TempContent(parameterName); if (EditorGUI.DropdownButton(area, content, FocusType.Passive)) { property = property.Copy(); var menu = new GenericMenu(); for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; Editor.AnimancerEditorUtilities.AddMenuItem(menu, parameter.name, parameter.type == AnimatorControllerParameterType.Float, () => { Editor.Serialization.ForEachTarget(property, (targetProperty) => { targetProperty.stringValue = parameter.name; }); }); } if (menu.GetItemCount() == 0) menu.AddDisabledItem(new GUIContent("No Parameters")); menu.ShowAsContext(); } GUI.color = color; EditorGUI.EndProperty(); } /************************************************************************************************************************/ private static bool HasFloatParameter(AnimatorController controller, string name) { if (string.IsNullOrEmpty(name)) return false; var parameters = controller.parameters; for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; if (parameter.type == AnimatorControllerParameterType.Float && name == parameters[i].name) { return true; } } return false; } /************************************************************************************************************************/ private static string GetFirstFloatParameterName(AnimatorController controller) { var parameters = controller.parameters; for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; if (parameter.type == AnimatorControllerParameterType.Float) { return parameter.name; } } return ""; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }