// Animancer // Copyright 2020 Kybernetik //
using System;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace Animancer
{
/// [Pro-Only]
/// An which blends an array of other states together using linear interpolation
/// between the specified thresholds.
///
/// This mixer type is similar to the 1D Blend Type in Mecanim Blend Trees.
///
public sealed class LinearMixerState : MixerState
{
/************************************************************************************************************************/
///
/// Constructs a new without connecting it to the .
///
private LinearMixerState(AnimancerPlayable root) : base(root) { }
///
/// Constructs a new and connects it to the `layer`.
///
public LinearMixerState(AnimancerLayer layer) : base(layer) { }
///
/// Constructs a new and connects it to the `parent` at the specified
/// `index`.
///
public LinearMixerState(AnimancerNode parent, int index) : base(parent, index) { }
/************************************************************************************************************************/
///
/// Initialises the and with one
/// state per clip and assigns thresholds evenly spaced between the specified min and max (inclusive).
///
public void Initialise(AnimationClip[] clips, float minThreshold = 0, float maxThreshold = 1)
{
Initialise(clips);
AssignLinearThresholds(minThreshold, maxThreshold);
}
/************************************************************************************************************************/
///
/// Initialises the with two ports and connects two states to them for
/// the specified clips at the specified thresholds (default 0 and 1).
///
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,
});
}
/************************************************************************************************************************/
///
/// Initialises the with three ports and connects three states to them for
/// the specified clips at the specified thresholds (default -1, 0, and 1).
///
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
///
/// Called whenever the thresholds are changed. In the Unity Editor this method calls
/// then while at runtime it only calls
/// the latter.
///
public override void OnThresholdsChanged()
{
AssertThresholdsSorted();
base.OnThresholdsChanged();
}
#endif
/************************************************************************************************************************/
///
/// Throws an unless the thresholds are sorted from lowest to highest.
///
///
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.");
}
}
/************************************************************************************************************************/
///
/// Recalculates the weights of all based on the current value of the
/// and the thresholds.
///
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;
}
/************************************************************************************************************************/
///
/// Assigns the thresholds to be evenly spaced between the specified min and max (inclusive).
///
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
/************************************************************************************************************************/
/// The number of parameters being managed by this state.
protected override int ParameterCount { get { return 1; } }
/// Returns the name of a parameter being managed by this state.
/// Thrown if this state doesn't manage any parameters.
protected override string GetParameterName(int index) { return "Parameter"; }
/// Returns the type of a parameter being managed by this state.
/// Thrown if this state doesn't manage any parameters.
protected override AnimatorControllerParameterType GetParameterType(int index) { return AnimatorControllerParameterType.Float; }
/// Returns the value of a parameter being managed by this state.
/// Thrown if this state doesn't manage any parameters.
protected override object GetParameterValue(int index) { return Parameter; }
/// Sets the value of a parameter being managed by this state.
/// Thrown if this state doesn't manage any parameters.
protected override void SetParameterValue(int index, object value) { Parameter = (float)value; }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Transition
/************************************************************************************************************************/
///
/// A serializable which can create a when
/// passed into .
///
///
/// Unfortunately the tool used to generate this documentation does not currently support nested types with
/// identical names, so only one Transition class will actually have a documentation page.
///
[Serializable]
public new class Transition : Transition
{
/************************************************************************************************************************/
///
/// Creates and returns a new connected to the `layer`.
///
/// This method also assigns it as the .
///
public override LinearMixerState CreateState(AnimancerLayer layer)
{
State = new LinearMixerState(layer);
InitialiseState();
return State;
}
/************************************************************************************************************************/
#region Drawer
#if UNITY_EDITOR
/************************************************************************************************************************/
/// [Editor-Only] Adds context menu functions for this transition.
protected override void AddItemsToContextMenu(UnityEditor.GenericMenu menu, UnityEditor.SerializedProperty property,
Editor.Serialization.PropertyAccessor accessor)
{
base.AddItemsToContextMenu(menu, property, accessor);
Drawer.AddItemsToContextMenu(menu);
}
/************************************************************************************************************************/
/// [Editor-Only] Draws the Inspector GUI for a .
[UnityEditor.CustomPropertyDrawer(typeof(Transition), true)]
public class Drawer : TransitionDrawer
{
/************************************************************************************************************************/
///
/// Fills the `menu` with functions relevant to the `rootProperty`.
///
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);
}
/************************************************************************************************************************/
/// will add some functions to the menu.
protected override bool HasThresholdContextMenu { get { return true; } }
/// Adds functions to the `menu` relating to the thresholds.
protected override void AddThresholdItemsToMenu(UnityEditor.GenericMenu menu)
{
AddItemsToContextMenu(menu, null);
}
/************************************************************************************************************************/
private static void AddCalculateThresholdsFunction(UnityEditor.GenericMenu menu, string label,
Func 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
/************************************************************************************************************************/
}
}