chore: initial commit
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// An <see cref="AnimancerState"/> which blends an array of other states together based on a two dimensional
|
||||
/// parameter and thresholds using Gradient Band Interpolation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This mixer type is similar to the 2D Freeform Cartesian 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/CartesianMixerState
|
||||
///
|
||||
public class CartesianMixerState : Vector2MixerState,
|
||||
ICopyable<CartesianMixerState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Precalculated values to speed up the recalculation of weights.</summary>
|
||||
private Vector2[][] _BlendFactors;
|
||||
|
||||
/// <summary>Indicates whether the <see cref="_BlendFactors"/> need to be recalculated.</summary>
|
||||
private bool _BlendFactorsAreDirty = true;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever the thresholds are changed. Indicates that the internal blend factors need to be
|
||||
/// recalculated and triggers weight recalculation.
|
||||
/// </summary>
|
||||
public override void OnThresholdsChanged()
|
||||
{
|
||||
_BlendFactorsAreDirty = true;
|
||||
base.OnThresholdsChanged();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void ForceRecalculateWeights()
|
||||
{
|
||||
var childCount = ChildCount;
|
||||
if (childCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (childCount == 1)
|
||||
{
|
||||
Playable.SetChildWeight(ChildStates[0], 1);
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateBlendFactors(childCount);
|
||||
|
||||
float totalWeight = 0;
|
||||
|
||||
var weights = GetTemporaryFloatArray(childCount);
|
||||
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var blendFactors = _BlendFactors[i];
|
||||
|
||||
var threshold = GetThreshold(i);
|
||||
var thresholdToParameter = Parameter - threshold;
|
||||
|
||||
float weight = 1;
|
||||
|
||||
for (int j = 0; j < childCount; j++)
|
||||
{
|
||||
if (j == i)
|
||||
continue;
|
||||
|
||||
var newWeight = 1 - Vector2.Dot(thresholdToParameter, blendFactors[j]);
|
||||
|
||||
if (weight > newWeight)
|
||||
weight = newWeight;
|
||||
}
|
||||
|
||||
if (weight < 0.01f)
|
||||
weight = 0;
|
||||
|
||||
weights[i] = weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
NormalizeAndApplyWeights(totalWeight, weights);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void CalculateBlendFactors(int childCount)
|
||||
{
|
||||
if (!_BlendFactorsAreDirty)
|
||||
return;
|
||||
|
||||
_BlendFactorsAreDirty = false;
|
||||
|
||||
// Resize the precalculated values.
|
||||
if (AnimancerUtilities.SetLength(ref _BlendFactors, childCount))
|
||||
{
|
||||
for (int i = 0; i < childCount; i++)
|
||||
_BlendFactors[i] = new Vector2[childCount];
|
||||
}
|
||||
|
||||
// Calculate the blend factors between each combination of thresholds.
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var blendFactors = _BlendFactors[i];
|
||||
|
||||
var thresholdI = GetThreshold(i);
|
||||
|
||||
var j = i + 1;
|
||||
for (; j < childCount; j++)
|
||||
{
|
||||
var thresholdIToJ = GetThreshold(j) - thresholdI;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (thresholdIToJ == default)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new ArgumentException(
|
||||
$"Mixer has multiple identical thresholds.\n{this.GetDescription()}");
|
||||
}
|
||||
#endif
|
||||
|
||||
thresholdIToJ /= thresholdIToJ.sqrMagnitude;
|
||||
|
||||
// Each factor is used in [i][j] with it's opposite in [j][i].
|
||||
blendFactors[j] = thresholdIToJ;
|
||||
_BlendFactors[j][i] = -thresholdIToJ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Clone(CloneContext context)
|
||||
{
|
||||
var clone = new CartesianMixerState();
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void CopyFrom(Vector2MixerState copyFrom, CloneContext context)
|
||||
=> this.CopyFromBase(copyFrom, context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(CartesianMixerState copyFrom, CloneContext context)
|
||||
{
|
||||
_BlendFactorsAreDirty = copyFrom._BlendFactorsAreDirty;
|
||||
if (!_BlendFactorsAreDirty)
|
||||
_BlendFactors = copyFrom._BlendFactors;
|
||||
|
||||
base.CopyFrom(copyFrom, context);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0aebebda5ee98d45930f31efb79a081
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,236 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// An <see cref="AnimancerState"/> which blends an array of other states together based on a two dimensional
|
||||
/// parameter and thresholds using Polar Gradient Band Interpolation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This mixer type is similar to the 2D Freeform Directional 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/DirectionalMixerState
|
||||
///
|
||||
public class DirectionalMixerState : Vector2MixerState,
|
||||
ICopyable<DirectionalMixerState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Precalculated magnitudes of all thresholds to speed up the recalculation of weights.</summary>
|
||||
private float[] _ThresholdMagnitudes;
|
||||
|
||||
/// <summary>Precalculated values to speed up the recalculation of weights.</summary>
|
||||
private Vector2[][] _BlendFactors;
|
||||
|
||||
/// <summary>Indicates whether the <see cref="_BlendFactors"/> need to be recalculated.</summary>
|
||||
private bool _BlendFactorsAreDirty = true;
|
||||
|
||||
/// <summary>The multiplier that controls how much an angle (in radians) is worth compared to normalized distance.</summary>
|
||||
private const float AngleFactor = 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever the thresholds are changed. Indicates that the internal blend factors need to be
|
||||
/// recalculated and triggers weight recalculation.
|
||||
/// </summary>
|
||||
public override void OnThresholdsChanged()
|
||||
{
|
||||
_BlendFactorsAreDirty = true;
|
||||
base.OnThresholdsChanged();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void ForceRecalculateWeights()
|
||||
{
|
||||
var childCount = ChildCount;
|
||||
if (childCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (childCount == 1)
|
||||
{
|
||||
var state = ChildStates[0];
|
||||
Playable.SetChildWeight(state, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateBlendFactors(childCount);
|
||||
|
||||
var parameterMagnitude = Parameter.magnitude;
|
||||
float totalWeight = 0;
|
||||
|
||||
var weights = GetTemporaryFloatArray(childCount);
|
||||
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var state = ChildStates[i];
|
||||
var blendFactors = _BlendFactors[i];
|
||||
|
||||
var thresholdI = GetThreshold(i);
|
||||
var magnitudeI = _ThresholdMagnitudes[i];
|
||||
|
||||
// Convert the threshold to polar coordinates (distance, angle)
|
||||
// and interpolate the weight based on those.
|
||||
var differenceIToParameter = parameterMagnitude - magnitudeI;
|
||||
var angleIToParameter = SignedAngle(thresholdI, Parameter) * AngleFactor;
|
||||
|
||||
float weight = 1;
|
||||
|
||||
for (int j = 0; j < childCount; j++)
|
||||
{
|
||||
if (j == i)
|
||||
continue;
|
||||
|
||||
var magnitudeJ = _ThresholdMagnitudes[j];
|
||||
var averageMagnitude = (magnitudeJ + magnitudeI) * 0.5f;
|
||||
|
||||
var polarIToParameter = new Vector2(
|
||||
differenceIToParameter / averageMagnitude,
|
||||
angleIToParameter);
|
||||
|
||||
var newWeight = 1 - Vector2.Dot(polarIToParameter, blendFactors[j]);
|
||||
|
||||
if (weight > newWeight)
|
||||
weight = newWeight;
|
||||
}
|
||||
|
||||
if (weight < 0.01f)
|
||||
weight = 0;
|
||||
|
||||
weights[i] = weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
NormalizeAndApplyWeights(totalWeight, weights);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void CalculateBlendFactors(int childCount)
|
||||
{
|
||||
if (!_BlendFactorsAreDirty)
|
||||
return;
|
||||
|
||||
_BlendFactorsAreDirty = false;
|
||||
|
||||
// Resize the precalculated values.
|
||||
if (_BlendFactors == null || _BlendFactors.Length != childCount)
|
||||
{
|
||||
_ThresholdMagnitudes = new float[childCount];
|
||||
|
||||
_BlendFactors = new Vector2[childCount][];
|
||||
for (int i = 0; i < childCount; i++)
|
||||
_BlendFactors[i] = new Vector2[childCount];
|
||||
}
|
||||
|
||||
// Calculate the magnitude of each threshold.
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
_ThresholdMagnitudes[i] = GetThreshold(i).magnitude;
|
||||
}
|
||||
|
||||
// Calculate the blend factors between each combination of thresholds.
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var blendFactors = _BlendFactors[i];
|
||||
|
||||
var thresholdI = GetThreshold(i);
|
||||
var magnitudeI = _ThresholdMagnitudes[i];
|
||||
|
||||
var j = 0;// i + 1;
|
||||
for (; j < childCount; j++)
|
||||
{
|
||||
if (i == j)
|
||||
continue;
|
||||
|
||||
var thresholdJ = GetThreshold(j);
|
||||
var magnitudeJ = _ThresholdMagnitudes[j];
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (thresholdI == thresholdJ)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new ArgumentException(
|
||||
$"Mixer has multiple identical thresholds.\n{this.GetDescription()}");
|
||||
}
|
||||
#endif
|
||||
|
||||
var averageMagnitude = (magnitudeI + magnitudeJ) * 0.5f;
|
||||
|
||||
// Convert the thresholds to polar coordinates (distance, angle) and interpolate the weight based on those.
|
||||
|
||||
var differenceIToJ = magnitudeJ - magnitudeI;
|
||||
|
||||
var angleIToJ = SignedAngle(thresholdI, thresholdJ);
|
||||
|
||||
var polarIToJ = new Vector2(
|
||||
differenceIToJ / averageMagnitude,
|
||||
angleIToJ * AngleFactor);
|
||||
|
||||
polarIToJ /= polarIToJ.sqrMagnitude;
|
||||
|
||||
// Each factor is used in [i][j] with it's opposite in [j][i].
|
||||
blendFactors[j] = polarIToJ;
|
||||
_BlendFactors[j][i] = -polarIToJ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static float SignedAngle(Vector2 a, Vector2 b)
|
||||
{
|
||||
// If either vector is exactly at the origin, the angle is 0.
|
||||
if ((a.x == 0 && a.y == 0) || (b.x == 0 && b.y == 0))
|
||||
{
|
||||
// Due to floating point error the formula below usually gives 0 but sometimes Pi,
|
||||
// which screws up our other calculations so we need it to always be 0 properly.
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Mathf.Atan2(
|
||||
a.x * b.y - a.y * b.x,
|
||||
a.x * b.x + a.y * b.y);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Clone(CloneContext context)
|
||||
{
|
||||
var clone = new DirectionalMixerState();
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void CopyFrom(Vector2MixerState copyFrom, CloneContext context)
|
||||
=> this.CopyFromBase(copyFrom, context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(DirectionalMixerState copyFrom, CloneContext context)
|
||||
{
|
||||
_ThresholdMagnitudes = copyFrom._ThresholdMagnitudes;
|
||||
_BlendFactorsAreDirty = copyFrom._BlendFactorsAreDirty;
|
||||
if (!_BlendFactorsAreDirty)
|
||||
_BlendFactors = copyFrom._BlendFactors;
|
||||
|
||||
base.CopyFrom(copyFrom, context);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df018869cc71dbd4cba0b6dc641e9aed
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,385 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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 since they must be sorted).</summary>
|
||||
public float MinThreshold
|
||||
=> GetThreshold(0);
|
||||
|
||||
/// <summary>The highest threshold (which is for the last child since they must be sorted).</summary>
|
||||
public float MaxThreshold
|
||||
=> GetThreshold(ChildCount - 1);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float NormalizedParameter
|
||||
{
|
||||
get
|
||||
{
|
||||
var min = MinThreshold;
|
||||
var max = MaxThreshold;
|
||||
|
||||
if (min < 0 && max > 0)// Interpolate -1 to 1.
|
||||
{
|
||||
var value = Parameter;
|
||||
|
||||
if (value > 0)
|
||||
value = AnimancerUtilities.InverseLerpUnclamped(0, max, value);
|
||||
else if (value < 0)
|
||||
value = AnimancerUtilities.InverseLerpUnclamped(0, min, -value);
|
||||
|
||||
return value;
|
||||
}
|
||||
else// Interpolate 0 to 1.
|
||||
{
|
||||
return 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;
|
||||
|
||||
base.CopyFrom(copyFrom, context);
|
||||
|
||||
ParameterName = copyFrom.ParameterName;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 142bfebac2959e14db634de96af8d899
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb1249f13af5b1749ba88452d05c1ba2
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,439 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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="ParentState.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="ParentState.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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void OnAddChild(AnimancerState child)
|
||||
{
|
||||
base.OnAddChild(child);
|
||||
SetWeightsDirty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls `calculate` for each of the <see cref="ParentState.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/parameters/#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 across all thresholds.</summary>
|
||||
/// <remarks>
|
||||
/// If the threshold range includes both negative and positive values, the normalized range is -1 to 1.
|
||||
/// Otherwise, the normalized range is 0 to 1.
|
||||
/// </remarks>
|
||||
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(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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2868fbbf5c6b88c418b35b0ded8a94d4
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,118 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A wrapper for managing a <see cref="Parameter{T}"/> in an <see cref="AnimancerNode"/>.</summary>
|
||||
/// <remarks>This type is mostly intended for internal use within Mixers.</remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/NodeParameter_1
|
||||
public struct NodeParameter<T>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The node that owns this parameter.</summary>
|
||||
public AnimancerNode Node { get; private set; }
|
||||
|
||||
/// <summary>The callback to invoke when the parameter changes.</summary>
|
||||
public event Action<T> OnParameterChanged;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Has this <see cref="NodeParameter{T}"/> been constructed properly?</summary>
|
||||
public readonly bool IsInitialized
|
||||
=> Node != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private StringReference _Key;
|
||||
|
||||
/// <summary>
|
||||
/// This will be used as a key in the <see cref="ParameterDictionary"/>
|
||||
/// so any changes to that parameter will invoke <see cref="OnParameterChanged"/>.
|
||||
/// </summary>
|
||||
public StringReference Key
|
||||
{
|
||||
readonly get => _Key;
|
||||
set
|
||||
{
|
||||
if (_Key.EqualsWhereEmptyIsNull(value))
|
||||
return;
|
||||
|
||||
UnBind();
|
||||
|
||||
_Key = value;
|
||||
|
||||
Bind();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="Key"/> and returns <c>true</c> if <see cref="Initialize"/> needs to be called.</summary>
|
||||
public bool SetKeyCheckNeedsInitialize(StringReference key)
|
||||
{
|
||||
if (_Key.EqualsWhereEmptyIsNull(key))
|
||||
return false;
|
||||
|
||||
if (IsInitialized)
|
||||
{
|
||||
UnBind();
|
||||
|
||||
_Key = key;
|
||||
|
||||
Bind();
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_Key = key;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initializes and binds the parameter.</summary>
|
||||
public void Initialize(AnimancerNode node, Action<T> onParameterChanged)
|
||||
{
|
||||
Node = node;
|
||||
OnParameterChanged = onParameterChanged;
|
||||
Bind();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Registers to the <see cref="AnimancerGraph.Parameters"/>.</summary>
|
||||
public readonly void Bind()
|
||||
{
|
||||
if (Node.Graph != null && !_Key.IsNullOrEmpty())
|
||||
Node.Graph.Parameters.AddOnValueChanged(_Key, OnParameterChanged, true);
|
||||
}
|
||||
|
||||
/// <summary>Registers to the <see cref="AnimancerGraph.Parameters"/> if <see cref="IsInitialized"/>.</summary>
|
||||
public readonly void BindIfInitialized()
|
||||
{
|
||||
if (IsInitialized)
|
||||
Bind();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Unregisters from the <see cref="AnimancerGraph.Parameters"/>.</summary>
|
||||
public readonly void UnBind()
|
||||
{
|
||||
if (Node.Graph != null && !_Key.IsNullOrEmpty())
|
||||
Node.Graph.Parameters.RemoveOnValueChanged(_Key, OnParameterChanged);
|
||||
}
|
||||
|
||||
/// <summary>Unregisters from the <see cref="AnimancerGraph.Parameters"/> if <see cref="IsInitialized"/>.</summary>
|
||||
public readonly void UnBindIfInitialized()
|
||||
{
|
||||
if (IsInitialized)
|
||||
UnBind();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c027a7c9350d63148b468216003415c0
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,264 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// An <see cref="AnimancerState"/> which blends an array of other states together
|
||||
/// based on a two dimensional parameter and thresholds.
|
||||
/// </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/Vector2MixerState
|
||||
///
|
||||
public abstract class Vector2MixerState : MixerState<Vector2>,
|
||||
ICopyable<Vector2MixerState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary><see cref="MixerState{TParameter}.Parameter"/>.x.</summary>
|
||||
public float ParameterX
|
||||
{
|
||||
get => Parameter.x;
|
||||
set => Parameter = new(value, Parameter.y);
|
||||
}
|
||||
|
||||
/// <summary><see cref="MixerState{TParameter}.Parameter"/>.y.</summary>
|
||||
public float ParameterY
|
||||
{
|
||||
get => Parameter.y;
|
||||
set => Parameter = new(Parameter.x, value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Parameter Binding
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private NodeParameter<float> _ParameterBindingX;
|
||||
|
||||
/// <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="ParameterX"/>.
|
||||
/// </summary>
|
||||
public StringReference ParameterNameX
|
||||
{
|
||||
get => _ParameterBindingX.Key;
|
||||
set
|
||||
{
|
||||
if (_ParameterBindingX.SetKeyCheckNeedsInitialize(value))
|
||||
_ParameterBindingX.Initialize(this, parameter => ParameterX = parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private NodeParameter<float> _ParameterBindingY;
|
||||
|
||||
/// <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="ParameterY"/>.
|
||||
/// </summary>
|
||||
public StringReference ParameterNameY
|
||||
{
|
||||
get => _ParameterBindingY.Key;
|
||||
set
|
||||
{
|
||||
if (_ParameterBindingY.SetKeyCheckNeedsInitialize(value))
|
||||
_ParameterBindingY.Initialize(this, parameter => ParameterY = parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetGraph(AnimancerGraph graph)
|
||||
{
|
||||
if (Graph == graph)
|
||||
return;
|
||||
|
||||
_ParameterBindingX.UnBindIfInitialized();
|
||||
_ParameterBindingY.UnBindIfInitialized();
|
||||
|
||||
base.SetGraph(graph);
|
||||
|
||||
_ParameterBindingX.BindIfInitialized();
|
||||
_ParameterBindingY.BindIfInitialized();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Destroy()
|
||||
{
|
||||
base.Destroy();
|
||||
|
||||
_ParameterBindingX.UnBindIfInitialized();
|
||||
_ParameterBindingY.UnBindIfInitialized();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void CopyFrom(MixerState<Vector2> copyFrom, CloneContext context)
|
||||
=> this.CopyFromBase(copyFrom, context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(Vector2MixerState copyFrom, CloneContext context)
|
||||
{
|
||||
base.CopyFrom(copyFrom, context);
|
||||
|
||||
ParameterNameX = copyFrom.ParameterNameX;
|
||||
ParameterNameY = copyFrom.ParameterNameY;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the lowest and highest threshold values on each axis.</summary>
|
||||
public void GetThresholdBounds(out Vector2 min, out Vector2 max, out bool isAroundZero)
|
||||
{
|
||||
var i = ChildCount - 1;
|
||||
min = max = GetThreshold(i);
|
||||
|
||||
i--;
|
||||
for (; i >= 0; i--)
|
||||
{
|
||||
var threshold = GetThreshold(i);
|
||||
|
||||
if (min.x > threshold.x)
|
||||
min.x = threshold.x;
|
||||
else if (max.x < threshold.x)
|
||||
max.x = threshold.x;
|
||||
|
||||
if (min.y > threshold.y)
|
||||
min.y = threshold.y;
|
||||
else if (max.y < threshold.y)
|
||||
max.y = threshold.y;
|
||||
}
|
||||
|
||||
isAroundZero =
|
||||
min.x < 0 && max.x > 0 &&
|
||||
min.y < 0 && max.y > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Vector2 NormalizedParameter
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = Parameter;
|
||||
|
||||
GetThresholdBounds(out var min, out var max, out bool isAroundZero);
|
||||
|
||||
if (isAroundZero)// Interpolate -1 to 1.
|
||||
{
|
||||
if (value.x > 0)
|
||||
value.x = AnimancerUtilities.InverseLerpUnclamped(0, max.x, value.x);
|
||||
else if (value.x < 0)
|
||||
value.x = AnimancerUtilities.InverseLerpUnclamped(0, min.x, -value.x);
|
||||
|
||||
if (value.y > 0)
|
||||
value.y = AnimancerUtilities.InverseLerpUnclamped(0, max.y, value.y);
|
||||
else if (value.y < 0)
|
||||
value.y = AnimancerUtilities.InverseLerpUnclamped(0, min.y, -value.y);
|
||||
|
||||
return value;
|
||||
}
|
||||
else// Interpolate 0 to 1.
|
||||
{
|
||||
return new(
|
||||
AnimancerUtilities.InverseLerpUnclamped(min.x, max.x, value.x),
|
||||
AnimancerUtilities.InverseLerpUnclamped(min.y, max.y, value.y));
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
GetThresholdBounds(out var min, out var max, out bool isAroundZero);
|
||||
|
||||
if (isAroundZero)// Interpolate -1 to 1.
|
||||
{
|
||||
if (value.x > 0)
|
||||
value.x = Mathf.LerpUnclamped(0, max.x, value.x);
|
||||
else if (value.x < 0)
|
||||
value.x = Mathf.LerpUnclamped(0, min.x, -value.x);
|
||||
|
||||
if (value.y > 0)
|
||||
value.y = Mathf.LerpUnclamped(0, max.y, value.y);
|
||||
else if (value.y < 0)
|
||||
value.y = Mathf.LerpUnclamped(0, min.y, -value.y);
|
||||
|
||||
Parameter = value;
|
||||
}
|
||||
else// Interpolate 0 to 1.
|
||||
{
|
||||
Parameter = new(
|
||||
Mathf.LerpUnclamped(min.x, max.x, value.x),
|
||||
Mathf.LerpUnclamped(min.y, max.y, value.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string GetParameterError(Vector2 value)
|
||||
=> value.IsFinite()
|
||||
? null
|
||||
: $"value.x and value.y {Strings.MustBeFinite}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void AppendParameter(StringBuilder text, Vector2 parameter)
|
||||
{
|
||||
text.Append('(')
|
||||
.Append(parameter.x)
|
||||
.Append(", ")
|
||||
.Append(parameter.y)
|
||||
.Append(')');
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Inspector
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void GetParameters(List<StateParameterDetails> parameters)
|
||||
{
|
||||
parameters.Add(new(
|
||||
"Parameter X",
|
||||
ParameterNameX,
|
||||
AnimatorControllerParameterType.Float,
|
||||
ParameterX));
|
||||
parameters.Add(new(
|
||||
"Parameter Y",
|
||||
ParameterNameY,
|
||||
AnimatorControllerParameterType.Float,
|
||||
ParameterY));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetParameters(List<StateParameterDetails> parameters)
|
||||
{
|
||||
var parameter = parameters[0];
|
||||
ParameterNameX = parameter.name;
|
||||
ParameterX = (float)parameter.value;
|
||||
|
||||
parameter = parameters[1];
|
||||
ParameterNameY = parameter.name;
|
||||
ParameterY = (float)parameter.value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f4c2bdc3cf59af44b4f88a784c0cdd6
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user