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.
CrowdControl/Assets/Plugins/Animancer/Internal/Mixer States/ManualMixerState.cs

1094 lines
46 KiB
C#

// Animancer // Copyright 2020 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
#endif
namespace Animancer
{
/// <summary>[Pro-Only]
/// An <see cref="AnimancerState"/> which blends multiple child states. Unlike other mixers, this class does not
/// perform any automatic weight calculations, it simple allows you to control the weight of all states manually.
/// <para></para>
/// This mixer type is similar to the Direct Blend Type in Mecanim Blend Trees.
/// </summary>
public class ManualMixerState : MixerState
{
/************************************************************************************************************************/
#region Properties
/************************************************************************************************************************/
/// <summary>The states managed by this mixer.</summary>
public AnimancerState[] States { get; protected set; }
/// <summary>The number of input ports in the <see cref="AnimationMixerPlayable"/>.</summary>
public override int PortCount { get { return States.Length; } }
/************************************************************************************************************************/
/// <summary>Returns the <see cref="States"/> array.</summary>
public override IList<AnimancerState> ChildStates { get { return States; } }
/************************************************************************************************************************/
/// <summary>
/// The weighted average <see cref="AnimancerState.Time"/> of each child state according to their
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
protected override float NewTime
{
get
{
var totalWeight = 0f;
var normalizedTime = 0f;
var length = 0f;
var count = States.Length;
while (--count >= 0)
{
var state = States[count];
if (state != null)
{
var weight = state.Weight;
totalWeight += weight;
normalizedTime += state.NormalizedTime * weight;
length += state.Length * weight;
}
}
if (totalWeight == 0)
return 0;
totalWeight = 1f / totalWeight;
return normalizedTime * totalWeight * length * totalWeight;
}
set
{
var count = States.Length;
if (value == 0)
goto ZeroTime;
var length = Length;
if (length == 0)
goto ZeroTime;
value /= length;// Normalize.
while (--count >= 0)
{
var state = States[count];
if (state != null)
state.NormalizedTime = value;
}
return;
// If the value is 0, we can set the child times slightly more efficiently.
ZeroTime:
while (--count >= 0)
{
var state = States[count];
if (state != null)
state.Time = 0;
}
}
}
/************************************************************************************************************************/
/// <summary>
/// The weighted average <see cref="AnimancerState.Length"/> of each child state according to their
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
public override float Length
{
get
{
var length = 0f;
var childWeight = CalculateTotalChildWeight();
if (childWeight == 0)
return 0;
childWeight = 1f / childWeight;
var count = States.Length;
while (--count >= 0)
{
var state = States[count];
if (state != null)
length += state.Length * state.Weight * childWeight;
}
return length;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialisation
/************************************************************************************************************************/
/// <summary>
/// Constructs a new <see cref="LinearMixerState"/> without connecting it to the <see cref="PlayableGraph"/>.
/// </summary>
protected ManualMixerState(AnimancerPlayable root) : base(root) { }
/// <summary>
/// Constructs a new <see cref="LinearMixerState"/> and connects it to the `layer`.
/// </summary>
public ManualMixerState(AnimancerLayer layer) : base(layer) { }
/// <summary>
/// Constructs a new <see cref="LinearMixerState"/> and connects it to the `parent` at the specified
/// `index`.
/// </summary>
public ManualMixerState(AnimancerNode parent, int index) : base(parent, index) { }
/************************************************************************************************************************/
/// <summary>
/// Initialises this mixer with the specified number of ports which can be filled individually by <see cref="CreateState"/>.
/// </summary>
public virtual void Initialise(int portCount)
{
if (portCount <= 1)
Debug.LogWarning(GetType() + " is being initialised with capacity <= 1. The purpose of a mixer is to mix multiple clips.");
_Playable.SetInputCount(portCount);
States = new AnimancerState[portCount];
}
/************************************************************************************************************************/
/// <summary>
/// Initialises this mixer with one state per clip.
/// </summary>
public void Initialise(params AnimationClip[] clips)
{
int count = clips.Length;
_Playable.SetInputCount(count);
if (count <= 1)
Debug.LogWarning(GetType() + " is being initialised without multiple clips. The purpose of a mixer is to mix multiple clips.");
States = new AnimancerState[count];
for (int i = 0; i < count; i++)
{
var clip = clips[i];
if (clip != null)
new ClipState(this, i, clip);
}
}
/************************************************************************************************************************/
/// <summary>
/// Creates and returns a new <see cref="ClipState"/> to play the `clip` with this
/// <see cref="MixerState"/> as its parent.
/// </summary>
public override ClipState CreateState(int index, AnimationClip clip)
{
var oldState = States[index];
if (oldState != null)
{
// If the old state has the specified `clip`, return it.
if (oldState.Clip == clip)
{
return oldState as ClipState;
}
else// Otherwise destroy and replace it.
{
oldState.Destroy();
}
}
return base.CreateState(index, clip);
}
/************************************************************************************************************************/
/// <summary>Connects the `state` to this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
protected internal override void OnAddChild(AnimancerState state)
{
OnAddChild(States, state);
}
/// <summary>Disconnects the `state` from this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
protected internal override void OnRemoveChild(AnimancerState state)
{
Validate.RemoveChild(state, States);
States[state.Index] = null;
state.DisconnectFromGraph();
}
/************************************************************************************************************************/
/// <summary>
/// Destroys the <see cref="Playable"/> of this mixer and its <see cref="States"/>.
/// </summary>
public override void Destroy()
{
DestroyChildren();
base.Destroy();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Child States
/************************************************************************************************************************/
/// <summary>
/// Calculates the sum of the <see cref="AnimancerNode.Weight"/> of all child states.
/// </summary>
public float CalculateTotalChildWeight()
{
if (States == null)
return 0;
var total = 0f;
var count = States.Length;
while (--count >= 0)
{
var state = States[count];
if (state != null)
total += state.Weight;
}
return total;
}
/************************************************************************************************************************/
/// <summary>
/// Destroys all <see cref="States"/> connected to this mixer. This operation cannot be undone.
/// </summary>
public void DestroyChildren()
{
if (States == null)
return;
var count = States.Length;
while (--count >= 0)
{
var state = States[count];
if (state != null)
state.Destroy();
}
States = null;
}
/************************************************************************************************************************/
/// <summary>
/// Does nothing. Manual mixers do not automatically recalculate their weights.
/// </summary>
public override void RecalculateWeights() { }
/************************************************************************************************************************/
/// <summary>
/// Sets the weight of all states after the `previousIndex` to 0.
/// </summary>
protected void DisableRemainingStates(int previousIndex)
{
var count = States.Length;
while (++previousIndex < count)
{
var state = States[previousIndex];
if (state == null)
continue;
state.Weight = 0;
}
}
/************************************************************************************************************************/
/// <summary>
/// Returns the state at the specified `index` if it is not null, otherwise increments the index and checks
/// again. Returns null if no state is found by the end of the <see cref="States"/> array.
/// </summary>
protected AnimancerState GetNextState(ref int index)
{
while (index < States.Length)
{
var state = States[index];
if (state != null)
return state;
index++;
}
return null;
}
/************************************************************************************************************************/
/// <summary>
/// Divides the weight of all states by the `totalWeight` so that they all add up to 1.
/// </summary>
protected void NormalizeWeights(float totalWeight)
{
if (totalWeight == 1)
return;
totalWeight = 1f / totalWeight;
int count = States.Length;
for (int i = 0; i < count; i++)
{
var state = States[i];
if (state == null)
continue;
state.Weight *= totalWeight;
}
}
/************************************************************************************************************************/
/// <summary>Gets a user-friendly key to identify the `state` in the Inspector.</summary>
public override string GetDisplayKey(AnimancerState state)
{
return string.Concat("[", state.Index.ToString(), "]");
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Transition
/************************************************************************************************************************/
/// <summary>
/// Base class for serializable <see cref="ITransition"/>s which can create a particular type of
/// <see cref="ManualMixerState"/> 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.
/// <para></para>
/// Even though it has the <see cref="SerializableAttribute"/>, this class won't actually get serialized
/// by Unity because it's generic and abstract. Each child class still needs to include the attribute.
/// </remarks>
[Serializable]
public abstract new class Transition<TMixer> : AnimancerState.Transition<TMixer>, IAnimationClipCollection
where TMixer : ManualMixerState
{
/************************************************************************************************************************/
[SerializeField, HideInInspector]
private AnimationClip[] _Clips;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="ClipState.Clip"/> to use for each state in the mixer.
/// </summary>
public AnimationClip[] Clips
{
get { return _Clips; }
set { _Clips = value; }
}
[SerializeField, HideInInspector]
private float[] _Speeds;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="AnimancerNode.Speed"/> to use for each state in the mixer.
/// <para></para>
/// If the size of this array doesn't match the <see cref="Clips"/>, it will be ignored.
/// </summary>
public float[] Speeds
{
get { return _Speeds; }
set { _Speeds = value; }
}
/************************************************************************************************************************/
[SerializeField, HideInInspector]
private bool[] _SynchroniseChildren;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="MixerState.SynchroniseChildren"/> flags for each state in the mixer.
/// <para></para>
/// The array can be null or empty. Any elements not in the array will be treated as true.
/// </summary>
public bool[] SynchroniseChildren
{
get { return _SynchroniseChildren; }
set { _SynchroniseChildren = value; }
}
/************************************************************************************************************************/
/// <summary>[<see cref="ITransitionDetailed"/>]
/// Returns true is any of the <see cref="Clips"/> are looping.
/// </summary>
public override bool IsLooping
{
get
{
var count = _Clips.Length;
for (int i = 0; i < count; i++)
{
var clip = _Clips[i];
if (clip == null)
continue;
if (clip.isLooping)
return true;
}
return false;
}
}
/// <summary>[<see cref="ITransitionDetailed"/>]
/// The maximum amount of time the animation is expected to take (in seconds).
/// </summary>
public override float MaximumDuration
{
get
{
if (_Clips == null)
return 0;
var duration = 0f;
var hasSpeeds = _Speeds != null && _Speeds.Length == _Clips.Length;
for (int i = 0; i < _Clips.Length; i++)
{
var clip = _Clips[i];
if (clip == null)
continue;
var length = clip.length;
if (hasSpeeds)
length *= _Speeds[i];
if (duration < length)
duration = length;
}
return duration;
}
}
/************************************************************************************************************************/
/// <summary>
/// Initialises the <see cref="AnimancerState.Transition{TState}.State"/> immediately after it is created.
/// </summary>
public virtual void InitialiseState()
{
State.Initialise(_Clips);
if (_Speeds != null && _Speeds.Length == _Clips.Length)
{
for (int i = 0; i < State.States.Length; i++)
{
State.States[i].Speed = _Speeds[i];
}
}
State.SynchroniseChildren = _SynchroniseChildren;
}
/************************************************************************************************************************/
/// <summary>Adds the <see cref="Clips"/> to the collection.</summary>
void IAnimationClipCollection.GatherAnimationClips(ICollection<AnimationClip> clips)
{
clips.Gather(_Clips);
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Adds context menu functions for this transition.</summary>
protected override void AddItemsToContextMenu(GenericMenu menu, SerializedProperty property,
Editor.Serialization.PropertyAccessor accessor)
{
base.AddItemsToContextMenu(menu, property, accessor);
Transition.Drawer.AddItemsToContextMenu(menu, property);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>
/// A serializable <see cref="ITransition"/> which can create a <see cref="ManualMixerState"/> 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 class Transition : Transition<ManualMixerState>
{
/************************************************************************************************************************/
/// <summary>
/// Creates and returns a new <see cref="ManualMixerState"/> connected to the `layer`.
/// <para></para>
/// This method also assigns it as the <see cref="AnimancerState.Transition{TState}.State"/>.
/// </summary>
public override ManualMixerState CreateState(AnimancerLayer layer)
{
State = new ManualMixerState(layer);
InitialiseState();
return State;
}
/************************************************************************************************************************/
#region Drawer
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="Transition"/>.</summary>
[CustomPropertyDrawer(typeof(Transition), true)]
public class Drawer : Editor.TransitionDrawer
{
/************************************************************************************************************************/
/// <summary>
/// The property this drawer is currently drawing.
/// <para></para>
/// Normally each property has its own drawer, but arrays share a single drawer for all elements.
/// </summary>
public static SerializedProperty CurrentProperty { get; private set; }
/// <summary>The <see cref="Transition{TState}.Clips"/> field.</summary>
public static SerializedProperty CurrentClips { get; private set; }
/// <summary>The <see cref="Transition{TState}.Speeds"/> field.</summary>
public static SerializedProperty CurrentSpeeds { get; private set; }
/// <summary>The <see cref="Transition{TState}.SynchroniseChildren"/> field.</summary>
public static SerializedProperty CurrentSynchroniseChildren { get; private set; }
private readonly Dictionary<string, ReorderableList>
PropertyPathToStates = new Dictionary<string, ReorderableList>();
/// <summary>
/// Gather the details of the `property`.
/// <para></para>
/// This method gets called by every <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/> call since
/// Unity uses the same <see cref="PropertyDrawer"/> instance for each element in a collection, so it
/// needs to gather the details associated with the current property.
/// </summary>
protected virtual ReorderableList GatherDetails(SerializedProperty property)
{
InitialiseMode(property);
GatherSubProperties(property);
var propertyPath = property.propertyPath;
ReorderableList states;
if (!PropertyPathToStates.TryGetValue(propertyPath, out states))
{
states = new ReorderableList(CurrentClips.serializedObject, CurrentClips)
{
drawHeaderCallback = DoStateListHeaderGUI,
elementHeightCallback = GetElementHeight,
drawElementCallback = DoElementGUI,
onAddCallback = OnAddElement,
onRemoveCallback = OnRemoveElement,
#if UNITY_2018_1_OR_NEWER
onReorderCallbackWithDetails = OnReorderList,
#else
onReorderCallback = OnReorderList,
onSelectCallback = OnListSelectionChanged,
#endif
};
PropertyPathToStates.Add(propertyPath, states);
}
return states;
}
/************************************************************************************************************************/
/// <summary>
/// Called every time a `property` is drawn to find the relevant child properties and store them to be
/// used in <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/>.
/// </summary>
protected virtual void GatherSubProperties(SerializedProperty property)
{
GatherSubPropertiesStatic(property);
}
/// <summary>
/// Called every time a `property` is drawn to find the relevant child properties and store them to be
/// used in <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/>.
/// </summary>
public static void GatherSubPropertiesStatic(SerializedProperty property)
{
CurrentProperty = property;
CurrentClips = property.FindPropertyRelative("_Clips");
CurrentSpeeds = property.FindPropertyRelative("_Speeds");
CurrentSynchroniseChildren = property.FindPropertyRelative("_SynchroniseChildren");
if (CurrentSpeeds.arraySize != 0)
CurrentSpeeds.arraySize = CurrentClips.arraySize;
}
/************************************************************************************************************************/
/// <summary>
/// Calculates the number of vertical pixels the `property` will occupy when it is drawn.
/// </summary>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var height = EditorGUI.GetPropertyHeight(property, label);
if (property.isExpanded)
{
var states = GatherDetails(property);
height += Editor.AnimancerGUI.StandardSpacing + states.GetHeight();
}
return height;
}
/************************************************************************************************************************/
/// <summary>
/// Draws the root `property` GUI and calls
/// <see cref="Editor.TransitionDrawer.DoPropertyGUI"/> for each of its children.
/// </summary>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
var originalProperty = property.Copy();
base.OnGUI(area, property, label);
if (!originalProperty.isExpanded)
return;
var states = GatherDetails(originalProperty);
var indentLevel = EditorGUI.indentLevel;
area.yMin = area.yMax - states.GetHeight();
EditorGUI.indentLevel++;
area = EditorGUI.IndentedRect(area);
EditorGUI.indentLevel = 0;
states.DoList(area);
EditorGUI.indentLevel = indentLevel;
TryCollapseSpeeds();
}
/************************************************************************************************************************/
/// <summary>Splits the specified `area` into separate sections.</summary>
protected void SplitListRect(Rect area, out Rect animation, out Rect speed, out Rect sync)
{
var spacing = Editor.AnimancerGUI.StandardSpacing;
area.width += 2;
sync = Editor.AnimancerGUI.StealFromRight(ref area, Editor.AnimancerGUI.ToggleWidth - spacing, spacing);
speed = Editor.AnimancerGUI.StealFromRight(ref area, 55, spacing);
animation = area;
}
/************************************************************************************************************************/
/// <summary>Draws the headdings of the state list.</summary>
protected virtual void DoStateListHeaderGUI(Rect area)
{
Rect animationArea, speedArea, syncArea;
SplitListRect(area, out animationArea, out speedArea, out syncArea);
DoAnimationLabelGUI(animationArea);
DoSpeedLabelGUI(speedArea);
DoSyncLabelGUI(syncArea);
}
/************************************************************************************************************************/
/// <summary>Draws an "Animation" label.</summary>
protected static void DoAnimationLabelGUI(Rect area)
{
EditorGUI.BeginProperty(area, GUIContent.none, CurrentClips);
GUI.Label(area, Editor.AnimancerGUI.TempContent("Animation",
"The animations that will be used for each child state"));
EditorGUI.EndProperty();
}
/// <summary>Draws a "Speed" label.</summary>
protected static void DoSpeedLabelGUI(Rect area)
{
EditorGUI.BeginProperty(area, GUIContent.none, CurrentSpeeds);
GUI.Label(area, Editor.AnimancerGUI.TempContent("Speed",
"Determines how fast each child state plays (Default = 1)"));
EditorGUI.EndProperty();
}
private static float _SyncLabelWidth;
/// <summary>Draws a "Sync" label.</summary>
protected static void DoSyncLabelGUI(Rect area)
{
const string Text = "Sync";
if (_SyncLabelWidth == 0)
_SyncLabelWidth = Editor.AnimancerGUI.CalculateLabelWidth(Text);
if (area.width < _SyncLabelWidth)
area.xMin = area.xMax - _SyncLabelWidth;
EditorGUI.BeginProperty(area, GUIContent.none, CurrentSynchroniseChildren);
GUI.Label(area, Editor.AnimancerGUI.TempContent(Text,
"Determines which child states have their normalized times constantly synchronised"));
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
/// <summary>Calculates the height of the state at the specified `index`.</summary>
protected virtual float GetElementHeight(int index)
{
return Editor.AnimancerGUI.LineHeight;
}
/************************************************************************************************************************/
/// <summary>Draws the GUI of the state at the specified `index`.</summary>
private void DoElementGUI(Rect area, int index, bool isActive, bool isFocused)
{
if (index < 0 || index > CurrentClips.arraySize)
return;
var clip = CurrentClips.GetArrayElementAtIndex(index);
var speed = CurrentSpeeds.arraySize > 0 ? CurrentSpeeds.GetArrayElementAtIndex(index) : null;
DoElementGUI(area, index, clip, speed);
}
/************************************************************************************************************************/
/// <summary>Draws the GUI of the state at the specified `index`.</summary>
protected virtual void DoElementGUI(Rect area, int index,
SerializedProperty clip, SerializedProperty speed)
{
Rect animationArea, speedArea, syncArea;
SplitListRect(area, out animationArea, out speedArea, out syncArea);
DoElementGUI(animationArea, speedArea, syncArea, index, clip, speed);
}
/// <summary>Draws the GUI of the state at the specified `index`.</summary>
protected void DoElementGUI(Rect animationArea, Rect speedArea, Rect syncArea, int index,
SerializedProperty clip, SerializedProperty speed)
{
EditorGUI.PropertyField(animationArea, clip, GUIContent.none);
if (speed != null)
{
EditorGUI.PropertyField(speedArea, speed, GUIContent.none);
}
else
{
EditorGUI.BeginProperty(speedArea, GUIContent.none, CurrentSpeeds);
var value = EditorGUI.FloatField(speedArea, 1);
if (value != 1)
{
CurrentSpeeds.InsertArrayElementAtIndex(0);
CurrentSpeeds.GetArrayElementAtIndex(0).floatValue = 1;
CurrentSpeeds.arraySize = CurrentClips.arraySize;
CurrentSpeeds.GetArrayElementAtIndex(index).floatValue = value;
}
EditorGUI.EndProperty();
}
DoSyncToggleGUI(syncArea, index);
}
/************************************************************************************************************************/
/// <summary>
/// Draws a toggle to enable or disable <see cref="MixerState.SynchroniseChildren"/> for the child at
/// the specified `index`.
/// </summary>
protected void DoSyncToggleGUI(Rect area, int index)
{
var syncFlagCount = CurrentSynchroniseChildren.arraySize;
var property = CurrentSynchroniseChildren;
var enabled = true;
if (index < syncFlagCount)
{
property = property.GetArrayElementAtIndex(index);
enabled = property.boolValue;
}
EditorGUI.BeginChangeCheck();
EditorGUI.BeginProperty(area, GUIContent.none, property);
enabled = GUI.Toggle(area, enabled, GUIContent.none);
EditorGUI.EndProperty();
if (EditorGUI.EndChangeCheck())
{
if (index < syncFlagCount)
{
property.boolValue = enabled;
for (int i = syncFlagCount - 1; i >= 0; i--)
{
if (CurrentSynchroniseChildren.GetArrayElementAtIndex(i).boolValue)
syncFlagCount = i;
else
break;
}
CurrentSynchroniseChildren.arraySize = syncFlagCount;
}
else
{
property.arraySize = index + 1;
for (int i = syncFlagCount; i < index; i++)
{
property.GetArrayElementAtIndex(i).boolValue = true;
}
property.GetArrayElementAtIndex(index).boolValue = enabled;
}
}
}
/************************************************************************************************************************/
/// <summary>
/// Called when adding a new state to the list to ensure that any other relevant arrays have new
/// elements added as well.
/// </summary>
protected virtual void OnAddElement(ReorderableList list)
{
var index = CurrentClips.arraySize;
CurrentClips.InsertArrayElementAtIndex(index);
if (CurrentSpeeds.arraySize > 0)
CurrentSpeeds.InsertArrayElementAtIndex(index);
}
/************************************************************************************************************************/
/// <summary>
/// Called when removing a state from the list to ensure that any other relevant arrays have elements
/// removed as well.
/// </summary>
protected virtual void OnRemoveElement(ReorderableList list)
{
var index = list.index;
RemoveArrayElement(CurrentClips, index);
if (CurrentSpeeds.arraySize > 0)
RemoveArrayElement(CurrentSpeeds, index);
}
/// <summary>
/// Removes the specified array element from the `property`.
/// <para></para>
/// If the element is not at its default value, the first call to
/// <see cref="SerializedProperty.DeleteArrayElementAtIndex"/> will only reset it, so this method will
/// call it again if necessary to ensure that it actually gets removed.
/// </summary>
protected static void RemoveArrayElement(SerializedProperty property, int index)
{
var count = property.arraySize;
property.DeleteArrayElementAtIndex(index);
if (property.arraySize == count)
property.DeleteArrayElementAtIndex(index);
}
/************************************************************************************************************************/
/// <summary>
/// Called when reordering states in the list to ensure that any other relevant arrays have their
/// corresponding elements reordered as well.
/// </summary>
protected virtual void OnReorderList(ReorderableList list, int oldIndex, int newIndex)
{
CurrentSpeeds.MoveArrayElement(oldIndex, newIndex);
}
#if !UNITY_2018_1_OR_NEWER
private int _SelectedIndex;
private void OnListSelectionChanged(ReorderableList list)
{
_SelectedIndex = list.index;
}
private void OnReorderList(ReorderableList list)
{
OnReorderList(list, _SelectedIndex, list.index);
}
#endif
/************************************************************************************************************************/
#region Speeds
/************************************************************************************************************************/
/// <summary>
/// Initialises every element in the <see cref="CurrentSpeeds"/> array from the `start` to the end of
/// the array to contain a value of 1.
/// </summary>
public static void InitialiseSpeeds(int start)
{
var count = CurrentSpeeds.arraySize;
while (start < count)
CurrentSpeeds.GetArrayElementAtIndex(start++).floatValue = 1;
}
/************************************************************************************************************************/
/// <summary>
/// If every element in the <see cref="CurrentSpeeds"/> array is 1, this method sets the array size to 0.
/// </summary>
public static void TryCollapseSpeeds()
{
var speedCount = CurrentSpeeds.arraySize;
if (speedCount <= 0)
return;
for (int i = 0; i < speedCount; i++)
{
if (CurrentSpeeds.GetArrayElementAtIndex(i).floatValue != 1)
return;
}
CurrentSpeeds.arraySize = 0;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
/// <summary>[Editor-Only] Adds context menu functions for this transition.</summary>
public static void AddItemsToContextMenu(GenericMenu menu, SerializedProperty property)
{
GatherSubPropertiesStatic(property);
AddPropertyModifierFunction(menu, "Reset Speeds", (_) => CurrentSpeeds.arraySize = 0);
AddPropertyModifierFunction(menu, "Normalize Durations", NormalizeDurations);
}
/************************************************************************************************************************/
/// <summary>
/// Recalculates the <see cref="CurrentSpeeds"/> depending on the <see cref="AnimationClip.length"/> of
/// their animations so that they all take the same amount of time to play fully.
/// </summary>
private static void NormalizeDurations(SerializedProperty property)
{
var speedCount = CurrentSpeeds.arraySize;
var lengths = new float[CurrentClips.arraySize];
if (lengths.Length <= 1)
return;
int nonZeroLengths = 0;
float totalLength = 0;
float totalSpeed = 0;
for (int i = 0; i < lengths.Length; i++)
{
var clip = CurrentClips.GetArrayElementAtIndex(i).objectReferenceValue as AnimationClip;
if (clip != null && clip.length > 0)
{
nonZeroLengths++;
totalLength += clip.length;
lengths[i] = clip.length;
if (speedCount > 0)
totalSpeed += CurrentSpeeds.GetArrayElementAtIndex(i).floatValue;
}
}
if (nonZeroLengths == 0)
return;
var averageLength = totalLength / nonZeroLengths;
var averageSpeed = speedCount > 0 ? totalSpeed / nonZeroLengths : 1;
CurrentSpeeds.arraySize = lengths.Length;
InitialiseSpeeds(speedCount);
for (int i = 0; i < lengths.Length; i++)
{
if (lengths[i] == 0)
continue;
CurrentSpeeds.GetArrayElementAtIndex(i).floatValue = averageSpeed * lengths[i] / averageLength;
}
TryCollapseSpeeds();
}
/************************************************************************************************************************/
/// <summary>
/// Adds a menu function that will call <see cref="GatherSubPropertiesStatic"/> then perform the specified
/// `action`.
/// </summary>
protected static void AddPropertyModifierFunction(GenericMenu menu, string label, Action<SerializedProperty> action)
{
Editor.Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, (property) =>
{
GatherSubPropertiesStatic(property);
action(property);
});
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endif
#endregion
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}