// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
using System.Runtime.CompilerServices;
namespace Animancer
{
/// [Pro-Only] An which plays a .
///
/// You can control this state very similarly to an via its property.
///
/// Documentation: Animator Controllers
///
/// https://kybernetik.com.au/animancer/api/Animancer/ControllerState
///
public partial class ControllerState : AnimancerState, ICopyable
{
/************************************************************************************************************************/
/// An that creates a .
public interface ITransition : ITransition { }
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
private RuntimeAnimatorController _Controller;
/// The which this state plays.
public RuntimeAnimatorController Controller
{
get => _Controller;
set => ChangeMainObject(ref _Controller, value);
}
/// The which this state plays.
public override Object MainObject
{
get => Controller;
set => Controller = (RuntimeAnimatorController)value;
}
/// The internal system which plays the .
public new AnimatorControllerPlayable Playable
{
get
{
Validate.AssertPlayable(this);
return _Playable;
}
}
private new AnimatorControllerPlayable _Playable;
/************************************************************************************************************************/
/// Determines what a layer does when is called.
public enum ActionOnStop
{
/// Reset the layer to the first state it was in.
DefaultState,
/// Rewind the current state's time to 0.
RewindTime,
/// Allow the current state to stay at its current time.
Continue,
}
private ActionOnStop[] _ActionsOnStop;
/// Determines what each layer does when is called.
///
/// If empty, all layers will reset to their .
///
/// If this array is smaller than the , any additional
/// layers will use the last value in this array.
///
public ActionOnStop[] ActionsOnStop
{
get => _ActionsOnStop;
set
{
_ActionsOnStop = value;
if (_Playable.IsValid())
GatherDefaultStates();
}
}
///
/// The of the default state on each layer, used to reset to
/// those states when is called for layers using
/// .
///
/// Gathered using .
public int[] DefaultStateHashes { get; set; }
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// [Assert-Only] Animancer Events doesn't work properly on s.
protected override string UnsupportedEventsMessage =>
"Animancer Events on " + nameof(ControllerState) + "s will probably not work as expected." +
" The events will be associated with the entire Animator Controller and be triggered by any of the" +
" states inside it. If you want to use events in an Animator Controller you will likely need to use" +
" Unity's regular Animation Event system.";
/// [Assert-Only]
/// does nothing on s.
///
protected override string UnsupportedSpeedMessage =>
nameof(PlayableExtensions) + "." + nameof(PlayableExtensions.SetSpeed) + " does nothing on " + nameof(ControllerState) +
"s so there is no way to directly control their speed." +
" The Animator Controller Speed page explains a possible workaround for this issue:" +
" https://kybernetik.com.au/animancer/docs/bugs/animator-controller-speed";
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// [Assert-Conditional] Asserts that the `value` is valid.
/// The `value` is NaN or Infinity.
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertParameterValue(float value, [CallerMemberName] string parameterName = null)
{
if (!value.IsFinite())
throw new ArgumentOutOfRangeException(parameterName, Strings.MustBeFinite);
}
/************************************************************************************************************************/
/// IK cannot be dynamically enabled on a .
public override void CopyIKFlags(AnimancerNode copyFrom) { }
/************************************************************************************************************************/
/// IK cannot be dynamically enabled on a .
public override bool ApplyAnimatorIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log($"IK cannot be dynamically enabled on a {nameof(ControllerState)}." +
" You must instead enable it on the desired layer inside the Animator Controller.", _Controller);
#endif
}
}
/************************************************************************************************************************/
/// IK cannot be dynamically enabled on a .
public override bool ApplyFootIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log($"IK cannot be dynamically enabled on a {nameof(ControllerState)}." +
" You must instead enable it on the desired state inside the Animator Controller.", _Controller);
#endif
}
}
/************************************************************************************************************************/
/// The number of parameters being wrapped by this state.
public virtual int ParameterCount => 0;
/// Returns the hash of a parameter being wrapped by this state.
/// This state doesn't wrap any parameters.
public virtual int GetParameterHash(int index) => throw new NotSupportedException();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Public API
/************************************************************************************************************************/
/// Creates a new to play the `controller`.
public ControllerState(RuntimeAnimatorController controller)
{
if (controller == null)
throw new ArgumentNullException(nameof(controller));
_Controller = controller;
}
/// Creates a new to play the `controller`.
public ControllerState(RuntimeAnimatorController controller, params ActionOnStop[] actionsOnStop)
: this(controller)
{
_ActionsOnStop = actionsOnStop;
}
/************************************************************************************************************************/
/// Creates and assigns the managed by this state.
protected override void CreatePlayable(out Playable playable)
{
playable = _Playable = AnimatorControllerPlayable.Create(Root._Graph, _Controller);
GatherDefaultStates();
}
/************************************************************************************************************************/
///
/// Stores the values of all parameters, calls , then restores the
/// parameter values.
///
public override void RecreatePlayable()
{
if (!_Playable.IsValid())
{
CreatePlayable();
return;
}
var parameterCount = _Playable.GetParameterCount();
var values = new object[parameterCount];
for (int i = 0; i < parameterCount; i++)
{
values[i] = AnimancerUtilities.GetParameterValue(_Playable, _Playable.GetParameter(i));
}
base.RecreatePlayable();
for (int i = 0; i < parameterCount; i++)
{
AnimancerUtilities.SetParameterValue(_Playable, _Playable.GetParameter(i), values[i]);
}
}
/************************************************************************************************************************/
///
/// Returns the current state on the specified `layer`, or the next state if it is currently in a transition.
///
public AnimatorStateInfo GetStateInfo(int layerIndex)
{
Validate.AssertPlayable(this);
return _Playable.IsInTransition(layerIndex) ?
_Playable.GetNextAnimatorStateInfo(layerIndex) :
_Playable.GetCurrentAnimatorStateInfo(layerIndex);
}
/************************************************************************************************************************/
///
/// The * of layer 0.
///
public override double RawTime
{
get
{
var info = GetStateInfo(0);
return info.normalizedTime * info.length;
}
set
{
Validate.AssertPlayable(this);
_Playable.PlayInFixedTime(0, 0, (float)value);
if (!IsPlaying)
{
_Playable.Play();
DelayedPause.Register(this);
}
}
}
/************************************************************************************************************************/
/// The current of layer 0.
public override float Length => GetStateInfo(0).length;
/************************************************************************************************************************/
/// Indicates whether the current state on layer 0 will loop back to the start when it reaches the end.
public override bool IsLooping => GetStateInfo(0).loop;
/************************************************************************************************************************/
/// Gathers the from the current states on each layer.
public void GatherDefaultStates()
{
Validate.AssertPlayable(this);
var layerCount = _Playable.GetLayerCount();
if (DefaultStateHashes == null || DefaultStateHashes.Length != layerCount)
DefaultStateHashes = new int[layerCount];
while (--layerCount >= 0)
DefaultStateHashes[layerCount] = _Playable.GetCurrentAnimatorStateInfo(layerCount).shortNameHash;
}
/************************************************************************************************************************/
///
/// Stops the animation and makes it inactive immediately so it no longer affects the output.
/// Also calls .
///
public override void Stop()
{
// Don't call base.Stop(); because it sets Time = 0; which uses PlayInFixedTime and interferes with
// resetting to the default states.
Weight = 0;
IsPlaying = false;
if (AutomaticallyClearEvents)
Events = null;
ApplyActionsOnStop();
if (_SmoothingVelocities != null)
_SmoothingVelocities.Clear();
}
/// Applies the to their corresponding layers.
/// is null.
public void ApplyActionsOnStop()
{
Validate.AssertPlayable(this);
var layerCount = Math.Min(DefaultStateHashes.Length, _Playable.GetLayerCount());
if (_ActionsOnStop == null || _ActionsOnStop.Length == 0)
{
for (int i = layerCount - 1; i >= 0; i--)
_Playable.Play(DefaultStateHashes[i], i, 0);
}
else
{
for (int i = layerCount - 1; i >= 0; i--)
{
var index = i < _ActionsOnStop.Length ? i : _ActionsOnStop.Length - 1;
switch (_ActionsOnStop[index])
{
case ActionOnStop.DefaultState:
_Playable.Play(DefaultStateHashes[i], i, 0);
break;
case ActionOnStop.RewindTime:
_Playable.Play(0, i, 0);
break;
case ActionOnStop.Continue:
break;
}
}
}
// Allowing the RawTime to be applied prevents the default state from being played because
// Animator Controllers don't properly respond to multiple Play calls in the same frame.
CancelSetTime();
}
/************************************************************************************************************************/
///
public override void GatherAnimationClips(ICollection clips)
{
if (_Controller != null)
clips.Gather(_Controller.animationClips);
}
/************************************************************************************************************************/
///
public override void Destroy()
{
_Controller = null;
base.Destroy();
}
/************************************************************************************************************************/
///
public override AnimancerState Clone(AnimancerPlayable root)
{
var clone = new ControllerState(_Controller);
clone.SetNewCloneRoot(root);
((ICopyable)clone).CopyFrom(this);
return clone;
}
///
void ICopyable.CopyFrom(ControllerState copyFrom)
{
_ActionsOnStop = copyFrom._ActionsOnStop;
if (copyFrom.Root != null &&
Root != null)
{
var layerCount = copyFrom._Playable.GetLayerCount();
for (int i = 0; i < layerCount; i++)
{
var info = copyFrom._Playable.GetCurrentAnimatorStateInfo(i);
_Playable.Play(info.shortNameHash, i, info.normalizedTime);
}
var parameterCount = copyFrom._Playable.GetParameterCount();
for (int i = 0; i < parameterCount; i++)
{
AnimancerUtilities.CopyParameterValue(
copyFrom._Playable,
_Playable,
copyFrom._Playable.GetParameter(i));
}
}
((ICopyable)this).CopyFrom(copyFrom);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Animator Controller Wrappers
/************************************************************************************************************************/
#region Cross Fade
/************************************************************************************************************************/
///
/// The default constant for fade duration parameters which causes it to use the
/// instead.
///
public const float DefaultFadeDuration = -1;
/************************************************************************************************************************/
///
/// Returns the `fadeDuration` if it is zero or positive. Otherwise returns the
/// .
///
public static float GetFadeDuration(float fadeDuration)
=> fadeDuration >= 0 ? fadeDuration : AnimancerPlayable.DefaultFadeDuration;
/************************************************************************************************************************/
/// Starts a transition from the current state to the specified state using normalized times.
/// If `fadeDuration` is negative, it uses the .
public void CrossFade(int stateNameHash,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.CrossFade(stateNameHash, GetFadeDuration(fadeDuration), layer, normalizedTime);
/************************************************************************************************************************/
/// Starts a transition from the current state to the specified state using normalized times.
/// If `fadeDuration` is negative, it uses the .
public void CrossFade(string stateName,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.CrossFade(stateName, GetFadeDuration(fadeDuration), layer, normalizedTime);
/************************************************************************************************************************/
/// Starts a transition from the current state to the specified state using times in seconds.
/// If `fadeDuration` is negative, it uses the .
public void CrossFadeInFixedTime(int stateNameHash,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float fixedTime = 0)
=> Playable.CrossFadeInFixedTime(stateNameHash, GetFadeDuration(fadeDuration), layer, fixedTime);
/************************************************************************************************************************/
/// Starts a transition from the current state to the specified state using times in seconds.
/// If `fadeDuration` is negative, it uses the .
public void CrossFadeInFixedTime(string stateName,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float fixedTime = 0)
=> Playable.CrossFadeInFixedTime(stateName, GetFadeDuration(fadeDuration), layer, fixedTime);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Play
/************************************************************************************************************************/
/// Plays the specified state immediately, starting from a particular normalized time.
public void Play(int stateNameHash,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.Play(stateNameHash, layer, normalizedTime);
/************************************************************************************************************************/
/// Plays the specified state immediately, starting from a particular normalized time.
public void Play(string stateName,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.Play(stateName, layer, normalizedTime);
/************************************************************************************************************************/
/// Plays the specified state immediately, starting from a particular time (in seconds).
public void PlayInFixedTime(int stateNameHash,
int layer = -1,
float fixedTime = 0)
=> Playable.PlayInFixedTime(stateNameHash, layer, fixedTime);
/************************************************************************************************************************/
/// Plays the specified state immediately, starting from a particular time (in seconds).
public void PlayInFixedTime(string stateName,
int layer = -1,
float fixedTime = 0)
=> Playable.PlayInFixedTime(stateName, layer, fixedTime);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Parameters
/************************************************************************************************************************/
/// Gets the value of the specified boolean parameter.
public bool GetBool(int id) => Playable.GetBool(id);
/// Gets the value of the specified boolean parameter.
public bool GetBool(string name) => Playable.GetBool(name);
/// Sets the value of the specified boolean parameter.
public void SetBool(int id, bool value) => Playable.SetBool(id, value);
/// Sets the value of the specified boolean parameter.
public void SetBool(string name, bool value) => Playable.SetBool(name, value);
/// Gets the value of the specified float parameter.
public float GetFloat(int id) => Playable.GetFloat(id);
/// Gets the value of the specified float parameter.
public float GetFloat(string name) => Playable.GetFloat(name);
/// Sets the value of the specified float parameter.
public void SetFloat(int id, float value) => Playable.SetFloat(id, value);
/// Sets the value of the specified float parameter.
public void SetFloat(string name, float value) => Playable.SetFloat(name, value);
/// Gets the value of the specified integer parameter.
public int GetInteger(int id) => Playable.GetInteger(id);
/// Gets the value of the specified integer parameter.
public int GetInteger(string name) => Playable.GetInteger(name);
/// Sets the value of the specified integer parameter.
public void SetInteger(int id, int value) => Playable.SetInteger(id, value);
/// Sets the value of the specified integer parameter.
public void SetInteger(string name, int value) => Playable.SetInteger(name, value);
/// Sets the specified trigger parameter to true.
public void SetTrigger(int id) => Playable.SetTrigger(id);
/// Sets the specified trigger parameter to true.
public void SetTrigger(string name) => Playable.SetTrigger(name);
/// Resets the specified trigger parameter to false.
public void ResetTrigger(int id) => Playable.ResetTrigger(id);
/// Resets the specified trigger parameter to false.
public void ResetTrigger(string name) => Playable.ResetTrigger(name);
/// Indicates whether the specified parameter is controlled by an .
public bool IsParameterControlledByCurve(int id) => Playable.IsParameterControlledByCurve(id);
/// Indicates whether the specified parameter is controlled by an .
public bool IsParameterControlledByCurve(string name) => Playable.IsParameterControlledByCurve(name);
/// Gets the details of one of the 's parameters.
public AnimatorControllerParameter GetParameter(int index) => Playable.GetParameter(index);
/// Gets the number of parameters in the .
public int GetParameterCount() => Playable.GetParameterCount();
/************************************************************************************************************************/
/// The number of parameters in the .
public int parameterCount => Playable.GetParameterCount();
/************************************************************************************************************************/
private AnimatorControllerParameter[] _Parameters;
/// The parameters in the .
///
/// This property allocates a new array when first accessed. To avoid that, you can use
/// and instead.
///
public AnimatorControllerParameter[] parameters
{
get
{
if (_Parameters == null)
{
var count = GetParameterCount();
_Parameters = new AnimatorControllerParameter[count];
for (int i = 0; i < count; i++)
_Parameters[i] = GetParameter(i);
}
return _Parameters;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Smoothed Set Float
/************************************************************************************************************************/
private Dictionary _SmoothingVelocities;
/// Sets the value of the specified float parameter with smoothing.
public float SetFloat(string name, float value, float dampTime, float deltaTime, float maxSpeed = float.PositiveInfinity)
=> SetFloat(Animator.StringToHash(name), value, dampTime, deltaTime, maxSpeed);
/// Sets the value of the specified float parameter with smoothing.
public float SetFloat(int id, float value, float dampTime, float deltaTime, float maxSpeed = float.PositiveInfinity)
{
if (_SmoothingVelocities == null)
_SmoothingVelocities = new Dictionary();
_SmoothingVelocities.TryGetValue(id, out var velocity);
value = Mathf.SmoothDamp(GetFloat(id), value, ref velocity, dampTime, maxSpeed, deltaTime);
SetFloat(id, value);
_SmoothingVelocities[id] = velocity;
return value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Misc
/************************************************************************************************************************/
// Layers.
/************************************************************************************************************************/
/// Gets the weight of the layer at the specified index.
public float GetLayerWeight(int layerIndex) => Playable.GetLayerWeight(layerIndex);
/// Sets the weight of the layer at the specified index.
public void SetLayerWeight(int layerIndex, float weight) => Playable.SetLayerWeight(layerIndex, weight);
/// Gets the number of layers in the .
public int GetLayerCount() => Playable.GetLayerCount();
/// The number of layers in the .
public int layerCount => Playable.GetLayerCount();
/// Gets the index of the layer with the specified name.
public int GetLayerIndex(string layerName) => Playable.GetLayerIndex(layerName);
/// Gets the name of the layer with the specified index.
public string GetLayerName(int layerIndex) => Playable.GetLayerName(layerIndex);
/************************************************************************************************************************/
// States.
/************************************************************************************************************************/
/// Returns information about the current state.
public AnimatorStateInfo GetCurrentAnimatorStateInfo(int layerIndex = 0) => Playable.GetCurrentAnimatorStateInfo(layerIndex);
/// Returns information about the next state being transitioned towards.
public AnimatorStateInfo GetNextAnimatorStateInfo(int layerIndex = 0) => Playable.GetNextAnimatorStateInfo(layerIndex);
/// Indicates whether the specified layer contains the specified state.
public bool HasState(int layerIndex, int stateID) => Playable.HasState(layerIndex, stateID);
/************************************************************************************************************************/
// Transitions.
/************************************************************************************************************************/
/// Indicates whether the specified layer is currently executing a transition.
public bool IsInTransition(int layerIndex = 0) => Playable.IsInTransition(layerIndex);
/// Gets information about the current transition.
public AnimatorTransitionInfo GetAnimatorTransitionInfo(int layerIndex = 0) => Playable.GetAnimatorTransitionInfo(layerIndex);
/************************************************************************************************************************/
// Clips.
/************************************************************************************************************************/
/// Gets information about the s currently being played.
public AnimatorClipInfo[] GetCurrentAnimatorClipInfo(int layerIndex = 0) => Playable.GetCurrentAnimatorClipInfo(layerIndex);
/// Gets information about the s currently being played.
public void GetCurrentAnimatorClipInfo(int layerIndex, List clips) => Playable.GetCurrentAnimatorClipInfo(layerIndex, clips);
/// Gets the number of s currently being played.
public int GetCurrentAnimatorClipInfoCount(int layerIndex = 0) => Playable.GetCurrentAnimatorClipInfoCount(layerIndex);
/// Gets information about the s currently being transitioned towards.
public AnimatorClipInfo[] GetNextAnimatorClipInfo(int layerIndex = 0) => Playable.GetNextAnimatorClipInfo(layerIndex);
/// Gets information about the s currently being transitioned towards.
public void GetNextAnimatorClipInfo(int layerIndex, List clips) => Playable.GetNextAnimatorClipInfo(layerIndex, clips);
/// Gets the number of s currently being transitioned towards.
public int GetNextAnimatorClipInfoCount(int layerIndex = 0) => Playable.GetNextAnimatorClipInfoCount(layerIndex);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}