// Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Animations;
namespace Animancer
{
/// [Pro-Only]
/// Base class for mixers which blend an array of child states together based on a .
///
///
/// Documentation: Mixers
///
/// https://kybernetik.com.au/animancer/api/Animancer/MixerState_1
///
public abstract class MixerState : ManualMixerState
{
/************************************************************************************************************************/
#region Properties
/************************************************************************************************************************/
/// The parameter values at which each of the child states are used and blended.
private TParameter[] _Thresholds = Array.Empty();
/************************************************************************************************************************/
private TParameter _Parameter;
/// The value used to calculate the weights of the child states.
///
/// Setting this value takes effect immediately (during the next animation update) without any
/// Smoothing.
///
/// The value is NaN or Infinity.
public TParameter Parameter
{
get => _Parameter;
set
{
#if UNITY_ASSERTIONS
var error = GetParameterError(value);
if (error != null)
throw new ArgumentOutOfRangeException(nameof(value), error);
#endif
_Parameter = value;
WeightsAreDirty = true;
RequireUpdate();
}
}
///
/// Returns an error message if the given `parameter` value can't be assigned to the .
/// Otherwise returns null.
///
public abstract string GetParameterError(TParameter parameter);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Thresholds
/************************************************************************************************************************/
///
/// Indicates whether the array of thresholds has been initialized with a size at least equal to the
/// .
///
public bool HasThresholds => _Thresholds.Length >= ChildCount;
/************************************************************************************************************************/
///
/// Returns the value of the threshold associated with the specified index.
///
public TParameter GetThreshold(int index) => _Thresholds[index];
/************************************************************************************************************************/
///
/// Sets the value of the threshold associated with the specified index.
///
public void SetThreshold(int index, TParameter threshold)
{
_Thresholds[index] = threshold;
OnThresholdsChanged();
}
/************************************************************************************************************************/
///
/// Assigns the specified array as the thresholds to use for blending.
///
/// WARNING: if you keep a reference to the `thresholds` array you must call
/// whenever any changes are made to it, otherwise this mixer may not blend correctly.
///
public void SetThresholds(params TParameter[] thresholds)
{
if (thresholds.Length != ChildCount)
throw new ArgumentOutOfRangeException(nameof(thresholds),
$"Threshold count ({thresholds.Length}) doesn't match child count ({ChildCount}).");
_Thresholds = thresholds;
OnThresholdsChanged();
}
/************************************************************************************************************************/
///
/// If the of the is not equal to the
/// , this method assigns a new array of that size and returns true.
///
public bool ValidateThresholdCount()
{
var count = ChildCount;
if (_Thresholds.Length != count)
{
_Thresholds = new TParameter[count];
return true;
}
else return false;
}
/************************************************************************************************************************/
///
/// Called whenever the thresholds are changed. By default this method simply indicates that the blend weights
/// need recalculating but it can be overridden by child classes to perform validation checks or optimisations.
///
public virtual void OnThresholdsChanged()
{
WeightsAreDirty = true;
RequireUpdate();
}
/************************************************************************************************************************/
///
/// Calls `calculate` for each of the and stores the returned value as
/// the threshold for that state.
///
public void CalculateThresholds(Func calculate)
{
ValidateThresholdCount();
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
if (state == null)
continue;
_Thresholds[i] = calculate(state);
}
OnThresholdsChanged();
}
/************************************************************************************************************************/
///
/// Stores the values of all parameters, calls , then restores the
/// parameter values.
///
public override void RecreatePlayable()
{
base.RecreatePlayable();
WeightsAreDirty = true;
RequireUpdate();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialisation
/************************************************************************************************************************/
///
public override void Initialize(int portCount)
{
base.Initialize(portCount);
_Thresholds = new TParameter[portCount];
OnThresholdsChanged();
}
/************************************************************************************************************************/
///
/// Initializes the and with one
/// state per clip and assigns the `thresholds`.
///
/// WARNING: if you keep a reference to the `thresholds` array, you must call
/// whenever any changes are made to it, otherwise this mixer may not blend
/// correctly.
///
public void Initialize(AnimationClip[] clips, TParameter[] thresholds)
{
Initialize(clips);
_Thresholds = thresholds;
OnThresholdsChanged();
}
///
/// Initializes the and with one
/// state per clip and assigns the thresholds by calling `calculateThreshold` for each state.
///
public void Initialize(AnimationClip[] clips, Func calculateThreshold)
{
Initialize(clips);
CalculateThresholds(calculateThreshold);
}
/************************************************************************************************************************/
///
/// Creates and returns a new to play the `clip` with this
/// as its parent, connects it to the specified `index`, and assigns the
/// `threshold` for it.
///
public ClipState CreateChild(int index, AnimationClip clip, TParameter threshold)
{
SetThreshold(index, threshold);
return CreateChild(index, clip);
}
///
/// Calls , sets this mixer as the state's parent, and
/// assigns the `threshold` for it.
///
public AnimancerState CreateChild(int index, Animancer.ITransition transition, TParameter threshold)
{
SetThreshold(index, threshold);
return CreateChild(index, transition);
}
/************************************************************************************************************************/
/// Assigns the `state` as a child of this mixer and assigns the `threshold` for it.
public void SetChild(int index, AnimancerState state, TParameter threshold)
{
SetChild(index, state);
SetThreshold(index, threshold);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Descriptions
/************************************************************************************************************************/
///
public override string GetDisplayKey(AnimancerState state) => $"[{state.Index}] {_Thresholds[state.Index]}";
/************************************************************************************************************************/
///
protected override void AppendDetails(StringBuilder text, string separator)
{
text.Append(separator);
text.Append($"{nameof(Parameter)}: ");
AppendParameter(text, Parameter);
text.Append(separator).Append("Thresholds: ");
for (int i = 0; i < _Thresholds.Length; i++)
{
if (i > 0)
text.Append(", ");
AppendParameter(text, _Thresholds[i]);
}
base.AppendDetails(text, separator);
}
/************************************************************************************************************************/
/// Appends the `parameter` in a viewer-friendly format.
public virtual void AppendParameter(StringBuilder description, TParameter parameter)
{
description.Append(parameter);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}