You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
371 lines
17 KiB
C#
371 lines
17 KiB
C#
// Animancer // Copyright 2020 Kybernetik //
|
|
|
|
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.Animations;
|
|
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.
|
|
/// <para></para>
|
|
/// This mixer type is similar to the 1D Blend Type in Mecanim Blend Trees.
|
|
/// </summary>
|
|
public sealed class LinearMixerState : MixerState<float>
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Constructs a new <see cref="LinearMixerState"/> without connecting it to the <see cref="PlayableGraph"/>.
|
|
/// </summary>
|
|
private LinearMixerState(AnimancerPlayable root) : base(root) { }
|
|
|
|
/// <summary>
|
|
/// Constructs a new <see cref="LinearMixerState"/> and connects it to the `layer`.
|
|
/// </summary>
|
|
public LinearMixerState(AnimancerLayer layer) : base(layer) { }
|
|
|
|
/// <summary>
|
|
/// Constructs a new <see cref="LinearMixerState"/> and connects it to the `parent` at the specified
|
|
/// `index`.
|
|
/// </summary>
|
|
public LinearMixerState(AnimancerNode parent, int index) : base(parent, index) { }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Initialises the <see cref="AnimationMixerPlayable"/> and <see cref="ManualMixerState.States"/> with one
|
|
/// state per clip and assigns thresholds evenly spaced between the specified min and max (inclusive).
|
|
/// </summary>
|
|
public void Initialise(AnimationClip[] clips, float minThreshold = 0, float maxThreshold = 1)
|
|
{
|
|
Initialise(clips);
|
|
AssignLinearThresholds(minThreshold, maxThreshold);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Initialises the <see cref="AnimationMixerPlayable"/> with two ports and connects two states to them for
|
|
/// the specified clips at the specified thresholds (default 0 and 1).
|
|
/// </summary>
|
|
public void Initialise(AnimationClip clip0, AnimationClip clip1, float threshold0 = 0, float threshold1 = 1)
|
|
{
|
|
_Playable.SetInputCount(2);
|
|
|
|
States = new AnimancerState[2];
|
|
new ClipState(this, 0, clip0);
|
|
new ClipState(this, 1, clip1);
|
|
|
|
SetThresholds(new float[]
|
|
{
|
|
threshold0,
|
|
threshold1,
|
|
});
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Initialises the <see cref="AnimationMixerPlayable"/> with three ports and connects three states to them for
|
|
/// the specified clips at the specified thresholds (default -1, 0, and 1).
|
|
/// </summary>
|
|
public void Initialise(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, float threshold0 = -1, float threshold1 = 0, float threshold2 = 1)
|
|
{
|
|
_Playable.SetInputCount(3);
|
|
|
|
States = new AnimancerState[3];
|
|
new ClipState(this, 0, clip0);
|
|
new ClipState(this, 1, clip1);
|
|
new ClipState(this, 2, clip2);
|
|
|
|
SetThresholds(new float[]
|
|
{
|
|
threshold0,
|
|
threshold1,
|
|
threshold2,
|
|
});
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// Called whenever the thresholds are changed. In the Unity Editor this method calls
|
|
/// <see cref="AssertThresholdsSorted"/> then <see cref="RecalculateWeights"/> while at runtime it only calls
|
|
/// the latter.
|
|
/// </summary>
|
|
public override void OnThresholdsChanged()
|
|
{
|
|
AssertThresholdsSorted();
|
|
|
|
base.OnThresholdsChanged();
|
|
}
|
|
#endif
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Throws an <see cref="ArgumentException"/> unless the thresholds are sorted from lowest to highest.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"/>
|
|
public void AssertThresholdsSorted()
|
|
{
|
|
if (!HasThresholds())
|
|
throw new InvalidOperationException("LinearAnimationMixer: no Thresholds have been assigned");
|
|
|
|
var previous = float.NegativeInfinity;
|
|
|
|
int count = States.Length;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var state = States[i];
|
|
if (state == null)
|
|
continue;
|
|
|
|
var next = GetThreshold(i);
|
|
if (next > previous)
|
|
previous = next;
|
|
else
|
|
throw new ArgumentException("LinearAnimationMixer: Thresholds are out of order. They must be sorted from lowest to highest with no equal values.");
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Recalculates the weights of all <see cref="ManualMixerState.States"/> based on the current value of the
|
|
/// <see cref="MixerState{TParameter}.Parameter"/> and the thresholds.
|
|
/// </summary>
|
|
public override void RecalculateWeights()
|
|
{
|
|
WeightsAreDirty = false;
|
|
|
|
// 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 index = 0;
|
|
var previousState = GetNextState(ref index);
|
|
if (previousState == null)
|
|
return;
|
|
|
|
var previousThreshold = GetThreshold(index);
|
|
|
|
if (Parameter <= previousThreshold)
|
|
{
|
|
previousState.Weight = 1;
|
|
DisableRemainingStates(index);
|
|
return;
|
|
}
|
|
|
|
var count = States.Length;
|
|
while (++index < count)
|
|
{
|
|
var nextState = GetNextState(ref index);
|
|
if (nextState == null)
|
|
break;
|
|
|
|
var nextThreshold = GetThreshold(index);
|
|
|
|
if (Parameter > previousThreshold && Parameter <= nextThreshold)
|
|
{
|
|
var t = (Parameter - previousThreshold) / (nextThreshold - previousThreshold);
|
|
previousState.Weight = 1 - t;
|
|
nextState.Weight = t;
|
|
DisableRemainingStates(index);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
previousState.Weight = 0;
|
|
}
|
|
|
|
previousState = nextState;
|
|
previousThreshold = nextThreshold;
|
|
}
|
|
|
|
previousState.Weight = Parameter > previousThreshold ? 1 : 0;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Assigns the thresholds to be evenly spaced between the specified min and max (inclusive).
|
|
/// </summary>
|
|
public void AssignLinearThresholds(float min = 0, float max = 1)
|
|
{
|
|
var count = States.Length;
|
|
|
|
var thresholds = new float[count];
|
|
|
|
var increment = (max - min) / (count - 1);
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
thresholds[i] =
|
|
i < count - 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);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#region Inspector
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The number of parameters being managed by this state.</summary>
|
|
protected override int ParameterCount { get { return 1; } }
|
|
|
|
/// <summary>Returns the name of a parameter being managed by this state.</summary>
|
|
/// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
|
|
protected override string GetParameterName(int index) { return "Parameter"; }
|
|
|
|
/// <summary>Returns the type of a parameter being managed by this state.</summary>
|
|
/// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
|
|
protected override AnimatorControllerParameterType GetParameterType(int index) { return AnimatorControllerParameterType.Float; }
|
|
|
|
/// <summary>Returns the value of a parameter being managed by this state.</summary>
|
|
/// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
|
|
protected override object GetParameterValue(int index) { return Parameter; }
|
|
|
|
/// <summary>Sets the value of a parameter being managed by this state.</summary>
|
|
/// <exception cref="NotSupportedException">Thrown if this state doesn't manage any parameters.</exception>
|
|
protected override void SetParameterValue(int index, object value) { Parameter = (float)value; }
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Transition
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// A serializable <see cref="ITransition"/> which can create a <see cref="LinearMixerState"/> when
|
|
/// passed into <see cref="AnimancerPlayable.Play(ITransition)"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Unfortunately the tool used to generate this documentation does not currently support nested types with
|
|
/// identical names, so only one <c>Transition</c> class will actually have a documentation page.
|
|
/// </remarks>
|
|
[Serializable]
|
|
public new class Transition : Transition<LinearMixerState, float>
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Creates and returns a new <see cref="LinearMixerState"/> connected to the `layer`.
|
|
/// <para></para>
|
|
/// This method also assigns it as the <see cref="AnimancerState.Transition{TState}.State"/>.
|
|
/// </summary>
|
|
public override LinearMixerState CreateState(AnimancerLayer layer)
|
|
{
|
|
State = new LinearMixerState(layer);
|
|
InitialiseState();
|
|
return State;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#region Drawer
|
|
#if UNITY_EDITOR
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Editor-Only] Adds context menu functions for this transition.</summary>
|
|
protected override void AddItemsToContextMenu(UnityEditor.GenericMenu menu, UnityEditor.SerializedProperty property,
|
|
Editor.Serialization.PropertyAccessor accessor)
|
|
{
|
|
base.AddItemsToContextMenu(menu, property, accessor);
|
|
Drawer.AddItemsToContextMenu(menu);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="Transition"/>.</summary>
|
|
[UnityEditor.CustomPropertyDrawer(typeof(Transition), true)]
|
|
public class Drawer : TransitionDrawer
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Fills the `menu` with functions relevant to the `rootProperty`.
|
|
/// </summary>
|
|
public static void AddItemsToContextMenu(UnityEditor.GenericMenu menu, string prefix = "Calculate Thresholds/")
|
|
{
|
|
AddPropertyModifierFunction(menu, prefix + "Evenly Spaced", (_) =>
|
|
{
|
|
var count = CurrentThresholds.arraySize;
|
|
if (count <= 1)
|
|
return;
|
|
|
|
var first = CurrentThresholds.GetArrayElementAtIndex(0).floatValue;
|
|
var last = CurrentThresholds.GetArrayElementAtIndex(count - 1).floatValue;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
CurrentThresholds.GetArrayElementAtIndex(i).floatValue = Mathf.Lerp(first, last, i / (float)(count - 1));
|
|
}
|
|
});
|
|
|
|
AddCalculateThresholdsFunction(menu, prefix + "From Speed",
|
|
(clip, threshold) => clip.apparentSpeed);
|
|
AddCalculateThresholdsFunction(menu, prefix + "From Velocity X",
|
|
(clip, threshold) => clip.averageSpeed.x);
|
|
AddCalculateThresholdsFunction(menu, prefix + "From Velocity Y",
|
|
(clip, threshold) => clip.averageSpeed.z);
|
|
AddCalculateThresholdsFunction(menu, prefix + "From Velocity Z",
|
|
(clip, threshold) => clip.averageSpeed.z);
|
|
AddCalculateThresholdsFunction(menu, prefix + "From Angular Speed (Rad)",
|
|
(clip, threshold) => clip.averageAngularSpeed * Mathf.Deg2Rad);
|
|
AddCalculateThresholdsFunction(menu, prefix + "From Angular Speed (Deg)",
|
|
(clip, threshold) => clip.averageAngularSpeed);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary><see cref="AddThresholdItemsToMenu"/> will add some functions to the menu.</summary>
|
|
protected override bool HasThresholdContextMenu { get { return true; } }
|
|
|
|
/// <summary>Adds functions to the `menu` relating to the thresholds.</summary>
|
|
protected override void AddThresholdItemsToMenu(UnityEditor.GenericMenu menu)
|
|
{
|
|
AddItemsToContextMenu(menu, null);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static void AddCalculateThresholdsFunction(UnityEditor.GenericMenu menu, string label,
|
|
Func<AnimationClip, float, float> calculateThreshold)
|
|
{
|
|
AddPropertyModifierFunction(menu, label, (property) =>
|
|
{
|
|
var count = CurrentClips.arraySize;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var clip = CurrentClips.GetArrayElementAtIndex(i).objectReferenceValue as AnimationClip;
|
|
if (clip == null)
|
|
continue;
|
|
|
|
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
|
|
|
|
threshold.floatValue = calculateThreshold(clip, threshold.floatValue);
|
|
}
|
|
});
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endif
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|