// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
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,
ICopyable
{
/************************************************************************************************************************/
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;
/************************************************************************************************************************/
/// The lowest threshold (which is for the first child because they must be sorted).
public float MinThreshold => GetThreshold(0);
/// The highest threshold (which is for the last child because they must be sorted).
public float MaxThreshold => GetThreshold(ChildCount - 1);
///
public override float NormalizedParameter
{
get => AnimancerUtilities.InverseLerpUnclamped(MinThreshold, MaxThreshold, Parameter);
set => Parameter = Mathf.LerpUnclamped(MinThreshold, MaxThreshold, value);
}
/************************************************************************************************************************/
#region Parameter Binding
/************************************************************************************************************************/
private NodeParameter _ParameterBinding;
///
/// If set, this will be used as a key in the so any
/// changes to that parameter will automatically set the .
///
public StringReference ParameterName
{
get => _ParameterBinding.Key;
set
{
if (_ParameterBinding.SetKeyCheckNeedsInitialize(value))
_ParameterBinding.Initialize(this, parameter => Parameter = parameter);
}
}
/************************************************************************************************************************/
///
public override void SetGraph(AnimancerGraph graph)
{
if (Graph == graph)
return;
_ParameterBinding.UnBindIfInitialized();
base.SetGraph(graph);
_ParameterBinding.BindIfInitialized();
}
/************************************************************************************************************************/
///
public override void Destroy()
{
base.Destroy();
_ParameterBinding.UnBindIfInitialized();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
///
public override AnimancerState Clone(CloneContext context)
{
var clone = new LinearMixerState();
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
///
public sealed override void CopyFrom(MixerState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
///
public virtual void CopyFrom(LinearMixerState copyFrom, CloneContext context)
{
_ExtrapolateSpeed = copyFrom._ExtrapolateSpeed;
ParameterName = copyFrom.ParameterName;
base.CopyFrom(copyFrom, context);
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
private bool _ShouldCheckThresholdSorting;
///
/// 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()
{
_ShouldCheckThresholdSorting = 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
_ShouldCheckThresholdSorting = false;
#endif
if (!HasThresholds)
{
MarkAsUsed(this);
throw new InvalidOperationException("Thresholds have not been initialized");
}
var previous = float.NegativeInfinity;
var childCount = ChildCount;
for (int i = 0; i < childCount; i++)
{
var state = ChildStates[i];
if (state == null)
continue;
var next = GetThreshold(i);
if (next > previous)
{
previous = next;
}
else
{
MarkAsUsed(this);
var reason = next == previous
? "Mixer has multiple identical thresholds."
: "Mixer has thresholds out of order.";
throw new ArgumentException(
$"{reason} They must be sorted from lowest to highest with no equal values." +
$"\n{this.GetDescription()}");
}
}
}
/************************************************************************************************************************/
///
protected override void ForceRecalculateWeights()
{
#if UNITY_ASSERTIONS
if (_ShouldCheckThresholdSorting)
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 childCount = ChildCount;
if (childCount == 0)
goto ResetExtrapolatedSpeed;
var index = 0;
var previousState = ChildStates[index];
var parameter = Parameter;
var previousThreshold = GetThreshold(index);
if (parameter <= previousThreshold)
{
DisableRemainingStates(index);
if (previousThreshold >= 0)
{
Playable.SetChildWeight(previousState, 1);
goto ResetExtrapolatedSpeed;
}
}
else
{
while (++index < childCount)
{
var nextState = ChildStates[index];
var nextThreshold = GetThreshold(index);
if (parameter > previousThreshold && parameter <= nextThreshold)
{
var t = (parameter - previousThreshold) / (nextThreshold - previousThreshold);
Playable.SetChildWeight(previousState, 1 - t);
Playable.SetChildWeight(nextState, t);
DisableRemainingStates(index);
goto ResetExtrapolatedSpeed;
}
else
{
Playable.SetChildWeight(previousState, 0);
}
previousState = nextState;
previousThreshold = nextThreshold;
}
}
Playable.SetChildWeight(previousState, 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 LinearMixerState AssignLinearThresholds(float min = 0, float max = 1)
{
#if UNITY_ASSERTIONS
if (min >= max)
{
MarkAsUsed(this);
throw new ArgumentException($"{nameof(min)} must be less than {nameof(max)}");
}
#endif
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);
return this;
}
/************************************************************************************************************************/
///
protected override void AppendDetails(StringBuilder text, string separator)
{
text.AppendField(separator, nameof(ExtrapolateSpeed), ExtrapolateSpeed);
base.AppendDetails(text, separator);
}
/************************************************************************************************************************/
#region Inspector
/************************************************************************************************************************/
///
public override void GetParameters(List parameters)
{
parameters.Add(new(
"Parameter",
ParameterName,
AnimatorControllerParameterType.Float,
Parameter));
}
///
public override void SetParameters(List parameters)
{
var parameter = parameters[0];
ParameterName = parameter.name;
Parameter = (float)parameter.value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}