You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/Plugins/Animancer/Internal/Controller States/ControllerState.cs

869 lines
39 KiB
C#

1 month ago
// 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
/************************************************************************************************************************/
}
}