123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
- using System;
- using System.Text;
- using UnityEngine;
- using UnityEngine.Playables;
- namespace Animancer
- {
- /// <summary>[Pro-Only]
- /// Base class for mixers which blend an array of child states together based on a <see cref="Parameter"/>.
- /// </summary>
- /// <remarks>
- /// <strong>Documentation:</strong>
- /// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/mixers">
- /// Mixers</see>
- /// </remarks>
- /// https://kybernetik.com.au/animancer/api/Animancer/MixerState_1
- ///
- public abstract class MixerState<TParameter> : ManualMixerState,
- ICopyable<MixerState<TParameter>>
- {
- /************************************************************************************************************************/
- #region Thresholds
- /************************************************************************************************************************/
- /// <summary>The parameter values at which each of the child states are used and blended.</summary>
- private TParameter[] _Thresholds = Array.Empty<TParameter>();
- /************************************************************************************************************************/
- /// <summary>
- /// Has the array of thresholds been initialized with a size at least equal to the
- /// <see cref="ManualMixerState.ChildCount"/>.
- /// </summary>
- public bool HasThresholds
- => _Thresholds.Length >= ChildCount;
- /************************************************************************************************************************/
- /// <summary>Returns the value of the threshold associated with the specified `index`.</summary>
- public TParameter GetThreshold(int index)
- => _Thresholds[index];
- /************************************************************************************************************************/
- /// <summary>Sets the value of the threshold associated with the specified `index`.</summary>
- public void SetThreshold(int index, TParameter threshold)
- {
- _Thresholds[index] = threshold;
- OnThresholdsChanged();
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Assigns the specified array as the thresholds to use for blending.
- /// <para></para>
- /// WARNING: if you keep a reference to the `thresholds` array you must call <see cref="OnThresholdsChanged"/>
- /// whenever any changes are made to it, otherwise this mixer may not blend correctly.
- /// </summary>
- 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();
- }
- /************************************************************************************************************************/
- /// <summary>
- /// If the <see cref="Array.Length"/> of the <see cref="_Thresholds"/> is below the
- /// <see cref="AnimancerNodeBase.ChildCount"/>, this method assigns a new array with size equal to the
- /// <see cref="ManualMixerState.ChildCapacity"/> and returns true.
- /// </summary>
- public bool ValidateThresholdCount()
- {
- if (_Thresholds.Length >= ChildCount)
- return false;
- _Thresholds = new TParameter[ChildCapacity];
- return true;
- }
- /************************************************************************************************************************/
- /// <summary>
- /// 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.
- /// </summary>
- public virtual void OnThresholdsChanged()
- {
- SetWeightsDirty();
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Calls `calculate` for each of the <see cref="ManualMixerState.ChildStates"/> and stores the returned value
- /// as the threshold for that state.
- /// </summary>
- public void CalculateThresholds(Func<AnimancerState, TParameter> calculate)
- {
- ValidateThresholdCount();
- for (int i = ChildCount - 1; i >= 0; i--)
- _Thresholds[i] = calculate(GetChild(i));
- OnThresholdsChanged();
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Stores the values of all parameters, calls <see cref="AnimancerNode.DestroyPlayable"/>, then restores the
- /// parameter values.
- /// </summary>
- public override void RecreatePlayable()
- {
- base.RecreatePlayable();
- SetWeightsDirty();
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Parameter
- /************************************************************************************************************************/
- private TParameter _Parameter;
- /// <summary>The value used to calculate the weights of the child states.</summary>
- /// <remarks>
- /// Setting this value takes effect immediately (during the next animation update) without any
- /// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/mixers#smoothing">Smoothing</see>.
- /// </remarks>
- /// <exception cref="ArgumentOutOfRangeException">The value is NaN or Infinity.</exception>
- 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();
- }
- }
- /// <summary>
- /// Returns an error message if the given `parameter` value can't be assigned to the <see cref="Parameter"/>.
- /// Otherwise returns null.
- /// </summary>
- public abstract string GetParameterError(TParameter parameter);
- /************************************************************************************************************************/
- /// <summary>The <see cref="Parameter"/> normalized into the range of 0 to 1 across all thresholds.</summary>
- public abstract TParameter NormalizedParameter { get; set; }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Weight Calculation
- /************************************************************************************************************************/
- /// <summary>Should the weights of all child states be recalculated?</summary>
- public bool WeightsAreDirty { get; private set; }
- /// <summary>Registers this mixer to recalculate its weights during the next animation update.</summary>
- public void SetWeightsDirty()
- {
- if (!WeightsAreDirty)
- {
- WeightsAreDirty = true;
- Graph?.RequirePreUpdate(this);
- }
- }
- /************************************************************************************************************************/
- /// <summary>
- /// If <see cref="WeightsAreDirty"/> this method recalculates the weights of all child states and returns true.
- /// </summary>
- public bool RecalculateWeights()
- {
- if (!WeightsAreDirty)
- return false;
- ForceRecalculateWeights();
- WeightsAreDirty = false;
- return true;
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Recalculates the weights of all child states based on the current value of the
- /// <see cref="MixerState{TParameter}.Parameter"/> and the thresholds.
- /// </summary>
- protected virtual void ForceRecalculateWeights() { }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void OnSetIsPlaying()
- {
- base.OnSetIsPlaying();
- if (WeightsAreDirty || SynchronizedChildCount > 0)
- Graph?.RequirePreUpdate(this);
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override double RawTime
- {
- get
- {
- if (_Playable.IsValid())
- RecalculateWeights();
- return base.RawTime;
- }
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override float Length
- {
- get
- {
- if (_Playable.IsValid())
- RecalculateWeights();
- return base.Length;
- }
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override Vector3 AverageVelocity
- {
- get
- {
- if (_Playable.IsValid())
- RecalculateWeights();
- return base.AverageVelocity;
- }
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void CreatePlayable(out Playable playable)
- {
- base.CreatePlayable(out playable);
- RecalculateWeights();
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void Update()
- {
- RecalculateWeights();
- base.Update();
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Initialization
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void OnChildCapacityChanged()
- {
- Array.Resize(ref _Thresholds, ChildCapacity);
- OnThresholdsChanged();
- }
- /************************************************************************************************************************/
- /// <summary>Assigns the `state` as a child of this mixer and assigns the `threshold` for it.</summary>
- public void Add(AnimancerState state, TParameter threshold)
- {
- Add(state);
- SetThreshold(state.Index, threshold);
- }
- /// <summary>
- /// Creates and returns a new <see cref="ClipState"/> to play the `clip` as a child of this mixer, and assigns
- /// the `threshold` for it.
- /// </summary>
- public ClipState Add(AnimationClip clip, TParameter threshold)
- {
- var state = Add(clip);
- SetThreshold(state.Index, threshold);
- return state;
- }
- /// <summary>
- /// Calls <see cref="AnimancerUtilities.CreateStateAndApply"/> then
- /// <see cref="Add(AnimancerState, TParameter)"/>.
- /// </summary>
- public AnimancerState Add(Animancer.ITransition transition, TParameter threshold)
- {
- var state = Add(transition);
- SetThreshold(state.Index, threshold);
- return state;
- }
- /// <summary>Calls one of the other <see cref="Add(object, TParameter)"/> overloads as appropriate.</summary>
- 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}'.");
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public sealed override void CopyFrom(ManualMixerState copyFrom, CloneContext context)
- => this.CopyFromBase(copyFrom, context);
- /// <inheritdoc/>
- public virtual void CopyFrom(MixerState<TParameter> 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
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override string GetDisplayKey(AnimancerState state)
- => $"[{state.Index}] {_Thresholds[state.Index]}";
- /************************************************************************************************************************/
- /// <inheritdoc/>
- 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);
- }
- /************************************************************************************************************************/
- /// <summary>Appends the `parameter` in a viewer-friendly format.</summary>
- public virtual void AppendParameter(StringBuilder description, TParameter parameter)
- {
- description.Append(parameter);
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- }
- }
|