// 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 /************************************************************************************************************************/ } }