// Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// [Pro-Only]
/// An which blends an array of other states together using linear interpolation
/// between the specified thresholds.
///
///
/// This mixer type is similar to the 1D Blend Type in Mecanim Blend Trees.
///
/// Documentation: Mixers
///
/// https://kybernetik.com.au/animancer/api/Animancer/LinearMixerState
///
public class LinearMixerState : MixerState
{
/************************************************************************************************************************/
/// An that creates a .
public new interface ITransition : ITransition { }
/************************************************************************************************************************/
private bool _ExtrapolateSpeed = true;
///
/// Should setting the above the highest threshold increase the
/// of this mixer proportionally?
///
public bool ExtrapolateSpeed
{
get => _ExtrapolateSpeed;
set
{
if (_ExtrapolateSpeed == value)
return;
_ExtrapolateSpeed = value;
if (!_Playable.IsValid())
return;
var speed = Speed;
var childCount = ChildCount;
if (value && childCount > 0)
{
var threshold = GetThreshold(childCount - 1);
if (Parameter > threshold)
speed *= Parameter / threshold;
}
_Playable.SetSpeed(speed);
}
}
/************************************************************************************************************************/
///
public override string GetParameterError(float value)
=> value.IsFinite() ? null : Strings.MustBeFinite;
/************************************************************************************************************************/
///
/// Initializes the and with one
/// state per clip and assigns thresholds evenly spaced between the specified min and max (inclusive).
///
public void Initialize(AnimationClip[] clips, float minThreshold = 0, float maxThreshold = 1)
{
#if UNITY_ASSERTIONS
if (minThreshold >= maxThreshold)
throw new ArgumentException($"{nameof(minThreshold)} must be less than {nameof(maxThreshold)}");
#endif
base.Initialize(clips);
AssignLinearThresholds(minThreshold, maxThreshold);
}
/************************************************************************************************************************/
///
/// Initializes the with two ports and connects two states to them for
/// the specified clips at the specified thresholds (default 0 and 1).
///
public void Initialize(AnimationClip clip0, AnimationClip clip1,
float threshold0 = 0, float threshold1 = 1)
{
Initialize(2);
CreateChild(0, clip0);
CreateChild(1, clip1);
SetThresholds(threshold0, threshold1);
#if UNITY_ASSERTIONS
AssertThresholdsSorted();
#endif
}
/************************************************************************************************************************/
///
/// Initializes the with three ports and connects three states to them for
/// the specified clips at the specified thresholds (default -1, 0, and 1).
///
public void Initialize(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2,
float threshold0 = -1, float threshold1 = 0, float threshold2 = 1)
{
Initialize(3);
CreateChild(0, clip0);
CreateChild(1, clip1);
CreateChild(2, clip2);
SetThresholds(threshold0, threshold1, threshold2);
#if UNITY_ASSERTIONS
AssertThresholdsSorted();
#endif
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
private bool _NeedToCheckThresholdSorting;
///
/// Called whenever the thresholds are changed. Indicates that needs to
/// be called by the next if UNITY_ASSERTIONS is defined, then calls
/// .
///
public override void OnThresholdsChanged()
{
_NeedToCheckThresholdSorting = true;
base.OnThresholdsChanged();
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
///
/// Throws an if the thresholds are not sorted from lowest to highest without
/// any duplicates.
///
///
/// The thresholds have not been initialized.
public void AssertThresholdsSorted()
{
#if UNITY_ASSERTIONS
_NeedToCheckThresholdSorting = false;
#endif
if (!HasThresholds)
throw new InvalidOperationException("Thresholds have not been initialized");
var previous = float.NegativeInfinity;
var childCount = ChildCount;
for (int i = 0; i < childCount; i++)
{
var state = GetChild(i);
if (state == null)
continue;
var next = GetThreshold(i);
if (next > previous)
previous = next;
else
throw new ArgumentException("Thresholds are out of order." +
" They must be sorted from lowest to highest with no equal values.");
}
}
/************************************************************************************************************************/
///
/// Recalculates the weights of all based on the current value of the
/// and the thresholds.
///
protected override void ForceRecalculateWeights()
{
WeightsAreDirty = false;
#if UNITY_ASSERTIONS
if (_NeedToCheckThresholdSorting)
AssertThresholdsSorted();
#endif
// Go through all states, figure out how much weight to give those with thresholds adjacent to the
// current parameter value using linear interpolation, and set all others to 0 weight.
var index = 0;
var previousState = GetNextState(ref index);
if (previousState == null)
goto ResetExtrapolatedSpeed;
var parameter = Parameter;
var previousThreshold = GetThreshold(index);
if (parameter <= previousThreshold)
{
DisableRemainingStates(index);
if (previousThreshold >= 0)
{
previousState.Weight = 1;
goto ResetExtrapolatedSpeed;
}
}
else
{
var childCount = ChildCount;
while (++index < childCount)
{
var nextState = GetNextState(ref index);
if (nextState == null)
break;
var nextThreshold = GetThreshold(index);
if (parameter > previousThreshold && parameter <= nextThreshold)
{
var t = (parameter - previousThreshold) / (nextThreshold - previousThreshold);
previousState.Weight = 1 - t;
nextState.Weight = t;
DisableRemainingStates(index);
goto ResetExtrapolatedSpeed;
}
else
{
previousState.Weight = 0;
}
previousState = nextState;
previousThreshold = nextThreshold;
}
}
previousState.Weight = 1;
if (ExtrapolateSpeed)
_Playable.SetSpeed(Speed * (parameter / previousThreshold));
return;
ResetExtrapolatedSpeed:
if (ExtrapolateSpeed && _Playable.IsValid())
_Playable.SetSpeed(Speed);
}
/************************************************************************************************************************/
///
/// Assigns the thresholds to be evenly spaced between the specified min and max (inclusive).
///
public void AssignLinearThresholds(float min = 0, float max = 1)
{
var childCount = ChildCount;
var thresholds = new float[childCount];
var increment = (max - min) / (childCount - 1);
for (int i = 0; i < childCount; i++)
{
thresholds[i] =
i < childCount - 1 ?
min + i * increment :// Assign each threshold linearly spaced between the min and max.
max;// and ensure that the last one is exactly at the max (to avoid floating-point error).
}
SetThresholds(thresholds);
}
/************************************************************************************************************************/
///
protected override void AppendDetails(StringBuilder text, string separator)
{
text.Append(separator)
.Append($"{nameof(ExtrapolateSpeed)}: ")
.Append(ExtrapolateSpeed);
base.AppendDetails(text, separator);
}
/************************************************************************************************************************/
#region Inspector
/************************************************************************************************************************/
///
protected override int ParameterCount => 1;
///
protected override string GetParameterName(int index) => "Parameter";
///
protected override AnimatorControllerParameterType GetParameterType(int index) => AnimatorControllerParameterType.Float;
///
protected override object GetParameterValue(int index) => Parameter;
///
protected override void SetParameterValue(int index, object value) => Parameter = (float)value;
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// [Editor-Only] Returns a for this state.
protected internal override Editor.IAnimancerNodeDrawer CreateDrawer() => new Drawer(this);
/************************************************************************************************************************/
///
public class Drawer : Drawer
{
/************************************************************************************************************************/
///
/// Creates a new to manage the Inspector GUI for the `state`.
///
public Drawer(LinearMixerState state) : base(state) { }
/************************************************************************************************************************/
///
protected override void AddContextMenuFunctions(UnityEditor.GenericMenu menu)
{
base.AddContextMenuFunctions(menu);
menu.AddItem(new GUIContent("Extrapolate Speed"), Target.ExtrapolateSpeed, () =>
{
Target.ExtrapolateSpeed = !Target.ExtrapolateSpeed;
});
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}