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