// 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 { /// [Pro-Only] /// An which plays a . /// You can control this state very similarly to an via its field. /// public class ControllerState : AnimancerState { /************************************************************************************************************************/ #region Fields and Auto-Properties /************************************************************************************************************************/ private RuntimeAnimatorController _Controller; /// The which this state plays. 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(); } } /// The which this state plays. public override Object MainObject { get { return Controller; } set { Controller = (RuntimeAnimatorController)value; } } /// The internal system which plays the . public AnimatorControllerPlayable Playable { get; private set; } /************************************************************************************************************************/ /// /// If false, will reset all layers to their default state. Default False. /// /// If you set this value to false after the constructor, you must assign the /// or call yourself. /// public bool KeepStateOnStop { get; set; } /// /// The of the default state on each layer, used to reset to /// those states when is called. /// public int[] DefaultStateHashes { get; set; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Public API /************************************************************************************************************************/ /// /// Constructs a new to play the `animatorController` without connecting /// it to the . /// protected ControllerState(AnimancerPlayable root, RuntimeAnimatorController animatorController, bool keepStateOnStop = false) : base(root) { KeepStateOnStop = keepStateOnStop; CreatePlayable(animatorController); } /// /// Constructs a new to play the `animatorController` and connects it to /// the `layer`. /// public ControllerState(AnimancerLayer layer, RuntimeAnimatorController animatorController, bool keepStateOnStop = false) : this(layer.Root, animatorController, keepStateOnStop) { layer.AddChild(this); } /// /// Constructs a new to play the `animatorController` and /// connects it to the `parent` at the specified `index`. /// 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(); } /************************************************************************************************************************/ /// /// The current state on layer 0, or the next state if it is currently in a transition. /// public AnimatorStateInfo StateInfo { get { return Playable.IsInTransition(0) ? Playable.GetNextAnimatorStateInfo(0) : Playable.GetCurrentAnimatorStateInfo(0); } } /************************************************************************************************************************/ /// /// The * of the /// /// protected override float NewTime { get { var info = StateInfo; return info.normalizedTime * info.length; } set { Playable.PlayInFixedTime(0, 0, value); } } /************************************************************************************************************************/ /// The current (on layer 0). public override float Length { get { return 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 { get { return StateInfo.loop; } } /************************************************************************************************************************/ /// /// Gathers the from the current states. /// 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; } /// /// Calls the base and if is false it also /// calls . /// public override void Stop() { base.Stop(); if (!KeepStateOnStop) ResetToDefaultStates(); } /// /// Resets all layers to their default state. /// /// Thrown if is null. /// /// Thrown if the size of is larger than the number of layers in the /// . /// public void ResetToDefaultStates() { for (int i = 0; i < DefaultStateHashes.Length; i++) Playable.Play(DefaultStateHashes[i], i, 0); } /************************************************************************************************************************/ /// /// Returns a string describing the type of this state and the name of the . /// public override string ToString() { if (_Controller != null) return string.Concat(base.ToString(), " (", _Controller.name, ")"); else return base.ToString() + " (null)"; } /************************************************************************************************************************/ /// [] /// Gathers all the animations in this state. /// public override void GatherAnimationClips(ICollection clips) { if (_Controller != null) clips.Gather(_Controller.animationClips); } /************************************************************************************************************************/ /// Destroys the . public override void Destroy() { _Controller = null; base.Destroy(); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Parameters /************************************************************************************************************************/ /// /// A wrapper for the name and hash of an to allow easy access. /// public struct Parameter { /************************************************************************************************************************/ private string _Name; private int _Hash; /************************************************************************************************************************/ /// /// The name of the wrapped parameter. This will be null if the was assigned directly. /// public string Name { get { return _Name; } set { _Name = value; _Hash = Animator.StringToHash(value); } } /************************************************************************************************************************/ /// /// The name hash of the wrapped parameter. /// public int Hash { get { return _Hash; } set { _Name = null; _Hash = value; } } /************************************************************************************************************************/ /// /// Constructs a new with the specified and uses /// to calculate the . /// public Parameter(string name) { _Name = name; _Hash = Animator.StringToHash(name); } /// /// Constructs a new with the specified and leaves the /// null. /// public Parameter(int hash) { _Name = null; _Hash = hash; } /************************************************************************************************************************/ /// /// Constructs a new with the specified and uses /// to calculate the . /// public static implicit operator Parameter(string name) { return new Parameter(name); } /// /// Constructs a new with the specified and leaves the /// null. /// public static implicit operator Parameter(int hash) { return new Parameter(hash); } /************************************************************************************************************************/ /// Returns the . public static implicit operator int(Parameter parameter) { return 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.EditorOnly)] public void ValidateHasParameter(RuntimeAnimatorController controller, AnimatorControllerParameterType type) { #if UNITY_EDITOR if (_ControllerToParameterHashAndType == null) _ControllerToParameterHashAndType = new Dictionary>(); // Get the parameter details. Dictionary parameterDetails; if (!_ControllerToParameterHashAndType.TryGetValue(controller, out parameterDetails)) { parameterDetails = new Dictionary(); 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 } /************************************************************************************************************************/ /// Returns a string containing the and . public override string ToString() { return string.Concat( "ControllerState.Parameter(Name:'", _Name, "', Hash:", _Hash.ToString(), ")"); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Inspector /************************************************************************************************************************/ /// The number of parameters being wrapped by this state. public virtual int ParameterCount { get { return 0; } } /// Returns the hash of a parameter being wrapped by this state. /// Thrown if 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 GetDrawer() { return new Drawer(this); } /************************************************************************************************************************/ /// [Editor-Only] Draws the Inspector GUI for an . public sealed class Drawer : Editor.ParametizedAnimancerStateDrawer { /************************************************************************************************************************/ /// /// Constructs a new to manage the Inspector GUI for the `state`. /// public Drawer(ControllerState state) : base(state) { } /************************************************************************************************************************/ /// Draws the details of the target state in the GUI. protected override void DoDetailsGUI(IAnimancerComponent owner) { GatherParameters(); base.DoDetailsGUI(owner); } /************************************************************************************************************************/ 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) { 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; } /************************************************************************************************************************/ /// The number of parameters being managed by the target state. public override int ParameterCount { get { return Parameters.Count; } } /// Returns the name of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override string GetParameterName(int index) { return Parameters[index].name; } /// Returns the type of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override AnimatorControllerParameterType GetParameterType(int index) { return Parameters[index].type; } /// Returns the value of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override object GetParameterValue(int index) { return Editor.AnimancerEditorUtilities.GetParameterValue(Target.Playable, Parameters[index]); } /// Sets the value of a parameter being managed by the target state. /// Thrown if the target state doesn't manage any parameters. public override void SetParameterValue(int index, object value) { Editor.AnimancerEditorUtilities.SetParameterValue(Target.Playable, Parameters[index], value); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Transition /************************************************************************************************************************/ /// /// Base class for serializable s which can create a particular type of /// when passed into . /// /// /// Unfortunately the tool used to generate this documentation does not currently support nested types with /// identical names, so only one Transition class will actually have a documentation page. /// /// Even though it has the , this class won't actually get serialized /// by Unity because it's generic and abstract. Each child class still needs to include the attribute. /// [Serializable] public abstract new class Transition : AnimancerState.Transition, IAnimationClipCollection where TState : ControllerState { /************************************************************************************************************************/ [SerializeField] private RuntimeAnimatorController _Controller; /// [] /// The that will be used for the created state. /// 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; /// [] /// If false, will reset all layers to their default state. /// /// If you set this value to false after the constructor, you must assign the /// or call yourself. /// public bool KeepStateOnStop { get { return _KeepStateOnStop; } set { _KeepStateOnStop = value; } } /************************************************************************************************************************/ /// [] /// The maximum amount of time the animation is expected to take (in seconds). /// 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; } } /************************************************************************************************************************/ /// Returns the . public static implicit operator RuntimeAnimatorController(Transition transition) { return transition != null ? transition.Controller : null; } /************************************************************************************************************************/ /// Adds all clips in the to the collection. void IAnimationClipCollection.GatherAnimationClips(ICollection clips) { if (_Controller != null) clips.Gather(_Controller.animationClips); } /************************************************************************************************************************/ } /************************************************************************************************************************/ /// /// A serializable which can create a when /// passed into . /// /// This class can be implicitly cast to and from . /// [Serializable] public class Transition : Transition { /************************************************************************************************************************/ /// /// Creates and returns a new connected to the `layer`. /// /// This method also assigns it as the . /// public override ControllerState CreateState(AnimancerLayer layer) { return new ControllerState(layer, Controller, KeepStateOnStop); } /************************************************************************************************************************/ /// Constructs a new . public Transition() { } /// Constructs a new with the specified Animator Controller. public Transition(RuntimeAnimatorController controller) { Controller = controller; } /************************************************************************************************************************/ /// Constructs a new with the specified Animator Controller. public static implicit operator Transition(RuntimeAnimatorController controller) { return new Transition(controller); } /************************************************************************************************************************/ #region Drawer #if UNITY_EDITOR /************************************************************************************************************************/ /// /// [Editor-Only] Draws the Inspector GUI for a or /// . /// [CustomPropertyDrawer(typeof(Transition<>), true)] [CustomPropertyDrawer(typeof(Transition), true)] public class Drawer : Editor.TransitionDrawer { /************************************************************************************************************************/ private readonly string[] Parameters; private readonly string[] ParameterPrefixes; /************************************************************************************************************************/ /// Constructs a new without any parameters. public Drawer() : this(null) { } /// Constructs a new and sets the . 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]; } } /************************************************************************************************************************/ /// /// Draws the `property` GUI in relation to the `rootProperty` which was passed into /// . /// 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; } } } } } /************************************************************************************************************************/ /// /// Draws a dropdown menu to select the name of a parameter in the `controller`. /// 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 /************************************************************************************************************************/ } }