// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 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, ICopyable> { /************************************************************************************************************************/ #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 /************************************************************************************************************************/ /// /// 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) 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() { 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--) _Thresholds[i] = calculate(GetChild(i)); OnThresholdsChanged(); } /************************************************************************************************************************/ /// /// Stores the values of all parameters, calls , then restores the /// parameter values. /// public override void RecreatePlayable() { base.RecreatePlayable(); WeightsAreDirty = true; RequireUpdate(); } /************************************************************************************************************************/ #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; } throw new ArgumentException($"Unable to add '{AnimancerUtilities.ToStringOrNull(child)}' as child of '{this}'."); } /************************************************************************************************************************/ /// void ICopyable>.CopyFrom(MixerState copyFrom) { ((ICopyable)this).CopyFrom(copyFrom); var childCount = copyFrom.ChildCount; if (copyFrom._Thresholds != null) { _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); text.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 /************************************************************************************************************************/ } }