chore: initial commit
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb3f0552aa807e340b39f53ea42a9a2c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: faa04e0e6ae08764dbf279c99225920e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5f34d72d5484544da63e422dca5d0a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b407e7bedc3c63946b3dc476d4540873
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86e53fc4d232c3c48b32aa883a16005f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f0aef2af7c620749ad340f2545e0832
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e5c41022aa077243bec1efac972f890
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbdbd2675cb58604a8b719a36eb1adad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 198f2234f919be64992de958b4dbef90
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 303b872471b0bca46b23add07d8a9e6f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user