// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
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,
ICopyable>
{
/************************************************************************************************************************/
#region Thresholds
/************************************************************************************************************************/
/// The parameter values at which each of the child states are used and blended.
private TParameter[] _Thresholds = Array.Empty();
/************************************************************************************************************************/
///
/// Has the array of thresholds 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)
{
MarkAsUsed(this);
throw new ArgumentOutOfRangeException(nameof(thresholds),
$"Threshold count ({thresholds.Length}) must not be less than child count ({ChildCount}).");
}
_Thresholds = thresholds;
OnThresholdsChanged();
}
/************************************************************************************************************************/
///
/// If the of the is below the
/// , this method assigns a new array with size equal to the
/// and returns true.
///
public bool ValidateThresholdCount()
{
if (_Thresholds.Length >= ChildCount)
return false;
_Thresholds = new TParameter[ChildCapacity];
return true;
}
/************************************************************************************************************************/
///
/// 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()
{
SetWeightsDirty();
}
/************************************************************************************************************************/
///
/// 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--)
_Thresholds[i] = calculate(GetChild(i));
OnThresholdsChanged();
}
/************************************************************************************************************************/
///
/// Stores the values of all parameters, calls , then restores the
/// parameter values.
///
public override void RecreatePlayable()
{
base.RecreatePlayable();
SetWeightsDirty();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Parameter
/************************************************************************************************************************/
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
if (Graph != null)
Validate.AssertPlayable(this);
var error = GetParameterError(value);
if (error != null)
{
MarkAsUsed(this);
throw new ArgumentOutOfRangeException(nameof(value), error);
}
#endif
_Parameter = value;
SetWeightsDirty();
}
}
///
/// Returns an error message if the given `parameter` value can't be assigned to the .
/// Otherwise returns null.
///
public abstract string GetParameterError(TParameter parameter);
/************************************************************************************************************************/
/// The normalized into the range of 0 to 1 across all thresholds.
public abstract TParameter NormalizedParameter { get; set; }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Weight Calculation
/************************************************************************************************************************/
/// Should the weights of all child states be recalculated?
public bool WeightsAreDirty { get; private set; }
/// Registers this mixer to recalculate its weights during the next animation update.
public void SetWeightsDirty()
{
if (!WeightsAreDirty)
{
WeightsAreDirty = true;
Graph?.RequirePreUpdate(this);
}
}
/************************************************************************************************************************/
///
/// If this method recalculates the weights of all child states and returns true.
///
public bool RecalculateWeights()
{
if (!WeightsAreDirty)
return false;
ForceRecalculateWeights();
WeightsAreDirty = false;
return true;
}
/************************************************************************************************************************/
///
/// Recalculates the weights of all child states based on the current value of the
/// and the thresholds.
///
protected virtual void ForceRecalculateWeights() { }
/************************************************************************************************************************/
///
protected override void OnSetIsPlaying()
{
base.OnSetIsPlaying();
if (WeightsAreDirty || SynchronizedChildCount > 0)
Graph?.RequirePreUpdate(this);
}
/************************************************************************************************************************/
///
public override double RawTime
{
get
{
if (_Playable.IsValid())
RecalculateWeights();
return base.RawTime;
}
}
/************************************************************************************************************************/
///
public override float Length
{
get
{
if (_Playable.IsValid())
RecalculateWeights();
return base.Length;
}
}
/************************************************************************************************************************/
///
public override Vector3 AverageVelocity
{
get
{
if (_Playable.IsValid())
RecalculateWeights();
return base.AverageVelocity;
}
}
/************************************************************************************************************************/
///
protected override void CreatePlayable(out Playable playable)
{
base.CreatePlayable(out playable);
RecalculateWeights();
}
/************************************************************************************************************************/
///
public override void Update()
{
RecalculateWeights();
base.Update();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
///
protected override void OnChildCapacityChanged()
{
Array.Resize(ref _Thresholds, ChildCapacity);
OnThresholdsChanged();
}
/************************************************************************************************************************/
/// Assigns the `state` as a child of this mixer and assigns the `threshold` for it.
public void Add(AnimancerState state, TParameter threshold)
{
Add(state);
SetThreshold(state.Index, threshold);
}
///
/// Creates and returns a new to play the `clip` as a child of this mixer, and assigns
/// the `threshold` for it.
///
public ClipState Add(AnimationClip clip, TParameter threshold)
{
var state = Add(clip);
SetThreshold(state.Index, threshold);
return state;
}
///
/// Calls then
/// .
///
public AnimancerState Add(Animancer.ITransition transition, TParameter threshold)
{
var state = Add(transition);
SetThreshold(state.Index, threshold);
return state;
}
/// Calls one of the other overloads as appropriate.
public AnimancerState Add(object child, TParameter threshold)
{
if (child is AnimationClip clip)
return Add(clip, threshold);
if (child is ITransition transition)
return Add(transition, threshold);
if (child is AnimancerState state)
{
Add(state, threshold);
return state;
}
MarkAsUsed(this);
throw new ArgumentException(
$"Unable to add '{AnimancerUtilities.ToStringOrNull(child)}' as child of '{this}'.");
}
/************************************************************************************************************************/
///
public sealed override void CopyFrom(ManualMixerState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
///
public virtual void CopyFrom(MixerState copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
var childCount = copyFrom.ChildCount;
if (copyFrom._Thresholds != null)
{
if (_Thresholds == null || _Thresholds.Length != childCount)
_Thresholds = new TParameter[childCount];
var count = Math.Min(childCount, copyFrom._Thresholds.Length);
Array.Copy(copyFrom._Thresholds, _Thresholds, count);
}
Parameter = copyFrom.Parameter;
}
/************************************************************************************************************************/
#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)
.Append($"{nameof(Parameter)}: ");
AppendParameter(text, Parameter);
text.Append(separator)
.Append("Thresholds: ");
var thresholdCount = Math.Min(ChildCapacity, _Thresholds.Length);
for (int i = 0; i < thresholdCount; 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
/************************************************************************************************************************/
}
}