123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
- using System;
- using System.Collections.Generic;
- using System.Text;
- using UnityEngine;
- using UnityEngine.Playables;
- namespace Animancer
- {
- /// <summary>[Pro-Only]
- /// An <see cref="AnimancerState"/> which blends an array of other states together
- /// using linear interpolation between the specified thresholds.
- /// </summary>
- /// <remarks>
- /// This mixer type is similar to the 1D Blend Type in Mecanim Blend Trees.
- /// <para></para>
- /// <strong>Documentation:</strong>
- /// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/mixers">
- /// Mixers</see>
- /// </remarks>
- /// https://kybernetik.com.au/animancer/api/Animancer/LinearMixerState
- public class LinearMixerState : MixerState<float>,
- ICopyable<LinearMixerState>
- {
- /************************************************************************************************************************/
- private bool _ExtrapolateSpeed = true;
- /// <summary>
- /// Should setting the <see cref="MixerState{TParameter}.Parameter"/> above the highest threshold
- /// increase the <see cref="AnimancerNodeBase.Speed"/> of this mixer proportionally?
- /// </summary>
- public bool ExtrapolateSpeed
- {
- get => _ExtrapolateSpeed;
- set
- {
- if (_ExtrapolateSpeed == value)
- return;
- _ExtrapolateSpeed = value;
- if (!_Playable.IsValid())
- return;
- var speed = Speed;
- var childCount = ChildCount;
- if (value && childCount > 0)
- {
- var threshold = GetThreshold(childCount - 1);
- if (Parameter > threshold)
- speed *= Parameter / threshold;
- }
- _Playable.SetSpeed(speed);
- }
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override string GetParameterError(float value)
- => value.IsFinite() ? null : Strings.MustBeFinite;
- /************************************************************************************************************************/
- /// <summary>The lowest threshold (which is for the first child because they must be sorted).</summary>
- public float MinThreshold => GetThreshold(0);
- /// <summary>The highest threshold (which is for the last child because they must be sorted).</summary>
- public float MaxThreshold => GetThreshold(ChildCount - 1);
- /// <inheritdoc/>
- public override float NormalizedParameter
- {
- get => AnimancerUtilities.InverseLerpUnclamped(MinThreshold, MaxThreshold, Parameter);
- set => Parameter = Mathf.LerpUnclamped(MinThreshold, MaxThreshold, value);
- }
- /************************************************************************************************************************/
- #region Parameter Binding
- /************************************************************************************************************************/
- private NodeParameter<float> _ParameterBinding;
- /// <summary>
- /// If set, this will be used as a key in the <see cref="ParameterDictionary"/> so any
- /// changes to that parameter will automatically set the <see cref="MixerState{TParameter}.Parameter"/>.
- /// </summary>
- public StringReference ParameterName
- {
- get => _ParameterBinding.Key;
- set
- {
- if (_ParameterBinding.SetKeyCheckNeedsInitialize(value))
- _ParameterBinding.Initialize(this, parameter => Parameter = parameter);
- }
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void SetGraph(AnimancerGraph graph)
- {
- if (Graph == graph)
- return;
- _ParameterBinding.UnBindIfInitialized();
- base.SetGraph(graph);
- _ParameterBinding.BindIfInitialized();
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void Destroy()
- {
- base.Destroy();
- _ParameterBinding.UnBindIfInitialized();
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override AnimancerState Clone(CloneContext context)
- {
- var clone = new LinearMixerState();
- clone.CopyFrom(this, context);
- return clone;
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public sealed override void CopyFrom(MixerState<float> copyFrom, CloneContext context)
- => this.CopyFromBase(copyFrom, context);
- /// <inheritdoc/>
- public virtual void CopyFrom(LinearMixerState copyFrom, CloneContext context)
- {
- _ExtrapolateSpeed = copyFrom._ExtrapolateSpeed;
- ParameterName = copyFrom.ParameterName;
- base.CopyFrom(copyFrom, context);
- }
- /************************************************************************************************************************/
- #if UNITY_ASSERTIONS
- /************************************************************************************************************************/
- private bool _ShouldCheckThresholdSorting;
- /// <summary>
- /// Called whenever the thresholds are changed. Indicates that <see cref="AssertThresholdsSorted"/> needs to
- /// be called by the next <see cref="ForceRecalculateWeights"/> if <c>UNITY_ASSERTIONS</c> is defined, then
- /// calls <see cref="MixerState{TParameter}.OnThresholdsChanged"/>.
- /// </summary>
- public override void OnThresholdsChanged()
- {
- _ShouldCheckThresholdSorting = true;
- base.OnThresholdsChanged();
- }
- /************************************************************************************************************************/
- #endif
- /************************************************************************************************************************/
- /// <summary>
- /// Throws an <see cref="ArgumentException"/> if the thresholds are not sorted from lowest to highest without
- /// any duplicates.
- /// </summary>
- /// <exception cref="ArgumentException"/>
- /// <exception cref="InvalidOperationException">The thresholds have not been initialized.</exception>
- public void AssertThresholdsSorted()
- {
- #if UNITY_ASSERTIONS
- _ShouldCheckThresholdSorting = false;
- #endif
- if (!HasThresholds)
- {
- MarkAsUsed(this);
- throw new InvalidOperationException("Thresholds have not been initialized");
- }
- var previous = float.NegativeInfinity;
- var childCount = ChildCount;
- for (int i = 0; i < childCount; i++)
- {
- var state = ChildStates[i];
- if (state == null)
- continue;
- var next = GetThreshold(i);
- if (next > previous)
- {
- previous = next;
- }
- else
- {
- MarkAsUsed(this);
- var reason = next == previous
- ? "Mixer has multiple identical thresholds."
- : "Mixer has thresholds out of order.";
- throw new ArgumentException(
- $"{reason} They must be sorted from lowest to highest with no equal values." +
- $"\n{this.GetDescription()}");
- }
- }
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void ForceRecalculateWeights()
- {
- #if UNITY_ASSERTIONS
- if (_ShouldCheckThresholdSorting)
- AssertThresholdsSorted();
- #endif
- // Go through all states, figure out how much weight to give those with thresholds adjacent to the
- // current parameter value using linear interpolation, and set all others to 0 weight.
- var childCount = ChildCount;
- if (childCount == 0)
- goto ResetExtrapolatedSpeed;
- var index = 0;
- var previousState = ChildStates[index];
- var parameter = Parameter;
- var previousThreshold = GetThreshold(index);
- if (parameter <= previousThreshold)
- {
- DisableRemainingStates(index);
- if (previousThreshold >= 0)
- {
- Playable.SetChildWeight(previousState, 1);
- goto ResetExtrapolatedSpeed;
- }
- }
- else
- {
- while (++index < childCount)
- {
- var nextState = ChildStates[index];
- var nextThreshold = GetThreshold(index);
- if (parameter > previousThreshold && parameter <= nextThreshold)
- {
- var t = (parameter - previousThreshold) / (nextThreshold - previousThreshold);
- Playable.SetChildWeight(previousState, 1 - t);
- Playable.SetChildWeight(nextState, t);
- DisableRemainingStates(index);
- goto ResetExtrapolatedSpeed;
- }
- else
- {
- Playable.SetChildWeight(previousState, 0);
- }
- previousState = nextState;
- previousThreshold = nextThreshold;
- }
- }
- Playable.SetChildWeight(previousState, 1);
- if (ExtrapolateSpeed)
- _Playable.SetSpeed(Speed * (parameter / previousThreshold));
- return;
- ResetExtrapolatedSpeed:
- if (ExtrapolateSpeed && _Playable.IsValid())
- _Playable.SetSpeed(Speed);
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Assigns the thresholds to be evenly spaced between the specified min and max (inclusive).
- /// </summary>
- public LinearMixerState AssignLinearThresholds(float min = 0, float max = 1)
- {
- #if UNITY_ASSERTIONS
- if (min >= max)
- {
- MarkAsUsed(this);
- throw new ArgumentException($"{nameof(min)} must be less than {nameof(max)}");
- }
- #endif
- var childCount = ChildCount;
- var thresholds = new float[childCount];
- var increment = (max - min) / (childCount - 1);
- for (int i = 0; i < childCount; i++)
- {
- thresholds[i] =
- i < childCount - 1 ?
- min + i * increment :// Assign each threshold linearly spaced between the min and max.
- max;// and ensure that the last one is exactly at the max (to avoid floating-point error).
- }
- SetThresholds(thresholds);
- return this;
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- protected override void AppendDetails(StringBuilder text, string separator)
- {
- text.AppendField(separator, nameof(ExtrapolateSpeed), ExtrapolateSpeed);
- base.AppendDetails(text, separator);
- }
- /************************************************************************************************************************/
- #region Inspector
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void GetParameters(List<StateParameterDetails> parameters)
- {
- parameters.Add(new(
- "Parameter",
- ParameterName,
- AnimatorControllerParameterType.Float,
- Parameter));
- }
- /// <inheritdoc/>
- public override void SetParameters(List<StateParameterDetails> parameters)
- {
- var parameter = parameters[0];
- ParameterName = parameter.name;
- Parameter = (float)parameter.value;
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- }
- }
|