chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ClipTransitionDrawer
[CustomPropertyDrawer(typeof(ClipTransition), true)]
public class ClipTransitionDrawer : TransitionDrawer
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ClipTransitionDrawer"/>.</summary>
public ClipTransitionDrawer()
: base(ClipTransition.ClipFieldName)
{ }
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb3f0552aa807e340b39f53ea42a9a2c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,201 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ControllerTransitionDrawer
[CustomPropertyDrawer(typeof(ControllerTransition<>), true)]
[CustomPropertyDrawer(typeof(ControllerTransition), true)]
public class ControllerTransitionDrawer : TransitionDrawer
{
/************************************************************************************************************************/
private readonly string[] Parameters;
private readonly string[] ParameterPropertySuffixes;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ControllerTransitionDrawer"/> without any parameters.</summary>
public ControllerTransitionDrawer()
: base(ControllerTransition.ControllerFieldName)
{ }
/// <summary>Creates a new <see cref="ControllerTransitionDrawer"/> and sets the <see cref="Parameters"/>.</summary>
public ControllerTransitionDrawer(params string[] parameters)
: base(ControllerTransition.ControllerFieldName)
{
Parameters = parameters;
if (parameters == null)
return;
ParameterPropertySuffixes = new string[parameters.Length];
for (int i = 0; i < ParameterPropertySuffixes.Length; i++)
{
ParameterPropertySuffixes[i] = "." + parameters[i];
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoChildPropertyGUI(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty property,
GUIContent label)
{
var path = property.propertyPath;
if (ParameterPropertySuffixes != null)
{
var controllerProperty = rootProperty.FindPropertyRelative(MainPropertyName);
if (controllerProperty.objectReferenceValue is AnimatorController controller)
{
for (int i = 0; i < ParameterPropertySuffixes.Length; i++)
{
if (path.EndsWith(ParameterPropertySuffixes[i]))
{
area.height = AnimancerGUI.LineHeight;
DoParameterGUI(area, controller, property);
return;
}
}
}
}
EditorGUI.BeginChangeCheck();
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
// When the controller changes, validate all parameters.
if (EditorGUI.EndChangeCheck() &&
Parameters != null &&
path.EndsWith(MainPropertyPathSuffix))
{
if (property.objectReferenceValue is AnimatorController controller)
{
for (int i = 0; i < Parameters.Length; i++)
{
property = rootProperty.FindPropertyRelative(Parameters[i]);
var parameterName = property.stringValue;
// If a parameter is missing, assign it to the first float parameter.
if (!HasFloatParameter(controller, parameterName))
{
parameterName = GetFirstFloatParameterName(controller);
if (!string.IsNullOrEmpty(parameterName))
property.stringValue = parameterName;
}
}
}
}
}
/************************************************************************************************************************/
/// <summary>Draws a dropdown menu to select the name of a parameter in the `controller`.</summary>
protected void DoParameterGUI(Rect area, AnimatorController controller, SerializedProperty property)
{
var parameterName = property.stringValue;
var parameters = controller.parameters;
using (var label = PooledGUIContent.Acquire(property))
{
var propertyLabel = EditorGUI.BeginProperty(area, label, property);
var xMax = area.xMax;
area.width = EditorGUIUtility.labelWidth;
EditorGUI.PrefixLabel(area, propertyLabel);
area.x += area.width;
area.xMax = xMax;
}
var color = GUI.color;
if (!HasFloatParameter(controller, parameterName))
GUI.color = AnimancerGUI.ErrorFieldColor;
using (var label = PooledGUIContent.Acquire(parameterName))
{
if (EditorGUI.DropdownButton(area, label, FocusType.Passive))
{
property = property.Copy();
var menu = new GenericMenu();
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
Serialization.AddPropertyModifierFunction(menu, property, parameter.name,
parameter.type == AnimatorControllerParameterType.Float,
(targetProperty) =>
{
targetProperty.stringValue = parameter.name;
});
}
if (menu.GetItemCount() == 0)
menu.AddDisabledItem(new("No Parameters"));
menu.ShowAsContext();
}
}
GUI.color = color;
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
private static bool HasFloatParameter(AnimatorController controller, string name)
{
if (string.IsNullOrEmpty(name))
return false;
var parameters = controller.parameters;
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
if (parameter.type == AnimatorControllerParameterType.Float &&
parameter.name == name)
{
return true;
}
}
return false;
}
/************************************************************************************************************************/
private static string GetFirstFloatParameterName(AnimatorController controller)
{
var parameters = controller.parameters;
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
if (parameter.type == AnimatorControllerParameterType.Float)
{
return parameter.name;
}
}
return "";
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: faa04e0e6ae08764dbf279c99225920e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DirectionalClipTransitionDrawer
[CustomPropertyDrawer(typeof(DirectionalClipTransition), true)]
public class DirectionalClipTransitionDrawer : TransitionDrawer
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DirectionalClipTransitionDrawer"/>.</summary>
public DirectionalClipTransitionDrawer()
: base(DirectionalClipTransition.AnimationSetField)
{ }
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoChildPropertyGUI(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty property,
GUIContent label)
{
var width = area.width;
var path = property.propertyPath;
if (path.EndsWith($".{ClipTransition.ClipFieldName}"))
{
if (property.objectReferenceValue != null)
{
var removeArea = AnimancerGUI.StealFromRight(
ref area, AnimancerGUI.LineHeight, AnimancerGUI.StandardSpacing);
var removeContent = AnimancerIcons.ClearIcon(
$"A {nameof(DirectionalClipTransition)}" +
$" will get its Clip from the Animation Set at runtime" +
$" so the Clip might as well be null until then.");
if (GUI.Button(removeArea, removeContent, AnimancerGUI.NoPaddingButtonStyle))
property.objectReferenceValue = null;
}
if (Context.Transition is DirectionalClipTransition directionalClipTransition &&
directionalClipTransition.AnimationSet != null)
{
var dropdownArea = AnimancerGUI.StealFromRight(
ref area, area.height, AnimancerGUI.StandardSpacing);
if (GUI.Button(dropdownArea, GUIContent.none, EditorStyles.popup))
PickAnimation(property, directionalClipTransition);
}
}
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
area.width = width;
}
/************************************************************************************************************************/
/// <summary>Shows a context menu to choose an <see cref="AnimationClip"/> from the `source`.</summary>
private void PickAnimation(SerializedProperty property, object source)
{
var menu = new GenericMenu();
using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
{
clips.GatherFromSource(source);
if (clips.Count == 0)
return;
property = property.Copy();
foreach (var clip in clips)
{
menu.AddPropertyModifierFunction(property, clip.name, true, modify =>
{
modify.objectReferenceValue = clip;
});
}
}
menu.ShowAsContext();
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5f34d72d5484544da63e422dca5d0a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,156 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/LinearMixerTransitionDrawer
[CustomPropertyDrawer(typeof(LinearMixerTransition), true)]
public class LinearMixerTransitionDrawer : MixerTransitionDrawer
{
/************************************************************************************************************************/
private static GUIContent _SortingErrorContent;
private static GUIStyle _SortingErrorStyle;
/// <inheritdoc/>
protected override void DoThresholdGUI(Rect area, int index)
{
var color = GUI.color;
var iconArea = default(Rect);
if (index > 0)
{
var previousThreshold = CurrentThresholds.GetArrayElementAtIndex(index - 1);
var currentThreshold = CurrentThresholds.GetArrayElementAtIndex(index);
if (previousThreshold.floatValue >= currentThreshold.floatValue)
{
iconArea = AnimancerGUI.StealFromRight(
ref area,
area.height,
AnimancerGUI.StandardSpacing);
GUI.color = AnimancerGUI.ErrorFieldColor;
}
}
base.DoThresholdGUI(area, index);
if (iconArea != default)
{
_SortingErrorContent ??= new(AnimancerIcons.Error)
{
tooltip =
"Linear Mixer Thresholds must always be unique" +
" and sorted in ascending order (click to sort)"
};
_SortingErrorStyle ??= new(GUI.skin.label)
{
padding = new(),
};
if (GUI.Button(iconArea, _SortingErrorContent, _SortingErrorStyle))
{
AnimancerGUI.Deselect();
Serialization.RecordUndo(Context.Property);
((LinearMixerTransition)Context.Transition).SortByThresholds();
}
}
GUI.color = color;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AddThresholdFunctionsToMenu(GenericMenu menu)
{
const string EvenlySpaced = "Evenly Spaced";
var count = CurrentThresholds.arraySize;
if (count <= 1)
{
menu.AddDisabledItem(new(EvenlySpaced));
}
else
{
var first = CurrentThresholds.GetArrayElementAtIndex(0).floatValue;
var last = CurrentThresholds.GetArrayElementAtIndex(count - 1).floatValue;
if (last == first)
last++;
AddPropertyModifierFunction(menu, $"{EvenlySpaced} ({first} to {last})", _ =>
{
for (int i = 0; i < count; i++)
{
CurrentThresholds.GetArrayElementAtIndex(i).floatValue =
Mathf.Lerp(first, last, i / (float)(count - 1));
}
});
}
AddCalculateThresholdsFunction(menu, "From Speed",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.magnitude
: float.NaN);
AddCalculateThresholdsFunction(menu, "From Velocity X",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.x
: float.NaN);
AddCalculateThresholdsFunction(menu, "From Velocity Y",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.y
: float.NaN);
AddCalculateThresholdsFunction(menu, "From Velocity Z",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.z
: float.NaN);
AddCalculateThresholdsFunction(menu, "From Angular Speed (Rad)",
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
? speed
: float.NaN);
AddCalculateThresholdsFunction(menu, "From Angular Speed (Deg)",
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
? speed * Mathf.Rad2Deg
: float.NaN);
}
/************************************************************************************************************************/
private void AddCalculateThresholdsFunction(
GenericMenu menu,
string label,
Func<Object, float, float> calculateThreshold)
{
AddPropertyModifierFunction(menu, label, (property) =>
{
var count = CurrentAnimations.arraySize;
for (int i = 0; i < count; i++)
{
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
if (state == null)
continue;
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
var value = calculateThreshold(state, threshold.floatValue);
if (!float.IsNaN(value))
threshold.floatValue = value;
}
});
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b407e7bedc3c63946b3dc476d4540873
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,931 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using Animancer.Units.Editor;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ManualMixerTransitionDrawer
[CustomPropertyDrawer(typeof(ManualMixerTransition), true)]
public class ManualMixerTransitionDrawer : TransitionDrawer
{
/************************************************************************************************************************/
/// <summary>Should two lines be used to draw each child?</summary>
public static readonly BoolPref
TwoLineMode = new(
nameof(ManualMixerTransitionDrawer) + "." + nameof(TwoLineMode),
"Two Line Mode",
true);
/************************************************************************************************************************/
/// <summary>The property currently being drawn.</summary>
/// <remarks>
/// Normally each property has its own drawer,
/// but arrays share a single drawer for all elements.
/// </remarks>
public static SerializedProperty CurrentProperty { get; private set; }
/// <summary>The <see cref="ManualMixerTransition{TState}.Animations"/> field.</summary>
public static SerializedProperty CurrentAnimations { get; private set; }
/// <summary>The <see cref="ManualMixerTransition{TState}.Speeds"/> field.</summary>
public static SerializedProperty CurrentSpeeds { get; private set; }
/// <summary>The <see cref="ManualMixerTransition{TState}.SynchronizeChildren"/> field.</summary>
public static SerializedProperty CurrentSynchronizeChildren { get; private set; }
private readonly Dictionary<string, ReorderableList>
PropertyPathToStates = new();
private ReorderableList _MultiSelectDummyList;
/************************************************************************************************************************/
/// <summary>Gather the details of the `property`.</summary>
/// <remarks>
/// 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.
/// </remarks>
protected virtual ReorderableList GatherDetails(SerializedProperty property)
{
InitializeMode(property);
GatherSubProperties(property);
if (property.hasMultipleDifferentValues)
{
return _MultiSelectDummyList ??= new(new List<Object>(), typeof(Object))
{
elementHeight = LineHeight,
displayAdd = false,
displayRemove = false,
footerHeight = 0,
drawHeaderCallback = DoAnimationHeaderGUI,
drawNoneElementCallback = area
=> EditorGUI.LabelField(area, "Multi-editing animations is not supported"),
};
}
if (CurrentAnimations == null)
return null;
var path = property.propertyPath;
if (!PropertyPathToStates.TryGetValue(path, out var states))
{
states = new(CurrentAnimations.serializedObject, CurrentAnimations)
{
drawHeaderCallback = DoChildListHeaderGUI,
elementHeightCallback = GetElementHeight,
drawElementCallback = DoElementGUI,
onAddCallback = OnAddElement,
onRemoveCallback = OnRemoveElement,
onReorderCallbackWithDetails = OnReorderList,
drawFooterCallback = DoChildListFooterGUI,
};
PropertyPathToStates.Add(path, states);
}
states.serializedProperty = CurrentAnimations;
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)
{
CurrentProperty = property;
CurrentAnimations = property.FindPropertyRelative(ManualMixerTransition.AnimationsField);
CurrentSpeeds = property.FindPropertyRelative(ManualMixerTransition.SpeedsField);
CurrentSynchronizeChildren = property.FindPropertyRelative(ManualMixerTransition.SynchronizeChildrenField);
if (!property.hasMultipleDifferentValues &&
CurrentAnimations != null &&
CurrentSpeeds != null &&
CurrentSpeeds.arraySize != 0)
CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
}
/************************************************************************************************************************/
/// <summary>
/// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
/// `function`.
/// </summary>
protected void AddPropertyModifierFunction(GenericMenu menu, string label,
MenuFunctionState state, Action<SerializedProperty> function)
{
Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, state, (property) =>
{
GatherSubProperties(property);
function(property);
});
}
/// <summary>
/// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
/// `function`.
/// </summary>
protected void AddPropertyModifierFunction(GenericMenu menu, string label,
Action<SerializedProperty> function)
{
Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, (property) =>
{
GatherSubProperties(property);
function(property);
});
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var height = EditorGUI.GetPropertyHeight(property, label);
if (property.isExpanded)
{
var states = GatherDetails(property);
if (states != null)
height += StandardSpacing +
states.GetHeight();
if (CurrentAnimations != null)
height -= StandardSpacing +
EditorGUI.GetPropertyHeight(CurrentAnimations, label);
if (CurrentSpeeds != null)
height -= StandardSpacing +
EditorGUI.GetPropertyHeight(CurrentSpeeds, label);
if (CurrentSynchronizeChildren != null)
height -= StandardSpacing +
EditorGUI.GetPropertyHeight(CurrentSynchronizeChildren, label);
}
return height;
}
/************************************************************************************************************************/
private SerializedProperty _RootProperty;
private ReorderableList _CurrentChildList;
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
_RootProperty = null;
base.OnGUI(area, property, label);
if (_RootProperty == null ||
!_RootProperty.isExpanded)
return;
using (new DrawerContext(_RootProperty))
{
if (Context.Transition == null)
return;
_CurrentChildList = GatherDetails(_RootProperty);
if (_CurrentChildList == null)
return;
var indentLevel = EditorGUI.indentLevel;
area.yMin = area.yMax - _CurrentChildList.GetHeight();
EditorGUI.indentLevel++;
area = EditorGUI.IndentedRect(area);
EditorGUI.indentLevel = 0;
_CurrentChildList.DoList(area);
EditorGUI.indentLevel = indentLevel;
TryCollapseArrays();
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoChildPropertyGUI(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty property,
GUIContent label)
{
if (Context.Transition != null)
{
area.height = 0;
// If we find the Animations property, hide it to draw it last.
var path = property.propertyPath;
if (path.EndsWith("." + ManualMixerTransition.AnimationsField))
{
_RootProperty = rootProperty;
return;
}
else if (_RootProperty != null)
{
// If we already found the Animations property, also hide Speeds and Synchronize Children.
if (path.EndsWith("." + ManualMixerTransition.SpeedsField) ||
path.EndsWith("." + ManualMixerTransition.SynchronizeChildrenField))
return;
}
}
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
}
/************************************************************************************************************************/
private static float _SpeedLabelWidth;
private static float _SyncLabelWidth;
/// <summary>Splits the specified `area` into separate sections.</summary>
protected static void SplitListRect(Rect area, bool isHeader,
out Rect animation, out Rect speed, out Rect sync)
{
if (_SpeedLabelWidth == 0)
_SpeedLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Speed");
if (_SyncLabelWidth == 0)
_SyncLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Sync");
var spacing = StandardSpacing;
var syncWidth = isHeader ?
_SyncLabelWidth :
ToggleWidth - spacing;
var speedWidth = _SpeedLabelWidth + _SyncLabelWidth - syncWidth;
if (!isHeader)
{
// Don't use Clamp because the max might be smaller than the min.
var max = Math.Max(area.height, area.width * 0.25f - 30);
speedWidth = Math.Min(speedWidth, max);
}
area.width += spacing;
if (TwoLineMode && !isHeader)
{
animation = area;
area.y += area.height;
sync = StealFromRight(ref area, syncWidth, spacing);
speed = area;
}
else
{
sync = StealFromRight(ref area, syncWidth, spacing);
speed = StealFromRight(ref area, speedWidth, spacing);
animation = area;
}
}
/************************************************************************************************************************/
#region Headers
/************************************************************************************************************************/
/// <summary>Draws the headdings of the child list.</summary>
protected virtual void DoChildListHeaderGUI(Rect area)
{
SplitListRect(area, true, out var animationArea, out var speedArea, out var syncArea);
DoAnimationHeaderGUI(animationArea);
DoSpeedHeaderGUI(speedArea);
DoSyncHeaderGUI(syncArea);
}
/************************************************************************************************************************/
/// <summary>Draws an "Animation" header.</summary>
protected void DoAnimationHeaderGUI(Rect area)
{
using (var label = PooledGUIContent.Acquire("Animation",
$"The animations that will be used for each child state" +
$"\n\nCtrl + Click to allow picking Transition Assets" +
$" (or anything that implements {nameof(ITransition)})"))
{
DoHeaderDropdownGUI(area, CurrentAnimations, label, menu =>
{
menu.AddItem(new(TwoLineMode.MenuItem), TwoLineMode.Value, () =>
{
TwoLineMode.Value = !TwoLineMode.Value;
ReSelectCurrentObjects();
});
});
}
}
/************************************************************************************************************************/
#region Speeds
/************************************************************************************************************************/
/// <summary>Draws a "Speed" header.</summary>
protected void DoSpeedHeaderGUI(Rect area)
{
using (var label = PooledGUIContent.Acquire("Speed", Strings.Tooltips.Speed))
{
DoHeaderDropdownGUI(area, CurrentSpeeds, label, menu =>
{
AddPropertyModifierFunction(menu, "Reset All to 1",
CurrentSpeeds.arraySize == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal,
(_) => CurrentSpeeds.arraySize = 0);
AddPropertyModifierFunction(menu, "Normalize Durations", MenuFunctionState.Normal, 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[CurrentAnimations.arraySize];
if (lengths.Length <= 1)
return;
int nonZeroLengths = 0;
float totalLength = 0;
float totalSpeed = 0;
for (int i = 0; i < lengths.Length; i++)
{
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
if (AnimancerUtilities.TryGetLength(state, out var length) &&
length > 0)
{
nonZeroLengths++;
totalLength += length;
lengths[i] = 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;
InitializeSpeeds(speedCount);
for (int i = 0; i < lengths.Length; i++)
{
if (lengths[i] == 0)
continue;
CurrentSpeeds.GetArrayElementAtIndex(i).floatValue = averageSpeed * lengths[i] / averageLength;
}
TryCollapseArrays();
}
/************************************************************************************************************************/
/// <summary>
/// Initializes 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 InitializeSpeeds(int start)
{
var count = CurrentSpeeds.arraySize;
while (start < count)
CurrentSpeeds.GetArrayElementAtIndex(start++).floatValue = 1;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Sync
/************************************************************************************************************************/
/// <summary>Draws a "Sync" header.</summary>
protected void DoSyncHeaderGUI(Rect area)
{
using (var label = PooledGUIContent.Acquire("Sync",
"Determines which child states have their normalized times constantly synchronized"))
{
DoHeaderDropdownGUI(area, CurrentSpeeds, label, menu =>
{
var syncCount = CurrentSynchronizeChildren.arraySize;
var allState = syncCount == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal;
AddPropertyModifierFunction(menu, "All", allState,
(_) => CurrentSynchronizeChildren.arraySize = 0);
var syncNone = syncCount == CurrentAnimations.arraySize;
if (syncNone)
{
for (int i = 0; i < syncCount; i++)
{
if (CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue)
{
syncNone = false;
break;
}
}
}
var noneState = syncNone ? MenuFunctionState.Selected : MenuFunctionState.Normal;
AddPropertyModifierFunction(menu, "None", noneState, (_) =>
{
var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
for (int i = 0; i < count; i++)
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
});
AddPropertyModifierFunction(menu, "Invert", MenuFunctionState.Normal, (_) =>
{
var count = CurrentSynchronizeChildren.arraySize;
for (int i = 0; i < count; i++)
{
var property = CurrentSynchronizeChildren.GetArrayElementAtIndex(i);
property.boolValue = !property.boolValue;
}
var newCount = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
for (int i = count; i < newCount; i++)
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
});
AddPropertyModifierFunction(menu, "Non-Stationary", MenuFunctionState.Normal, (_) =>
{
var count = CurrentAnimations.arraySize;
for (int i = 0; i < count; i++)
{
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
if (state == null)
continue;
if (i >= syncCount)
{
CurrentSynchronizeChildren.arraySize = i + 1;
for (int j = syncCount; j < i; j++)
CurrentSynchronizeChildren.GetArrayElementAtIndex(j).boolValue = true;
syncCount = i + 1;
}
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue =
AnimancerUtilities.TryGetAverageVelocity(state, out var velocity) &&
velocity != default;
}
TryCollapseSync();
});
});
}
}
/************************************************************************************************************************/
private static void SyncNone()
{
var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
for (int i = 0; i < count; i++)
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>Draws the GUI for a header dropdown button.</summary>
public static void DoHeaderDropdownGUI(
Rect area,
SerializedProperty property,
GUIContent content,
Action<GenericMenu> populateMenu)
{
if (property != null)
EditorGUI.BeginProperty(area, GUIContent.none, property);
if (populateMenu != null)
{
if (EditorGUI.DropdownButton(area, content, FocusType.Passive))
{
var menu = new GenericMenu();
populateMenu(menu);
menu.ShowAsContext();
}
}
else
{
GUI.Label(area, content);
}
if (property != null)
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
/// <summary>Draws the footer of the child list.</summary>
protected virtual void DoChildListFooterGUI(Rect area)
{
ReorderableList.defaultBehaviours.DrawFooter(area, _CurrentChildList);
EditorGUI.BeginChangeCheck();
area.xMax = EditorGUIUtility.labelWidth + IndentSize;
area.y++;
area.height = LineHeight;
using (var label = PooledGUIContent.Acquire("Count"))
{
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var labelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = CalculateLabelWidth(label.text);
var count = EditorGUI.DelayedIntField(area, label, _CurrentChildList.count);
if (EditorGUI.EndChangeCheck())
ResizeList(count);
EditorGUIUtility.labelWidth = labelWidth;
EditorGUI.indentLevel = indentLevel;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>Calculates the height of the state at the specified `index`.</summary>
protected virtual float GetElementHeight(int index)
=> TwoLineMode
? LineHeight * 2
: 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 > CurrentAnimations.arraySize)
return;
area.height = LineHeight;
var state = CurrentAnimations.GetArrayElementAtIndex(index);
var speed = CurrentSpeeds.arraySize > 0
? CurrentSpeeds.GetArrayElementAtIndex(index)
: null;
DoElementGUI(area, index, state, speed);
}
/************************************************************************************************************************/
/// <summary>Draws the GUI of the animation at the specified `index`.</summary>
protected virtual void DoElementGUI(Rect area, int index,
SerializedProperty animation, SerializedProperty speed)
{
SplitListRect(area, false, out var animationArea, out var speedArea, out var syncArea);
DoAnimationField(animationArea, animation);
DoSpeedFieldGUI(speedArea, speed, index);
DoSyncToggleGUI(syncArea, index);
}
/************************************************************************************************************************/
/// <summary>
/// Draws an <see cref="EditorGUI.ObjectField(Rect, GUIContent, Object, Type, bool)"/> that accepts
/// <see cref="AnimationClip"/>s and <see cref="ITransition"/>s
/// </summary>
public static void DoAnimationField(Rect area, SerializedProperty property)
{
EditorGUI.BeginProperty(area, GUIContent.none, property);
var targetObject = property.serializedObject.targetObject;
var oldReference = property.objectReferenceValue;
var currentEvent = Event.current;
var isDrag =
currentEvent.type == EventType.DragUpdated ||
currentEvent.type == EventType.DragPerform;
var type =
isDrag ||
currentEvent.control ||
currentEvent.commandName == "ObjectSelectorUpdated"
? typeof(Object)
: typeof(AnimationClip);
var allowSceneObjects = targetObject != null && !EditorUtility.IsPersistent(targetObject);
EditorGUI.BeginChangeCheck();
var newReference = EditorGUI.ObjectField(
area,
GUIContent.none,
oldReference,
type,
allowSceneObjects);
if (EditorGUI.EndChangeCheck())
{
if (newReference == null ||
(IsClipOrTransition(newReference) && newReference != targetObject))
property.objectReferenceValue = newReference;
}
if (isDrag && area.Contains(currentEvent.mousePosition))
{
var objects = DragAndDrop.objectReferences;
if (objects.Length != 1 ||
!IsClipOrTransition(objects[0]) ||
objects[0] == targetObject)
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
}
EditorGUI.EndProperty();
}
/// <summary>Is the `clipOrTransition` an <see cref="AnimationClip"/> or <see cref="ITransition"/>?</summary>
public static bool IsClipOrTransition(object clipOrTransition)
=> clipOrTransition is AnimationClip || clipOrTransition is ITransition;
/************************************************************************************************************************/
private static CompactUnitConversionCache _XSuffixCache;
/// <summary>
/// Draws a toggle to enable or disable <see cref="ManualMixerState.SynchronizedChildren"/> for the child
/// at the specified `index`.
/// </summary>
protected void DoSpeedFieldGUI(Rect area, SerializedProperty speed, int index)
{
if (speed != null)
{
EditorGUI.PropertyField(area, speed, GUIContent.none);
}
else// If this element doesn't have its own speed property, just show 1.
{
EditorGUI.BeginProperty(area, GUIContent.none, CurrentSpeeds);
_XSuffixCache ??= new("x");
var value = UnitsAttributeDrawer.DoSpecialFloatField(
area,
null,
1,
_XSuffixCache);
// Middle Click toggles from 1 to -1.
if (TryUseClickEvent(area, 2))
value = -1;
if (value != 1)
{
CurrentSpeeds.InsertArrayElementAtIndex(0);
CurrentSpeeds.GetArrayElementAtIndex(0).floatValue = 1;
CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
CurrentSpeeds.GetArrayElementAtIndex(index).floatValue = value;
}
EditorGUI.EndProperty();
}
}
/************************************************************************************************************************/
/// <summary>
/// Draws a toggle to enable or disable <see cref="ManualMixerState.SynchronizedChildren"/>
/// for the child at the specified `index`.
/// </summary>
protected void DoSyncToggleGUI(Rect area, int index)
{
var syncProperty = CurrentSynchronizeChildren;
var syncFlagCount = syncProperty.arraySize;
var enabled = true;
if (index < syncFlagCount)
{
syncProperty = syncProperty.GetArrayElementAtIndex(index);
enabled = syncProperty.boolValue;
}
EditorGUI.BeginChangeCheck();
EditorGUI.BeginProperty(area, GUIContent.none, syncProperty);
enabled = GUI.Toggle(area, enabled, GUIContent.none);
EditorGUI.EndProperty();
if (EditorGUI.EndChangeCheck())
{
if (index < syncFlagCount)
{
syncProperty.boolValue = enabled;
}
else
{
syncProperty.arraySize = index + 1;
for (int i = syncFlagCount; i < index; i++)
{
syncProperty.GetArrayElementAtIndex(i).boolValue = true;
}
syncProperty.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>
private void OnAddElement(ReorderableList list)
{
var index = list.index;
if (index < 0 || Event.current.button == 1)// Right Click to add at the end.
{
index = CurrentAnimations.arraySize - 1;
if (index < 0)
index = 0;
}
OnAddElement(index);
}
/// <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(int index)
{
CurrentAnimations.InsertArrayElementAtIndex(index);
if (CurrentSpeeds.arraySize > 0)
CurrentSpeeds.InsertArrayElementAtIndex(index);
if (CurrentSynchronizeChildren.arraySize > index)
CurrentSynchronizeChildren.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;
Serialization.RemoveArrayElement(CurrentAnimations, index);
if (CurrentSpeeds.arraySize > index)
Serialization.RemoveArrayElement(CurrentSpeeds, index);
if (CurrentSynchronizeChildren.arraySize > index)
Serialization.RemoveArrayElement(CurrentSynchronizeChildren, index);
}
/************************************************************************************************************************/
/// <summary>Sets the number of items in the child list.</summary>
protected virtual void ResizeList(int size)
{
CurrentAnimations.arraySize = size;
if (CurrentSpeeds.arraySize > size)
CurrentSpeeds.arraySize = size;
if (CurrentSynchronizeChildren.arraySize > size)
CurrentSynchronizeChildren.arraySize = size;
}
/************************************************************************************************************************/
/// <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);
var syncCount = CurrentSynchronizeChildren.arraySize;
if (Math.Max(oldIndex, newIndex) >= syncCount)
{
CurrentSynchronizeChildren.arraySize++;
CurrentSynchronizeChildren.GetArrayElementAtIndex(syncCount).boolValue = true;
CurrentSynchronizeChildren.arraySize = newIndex + 1;
}
CurrentSynchronizeChildren.MoveArrayElement(oldIndex, newIndex);
}
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="TryCollapseSpeeds"/> and <see cref="TryCollapseSync"/>.
/// </summary>
public static void TryCollapseArrays()
{
if (CurrentProperty == null ||
CurrentProperty.hasMultipleDifferentValues)
return;
TryCollapseSpeeds();
TryCollapseSync();
}
/************************************************************************************************************************/
/// <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 property = CurrentSpeeds;
if (property == null)
return;
var speedCount = property.arraySize;
if (speedCount <= 0)
return;
for (int i = 0; i < speedCount; i++)
{
if (property.GetArrayElementAtIndex(i).floatValue != 1)
return;
}
property.arraySize = 0;
}
/************************************************************************************************************************/
/// <summary>
/// Removes any true elements from the end of the <see cref="CurrentSynchronizeChildren"/> array.
/// </summary>
public static void TryCollapseSync()
{
var property = CurrentSynchronizeChildren;
if (property == null)
return;
var count = property.arraySize;
var changed = false;
for (int i = count - 1; i >= 0; i--)
{
if (property.GetArrayElementAtIndex(i).boolValue)
{
count = i;
changed = true;
}
else
{
break;
}
}
if (changed)
property.arraySize = count;
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 86e53fc4d232c3c48b32aa883a16005f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,223 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/MixerTransition2DDrawer
[CustomPropertyDrawer(typeof(MixerTransition2D), true)]
public class MixerTransition2DDrawer : MixerTransitionDrawer
{
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="MixerTransition2DDrawer"/> using a wider
/// `thresholdWidth` than usual to accomodate both the X and Y values.
/// </summary>
public MixerTransition2DDrawer()
: base(StandardThresholdWidth * 2 + 20)
{ }
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AddThresholdFunctionsToMenu(GenericMenu menu)
{
AddCalculateThresholdsFunction(menu, "From Velocity/XY", (state, threshold) =>
{
if (AnimancerUtilities.TryGetAverageVelocity(state, out var velocity))
return new(velocity.x, velocity.y);
else
return new(float.NaN, float.NaN);
});
AddCalculateThresholdsFunction(menu, "From Velocity/XZ", (state, threshold) =>
{
if (AnimancerUtilities.TryGetAverageVelocity(state, out var velocity))
return new(velocity.x, velocity.z);
else
return new(float.NaN, float.NaN);
});
AddCalculateThresholdsFunctionPerAxis(menu, "From Speed",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.magnitude
: float.NaN);
AddCalculateThresholdsFunctionPerAxis(menu, "From Velocity X",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.x
: float.NaN);
AddCalculateThresholdsFunctionPerAxis(menu, "From Velocity Y",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.y
: float.NaN);
AddCalculateThresholdsFunctionPerAxis(menu, "From Velocity Z",
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
? velocity.z
: float.NaN);
AddCalculateThresholdsFunctionPerAxis(menu, "From Angular Speed (Rad)",
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
? speed
: float.NaN);
AddCalculateThresholdsFunctionPerAxis(menu, "From Angular Speed (Deg)",
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
? speed * Mathf.Rad2Deg
: float.NaN);
AddPropertyModifierFunction(menu, "Initialize 4 Directions", Initialize4Directions);
AddPropertyModifierFunction(menu, "Initialize 8 Directions", Initialize8Directions);
AddPropertyModifierFunction(menu, "Normalize", NormalizeThresholds);
}
/************************************************************************************************************************/
private void Initialize4Directions(SerializedProperty property)
{
var oldSpeedCount = CurrentSpeeds.arraySize;
CurrentAnimations.arraySize = CurrentThresholds.arraySize = CurrentSpeeds.arraySize = 5;
CurrentThresholds.GetArrayElementAtIndex(0).vector2Value = default;
CurrentThresholds.GetArrayElementAtIndex(1).vector2Value = Vector2.up;
CurrentThresholds.GetArrayElementAtIndex(2).vector2Value = Vector2.right;
CurrentThresholds.GetArrayElementAtIndex(3).vector2Value = Vector2.down;
CurrentThresholds.GetArrayElementAtIndex(4).vector2Value = Vector2.left;
InitializeSpeeds(oldSpeedCount);
var type = property.FindPropertyRelative(MixerTransition2D.TypeField);
type.enumValueIndex = (int)MixerTransition2D.MixerType.Directional;
}
/************************************************************************************************************************/
private void Initialize8Directions(SerializedProperty property)
{
var oldSpeedCount = CurrentSpeeds.arraySize;
var diagonal = 1 / Mathf.Sqrt(2);
CurrentAnimations.arraySize = CurrentThresholds.arraySize = CurrentSpeeds.arraySize = 9;
CurrentThresholds.GetArrayElementAtIndex(0).vector2Value = default;
CurrentThresholds.GetArrayElementAtIndex(1).vector2Value = Vector2.up;
CurrentThresholds.GetArrayElementAtIndex(2).vector2Value = new(diagonal, diagonal);
CurrentThresholds.GetArrayElementAtIndex(3).vector2Value = Vector2.right;
CurrentThresholds.GetArrayElementAtIndex(4).vector2Value = new(diagonal, -diagonal);
CurrentThresholds.GetArrayElementAtIndex(5).vector2Value = Vector2.down;
CurrentThresholds.GetArrayElementAtIndex(6).vector2Value = new(-diagonal, -diagonal);
CurrentThresholds.GetArrayElementAtIndex(7).vector2Value = Vector2.left;
CurrentThresholds.GetArrayElementAtIndex(8).vector2Value = new(-diagonal, diagonal);
InitializeSpeeds(oldSpeedCount);
var type = property.FindPropertyRelative(MixerTransition2D.TypeField);
type.enumValueIndex = (int)MixerTransition2D.MixerType.Directional;
}
/************************************************************************************************************************/
private void NormalizeThresholds(SerializedProperty property)
{
var thresholdCount = CurrentThresholds.arraySize;
if (thresholdCount == 0)
return;
var largestSquaredMagnitude = 0f;
for (int i = 0; i < thresholdCount; i++)
{
var threshold = CurrentThresholds.GetArrayElementAtIndex(i).vector2Value;
var squaredMagnitude = threshold.sqrMagnitude;
if (largestSquaredMagnitude < squaredMagnitude)
largestSquaredMagnitude = squaredMagnitude;
}
if (largestSquaredMagnitude == 0f)
return;
var inverseSquaredMagnitude = 1 / Mathf.Sqrt(largestSquaredMagnitude);
for (int i = 0; i < thresholdCount; i++)
{
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
threshold.vector2Value *= inverseSquaredMagnitude;
}
}
/************************************************************************************************************************/
private void AddCalculateThresholdsFunction(
GenericMenu menu,
string label,
Func<Object, Vector2, Vector2> calculateThreshold)
{
var functionState = CurrentAnimations == null || CurrentThresholds == null
? MenuFunctionState.Disabled
: MenuFunctionState.Normal;
AddPropertyModifierFunction(menu, label, functionState, property =>
{
GatherSubProperties(property);
if (CurrentAnimations == null ||
CurrentThresholds == null)
return;
var count = CurrentAnimations.arraySize;
for (int i = 0; i < count; i++)
{
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
if (state == null)
continue;
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
var value = calculateThreshold(state, threshold.vector2Value);
if (!AnimancerEditorUtilities.IsNaN(value))
threshold.vector2Value = value;
}
});
}
/************************************************************************************************************************/
private void AddCalculateThresholdsFunctionPerAxis(GenericMenu menu, string label,
Func<Object, float, float> calculateThreshold)
{
AddCalculateThresholdsFunction(menu, "X/" + label, 0, calculateThreshold);
AddCalculateThresholdsFunction(menu, "Y/" + label, 1, calculateThreshold);
}
private void AddCalculateThresholdsFunction(GenericMenu menu, string label, int axis,
Func<Object, float, float> calculateThreshold)
{
AddPropertyModifierFunction(menu, label, (property) =>
{
var count = CurrentAnimations.arraySize;
for (int i = 0; i < count; i++)
{
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
if (state == null)
continue;
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
var value = threshold.vector2Value;
var newValue = calculateThreshold(state, value[axis]);
if (!float.IsNaN(newValue))
value[axis] = newValue;
threshold.vector2Value = value;
}
});
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5f0aef2af7c620749ad340f2545e0832
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,262 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="MixerTransition{TMixer, TParameter}"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions">
/// Transitions</see> and
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/mixers">
/// Mixers</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/MixerTransitionDrawer
public class MixerTransitionDrawer : ManualMixerTransitionDrawer
{
/************************************************************************************************************************/
/// <summary>The number of horizontal pixels the "Threshold" label occupies.</summary>
private readonly float ThresholdWidth;
/************************************************************************************************************************/
private static float _StandardThresholdWidth;
/// <summary>
/// The number of horizontal pixels the word "Threshold" occupies when drawn with the
/// <see cref="EditorStyles.popup"/> style.
/// </summary>
protected static float StandardThresholdWidth
{
get
{
if (_StandardThresholdWidth == 0)
_StandardThresholdWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Threshold");
return _StandardThresholdWidth;
}
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="MixerTransitionDrawer"/> using the default <see cref="StandardThresholdWidth"/>.
/// </summary>
public MixerTransitionDrawer()
: this(StandardThresholdWidth)
{ }
/// <summary>
/// Creates a new <see cref="MixerTransitionDrawer"/> using a custom width for its threshold labels.
/// </summary>
protected MixerTransitionDrawer(float thresholdWidth)
=> ThresholdWidth = thresholdWidth;
/************************************************************************************************************************/
/// <summary>
/// The serialized <see cref="MixerTransition{TMixer, TParameter}.Thresholds"/> of the
/// <see cref="ManualMixerTransition.ManualMixerTransitionDrawer.CurrentProperty"/>.
/// </summary>
protected static SerializedProperty CurrentThresholds { get; private set; }
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void GatherSubProperties(SerializedProperty property)
{
base.GatherSubProperties(property);
CurrentThresholds = property.FindPropertyRelative(MixerTransition2D.ThresholdsField);
if (CurrentAnimations == null ||
CurrentThresholds == null ||
property.hasMultipleDifferentValues)
return;
var count = Math.Max(CurrentAnimations.arraySize, CurrentThresholds.arraySize);
CurrentAnimations.arraySize = count;
CurrentThresholds.arraySize = count;
if (CurrentSpeeds != null &&
CurrentSpeeds.arraySize != 0)
CurrentSpeeds.arraySize = count;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var height = base.GetPropertyHeight(property, label);
if (property.isExpanded)
{
if (CurrentThresholds != null)
{
height -= StandardSpacing +
EditorGUI.GetPropertyHeight(CurrentThresholds, label);
}
}
return height;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoChildPropertyGUI(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty property,
GUIContent label)
{
if (property.propertyPath.EndsWith($".{MixerTransition2D.ThresholdsField}"))
return;
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
}
/************************************************************************************************************************/
/// <summary>Splits the specified `area` into separate sections.</summary>
protected void SplitListRect(
Rect area,
bool isHeader,
out Rect animation,
out Rect threshold,
out Rect speed,
out Rect sync)
{
SplitListRect(area, isHeader, out animation, out speed, out sync);
if (TwoLineMode && !isHeader)
{
threshold = StealFromLeft(ref speed, ThresholdWidth, StandardSpacing);
}
else
{
threshold = animation;
var xMin = threshold.xMin = EditorGUIUtility.labelWidth + IndentSize;
animation.xMax = xMin - StandardSpacing;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoChildListHeaderGUI(Rect area)
{
SplitListRect(
area,
true,
out var animationArea,
out var thresholdArea,
out var speedArea,
out var syncArea);
DoAnimationHeaderGUI(animationArea);
var attribute = AttributeCache<ThresholdLabelAttribute>.FindAttribute(CurrentThresholds);
var text = attribute != null
? attribute.Label
: "Threshold";
using (var label = PooledGUIContent.Acquire(text,
"The parameter values at which each child state will be fully active"))
DoHeaderDropdownGUI(thresholdArea, CurrentThresholds, label, AddThresholdFunctionsToMenu);
DoSpeedHeaderGUI(speedArea);
DoSyncHeaderGUI(syncArea);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoElementGUI(
Rect area,
int index,
SerializedProperty animation,
SerializedProperty speed)
{
SplitListRect(
area,
false,
out var animationArea,
out var thresholdArea,
out var speedArea,
out var syncArea);
DoAnimationField(animationArea, animation);
DoThresholdGUI(thresholdArea, index);
DoSpeedFieldGUI(speedArea, speed, index);
DoSyncToggleGUI(syncArea, index);
}
/************************************************************************************************************************/
/// <summary>Draws the GUI of the threshold at the specified `index`.</summary>
protected virtual void DoThresholdGUI(Rect area, int index)
{
var threshold = CurrentThresholds.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(area, threshold, GUIContent.none);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnAddElement(int index)
{
base.OnAddElement(index);
if (CurrentThresholds.arraySize > 0)
CurrentThresholds.InsertArrayElementAtIndex(index);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnRemoveElement(ReorderableList list)
{
base.OnRemoveElement(list);
Serialization.RemoveArrayElement(CurrentThresholds, list.index);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void ResizeList(int size)
{
base.ResizeList(size);
CurrentThresholds.arraySize = size;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnReorderList(ReorderableList list, int oldIndex, int newIndex)
{
base.OnReorderList(list, oldIndex, newIndex);
CurrentThresholds.MoveArrayElement(oldIndex, newIndex);
}
/************************************************************************************************************************/
/// <summary>Adds functions to the `menu` relating to the thresholds.</summary>
protected virtual void AddThresholdFunctionsToMenu(GenericMenu menu) { }
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8e5c41022aa077243bec1efac972f890
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,319 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/PlayableAssetTransitionDrawer
[CustomPropertyDrawer(typeof(PlayableAssetTransition), true)]
public class PlayableAssetTransitionDrawer : TransitionDrawer
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="PlayableAssetTransitionDrawer"/>.</summary>
public PlayableAssetTransitionDrawer()
: base(PlayableAssetTransition.AssetField)
{ }
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
_CurrentAsset = null;
var height = base.GetPropertyHeight(property, label);
if (property.isExpanded)
{
var bindings = property.FindPropertyRelative(PlayableAssetTransition.BindingsField);
if (bindings != null)
{
bindings.isExpanded = true;
height -= StandardSpacing + LineHeight;
}
}
return height;
}
/************************************************************************************************************************/
private PlayableAsset _CurrentAsset;
/// <inheritdoc/>
protected override void DoMainPropertyGUI(
Rect area,
out Rect labelArea,
SerializedProperty rootProperty,
SerializedProperty mainProperty)
{
_CurrentAsset = mainProperty.objectReferenceValue as PlayableAsset;
base.DoMainPropertyGUI(area, out labelArea, rootProperty, mainProperty);
}
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
base.OnGUI(area, property, label);
_CurrentAsset = null;
}
/// <inheritdoc/>
protected override void DoChildPropertyGUI(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty property,
GUIContent label)
{
var path = property.propertyPath;
if (path.EndsWith($".{PlayableAssetTransition.BindingsField}"))
{
DoBindingsGUI(ref area, property, label);
return;
}
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
}
/************************************************************************************************************************/
private void DoBindingsGUI(
ref Rect area,
SerializedProperty property,
GUIContent label)
{
var outputCount = GetOutputCount(out var outputEnumerator, out var firstBindingIsAnimation);
// Bindings.
property.Next(true);
// Array.
property.Next(true);
// Array Size.
DoBindingsCountGUI(area, property, label, outputCount, firstBindingIsAnimation, out var bindingCount);
EditorGUI.indentLevel++;
for (int i = 0; i < bindingCount; i++)
{
NextVerticalArea(ref area);
if (!property.Next(false))
{
EditorGUI.LabelField(area, "Binding Count Mismatch");
break;
}
// First Array Item.
if (outputEnumerator != null && outputEnumerator.MoveNext())
{
DoBindingGUI(area, property, label, outputEnumerator, i);
}
else
{
var color = GUI.color;
GUI.color = WarningFieldColor;
EditorGUI.PropertyField(area, property, false);
GUI.color = color;
}
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
private int GetOutputCount(
out IEnumerator<PlayableBinding> outputEnumerator,
out bool firstBindingIsAnimation)
{
var outputCount = 0;
firstBindingIsAnimation = false;
if (_CurrentAsset != null)
{
var outputs = _CurrentAsset.outputs;
_CurrentAsset = null;
outputEnumerator = outputs.GetEnumerator();
while (outputEnumerator.MoveNext())
{
PlayableAssetState.GetBindingDetails(
outputEnumerator.Current, out var _, out var _, out var isMarkers);
if (isMarkers)
continue;
if (outputCount == 0 && outputEnumerator.Current.outputTargetType == typeof(Animator))
firstBindingIsAnimation = true;
outputCount++;
}
outputEnumerator = outputs.GetEnumerator();
}
else outputEnumerator = null;
return outputCount;
}
/************************************************************************************************************************/
private void DoBindingsCountGUI(
Rect area,
SerializedProperty property,
GUIContent label,
int outputCount,
bool firstBindingIsAnimation,
out int bindingCount)
{
var color = GUI.color;
var sizeArea = area;
bindingCount = property.intValue;
// Button to fix the number of bindings in the array.
if (bindingCount != outputCount && !(bindingCount == 0 && outputCount == 1 && firstBindingIsAnimation))
{
GUI.color = WarningFieldColor;
var labelText = label.text;
var style = MiniButtonStyle;
var countLabel = outputCount.ToStringCached();
var fixSizeWidth = style.CalculateWidth(countLabel);
var fixSizeArea = StealFromRight(
ref sizeArea, fixSizeWidth, StandardSpacing);
if (GUI.Button(fixSizeArea, countLabel, style))
property.intValue = bindingCount = outputCount;
label.text = labelText;
}
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(sizeArea, property, label, false);
if (EditorGUI.EndChangeCheck())
bindingCount = property.intValue;
GUI.color = color;
}
/************************************************************************************************************************/
private void DoBindingGUI(
Rect area,
SerializedProperty property,
GUIContent label,
IEnumerator<PlayableBinding> outputEnumerator,
int trackIndex)
{
CheckIfSkip:
PlayableAssetState.GetBindingDetails(
outputEnumerator.Current,
out var name,
out var bindingType,
out var isMarkers);
if (isMarkers)
{
outputEnumerator.MoveNext();
goto CheckIfSkip;
}
label.text = name;
var targetObject = property.serializedObject.targetObject;
var allowSceneObjects =
targetObject != null &&
!EditorUtility.IsPersistent(targetObject);
label = EditorGUI.BeginProperty(area, label, property);
var fieldArea = area;
var obj = property.objectReferenceValue;
var objExists = obj != null;
if (objExists)
DoRemoveButtonIfNecessary(ref fieldArea, property, trackIndex, ref bindingType, ref obj);
if (bindingType != null || objExists)
{
property.objectReferenceValue =
EditorGUI.ObjectField(fieldArea, label, obj, bindingType, allowSceneObjects);
}
else
{
EditorGUI.LabelField(fieldArea, label);
}
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
private static void DoRemoveButtonIfNecessary(
ref Rect area,
SerializedProperty property,
int trackIndex,
ref Type bindingType,
ref Object obj)
{
if (trackIndex == 0 && bindingType == typeof(Animator))
{
DoRemoveButton(ref area, property, ref obj,
"This Animation Track is the first Track" +
" so it will automatically control the Animancer output" +
" and likely doesn't need a binding.");
}
else if (bindingType == null)
{
DoRemoveButton(ref area, property, ref obj,
"This Track doesn't need a binding.");
bindingType = typeof(Object);
}
else if (!bindingType.IsAssignableFrom(obj.GetType()))
{
DoRemoveButton(ref area, property, ref obj,
"This binding has the wrong type for this Track.");
}
}
/************************************************************************************************************************/
private static void DoRemoveButton(
ref Rect area,
SerializedProperty property,
ref Object obj,
string tooltip)
{
GUI.color = WarningFieldColor;
var removeArea = StealFromRight(
ref area,
area.height,
StandardSpacing);
if (GUI.Button(
removeArea,
AnimancerIcons.ClearIcon(tooltip),
NoPaddingButtonStyle))
property.objectReferenceValue = obj = null;
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bbdbd2675cb58604a8b719a36eb1adad
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,276 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor
{
/// <summary><see cref="PropertyDrawer"/> for <see cref="ControllerState.SerializableParameterBindings"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableParameterBindingsDrawer
[CustomPropertyDrawer(typeof(ControllerState.SerializableParameterBindings), true)]
public class SerializableParameterBindingsDrawer : PropertyDrawer
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
if (!property.isExpanded)
return LineHeight;
GetFields(property, out var mode, out var bindings);
var count = bindings.arraySize;
if (count > 0 && mode.boolValue)
count = 1 + Mathf.CeilToInt(count * 0.5f);
return CalculateHeight(count + 3);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
area.height = LineHeight;
var isExpanded = EditorGUI.PropertyField(area, property, label, false);
if (!isExpanded)
return;
EditorGUI.indentLevel++;
NextVerticalArea(ref area);
GetFields(property, out var mode, out var bindings);
var parameterList = GetContextParameterList(property);
var bindingCount = bindings.arraySize;
DoModeGUI(ref area, mode, bindingCount, parameterList);
var modeValue = mode.boolValue;
DoBindingCountGUI(ref area, bindings, modeValue, ref bindingCount, parameterList);
DoBindingsGUI(area, bindings, modeValue, bindingCount, parameterList);
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
private void DoModeGUI(
ref Rect area,
SerializedProperty mode,
int bindingCount,
string parameterList)
{
using var label = PooledGUIContent.Acquire();
if (bindingCount == 0)
{
label.text = "Bind All Parameters";
label.tooltip =
"If enabled, all parameters in the Animator Controller will be bound" +
" to Animancer parameters with the same name and the Bindings array can be left empty." +
parameterList;
}
else
{
label.text = "Rebind Names";
label.tooltip =
"If enabled, the Bindings array will be taken in pairs so that each" +
" Animator Controller parameter can be bound to an Animancer Parameter with different name." +
parameterList;
}
EditorGUI.PropertyField(area, mode, label, false);
NextVerticalArea(ref area);
}
/************************************************************************************************************************/
private void DoBindingCountGUI(
ref Rect area,
SerializedProperty bindings,
bool mode,
ref int bindingCount,
string parameterList)
{
using var label = PooledGUIContent.Acquire(
"Bindings",
"The names of parameters in the Animator Controller to bind to Animancer parameters." +
"\n<> Leave this array empty and enable the toggle if you want to bind all parameters." +
parameterList);
var newCount = bindingCount;
if (mode && bindingCount > 0)
newCount /= 2;
newCount = EditorGUI.DelayedIntField(area, label, newCount);
if (newCount < 0)
newCount = 0;
else if (mode && newCount > 0)
newCount *= 2;
if (bindingCount != newCount)
{
bindingCount = newCount;
bindings.arraySize = newCount;
}
NextVerticalArea(ref area);
}
/************************************************************************************************************************/
private void DoBindingsGUI(
Rect area,
SerializedProperty bindings,
bool mode,
int bindingCount,
string parameterList)
{
if (bindingCount <= 0)
return;
using var label = PooledGUIContent.Acquire();
if (mode)
{
var controllerArea = EditorGUI.IndentedRect(area);
controllerArea.xMin -= 1;// Not sure why.
var animancerArea = StealFromRight(ref controllerArea, controllerArea.width * 0.5f, StandardSpacing);
label.text = "Controller";
label.tooltip = "The name of the Animator Controller parameter" + parameterList;
GUI.Label(controllerArea, label);
label.text = "Animancer";
label.tooltip = "The name of the Animancer parameter";
GUI.Label(animancerArea, label);
NextVerticalArea(ref controllerArea);
NextVerticalArea(ref animancerArea);
for (int i = 0; i < bindingCount; i++)
{
DoBindingGUI(ref controllerArea, bindings, i, GUIContent.none);
i++;
DoBindingGUI(ref animancerArea, bindings, i, GUIContent.none);
}
}
else
{
EditorGUI.indentLevel++;
label.tooltip = "";
for (int i = 0; i < bindingCount; i++)
{
label.text = "Binding " + i;
DoBindingGUI(ref area, bindings, i, label);
}
EditorGUI.indentLevel--;
}
}
/************************************************************************************************************************/
private static void DoBindingGUI(ref Rect area, SerializedProperty bindings, int index, GUIContent label)
{
var indentLevel = EditorGUI.indentLevel;
if (string.IsNullOrEmpty(label.text))
EditorGUI.indentLevel = 0;
var binding = bindings.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(area, binding, label);
NextVerticalArea(ref area);
EditorGUI.indentLevel = indentLevel;
}
/************************************************************************************************************************/
private void GetFields(
SerializedProperty root,
out SerializedProperty mode,
out SerializedProperty bindings)
{
mode = root.FindPropertyRelative(ControllerState.SerializableParameterBindings.ModeFieldName);
bindings = root.FindPropertyRelative(ControllerState.SerializableParameterBindings.BindingsFieldName);
}
/************************************************************************************************************************/
private string GetContextParameterList(SerializedProperty property)
{
var path = property.propertyPath;
var lastDot = path.LastIndexOf('.');
if (lastDot < 0)
return null;
path = path[..(lastDot + 1)] + ControllerTransition.ControllerFieldName;
property = property.serializedObject.FindProperty(path);
if (property == null ||
property.objectReferenceValue is not AnimatorController animatorController)
return null;
return GetParameterList(animatorController);
}
private readonly Dictionary<AnimatorController, string>
ControllerToParameterList = new();
private string GetParameterList(AnimatorController animatorController)
{
if (animatorController == null)
return null;
if (ControllerToParameterList.TryGetValue(animatorController, out var parameterList))
return parameterList;
var text = StringBuilderPool.Instance.Acquire();
var parameters = animatorController.parameters;
if (parameters.Length > 0)
{
text.Append("\n\nParameters in ")
.Append(animatorController.name)
.Append(':');
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
text.Append("\n<> ")
.Append(parameter.type)
.Append(' ')
.Append(parameter.name);
}
}
parameterList = text.ReleaseToString();
ControllerToParameterList.Add(animatorController, parameterList);
return parameterList;
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 198f2234f919be64992de958b4dbef90
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TransitionAssetReferenceDrawer
[CustomPropertyDrawer(typeof(TransitionAssetReference), true)]
public class TransitionAssetReferenceDrawer : TransitionDrawer
{
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 303b872471b0bca46b23add07d8a9e6f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,646 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using Animancer.Editor.Previews;
using Animancer.Units.Editor;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="ITransition"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TransitionDrawer
[CustomPropertyDrawer(typeof(ITransition), true)]
[CustomPropertyDrawer(typeof(TransitionAssetBase), true)]
public class TransitionDrawer : PropertyDrawer,
IPolymorphic
{
/************************************************************************************************************************/
/// <summary>The visual state of a drawer.</summary>
private enum Mode
{
Uninitialized,
Normal,
AlwaysExpanded,
}
/// <summary>The current state of this drawer.</summary>
private Mode _Mode;
/************************************************************************************************************************/
/// <summary>
/// If set, the field with this name will be drawn on the header line
/// with the foldout arrow instead of in its regular place.
/// </summary>
protected readonly string MainPropertyName;
/// <summary>"." + <see cref="MainPropertyName"/> (to avoid creating garbage repeatedly).</summary>
protected readonly string MainPropertyPathSuffix;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionDrawer"/>.</summary>
public TransitionDrawer() { }
/// <summary>Creates a new <see cref="TransitionDrawer"/> and sets the <see cref="MainPropertyName"/>.</summary>
public TransitionDrawer(string mainPropertyName)
{
MainPropertyName = mainPropertyName;
MainPropertyPathSuffix = "." + mainPropertyName;
}
/************************************************************************************************************************/
/// <summary>Returns the property specified by the <see cref="MainPropertyName"/>.</summary>
private SerializedProperty GetMainProperty(SerializedProperty rootProperty)
=> MainPropertyName == null
? null
: rootProperty.FindPropertyRelative(MainPropertyName);
/************************************************************************************************************************/
/// <summary>Returns the number of vertical pixels the `property` will occupy when it is drawn.</summary>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
using (new DrawerContext(property))
{
InitializeMode(property);
var height = EditorGUI.GetPropertyHeight(property, label, true);
if (property.isExpanded)
{
if (property.propertyType != SerializedPropertyType.ManagedReference)
{
var mainProperty = GetMainProperty(property);
if (mainProperty != null)
height -= EditorGUI.GetPropertyHeight(mainProperty) + AnimancerGUI.StandardSpacing;
}
// The End Time from the Event Sequence is drawn out in the main transition so we need to add it.
// But rather than figuring out which array element actually holds the end time, we just use the
// Start Time field since it will have the same height.
var startTime = property.FindPropertyRelative(NormalizedStartTimeFieldName);
if (startTime != null)
height += EditorGUI.GetPropertyHeight(startTime) + AnimancerGUI.StandardSpacing;
}
return height;
}
}
/************************************************************************************************************************/
/// <summary>Draws the root `property` GUI and calls <see cref="DoChildPropertyGUI"/> for each of its children.</summary>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
InitializeMode(property);
// Highlight the whole area if this transition is currently being previewed.
var isPreviewing = TransitionPreviewWindow.IsPreviewing(property);
if (isPreviewing)
{
var highlightArea = area;
highlightArea.xMin -= AnimancerGUI.IndentSize;
EditorGUI.DrawRect(highlightArea, new(0.35f, 0.5f, 1, 0.2f));
}
if (property.propertyType == SerializedPropertyType.ObjectReference)
{
DoObjectReferenceGUI(area, property, label);
return;
}
var headerArea = area;
if (property.propertyType == SerializedPropertyType.ManagedReference)
DoPreviewButtonGUI(ref headerArea, property, isPreviewing);
using (new TypeSelectionButton(headerArea, property, true))
{
DoPropertyGUI(area, property, label, isPreviewing);
}
}
/************************************************************************************************************************/
private readonly CachedEditor NestedEditor = new();
private static GUIStyle _NestAreaStyle;
private void DoObjectReferenceGUI(Rect area, SerializedProperty property, GUIContent label)
{
EditorGUI.PropertyField(area, property, label, property.isExpanded);
if (property.hasMultipleDifferentValues)
return;
var value = property.objectReferenceValue;
if (value == null)
return;
property.isExpanded = EditorGUI.Foldout(area, property.isExpanded, GUIContent.none, true);
if (!property.isExpanded)
return;
const float NegativePadding = 4;
EditorGUIUtility.labelWidth -= NegativePadding;
if (_NestAreaStyle == null)
{
_NestAreaStyle = new GUIStyle(GUI.skin.box);
var rect = _NestAreaStyle.margin;
rect.bottom = rect.top = 0;
_NestAreaStyle.margin = rect;
}
EditorGUI.indentLevel++;
GUILayout.BeginVertical(_NestAreaStyle);
try
{
NestedEditor.GetEditor(value).OnInspectorGUI();
}
catch (ExitGUIException)
{
}
catch (Exception exception)
{
Debug.LogException(exception);
}
GUILayout.EndVertical();
EditorGUI.indentLevel--;
EditorGUIUtility.labelWidth += NegativePadding;
}
/************************************************************************************************************************/
private void DoPropertyGUI(Rect area, SerializedProperty property, GUIContent label, bool isPreviewing)
{
using (new DrawerContext(property))
{
var indent = !string.IsNullOrEmpty(label.text);
EditorGUI.BeginChangeCheck();
var mainProperty = GetMainProperty(property);
DoHeaderGUI(ref area, property, mainProperty, label, isPreviewing);
DoChildPropertiesGUI(area, property, mainProperty, indent);
if (EditorGUI.EndChangeCheck() && isPreviewing)
TransitionPreviewWindow.PreviewNormalizedTime = TransitionPreviewWindow.PreviewNormalizedTime;
}
}
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="_Mode"/> is <see cref="Mode.Uninitialized"/>, this method determines how it should start
/// based on the number of properties in the `serializedObject`. If the only serialized field is an
/// <see cref="ITransition"/> then it should start expanded.
/// </summary>
protected void InitializeMode(SerializedProperty property)
{
if (_Mode == Mode.Uninitialized)
{
if (property.depth > 0)
{
_Mode = Mode.Normal;
return;
}
_Mode = Mode.AlwaysExpanded;
var iterator = property.serializedObject.GetIterator();
iterator.Next(true);
var count = 0;
do
{
switch (iterator.propertyPath)
{
// Ignore MonoBehaviour inherited fields.
case "m_ObjectHideFlags":
case "m_Script":
break;
default:
count++;
if (count > 1)
{
_Mode = Mode.Normal;
return;
}
break;
}
}
while (iterator.NextVisible(false));
}
if (_Mode == Mode.AlwaysExpanded)
property.isExpanded = true;
}
/************************************************************************************************************************/
/// <summary>Draws the root property of a transition with an optional main property on the same line.</summary>
protected virtual void DoHeaderGUI(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty mainProperty,
GUIContent label,
bool isPreviewing)
{
area.height = AnimancerGUI.LineHeight;
var labelArea = area;
AnimancerGUI.NextVerticalArea(ref area);
if (rootProperty.propertyType != SerializedPropertyType.ManagedReference)
DoPreviewButtonGUI(ref labelArea, rootProperty, isPreviewing);
// Draw the Root Property after the Main Property to give better spacing between the label and field.
// Drawing the main property might assign its details to the label so we keep our own copy.
using (var rootLabel = PooledGUIContent.Acquire(label.text, label.tooltip))
{
// Main Property.
DoMainPropertyGUI(labelArea, out labelArea, rootProperty, mainProperty);
// Root Property.
var propertyLabel = EditorGUI.BeginProperty(labelArea, rootLabel, rootProperty);
EditorGUI.LabelField(labelArea, propertyLabel);
EditorGUI.EndProperty();
if (_Mode != Mode.AlwaysExpanded)
{
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
rootProperty.isExpanded =
EditorGUI.Foldout(labelArea, rootProperty.isExpanded, GUIContent.none, true);
EditorGUIUtility.hierarchyMode = hierarchyMode;
}
}
}
/************************************************************************************************************************/
/// <summary>Draws the GUI the the target transition's main property.</summary>
protected virtual void DoMainPropertyGUI(
Rect area,
out Rect labelArea,
SerializedProperty rootProperty,
SerializedProperty mainProperty)
{
labelArea = area;
if (mainProperty == null)
return;
var fullArea = area;
labelArea = AnimancerGUI.StealFromLeft(
ref area,
EditorGUIUtility.labelWidth,
AnimancerGUI.StandardSpacing);
var mainPropertyReferenceIsMissing =
mainProperty.propertyType == SerializedPropertyType.ObjectReference &&
mainProperty.objectReferenceValue == null;
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
if (rootProperty.propertyType == SerializedPropertyType.ManagedReference)
{
if (rootProperty.isExpanded ||
_Mode == Mode.AlwaysExpanded)
{
EditorGUI.indentLevel++;
AnimancerGUI.NextVerticalArea(ref fullArea);
using (var label = PooledGUIContent.Acquire(mainProperty))
EditorGUI.PropertyField(fullArea, mainProperty, label, true);
EditorGUI.indentLevel--;
}
}
else
{
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUI.PropertyField(area, mainProperty, GUIContent.none, true);
EditorGUI.indentLevel = indentLevel;
}
EditorGUIUtility.hierarchyMode = hierarchyMode;
// If the main Object reference was just assigned and all fields were at their type default,
// reset the value to run its default constructor and field initializers then reassign the reference.
var reference = mainProperty.objectReferenceValue;
if (mainPropertyReferenceIsMissing && reference != null)
{
mainProperty.objectReferenceValue = null;
if (Serialization.IsDefaultValueByType(rootProperty))
rootProperty.GetAccessor().ResetValue(rootProperty);
mainProperty.objectReferenceValue = reference;
}
}
/************************************************************************************************************************/
/// <summary>Draws a small button using the <see cref="TransitionPreviewWindow.Icon"/>.</summary>
private static void DoPreviewButtonGUI(ref Rect area, SerializedProperty property, bool isPreviewing)
{
if (property.serializedObject.targetObjects.Length != 1 ||
!TransitionPreviewWindow.CanBePreviewed(property))
return;
var enabled = GUI.enabled;
var currentEvent = Event.current;
if (currentEvent.button == 1)// Ignore Right Clicks on the Preview Button.
{
switch (currentEvent.type)
{
case EventType.MouseDown:
case EventType.MouseUp:
case EventType.ContextClick:
GUI.enabled = false;
break;
}
}
var tooltip = isPreviewing ? TransitionPreviewWindow.Inspector.CloseTooltip : "Preview this transition";
if (DoPreviewButtonGUI(ref area, isPreviewing, tooltip))
TransitionPreviewWindow.OpenOrClose(property);
GUI.enabled = enabled;
}
/// <summary>Draws a small button using the <see cref="TransitionPreviewWindow.Icon"/>.</summary>
public static bool DoPreviewButtonGUI(ref Rect area, bool selected, string tooltip)
{
var width = AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing * 2;
var buttonArea = AnimancerGUI.StealFromRight(ref area, width, AnimancerGUI.StandardSpacing);
buttonArea.height = AnimancerGUI.LineHeight;
using (var content = PooledGUIContent.Acquire("", tooltip))
{
content.image = TransitionPreviewWindow.Icon;
return GUI.Toggle(buttonArea, selected, content, PreviewButtonStyle) != selected;
}
}
/************************************************************************************************************************/
private static GUIStyle _PreviewButtonStyle;
/// <summary>The style used for the button that opens the <see cref="TransitionPreviewWindow"/>.</summary>
public static GUIStyle PreviewButtonStyle
=> _PreviewButtonStyle ??= new(AnimancerGUI.MiniButtonStyle)
{
padding = new(0, 0, 0, 1),
fixedWidth = 0,
fixedHeight = 0,
};
/************************************************************************************************************************/
private void DoChildPropertiesGUI(Rect area, SerializedProperty rootProperty, SerializedProperty mainProperty, bool indent)
{
if (!rootProperty.isExpanded && _Mode != Mode.AlwaysExpanded)
return;
// Skip over the main property if it was already drawn by the header.
if (rootProperty.propertyType == SerializedPropertyType.ManagedReference &&
mainProperty != null)
AnimancerGUI.NextVerticalArea(ref area);
if (indent)
EditorGUI.indentLevel++;
var property = rootProperty.Copy();
SerializedProperty eventsProperty = null;
var depth = property.depth;
if (property.NextVisible(true))
{
while (property.depth > depth)
{
// Grab the Events property and draw it last.
var path = property.propertyPath;
if (eventsProperty == null && path.EndsWith("._Events"))
{
eventsProperty = property.Copy();
}
// Don't draw the main property again.
else if (mainProperty != null && path.EndsWith(MainPropertyPathSuffix))
{
}
else
{
if (eventsProperty != null)
{
var type = Context.Transition.GetType();
var accessor = property.GetAccessor();
var field = Serialization.GetField(type, accessor.Name);
if (field != null && field.IsDefined(typeof(DrawAfterEventsAttribute), false))
{
using (var eventsLabel = PooledGUIContent.Acquire(eventsProperty))
DoChildPropertyGUI(ref area, rootProperty, eventsProperty, eventsLabel);
AnimancerGUI.NextVerticalArea(ref area);
eventsProperty = null;
}
}
using (var label = PooledGUIContent.Acquire(property))
DoChildPropertyGUI(ref area, rootProperty, property, label);
AnimancerGUI.NextVerticalArea(ref area);
}
if (!property.NextVisible(false))
break;
}
}
if (eventsProperty != null)
{
using (var label = PooledGUIContent.Acquire(eventsProperty))
DoChildPropertyGUI(ref area, rootProperty, eventsProperty, label);
}
if (indent)
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>
/// Draws the `property` GUI in relation to the `rootProperty` which was passed into <see cref="OnGUI"/>.
/// </summary>
protected virtual void DoChildPropertyGUI(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty property,
GUIContent label)
{
// If we keep using the GUIContent that was passed into OnGUI then GetPropertyHeight will change it to
// match the 'property' which we don't want.
using (var content = PooledGUIContent.Acquire(label.text, label.tooltip))
{
area.height = EditorGUI.GetPropertyHeight(property, content, true);
if (TryDoStartTimeField(ref area, rootProperty, property, content))
return;
EditorGUI.PropertyField(area, property, content, true);
}
}
/************************************************************************************************************************/
/// <summary>The name of the backing field of <c>ClipTransition.NormalizedStartTime</c>.</summary>
public const string NormalizedStartTimeFieldName = "_NormalizedStartTime";
/// <summary>
/// If the `property` is a "Start Time" field, this method draws it as well as the "End Time" below it and
/// returns true.
/// </summary>
public static bool TryDoStartTimeField(
ref Rect area,
SerializedProperty rootProperty,
SerializedProperty property,
GUIContent label)
{
if (!property.propertyPath.EndsWith("." + NormalizedStartTimeFieldName))
return false;
// Start Time.
label.text = "Start Time";
AnimationTimeAttributeDrawer.SetNextDefaultValue(
AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(Context.Transition.Speed));
EditorGUI.PropertyField(area, property, label, false);
AnimancerGUI.NextVerticalArea(ref area);
// End Time.
var events = rootProperty.FindPropertyRelative("_Events");
using (var context = SerializableEventSequenceDrawer.Context.Get(events))
{
var areaCopy = area;
var index = Mathf.Max(0, context.Times.Count - 1);
SerializableEventSequenceDrawer.DoTimeGUI(ref areaCopy, context, index, true);
}
return true;
}
/************************************************************************************************************************/
#region Context
/************************************************************************************************************************/
/// <summary>The current <see cref="DrawerContext"/>.</summary>
public static DrawerContext Context { get; private set; }
/************************************************************************************************************************/
/// <summary>Details used to draw an <see cref="ITransition"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DrawerContext
public readonly struct DrawerContext : IDisposable
{
/************************************************************************************************************************/
/// <summary>The stack of active contexts.</summary>
public static readonly List<DrawerContext> Stack = new();
/************************************************************************************************************************/
/// <summary>The main property representing the <see cref="ITransition"/> field.</summary>
public readonly SerializedProperty Property;
/// <summary>The actual transition object rerieved from the <see cref="Property"/>.</summary>
public readonly ITransition Transition;
/// <summary>The cached value of <see cref="ITransition.MaximumLength"/>.</summary>
public readonly float MaximumLength;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DrawerContext"/>.</summary>
/// <remarks>Be sure to <see cref="Dispose"/> it when done.</remarks>
public DrawerContext(
SerializedProperty transitionProperty)
: this(transitionProperty, transitionProperty.GetValue<ITransition>())
{ }
/// <summary>Creates a new <see cref="DrawerContext"/>.</summary>
/// <remarks>Be sure to <see cref="Dispose"/> it when done.</remarks>
public DrawerContext(
ITransition transition)
: this(null, transition)
{ }
/// <summary>Creates a new <see cref="DrawerContext"/>.</summary>
/// <remarks>Be sure to <see cref="Dispose"/> it when done.</remarks>
public DrawerContext(
SerializedProperty transitionProperty,
ITransition transition)
{
Property = transitionProperty;
Transition = transition;
AnimancerUtilities.TryGetLength(Transition, out MaximumLength);
if (Property != null)
EditorGUI.BeginChangeCheck();
Stack.Add(this);
Context = this;
}
/************************************************************************************************************************/
/// <summary>Applies any modified properties and decrements the stack.</summary>
public void Dispose()
{
Debug.Assert(
Transition == Context.Transition,
$"{nameof(DrawerContext)}.{nameof(Dispose)}" +
$" must be called in the reverse order in which instances were created." +
$" Recommended: using (new DrawerContext(property)) to ensure correct disposal.");
if (Property != null && EditorGUI.EndChangeCheck())
Property.serializedObject.ApplyModifiedProperties();
Stack.RemoveAt(Stack.Count - 1);
Context = Stack.Count > 0
? Stack[^1]
: default;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: e77e914e65226dc439316086147dc410
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: