chore: initial commit

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,118 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Icon textures used throughout Animancer.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerIcons
public static class AnimancerIcons
{
/************************************************************************************************************************/
/// <summary>A standard icon for information.</summary>
public static readonly Texture Info = Load("console.infoicon");
/// <summary>A standard icon for warnings.</summary>
public static readonly Texture Warning = Load("console.warnicon");
/// <summary>A standard icon for errors.</summary>
public static readonly Texture Error = Load("console.erroricon");
/************************************************************************************************************************/
private static Texture _ScriptableObject;
/// <summary>The icon for <see cref="UnityEngine.ScriptableObject"/>.</summary>
public static Texture ScriptableObject
{
get
{
if (_ScriptableObject == null)
{
_ScriptableObject = Load("d_ScriptableObject Icon");
if (_ScriptableObject == null)
_ScriptableObject = AssetPreview.GetMiniTypeThumbnail(typeof(StringAsset));
}
return _ScriptableObject;
}
}
/************************************************************************************************************************/
/// <summary>Loads an icon texture.</summary>
public static Texture Load(string name, FilterMode filterMode = FilterMode.Bilinear)
{
var icon = EditorGUIUtility.Load(name) as Texture;
if (icon != null)
icon.filterMode = filterMode;
return icon;
}
/// <summary>Loads an icon `texture` if it was <c>null</c>.</summary>
public static Texture Load(ref Texture texture, string name, FilterMode filterMode = FilterMode.Bilinear)
=> texture != null
? texture
: texture = Load(name, filterMode);
/************************************************************************************************************************/
private static GUIContent
_PlayIcon,
_PauseIcon,
_StepBackwardIcon,
_StepForwardIcon,
_AddIcon,
_ClearIcon,
_CopyIcon;
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a play button.</summary>
public static GUIContent PlayIcon
=> IconContent(ref _PlayIcon, "PlayButton");
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a pause button.</summary>
public static GUIContent PauseIcon
=> IconContent(ref _PauseIcon, "PauseButton");
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a step backward button.</summary>
public static GUIContent StepBackwardIcon
=> IconContent(ref _StepBackwardIcon, "Animation.PrevKey");
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a step forward button.</summary>
public static GUIContent StepForwardIcon
=> IconContent(ref _StepForwardIcon, "Animation.NextKey");
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for an add button.</summary>
public static GUIContent AddIcon(string tooltip = "Add")
=> IconContent(ref _AddIcon, "Toolbar Plus", tooltip);
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a clear button.</summary>
public static GUIContent ClearIcon(string tooltip = "Clear")
=> IconContent(ref _ClearIcon, "Grid.EraserTool", tooltip);
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a copy button.</summary>
public static GUIContent CopyIcon(string tooltip = "Copy to clipboard")
=> IconContent(ref _CopyIcon, "UnityEditor.ConsoleWindow", tooltip);
/************************************************************************************************************************/
/// <summary>Calls <see cref="EditorGUIUtility.IconContent(string)"/> if the `content` was null.</summary>
public static GUIContent IconContent(ref GUIContent content, string name, string tooltip = "")
{
content ??= EditorGUIUtility.IconContent(name);
content.tooltip = tooltip;
return content;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,88 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] <see cref="GUIStyle"/>s for a group of connected buttons.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ButtonGroupStyles
public struct ButtonGroupStyles
{
/************************************************************************************************************************/
/// <summary>The style for the button on the far left.</summary>
public GUIStyle left;
/// <summary>The style for any buttons in the middle.</summary>
public GUIStyle middle;
/// <summary>The style for the button on the far right.</summary>
public GUIStyle right;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ButtonGroupStyles"/>.</summary>
public ButtonGroupStyles(
GUIStyle left,
GUIStyle middle,
GUIStyle right)
{
this.left = left;
this.middle = middle;
this.right = right;
}
/************************************************************************************************************************/
/// <summary>Copies any <c>null</c> values from another group.</summary>
public void CopyMissingStyles(ButtonGroupStyles copyFrom)
{
left ??= copyFrom.left;
middle ??= copyFrom.middle;
right ??= copyFrom.right;
}
/************************************************************************************************************************/
/// <summary>The default styles for a mini button.</summary>
public static ButtonGroupStyles MiniButton => new(
EditorStyles.miniButtonLeft,
EditorStyles.miniButtonMid,
EditorStyles.miniButtonRight);
/************************************************************************************************************************/
private static ButtonGroupStyles _Button;
/// <summary>The default styles for a button.</summary>
public static ButtonGroupStyles Button
{
get
{
_Button.left ??= MiniToRegularButtonStyle(EditorStyles.miniButtonLeft);
_Button.middle ??= MiniToRegularButtonStyle(EditorStyles.miniButtonMid);
_Button.right ??= MiniToRegularButtonStyle(EditorStyles.miniButtonRight);
return _Button;
}
}
/************************************************************************************************************************/
/// <summary>Creates a copy of the `style` with the size of a regular button.</summary>
public static GUIStyle MiniToRegularButtonStyle(GUIStyle style)
=> new(style)
{
fixedHeight = 0,
padding = GUI.skin.button.padding,
stretchWidth = false,
};
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4cf051da751d8b14ab331c9a4d43511d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,56 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A custom GUI for an <see cref="AnimancerEvent.Dispatcher"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEventDispatcherDrawer
[CustomGUI(typeof(AnimancerEvent.Dispatcher))]
public class AnimancerEventDispatcherDrawer : CustomGUI<AnimancerEvent.Dispatcher>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
var state = Value.State;
var events = state?.SharedEvents;
if (events == null)
{
EditorGUILayout.LabelField("Event Dispatcher", "Null");
return;
}
var targetPath = state != null
? state.GetPath()
: "Null";
var eventSequenceDrawer = EventSequenceDrawer.Get(events);
var area = AnimancerGUI.LayoutRect(eventSequenceDrawer.CalculateHeight(events));
using (var label = PooledGUIContent.Acquire("Event Dispatcher"))
using (var summary = PooledGUIContent.Acquire(targetPath))
eventSequenceDrawer.DoGUI(ref area, events, label, summary);
if (eventSequenceDrawer.IsExpanded && state != null)
{
EditorGUI.indentLevel++;
var enabled = GUI.enabled;
GUI.enabled = false;
EditorGUILayout.Toggle("Has Owned Events", state.HasOwnedEvents);
GUI.enabled = enabled;
EditorGUI.indentLevel--;
}
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,73 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="float"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/FloatGUI
///
[CustomGUI(typeof(float))]
public class FloatGUI : CustomGUI<float>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
=> Value = EditorGUILayout.FloatField(Label, Value);
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="int"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/IntGUI
///
[CustomGUI(typeof(int))]
public class IntGUI : CustomGUI<int>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
=> Value = EditorGUILayout.IntField(Label, Value);
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="string"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/StringGUI
///
[CustomGUI(typeof(string))]
public class StringGUI : CustomGUI<string>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
=> Value = EditorGUILayout.TextField(Label, Value);
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="Object"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ObjectGUI_1
///
[CustomGUI(typeof(Object))]
public class ObjectGUI<T> : CustomGUI<T>
where T : Object
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
=> Value = AnimancerGUI.DoObjectFieldGUI(Label, Value, true);
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,92 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws a custom GUI for an object.</summary>
/// <remarks>
/// Every non-abstract type implementing this interface must have at least one <see cref="CustomGUIAttribute"/>.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ICustomGUI
///
public interface ICustomGUI
{
/************************************************************************************************************************/
/// <summary>The optional label to draw in front of the field.</summary>
GUIContent Label { get; set; }
/// <summary>The target object for which this GUI will be drawn.</summary>
object Value { get; set; }
/// <summary>Draws the GUI for the <see cref="Value"/>.</summary>
void DoGUI();
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Draws a custom GUI for an object.</summary>
/// <remarks>
/// Every non-abstract type inheriting from this class must have at least one <see cref="CustomGUIAttribute"/>.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUI_1
///
public abstract class CustomGUI<T> : ICustomGUI
{
/************************************************************************************************************************/
/// <summary>The object for which this GUI will be drawn.</summary>
public T Value { get; protected set; }
/// <inheritdoc/>
object ICustomGUI.Value
{
get => Value;
set => Value = (T)value;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public GUIContent Label { get; set; } = GUIContent.none;
/************************************************************************************************************************/
/// <inheritdoc/>
public abstract void DoGUI();
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Extension methods for <see cref="ICustomGUI"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUIExtensions
///
public static class CustomGUIExtensions
{
/************************************************************************************************************************/
/// <summary>Sets the <see cref="ICustomGUI.Label"/>.</summary>
public static void SetLabel(
this ICustomGUI customGUI,
string text,
string tooltip = null,
Texture image = null)
{
var label = customGUI.Label;
if (label == null || label == GUIContent.none)
customGUI.Label = label = new(text);
label.text = text;
label.tooltip = tooltip;
label.image = image;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,35 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// Attribute for classes which implement <see cref="CustomGUI{T}"/> to specify the type of objects they apply to.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUIAttribute
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class CustomGUIAttribute : Attribute
{
/************************************************************************************************************************/
/// <summary>The type of object which the attributed <see cref="CustomGUI{T}"/> class applies to.</summary>
public readonly Type TargetType;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="CustomGUIAttribute"/>.</summary>
public CustomGUIAttribute(Type targetType)
{
TargetType = targetType;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,152 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
//#define LOG_CUSTOM_GUI_FACTORY
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws a custom GUI for an object.</summary>
/// <remarks>
/// Every non-abstract type implementing this interface must have at least one <see cref="CustomGUIAttribute"/>.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUIFactory
///
public static class CustomGUIFactory
{
/************************************************************************************************************************/
private static readonly Dictionary<Type, Type>
TargetTypeToGUIType = new();
static CustomGUIFactory()
{
foreach (var guiType in TypeCache.GetTypesWithAttribute(typeof(CustomGUIAttribute)))
{
if (guiType.IsAbstract ||
guiType.IsInterface)
continue;
if (!typeof(ICustomGUI).IsAssignableFrom(guiType))
{
Debug.LogWarning(
$"{guiType.FullName} has a {nameof(CustomGUIAttribute)}" +
$" but doesn't implement {nameof(ICustomGUI)}.");
continue;
}
var attribute = guiType.GetCustomAttribute<CustomGUIAttribute>();
if (attribute.TargetType != null)
{
TargetTypeToGUIType.Add(attribute.TargetType, guiType);
}
}
}
/************************************************************************************************************************/
private static readonly ConditionalWeakTable<object, ICustomGUI>
TargetToGUI = new();
/// <summary>Returns an existing <see cref="ICustomGUI"/> for the `targetType` or creates one if necessary.</summary>
/// <remarks>Returns null if the `targetType` is null or no valid <see cref="ICustomGUI"/> type is found.</remarks>
public static ICustomGUI GetOrCreateForType(Type targetType)
{
if (targetType == null)
return null;
if (TargetToGUI.TryGetValue(targetType, out var gui))
return gui;
gui = Create(targetType);
TargetToGUI.Add(targetType, gui);
return gui;
}
/// <summary>Returns an existing <see cref="ICustomGUI"/> for the `value` or creates one if necessary.</summary>
/// <remarks>Returns null if the `value` is null or no valid <see cref="ICustomGUI"/> type is found.</remarks>
public static ICustomGUI GetOrCreateForObject(object value)
{
if (value == null)
return null;
if (TargetToGUI.TryGetValue(value, out var gui))
return gui;
gui = Create(value.GetType());
if (gui != null)
gui.Value = value;
TargetToGUI.Add(value, gui);
return gui;
}
/************************************************************************************************************************/
/// <summary>Creates an <see cref="ICustomGUI"/> for the `targetType`.</summary>
/// <remarks>Returns null if the `value` is null or no valid <see cref="ICustomGUI"/> type is found.</remarks>
public static ICustomGUI Create(Type targetType)
{
if (!TryGetGUIType(targetType, out var guiType))
return null;
try
{
if (guiType.IsGenericTypeDefinition)
guiType = guiType.MakeGenericType(targetType);
var gui = (ICustomGUI)Activator.CreateInstance(guiType);
return gui;
}
catch (Exception exception)
{
Debug.LogException(exception);
return null;
}
}
/************************************************************************************************************************/
/// <summary>Tries to determine the valid <see cref="ICustomGUI"/> type for drawing the `target`.</summary>
public static bool TryGetGUIType(Type target, out Type gui)
{
// Try the target and its base types.
var type = target;
while (type != null && type != typeof(object))
{
if (TargetTypeToGUIType.TryGetValue(type, out gui))
return true;
type = type.BaseType;
}
// Try any interfaces.
var interfaces = target.GetInterfaces();
for (int i = 0; i < interfaces.Length; i++)
if (TargetTypeToGUIType.TryGetValue(interfaces[i], out gui))
return true;
// Try base object.
return TargetTypeToGUIType.TryGetValue(typeof(object), out gui);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,148 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] An <see cref="ICustomGUI"/> for <see cref="MulticastDelegate"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DelegateGUI
[CustomGUI(typeof(MulticastDelegate))]
public class DelegateGUI : CustomGUI<MulticastDelegate>
{
/************************************************************************************************************************/
private static readonly HashSet<MulticastDelegate>
ExpandedItems = new();
/************************************************************************************************************************/
/// <summary>Calculates the number of vertical pixels required to draw the specified <see cref="MulticastDelegate"/>.</summary>
public static float CalculateHeight(MulticastDelegate del)
=> AnimancerGUI.CalculateHeight(CalculateLineCount(del));
/// <summary>Calculates the number of lines required to draw the specified <see cref="MulticastDelegate"/>.</summary>
public static int CalculateLineCount(MulticastDelegate del)
=> del == null || !ExpandedItems.Contains(del)
? 1
: 1 + CalculateLineCount(AnimancerReflection.GetInvocationList(del));
/// <summary>Calculates the number of lines required to draw the specified `invocationList`.</summary>
public static int CalculateLineCount(Delegate[] invocationList)
=> invocationList == null
? 3
: invocationList.Length * 3;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
var area = AnimancerGUI.LayoutRect(CalculateHeight(Value));
DoGUI(ref area, Label, Value);
}
/// <summary>Draws the GUI for the given delegate.</summary>
public static void DoGUI(
ref Rect area,
GUIContent label,
MulticastDelegate del,
GUIContent valueLabel = null)
{
area.height = AnimancerGUI.LineHeight;
var delegates = AnimancerReflection.GetInvocationList(del);
var isExpanded = del != null && AnimancerGUI.DoHashedFoldoutGUI(area, ExpandedItems, del);
if (valueLabel != null)
{
EditorGUI.LabelField(area, label, valueLabel);
}
else
{
var count = delegates == null ? 0 : delegates.Length;
using (var countLabel = PooledGUIContent.Acquire(count.ToStringCached()))
EditorGUI.LabelField(area, label, countLabel);
}
AnimancerGUI.NextVerticalArea(ref area);
if (!isExpanded)
return;
EditorGUI.indentLevel++;
if (delegates == null)
{
DoSingleGUI(ref area, del);
}
else
{
for (int i = 0; i < delegates.Length; i++)
DoSingleGUI(ref area, delegates[i]);
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
private const int TargetFieldCacheCapacity = 128;
private static readonly Dictionary<object, FastObjectField>
TargetFieldCache = new(TargetFieldCacheCapacity);
/// <summary>Draws the target and name of the specified <see cref="Delegate"/>.</summary>
public static void DoSingleGUI(ref Rect area, Delegate del)
{
area.height = AnimancerGUI.LineHeight;
if (del == null)
{
EditorGUI.LabelField(area, "Delegate", "Null");
AnimancerGUI.NextVerticalArea(ref area);
return;
}
var method = del.Method;
EditorGUI.LabelField(area, "Method", method.ToString());
AnimancerGUI.NextVerticalArea(ref area);
EditorGUI.LabelField(area, "Declaring Type", method.DeclaringType.GetNameCS());
AnimancerGUI.NextVerticalArea(ref area);
var target = del.Target;
FastObjectField field;
if (target is not null)
TargetFieldCache.TryGetValue(target, out field);
else
field = FastObjectField.Null;
field.Draw(area, "Target", target);
if (target is not null)
{
if (TargetFieldCache.Count == TargetFieldCacheCapacity)
TargetFieldCache.Clear();
TargetFieldCache[target] = field;
}
AnimancerGUI.NextVerticalArea(ref area);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,139 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A custom GUI for <see cref="FadeGroup"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/FadeGroupDrawer
[CustomGUI(typeof(FadeGroup))]
public class FadeGroupDrawer : CustomGUI<FadeGroup>
{
/************************************************************************************************************************/
private bool _IsExpanded;
private AnimationCurve _DisplayCurve;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
_IsExpanded = EditorGUILayout.Foldout(_IsExpanded, "", true);
var area = GUILayoutUtility.GetLastRect();
InitializeDisplayCurve(ref _DisplayCurve);
_DisplayCurve = EditorGUI.CurveField(area, TargetName, _DisplayCurve);
if (_IsExpanded)
DoDetailsGUI();
}
/************************************************************************************************************************/
/// <summary>The display name of the target.</summary>
protected virtual string TargetName
{
get
{
var name = Value.GetType().GetNameCS(false);
if (!Value.IsValid)
name += " (Cancelled)";
return name;
}
}
/************************************************************************************************************************/
private static readonly Keyframe[] DisplayCurveKeyframes = new Keyframe[16];
/// <summary>Initializes the `curve` to represent the target's fade values over normalized time.</summary>
protected virtual void InitializeDisplayCurve(ref AnimationCurve curve)
{
curve ??= new();
try
{
var increment = 1f / (DisplayCurveKeyframes.Length - 1);
for (int i = 0; i < DisplayCurveKeyframes.Length; i++)
{
var progress = increment * i;
var weight = Value.Easing != null
? Value.Easing(progress)
: progress;
DisplayCurveKeyframes[i] = new(progress, weight);
}
}
catch (Exception exception)
{
Debug.LogException(exception);
Array.Clear(DisplayCurveKeyframes, 0, DisplayCurveKeyframes.Length);
}
curve.keys = DisplayCurveKeyframes;
}
/************************************************************************************************************************/
/// <summary>Draws the GUI for the target's fields.</summary>
protected virtual void DoDetailsGUI()
{
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
Value.NormalizedTime = EditorGUILayout.Slider("Normalized Time", Value.NormalizedTime, 0, 1);
if (EditorGUI.EndChangeCheck())
{
Value.NormalizedTime = Mathf.Clamp(Value.NormalizedTime, 0, 0.99f);
Value.ApplyWeights();
}
EditorGUI.BeginChangeCheck();
var fadeDuration = EditorGUILayout.FloatField("Fade Duration", Value.FadeDuration);
if (EditorGUI.EndChangeCheck())
Value.FadeDuration = fadeDuration;
EditorGUILayout.LabelField(
Value.TargetWeight > 0 ? "Fade In" : "Fade Out",
"To " + Value.TargetWeight);
EditorGUI.indentLevel++;
DoNodeWeightGUI(Value.FadeIn);
EditorGUI.indentLevel--;
var fadeOutCount = Value.FadeOut.Count;
if (fadeOutCount > 0)
{
EditorGUILayout.LabelField("Fade Out", fadeOutCount.ToStringCached());
EditorGUI.indentLevel++;
for (int i = 0; i < fadeOutCount; i++)
DoNodeWeightGUI(Value.FadeOut[i]);
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Draws the GUI for the given `nodeWeight`.</summary>
private void DoNodeWeightGUI(NodeWeight nodeWeight)
{
EditorGUILayout.LabelField(nodeWeight.Node?.GetPath());
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,43 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// A default <see cref="ICustomGUI"/> which simply draws the <see cref="object.ToString"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/LabelGUI
[CustomGUI(typeof(object))]
public class LabelGUI : CustomGUI<object>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
string text;
try
{
text = Value != null
? Value.ToString()
: "Null";
}
catch (Exception exception)
{
text = exception.ToString();
}
using (var value = PooledGUIContent.Acquire(text))
EditorGUILayout.LabelField(Label, value);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1649432a594f0b149b2190e2160b59e6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,535 @@
// 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 static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// A custom Inspector for an <see cref="AnimancerLayer"/> which sorts and exposes some of its internal values.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerLayerDrawer
///
[CustomGUI(typeof(AnimancerLayer))]
public class AnimancerLayerDrawer : AnimancerNodeDrawer<AnimancerLayer>
{
/************************************************************************************************************************/
/// <summary>The states in the target layer which have non-zero <see cref="AnimancerNode.Weight"/>.</summary>
public readonly List<AnimancerState> ActiveStates = new();
/// <summary>The states in the target layer which have zero <see cref="AnimancerNode.Weight"/>.</summary>
public readonly List<AnimancerState> InactiveStates = new();
/************************************************************************************************************************/
#region Gathering
/************************************************************************************************************************/
/// <summary>Initializes an editor in the list for each layer in the `graph`.</summary>
/// <remarks>
/// The `count` indicates the number of elements actually being used.
/// Spare elements are kept in the list in case they need to be used again later.
/// </remarks>
internal static void GatherLayerEditors(
AnimancerGraph graph,
List<AnimancerLayerDrawer> editors,
out int count)
{
count = graph.Layers.Count;
for (int i = 0; i < count; i++)
{
AnimancerLayerDrawer editor;
if (editors.Count <= i)
{
editor = new();
editors.Add(editor);
}
else
{
editor = editors[i];
}
editor.GatherStates(graph.Layers[i]);
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the target `layer` and sorts its states and their keys into the active/inactive lists.
/// </summary>
private void GatherStates(AnimancerLayer layer)
{
Value = layer;
ActiveStates.Clear();
InactiveStates.Clear();
foreach (var state in layer)
{
if (state.IsActive ||
(!AnimancerGraphDrawer.SeparateActiveFromInactiveStates && AnimancerGraphDrawer.ShowInactiveStates))
{
ActiveStates.Add(state);
continue;
}
if (AnimancerGraphDrawer.ShowInactiveStates)
InactiveStates.Add(state);
}
SortAndGatherKeys(ActiveStates);
SortAndGatherKeys(InactiveStates);
}
/************************************************************************************************************************/
/// <summary>
/// Sorts any entries that use another state as their key to come right after that state.
/// See <see cref="AnimancerLayer.Play(AnimancerState, float, FadeMode)"/>.
/// </summary>
private static void SortAndGatherKeys(List<AnimancerState> states)
{
var count = states.Count;
if (count == 0)
return;
AnimancerGraphDrawer.ApplySortStatesByName(states);
// Sort any states that use another state as their key to be right after the key.
for (int i = 0; i < count; i++)
{
var state = states[i];
var key = state.Key;
if (key is not AnimancerState keyState)
continue;
var keyStateIndex = states.IndexOf(keyState);
if (keyStateIndex < 0 || keyStateIndex + 1 == i)
continue;
states.RemoveAt(i);
if (keyStateIndex < i)
keyStateIndex++;
states.Insert(keyStateIndex, state);
i--;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>Draws the layer's name and weight.</summary>
protected override void DoLabelGUI(Rect area)
{
var label = Value.IsAdditive ? "Additive" : "Override";
if (Value._Mask != null)
label = $"{label} ({Value._Mask.GetCachedName()})";
area.xMin += FoldoutIndent;
DoWeightLabel(ref area, Value.Weight, Value.EffectiveWeight);
EditorGUIUtility.labelWidth -= FoldoutIndent;
EditorGUI.LabelField(area, Value.ToString(), label);
EditorGUIUtility.labelWidth += FoldoutIndent;
}
/************************************************************************************************************************/
/// <summary>The number of pixels of indentation required to fit the foldout arrow.</summary>
const float FoldoutIndent = 12;
/// <inheritdoc/>
protected override void DoFoldoutGUI(Rect area)
{
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
area.xMin += FoldoutIndent;
IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);
EditorGUIUtility.hierarchyMode = hierarchyMode;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoDetailsGUI()
{
EditorGUI.indentLevel++;
base.DoDetailsGUI();
if (IsExpanded)
{
GUILayout.BeginHorizontal();
GUILayout.Space(FoldoutIndent);
GUILayout.BeginVertical();
DoLayerDetailsGUI();
DoNodeDetailsGUI();
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
DoStatesGUI();
}
/************************************************************************************************************************/
/// <summary>
/// Draws controls for <see cref="AnimancerLayer.IsAdditive"/> and <see cref="AnimancerLayer._Mask"/>.
/// </summary>
private void DoLayerDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
area.xMin += ExtraLeftPadding;
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var additiveLabel = "Is Additive";
var additiveWidth = GUI.skin.toggle.CalculateWidth(additiveLabel) + StandardSpacing * 2;
var additiveArea = StealFromLeft(ref area, additiveWidth, StandardSpacing);
var maskArea = area;
// Additive.
EditorGUIUtility.labelWidth = CalculateLabelWidth(additiveLabel);
EditorGUI.BeginChangeCheck();
var isAdditive = EditorGUI.Toggle(additiveArea, additiveLabel, Value.IsAdditive);
if (EditorGUI.EndChangeCheck())
Value.IsAdditive = isAdditive;
// Mask.
using (var label = PooledGUIContent.Acquire("Mask"))
{
EditorGUIUtility.labelWidth = CalculateLabelWidth(label.text);
EditorGUI.BeginChangeCheck();
var mask = DoObjectFieldGUI(maskArea, label, Value.Mask, false);
if (EditorGUI.EndChangeCheck())
Value.Mask = mask;
}
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
}
/************************************************************************************************************************/
private void DoStatesGUI()
{
if (!AnimancerGraphDrawer.ShowInactiveStates)
{
DoStatesGUI("Active States", ActiveStates);
}
else if (AnimancerGraphDrawer.SeparateActiveFromInactiveStates)
{
DoStatesGUI("Active States", ActiveStates);
DoStatesGUI("Inactive States", InactiveStates);
}
else
{
DoStatesGUI("States", ActiveStates);
}
if (Value.Weight != 0 &&
!Value.IsAdditive &&
!Mathf.Approximately(Value.GetTotalChildWeight(), 1))
{
var message =
"The total Weight of all states in this layer does not equal 1" +
" which will likely give undesirable results.";
if (AreAllStatesFadingOut())
message +=
" If you no longer want anything playing on a layer," +
" you should fade out that layer instead of fading out its states.";
message += " Click here for more information.";
EditorGUILayout.HelpBox(message, MessageType.Warning);
if (TryUseClickEventInLastRect())
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Layers);
}
}
/************************************************************************************************************************/
/// <summary>Are all the target's states fading out to 0?</summary>
private bool AreAllStatesFadingOut()
{
var count = Value.ActiveStates.Count;
if (count == 0)
return false;
for (int i = 0; i < count; i++)
{
var state = Value.ActiveStates[i];
if (state.TargetWeight != 0)
return false;
}
return true;
}
/************************************************************************************************************************/
/// <summary>Draws all `states` in the given list.</summary>
public void DoStatesGUI(string label, List<AnimancerState> states)
{
var area = LayoutSingleLineRect();
const string Label = "Weight";
var width = CalculateLabelWidth(Label);
GUI.Label(StealFromRight(ref area, width), Label);
EditorGUI.LabelField(area, label, states.Count.ToStringCached());
EditorGUI.indentLevel++;
for (int i = 0; i < states.Count; i++)
{
DoStateGUI(states[i]);
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Cached Inspectors that have already been created for states.</summary>
private readonly Dictionary<AnimancerState, ICustomGUI>
StateInspectors = new();
/// <summary>Draws the Inspector for the given `state`.</summary>
private void DoStateGUI(AnimancerState state)
{
if (!StateInspectors.TryGetValue(state, out var inspector))
{
inspector = CustomGUIFactory.GetOrCreateForObject(state);
StateInspectors.Add(state, inspector);
}
inspector?.DoGUI();
DoChildStatesGUI(state);
}
/************************************************************************************************************************/
/// <summary>Draws all child states of the `state`.</summary>
private void DoChildStatesGUI(AnimancerState state)
{
if (!state._IsInspectorExpanded)
return;
EditorGUI.indentLevel++;
foreach (var child in state)
if (child != null)
DoStateGUI(child);
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoHeaderGUI()
{
if (!AnimancerGraphDrawer.ShowSingleLayerHeader &&
Value.Graph.Layers.Count == 1 &&
Value.Weight == 1 &&
Value.TargetWeight == 1 &&
Value.Speed == 1 &&
!Value.IsAdditive &&
Value._Mask == null &&
Value.Graph.Component != null &&
Value.Graph.Component.Animator != null &&
Value.Graph.Component.Animator.runtimeAnimatorController == null)
return;
base.DoHeaderGUI();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
if (!Value.IsValid())
return;
base.DoGUI();
var area = GUILayoutUtility.GetLastRect();
HandleDragAndDropToPlay(area, Value);
}
/************************************************************************************************************************/
/// <summary>
/// If <see cref="AnimationClip"/>s or <see cref="IAnimationClipSource"/>s are dropped inside the `dropArea`,
/// this method creates a new state in the `target` for each animation.
/// </summary>
public static void HandleDragAndDropToPlay(Rect area, object layerOrGraph)
{
if (layerOrGraph == null)
return;
_DragAndDropPlayTarget = layerOrGraph;
_DragAndDropPlayHandler ??= HandleDragAndDropToPlay;
_DragAndDropPlayHandler.Handle(area);
_DragAndDropPlayTarget = null;
}
private static DragAndDropHandler<Object> _DragAndDropPlayHandler;
private static object _DragAndDropPlayTarget;
private static AnimancerLayer DragAndDropPlayTargetLayer
=> _DragAndDropPlayTarget as AnimancerLayer
?? (_DragAndDropPlayTarget is AnimancerGraph graph ? graph.Layers[0] : null);
/// <summary>Handles drag and drop events to play animations and transitions.</summary>
public static bool HandleDragAndDropToPlay(Object obj, bool isDrop)
{
if (_DragAndDropPlayTarget == null)
return false;
if (obj is AnimationClip clip)
{
if (clip.legacy)
return false;
if (isDrop)
DragAndDropPlayTargetLayer.Play(clip);
return true;
}
if (obj is ITransition transition)
{
if (isDrop)
DragAndDropPlayTargetLayer.Play(transition);
return true;
}
var transitionAsset = TryCreateTransitionAttribute.TryCreateTransitionAsset(obj);
if (transitionAsset != null)
{
if (isDrop)
DragAndDropPlayTargetLayer.Play(transitionAsset);
if (!EditorUtility.IsPersistent(transitionAsset))
Object.DestroyImmediate(transitionAsset);
return true;
}
using (ListPool<AnimationClip>.Instance.Acquire(out var clips))
{
clips.GatherFromSource(obj);
var anyValid = false;
for (int i = 0; i < clips.Count; i++)
{
clip = clips[i];
if (clip.legacy)
continue;
if (!isDrop)
return true;
anyValid = true;
DragAndDropPlayTargetLayer.Play(clip);
}
if (anyValid)
return true;
}
return false;
}
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void PopulateContextMenu(GenericMenu menu)
{
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.CurrentState)}: {Value.CurrentState}"));
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.CommandCount)}: {Value.CommandCount}"));
menu.AddFunction("Stop",
HasAnyStates((state) => state.IsPlaying || state.Weight != 0),
() => Value.Stop());
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade In",
Value.Index > 0 && Value.Weight != 1, Value,
(duration) => Value.StartFade(1, duration));
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade Out",
Value.Index > 0 && Value.Weight != 0, Value,
(duration) => Value.StartFade(0, duration));
AnimancerNodeBase.AddContextMenuIK(menu, Value);
menu.AddSeparator("");
menu.AddFunction("Destroy States",
ActiveStates.Count > 0 || InactiveStates.Count > 0,
() => Value.DestroyStates());
AnimancerGraphDrawer.AddRootFunctions(menu, Value.Graph);
menu.AddSeparator("");
AnimancerGraphDrawer.AddDisplayOptions(menu);
AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers);
menu.ShowAsContext();
}
/************************************************************************************************************************/
private bool HasAnyStates(Func<AnimancerState, bool> condition)
{
foreach (var state in Value)
{
if (condition(state))
return true;
}
return false;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,416 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerNode"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerNodeDrawer_1
///
public abstract class AnimancerNodeDrawer<T> : CustomGUI<T>
where T : AnimancerNode
{
/************************************************************************************************************************/
/// <summary>Extra padding for the left side of the labels.</summary>
public const float ExtraLeftPadding = 3;
/************************************************************************************************************************/
/// <summary>Should the target node's details be expanded in the Inspector?</summary>
public ref bool IsExpanded
=> ref Value._IsInspectorExpanded;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
if (!Value.IsValid())
return;
GUILayout.BeginVertical();
{
DoHeaderGUI();
DoDetailsGUI();
}
GUILayout.EndVertical();
if (TryUseClickEvent(GUILayoutUtility.GetLastRect(), 1))
OpenContextMenu();
}
/************************************************************************************************************************/
/// <summary>Draws the name and other details of the <see cref="CustomGUI{T}.Value"/> in the GUI.</summary>
protected virtual void DoHeaderGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
DoLabelGUI(area);
DoFoldoutGUI(area);
}
/************************************************************************************************************************/
/// <summary>
/// Draws a field for the <see cref="AnimancerState.MainObject"/> if it has one, otherwise just a simple text
/// label.
/// </summary>
protected abstract void DoLabelGUI(Rect area);
/// <summary>Draws a foldout arrow to expand/collapse the node details.</summary>
protected abstract void DoFoldoutGUI(Rect area);
/************************************************************************************************************************/
private FastObjectField _DebugNameField;
/// <summary>Draws the details of the <see cref="CustomGUI{T}.Value"/>.</summary>
protected virtual void DoDetailsGUI()
{
if (!IsExpanded)
return;
var debugName = Value.DebugName;
if (debugName == null)
return;
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
_DebugNameField.Draw(area, "Debug Name", debugName);
}
/************************************************************************************************************************/
private static readonly int FloatFieldHash = "EditorTextField".GetHashCode();
/// <summary>
/// Draws controls for <see cref="AnimancerState.IsPlaying"/>, <see cref="AnimancerNodeBase.Speed"/>, and
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
protected void DoNodeDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area.xMin += EditorGUI.indentLevel * IndentSize + ExtraLeftPadding;
var xMin = area.xMin;
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
// Is Playing.
if (Value is AnimancerState state)
{
var buttonArea = StealFromLeft(ref area, LineHeight, StandardSpacing);
state.IsPlaying = DoPlayPauseToggle(buttonArea, state.IsPlaying);
}
SplitHorizontally(area, "Speed", "Weight",
out var speedWidth,
out var weightWidth,
out var speedRect,
out var weightRect);
// Speed.
EditorGUIUtility.labelWidth = speedWidth;
EditorGUI.BeginChangeCheck();
var speed = EditorGUI.FloatField(speedRect, "Speed", Value.Speed);
if (EditorGUI.EndChangeCheck())
Value.Speed = speed;
if (TryUseClickEvent(speedRect, 2))
Value.Speed = Value.Speed != 1 ? 1 : 0;
// Weight.
EditorGUIUtility.labelWidth = weightWidth;
EditorGUI.BeginChangeCheck();
var weight = EditorGUI.FloatField(weightRect, "Weight", Value.Weight);
if (EditorGUI.EndChangeCheck())
SetWeight(Mathf.Max(weight, 0));
if (TryUseClickEvent(weightRect, 2))
SetWeight(Value.Weight != 1 ? 1 : 0);
// Real Speed.
// Mixer Synchronization changes the internal Playable Speed without setting the State Speed.
speed = (float)Value._Playable.GetSpeed();
if (Value.Speed != speed)
{
using (new EditorGUI.DisabledScope(true))
{
area = LayoutSingleLineRect(SpacingMode.Before);
area.xMin = xMin;
var label = BeginTightLabel("Real Speed");
EditorGUIUtility.labelWidth = CalculateLabelWidth(label);
EditorGUI.FloatField(area, label, speed);
EndTightLabel();
}
}
else// Add a dummy ID so that subsequent IDs don't change when the Real Speed appears or disappears.
{
GUIUtility.GetControlID(FloatFieldHash, FocusType.Keyboard);
}
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
DoFadeDetailsGUI();
}
/************************************************************************************************************************/
/// <summary>Indicates whether changing the <see cref="AnimancerNode.Weight"/> should normalize its siblings.</summary>
protected virtual bool AutoNormalizeSiblingWeights
=> false;
private void SetWeight(float weight)
{
if (weight < 0 ||
weight > 1 ||
Mathf.Approximately(Value.Weight, 1) ||
!AutoNormalizeSiblingWeights)
goto JustSetWeight;
var parent = Value.Parent;
if (parent == null)
goto JustSetWeight;
var totalWeight = 0f;
var siblingCount = parent.ChildCount;
for (int i = 0; i < siblingCount; i++)
{
var sibling = parent.GetChildNode(i);
if (sibling.IsValid())
totalWeight += sibling.Weight;
}
// If the weights weren't previously normalized, don't normalize them now.
if (!Mathf.Approximately(totalWeight, 1))
goto JustSetWeight;
var siblingWeightMultiplier = (totalWeight - weight) / (totalWeight - Value.Weight);
for (int i = 0; i < siblingCount; i++)
{
var sibling = parent.GetChildNode(i);
if (sibling != Value && sibling.IsValid())
sibling.Weight *= siblingWeightMultiplier;
}
JustSetWeight:
Value.Weight = weight;
}
/************************************************************************************************************************/
private float
_FadeDuration = float.NaN,
_TargetWeight = float.NaN;
/// <summary>
/// Draws the <see cref="AnimancerNode.FadeSpeed"/>
/// and <see cref="AnimancerNode.TargetWeight"/>.
/// </summary>
private void DoFadeDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
area.xMin += ExtraLeftPadding;
var durationLabel = "Fade Duration";
var targetLabel = "Target Weight";
SplitHorizontally(
area,
durationLabel,
targetLabel,
out var durationWidth,
out var weightWidth,
out var durationRect,
out var weightRect);
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUI.BeginChangeCheck();
var fade = Value.FadeGroup;
var fadeDuration = DoFadeDurationGUI(durationWidth, durationRect, durationLabel, fade);
var targetWeight = DoTargetWeightGUI(weightWidth, weightRect, targetLabel, fade);
if (EditorGUI.EndChangeCheck())
SetFade(targetWeight, fadeDuration);
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
}
/************************************************************************************************************************/
private float DoFadeDurationGUI(
float labelWidth,
Rect area,
string label,
FadeGroup fade)
{
EditorGUIUtility.labelWidth = labelWidth;
var fadeDuration = fade != null ? fade.FadeDuration : _FadeDuration;
fadeDuration = EditorGUI.DelayedFloatField(area, label, fadeDuration);
if (fadeDuration > 0)
{
}
else// NaN or Negative.
{
fadeDuration = _FadeDuration = float.NaN;
}
if (TryUseClickEvent(area, 2))
{
var defaultFadeDuration = AnimancerGraph.DefaultFadeDuration;
if (fadeDuration != 0 || defaultFadeDuration == 0)
{
fadeDuration = 0;
}
else
{
var fadeDistance = Math.Abs(Value.Weight - Value.TargetWeight);
if (fadeDistance != 0)
{
fadeDuration = fadeDistance / defaultFadeDuration;
}
else
{
fadeDuration = defaultFadeDuration;
}
}
}
return fadeDuration;
}
/************************************************************************************************************************/
private float DoTargetWeightGUI(
float labelWidth,
Rect area,
string label,
FadeGroup fade)
{
EditorGUIUtility.labelWidth = labelWidth;
var targetWeight = fade != null
? fade.TargetWeight
: _TargetWeight.IsFinite()
? _TargetWeight
: Value.Weight;
targetWeight = EditorGUI.DelayedFloatField(area, label, targetWeight);
if (targetWeight >= 0)
{
}
else// NaN or Negative.
{
targetWeight = _TargetWeight = float.NaN;
}
if (TryUseClickEvent(area, 2))
{
if (targetWeight != Value.Weight)
targetWeight = Value.Weight;
else if (targetWeight != 1)
targetWeight = 1;
else
targetWeight = 0;
}
return targetWeight;
}
/************************************************************************************************************************/
/// <summary>Starts a fade or changes the details of an existing one.</summary>
private void SetFade(float targetWeight, float fadeDuration)
{
_TargetWeight = targetWeight;
_FadeDuration = fadeDuration;
if (!targetWeight.IsFinite() ||
!fadeDuration.IsFinite() ||
targetWeight == Value.Weight ||
fadeDuration <= 0)
return;
// If it's a state attached to a layer, start a proper cross fade.
if (Value is AnimancerState state &&
state.Parent is AnimancerLayer layer)
{
layer.Play(state, fadeDuration, FadeMode.FixedDuration);
// That might not have started a fade if the state was already playing,
// So just continue to verify its details.
}
var fade = Value.FadeGroup;
if (fade != null && fade.FadeIn.Node == Value)
{
fade.TargetWeight = targetWeight;
fade.FadeDuration = fadeDuration;
return;
}
Value.StartFade(targetWeight, fadeDuration);
}
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
/// <summary>
/// The menu label prefix used for details about the <see cref="CustomGUI{T}.Value"/>.
/// </summary>
protected const string DetailsPrefix = "Details/";
/// <summary>
/// Checks if the current event is a context menu click within the `clickArea` and opens a context menu with various
/// functions for the <see cref="CustomGUI{T}.Value"/>.
/// </summary>
protected void OpenContextMenu()
{
var menu = new GenericMenu();
menu.AddDisabledItem(new(Value.ToString()));
PopulateContextMenu(menu);
menu.AddItem(new(DetailsPrefix + "Log Details"), false,
() => Debug.Log(Value.GetDescription(), Value.Graph?.Component as Object));
menu.AddItem(new(DetailsPrefix + "Log Details Of Everything"), false,
() => Debug.Log(Value.Graph.GetDescription(), Value.Graph?.Component as Object));
AnimancerGraphDrawer.AddPlayableGraphVisualizerFunction(menu, DetailsPrefix, Value.Graph._PlayableGraph);
menu.ShowAsContext();
}
/// <summary>Adds functions relevant to the <see cref="CustomGUI{T}.Value"/>.</summary>
protected abstract void PopulateContextMenu(GenericMenu menu);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,716 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGraphDrawer;
using static Animancer.Editor.AnimancerGUI;
using static Animancer.Editor.AnimancerStateDrawerColors;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1
[CustomGUI(typeof(AnimancerState))]
public class AnimancerStateDrawer<T> : AnimancerNodeDrawer<T>
where T : AnimancerState
{
/************************************************************************************************************************/
/// <inheritdoc/>
protected override bool AutoNormalizeSiblingWeights
=> AutoNormalizeWeights
&& typeof(T) != typeof(ManualMixerState);
/************************************************************************************************************************/
private FastObjectField _NameField;
private FastObjectField _MainObjectField;
/// <summary>Draws the state's main label with a bar to indicate its current time.</summary>
protected override void DoLabelGUI(Rect area)
{
area = area.Expand(StandardSpacing, 0);
var wholeArea = area;
var effectiveWeight = Value.EffectiveWeight;
var highlightArea = default(Rect);
var isRepaint = Event.current.type == EventType.Repaint;
if (isRepaint)
{
EditorGUI.DrawRect(wholeArea, HeaderBackgroundColor);
highlightArea = DoTimeHighlightBarGUI(wholeArea, effectiveWeight);
DoEventsGUI(wholeArea);
ObjectHighlightGUI.Draw(wholeArea, Value);
}
DoWeightLabel(ref area, Value.Weight, effectiveWeight);
AnimationBindings.DoBindingMatchGUI(ref area, Value);
HandleLabelClick(wholeArea);
area = EditorGUI.IndentedRect(area);
var name = Value.DebugName ?? Value.Key;
var mainObject = Value.MainObject;
if (mainObject == null)
{
var value = name ?? Value;
var drawPing = value != Value;
_NameField.Draw(area, value, drawPing);
}
else if (ReferenceEquals(name, mainObject) ||
(name is Object nameObject && nameObject == mainObject) ||
(name is ITransition && Current != null && !Current.IsMainObjectUsedMultipleTimes(mainObject)))
{
_MainObjectField.Draw(area, mainObject, false);
}
else
{
if (name != null)
{
var nameArea = StealFromLeft(ref area, EditorGUIUtility.labelWidth - IndentSize);
_NameField.Draw(nameArea, name, true);
}
_MainObjectField.Draw(area, mainObject, false);
}
if (isRepaint)
DoDetailLinesGUI(wholeArea, highlightArea, effectiveWeight);
}
/************************************************************************************************************************/
/// <summary>Draws a progress bar to show the animation time.</summary>
public Rect DoTimeHighlightBarGUI(Rect area, float effectiveWeight)
=> DoTimeHighlightBarGUI(
area,
Value.IsPlaying,
effectiveWeight,
Value.Time,
Value.EffectiveSpeed,
Value.Length,
Value.IsLooping);
/// <summary>Draws a progress bar to show the animation time.</summary>
public static Rect DoTimeHighlightBarGUI(
Rect area,
bool isPlaying,
float effectiveWeight,
float time,
float speed,
float length,
bool isLooping)
{
if (ScaleTimeBarByWeight)
{
var height = area.height;
area.height *= Mathf.Clamp01(effectiveWeight);
area.y += height - area.height;
}
var color = isPlaying ? PlayingBarColor : PausedBarColor;
var wrappedTime = GetWrappedTime(time, length, isLooping);
if (length == 0)
{
if (time == 0)
return area;
}
else
{
if (speed >= 0 || time == 0)
{
area.width *= Mathf.Clamp01(wrappedTime / length);
}
else
{
var xMax = area.xMax;
area.x += area.width * Mathf.Clamp01(wrappedTime / length);
area.x = Mathf.Floor(area.x);
area.xMax = xMax;
}
}
EditorGUI.DrawRect(area, color);
return area;
}
/************************************************************************************************************************/
/// <summary>Draws lines for the current weight, time, and fade destination.</summary>
public void DoDetailLinesGUI(
Rect totalArea,
Rect highlightArea,
float effectiveWeight)
{
var length = Value.Length;
var speed = Value.Speed;
var speedSign = speed >= 0 ? 1 : -1;
var currentX = speed >= 0 ? highlightArea.xMax : highlightArea.xMin - 1;
var forwardEdge = speed >= 0 ? totalArea.xMax : totalArea.xMin - 1;
var color = FadeLineColor;
color.a = color.a * effectiveWeight * 0.75f + 0.25f;
if (Value.Time != 0 || Value.IsPlaying || Value.Weight != 0)
{
EditorGUI.DrawRect(
new(highlightArea.x, highlightArea.yMin, highlightArea.width, 1),
color);
if (length == 0)
return;
EditorGUI.DrawRect(
new(currentX - speedSign, totalArea.y, 1, totalArea.height),
color);
}
else if (length == 0)
{
return;
}
if (!Value.IsPlaying)
return;
var fade = Value.FadeGroup;
if (fade == null || !fade.IsValid)
return;
var currentCorner = new Vector2(currentX, highlightArea.yMin);
var targetWeight = Value.TargetWeight;
var remainingFadeDuration = fade.RemainingFadeDuration;
var targetCorner = new Vector2(
currentCorner.x + speed * remainingFadeDuration / Value.Length * totalArea.width,
Mathf.Lerp(totalArea.yMax, totalArea.yMin, targetWeight));
var intersect = Mathf.InverseLerp(currentCorner.x, targetCorner.x, forwardEdge);
var end = Vector2.LerpUnclamped(currentCorner, targetCorner, intersect);
BeginTriangles(color);
DrawLineBatched(
currentCorner,
end,
1);
if (intersect < 1 && Value.IsLooping)
{
end.x -= speedSign * totalArea.width;
targetCorner.x -= speedSign * totalArea.width;
DrawLineBatched(
end,
targetCorner,
1);
}
EndTriangles();
}
/************************************************************************************************************************/
/// <summary>Draws marks on the timeline for each event.</summary>
private void DoEventsGUI(Rect area)
{
if (!ShowEvents)
return;
DoAnimancerEventsGUI(area);
DoAnimationEventsGUI(area);
}
/// <summary>Draws marks on the timeline for each Animancer Event.</summary>
private void DoAnimancerEventsGUI(Rect area)
{
var events = Value.SharedEvents;
if (events == null)
return;
for (int i = 0; i < events.Count; i++)
DoEventTick(area, events[i].normalizedTime);
if (events.OnEnd != null)
DoEventTick(area, events.GetRealNormalizedEndTime(Value.Speed));
}
/// <summary>Draws marks on the timeline for each Animation Event.</summary>
private void DoAnimationEventsGUI(Rect area)
{
var clip = Value.MainObject as AnimationClip;
if (clip == null)
return;
var inverseLength = 1f / Value.Length;
var events = clip.GetCachedEvents();
for (int i = 0; i < events.Length; i++)
DoEventTick(area, events[i].time * inverseLength);
}
/// <summary>Draws a mark on the timeline for an event.</summary>
private static void DoEventTick(Rect area, float normalizedTime)
{
if (normalizedTime >= 0 && normalizedTime <= 1)
{
var x = area.x + area.width * normalizedTime;
var eventArea = new Rect(x - 1, area.y, 2, area.height * 0.3f);
EditorGUI.DrawRect(eventArea, EventTickColor);
}
}
/************************************************************************************************************************/
/// <summary>Handles clicks on the label area.</summary>
private void HandleLabelClick(Rect area)
{
var currentEvent = Event.current;
if (currentEvent.type != EventType.MouseUp ||
currentEvent.button != 0 ||
!area.Contains(currentEvent.mousePosition))
return;
currentEvent.Use(0);
if (currentEvent.control)
FadeInTarget();
else
ToggleExpanded(currentEvent.alt);
}
/// <summary>Fades in the target state (or its parent state if not directly attached to a layer).</summary>
private void FadeInTarget()
{
Value.Graph.UnpauseGraph();
AnimancerState target = Value;
while (target != null)
{
var parent = target.Parent;
if (parent is AnimancerLayer layer)
{
var fadeDuration = target.CalculateEditorFadeDuration(
AnimancerGraph.DefaultFadeDuration);
layer.Play(target, fadeDuration);
return;
}
target = parent as AnimancerState;
}
}
/// <summary>Toggles the target's details between expanded and collapsed.</summary>
private void ToggleExpanded(bool toggleSiblings)
{
IsExpanded = !IsExpanded;
if (toggleSiblings)
{
var parent = Value.Parent;
var childCount = parent.ChildCount;
for (int i = 0; i < childCount; i++)
parent.GetChildNode(i)._IsInspectorExpanded = IsExpanded;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoFoldoutGUI(Rect area)
{
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);
EditorGUIUtility.hierarchyMode = hierarchyMode;
}
/************************************************************************************************************************/
/// <summary>
/// Gets the current <see cref="AnimancerState.Time"/>.
/// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
/// </summary>
private float GetWrappedTime(out float length)
=> GetWrappedTime(Value.Time, length = Value.Length, Value.IsLooping);
/// <summary>
/// Gets the current <see cref="AnimancerState.Time"/>.
/// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
/// </summary>
private static float GetWrappedTime(float time, float length, bool isLooping)
{
var wrappedTime = time;
if (isLooping)
{
wrappedTime = AnimancerUtilities.Wrap(wrappedTime, length);
if (wrappedTime == 0 && time != 0)
wrappedTime = length;
}
return wrappedTime;
}
/************************************************************************************************************************/
private FastObjectField _KeyField;
private FastObjectField _OwnerField;
/************************************************************************************************************************/
/// <summary>The display name of the <see cref="AnimancerState.MainObject"/> field.</summary>
public virtual string MainObjectName
=> "Main Object";
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoDetailsGUI()
{
base.DoDetailsGUI();
if (!IsExpanded)
return;
EditorGUI.indentLevel++;
DoOptionalReferenceGUI(ref _KeyField, "Key", Value.Key);
DoOptionalReferenceGUI(ref _OwnerField, "Owner", Value.Owner);
var mainObject = Value.MainObject;
if (mainObject != null)
{
var mainObjectType = Value.MainObjectType ?? typeof(Object);
EditorGUI.BeginChangeCheck();
var area = LayoutSingleLineRect(SpacingMode.Before);
mainObject = EditorGUI.ObjectField(
area,
MainObjectName,
mainObject,
mainObjectType,
true);
if (EditorGUI.EndChangeCheck())
Value.MainObject = mainObject;
}
DoTimeSliderGUI();
DoNodeDetailsGUI();
DoOnEndGUI();
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Draws a `reference` if it isn't <c>null</c>.</summary>
private static void DoOptionalReferenceGUI(ref FastObjectField field, string label, object reference)
{
if (reference != null)
field.Draw(LayoutSingleLineRect(SpacingMode.Before), label, reference);
}
/************************************************************************************************************************/
/// <summary>Draws a slider for controlling the current <see cref="AnimancerState.Time"/>.</summary>
private void DoTimeSliderGUI()
{
if (Value.Length <= 0)
return;
var time = GetWrappedTime(out var length);
if (length == 0)
return;
var area = LayoutSingleLineRect(SpacingMode.Before);
var normalized = DoNormalizedTimeToggle(ref area);
string label;
float max;
if (normalized)
{
label = "Normalized Time";
time /= length;
max = 1;
}
else
{
label = "Time";
max = length;
}
DoLoopCounterGUI(ref area, length);
EditorGUI.BeginChangeCheck();
label = BeginTightLabel(label);
time = EditorGUI.Slider(area, label, time, 0, max);
EndTightLabel();
if (TryUseClickEvent(area, 2))
time = 0;
if (EditorGUI.EndChangeCheck())
{
if (normalized)
Value.NormalizedTime = time;
else
Value.Time = time;
}
}
/************************************************************************************************************************/
private static bool DoNormalizedTimeToggle(ref Rect area)
{
using (var label = PooledGUIContent.Acquire("N"))
{
var style = MiniButtonStyle;
var width = style.CalculateWidth(label);
var toggleArea = StealFromRight(ref area, width);
UseNormalizedTimeSliders.Value = GUI.Toggle(toggleArea, UseNormalizedTimeSliders, label, style);
}
return UseNormalizedTimeSliders;
}
/************************************************************************************************************************/
private static ConversionCache<int, string> _LoopCounterCache;
private void DoLoopCounterGUI(ref Rect area, float length)
{
_LoopCounterCache ??= new(x => $"x{x}");
string label;
var normalizedTime = Value.Time / length;
if (float.IsNaN(normalizedTime))
{
label = "NaN";
}
else
{
var loops = Mathf.FloorToInt(Value.Time / length);
label = _LoopCounterCache.Convert(loops);
}
var width = CalculateLabelWidth(label);
var labelArea = StealFromRight(ref area, width);
GUI.Label(labelArea, label);
}
/************************************************************************************************************************/
private void DoOnEndGUI()
{
var events = Value.SharedEvents;
if (events == null)
return;
var drawer = EventSequenceDrawer.Get(events);
var area = LayoutRect(drawer.CalculateHeight(events), SpacingMode.Before);
using (var label = PooledGUIContent.Acquire("Events"))
drawer.DoGUI(ref area, events, label);
}
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void PopulateContextMenu(GenericMenu menu)
{
AddContextMenuFunctions(menu);
menu.AddFunction("Play",
!Value.IsPlaying || Value.Weight != 1,
() =>
{
AnimancerState.SkipNextExpectFade();
Value.Graph.UnpauseGraph();
Value.Graph.Layers[0].Play(Value);
});
AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade (Ctrl + Click)",
Value.Weight != 1,
Value,
duration =>
{
AnimancerState.SkipNextExpectFade();
Value.Graph.UnpauseGraph();
Value.Graph.Layers[0].Play(Value, duration);
});
menu.AddSeparator("");
menu.AddItem(new("Destroy State"),
false,
() => Value.Destroy());
menu.AddSeparator("");
AddDisplayOptions(menu);
AnimancerEditorUtilities.AddDocumentationLink(
menu,
"State Documentation",
Strings.DocsURLs.States);
}
/************************************************************************************************************************/
/// <summary>Adds the details of this state to the `menu`.</summary>
protected virtual void AddContextMenuFunctions(GenericMenu menu)
{
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Key)}: {AnimancerUtilities.ToStringOrNull(Value.Key)}"));
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Owner)}: {AnimancerUtilities.ToStringOrNull(Value.Owner)}"));
var length = Value.Length;
if (!float.IsNaN(length))
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Length)}: {length}"));
menu.AddDisabledItem(new($"{DetailsPrefix}Playable Path: {Value.GetPath()}"));
var mainAsset = Value.MainObject;
if (mainAsset != null)
{
var assetPath = AssetDatabase.GetAssetPath(mainAsset);
if (assetPath != null)
menu.AddDisabledItem(new($"{DetailsPrefix}Asset Path: {assetPath.Replace("/", "->")}"));
}
var events = Value.SharedEvents;
if (events != null)
{
for (int i = 0; i < events.Count; i++)
{
var index = i;
var name = events.GetName(i);
AddEventFunctions(
menu,
name.IsNullOrEmpty() ? "Event " + index : name,
name,
events[index],
() => events.SetCallback(index, AnimancerEvent.InvokeBoundCallback),
() => events.Remove(index));
}
AddEventFunctions(
menu,
"End Event",
default,
events.EndEvent,
() => events.EndEvent = new(float.NaN, null), null);
}
}
/************************************************************************************************************************/
private void AddEventFunctions(
GenericMenu menu,
string displayName,
StringReference name,
AnimancerEvent animancerEvent,
GenericMenu.MenuFunction clearEvent,
GenericMenu.MenuFunction removeEvent)
{
displayName = $"Events/{displayName}/";
menu.AddDisabledItem(new($"{displayName}{nameof(AnimancerState.NormalizedTime)}: {animancerEvent.normalizedTime}"));
bool canInvoke;
if (animancerEvent.callback == null)
{
menu.AddDisabledItem(new(displayName + "Callback: null"));
canInvoke = false;
}
else if (animancerEvent.callback == AnimancerEvent.DummyCallback)
{
menu.AddDisabledItem(new(displayName + "Callback: Dummy"));
canInvoke = false;
}
else
{
var label = displayName +
(animancerEvent.callback.Target != null
? ("Target: " + animancerEvent.callback.Target)
: "Target: null");
var targetObject = animancerEvent.callback.Target as Object;
menu.AddFunction(label,
targetObject != null,
() => Selection.activeObject = targetObject);
menu.AddDisabledItem(new(
$"{displayName}Declaring Type: {animancerEvent.callback.Method.DeclaringType.GetNameCS()}"));
menu.AddDisabledItem(new(
$"{displayName}Method: {animancerEvent.callback.Method}"));
canInvoke = true;
}
if (clearEvent != null)
menu.AddFunction(displayName + "Clear", canInvoke || !float.IsNaN(animancerEvent.normalizedTime), clearEvent);
if (removeEvent != null)
menu.AddFunction(displayName + "Remove", true, removeEvent);
menu.AddFunction(displayName + "Invoke", canInvoke, () => animancerEvent.DelayInvoke(name, Value));
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Colors used by <see cref="AnimancerStateDrawer{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawerColors
public static class AnimancerStateDrawerColors
{
/************************************************************************************************************************/
/// <summary>Colors used by this system.</summary>
public static readonly Color
HeaderBackgroundColor = Grey(0.35f, 0.35f),
PlayingBarColor = new(0.15f, 0.7f, 0.15f, 0.4f),// Green = Playing.
PausedBarColor = new(0.7f, 0.7f, 0.15f, 0.4f),// Yelow = Paused.
FadeLineColor = new(0.3f, 1, 0.3f, 1);
/// <summary>Colors used by this system.</summary>
public static Color EventTickColor
=> Grey(EditorGUIUtility.isProSkin ? 0.8f : 0.2f, 0.8f);
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,36 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ClipStateDrawer
[CustomGUI(typeof(ClipState))]
public class ClipStateDrawer : AnimancerStateDrawer<ClipState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string MainObjectName
=> "Clip";
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AddContextMenuFunctions(UnityEditor.GenericMenu menu)
{
menu.AddDisabledItem(new(
$"{DetailsPrefix}Animation Type: {AnimationBindings.GetAnimationType(Value.Clip)}"));
base.AddContextMenuFunctions(menu);
AnimancerNodeBase.AddContextMenuIK(menu, Value);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,23 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ControllerStateDrawer
[CustomGUI(typeof(ControllerState))]
public class ControllerStateDrawer : ParametizedAnimancerStateDrawer<ControllerState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string MainObjectName
=> "Animator Controller";
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,32 @@
// 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/LinearMixerStateDrawer
[CustomGUI(typeof(LinearMixerState))]
public class LinearMixerStateDrawer : ParametizedAnimancerStateDrawer<LinearMixerState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AddContextMenuFunctions(GenericMenu menu)
{
base.AddContextMenuFunctions(menu);
menu.AddItem(new("Extrapolate Speed"), Value.ExtrapolateSpeed, () =>
{
Value.ExtrapolateSpeed = !Value.ExtrapolateSpeed;
});
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,203 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ParametizedAnimancerStateDrawer_1
[CustomGUI(typeof(ManualMixerState))]
public class ParametizedAnimancerStateDrawer<T> : AnimancerStateDrawer<T>
where T : AnimancerState, IParametizedState
{
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void DoDetailsGUI()
{
base.DoDetailsGUI();
if (!IsExpanded)
return;
EditorGUI.indentLevel++;
var parameters = ListPool.Acquire<StateParameterDetails>();
Value.GetParameters(parameters);
DoParametersGUI(parameters);
ListPool.Release(parameters);
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Draws fields for all `parameters`.</summary>
private void DoParametersGUI(List<StateParameterDetails> parameters)
{
if (parameters.Count == 0)
return;
var labelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth -= AnimancerGUI.IndentSize;
EditorGUI.BeginChangeCheck();
for (int i = 0; i < parameters.Count; i++)
parameters[i] = DoParameterGUI(i, parameters[i]);
EditorGUIUtility.labelWidth = labelWidth;
if (EditorGUI.EndChangeCheck())
Value.SetParameters(parameters);
}
/************************************************************************************************************************/
/// <summary>Draws fields for the `parameter`.</summary>
private StateParameterDetails DoParameterGUI(int index, StateParameterDetails parameter)
{
var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before);
var indentLevel = EditorGUI.indentLevel;
var labelWidth = EditorGUIUtility.labelWidth;
var label = parameter.label;
if (parameter.SupportsBinding && Value.Graph.HasParameters)
{
area = EditorGUI.IndentedRect(area);
EditorGUI.indentLevel = 0;
parameter = DoBindingGUI(ref area, index, parameter, ref label);
}
else
{
EditorGUIUtility.labelWidth += AnimancerGUI.IndentSize;
}
switch (parameter.type)
{
case AnimatorControllerParameterType.Float:
parameter.value = EditorGUI.FloatField(area, label, (float)parameter.value);
break;
case AnimatorControllerParameterType.Int:
parameter.value = EditorGUI.IntField(area, label, (int)parameter.value);
break;
case AnimatorControllerParameterType.Bool:
parameter.value = EditorGUI.Toggle(area, label, (bool)parameter.value);
break;
case AnimatorControllerParameterType.Trigger:
parameter.value = EditorGUI.Toggle(area, label, (bool)parameter.value, EditorStyles.radioButton);
break;
default:
EditorGUI.LabelField(area, label, "Unsupported Type: " + parameter.type);
break;
}
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
return parameter;
}
/************************************************************************************************************************/
/// <summary>Draws a dropdown for the `parameter`'s name binding.</summary>
private StateParameterDetails DoBindingGUI(
ref Rect area,
int index,
StateParameterDetails parameter,
ref string fieldLabel)
{
if (!parameter.SupportsBinding)
return parameter;
var spacing = AnimancerGUI.StandardSpacing;
float width;
if (string.IsNullOrEmpty(parameter.name))
{
width = area.height + spacing;
EditorGUIUtility.labelWidth -= width + AnimancerGUI.IndentSize + spacing;
}
else
{
width = EditorGUIUtility.labelWidth - AnimancerGUI.IndentSize;
fieldLabel = "";
}
var labelArea = AnimancerGUI.StealFromLeft(
ref area,
width,
spacing);
using (var label = PooledGUIContent.Acquire(parameter.name))
{
if (EditorGUI.DropdownButton(labelArea, label, FocusType.Passive))
ShowBindingSelectionMenu(labelArea, index, parameter.name);
}
return parameter;
}
/************************************************************************************************************************/
/// <summary>Shows a context menu for selecting the parameter binding.</summary>
private void ShowBindingSelectionMenu(Rect area, int index, string currentName)
{
var menu = new GenericMenu();
menu.AddItem(
new("None"),
string.IsNullOrEmpty(currentName),
() => SetParameterName(index, null));
menu.AddSeparator("");
foreach (var parameter in Value.Graph.Parameters)
{
if (parameter.ValueType != typeof(float))
continue;
var name = parameter.Key;
menu.AddItem(
new(name),
name == currentName,
() => SetParameterName(index, name));
}
menu.DropDown(area);
}
/************************************************************************************************************************/
/// <summary>Sets the name binding of the specified parameter.</summary>
private void SetParameterName(int index, string name)
{
var parameters = ListPool.Acquire<StateParameterDetails>();
Value.GetParameters(parameters);
var modify = parameters[index];
modify.name = name;
parameters[index] = modify;
Value.SetParameters(parameters);
ListPool.Release(parameters);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,24 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
namespace Animancer.Editor
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/PlayableAssetStateDrawer
[CustomGUI(typeof(PlayableAssetState))]
public class PlayableAssetStateDrawer : AnimancerStateDrawer<PlayableAssetState>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string MainObjectName
=> "Playable Asset";
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,139 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A custom GUI for <see cref="IParameter"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ParameterGUI_1
///
[CustomGUI(typeof(IParameter))]
public class ParameterGUI<TParameter> : CustomGUI<TParameter>
where TParameter : IParameter
{
/************************************************************************************************************************/
private static readonly HashSet<string>
ExpandedNames = new();
/************************************************************************************************************************/
private static ICustomGUI _ValueGUI;
private static ICustomGUI _DelegateGUI;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void DoGUI()
{
ParameterDictionary.IsDrawingInspector = true;
var isExpanded = DoFoldoutGUI(out var startArea);
DoValueGUI();
if (isExpanded)
{
EditorGUI.indentLevel++;
DoDetailsGUI();
EditorGUI.indentLevel--;
}
ParameterDictionary.IsDrawingInspector = false;
var endArea = GUILayoutUtility.GetLastRect();
var totalArea = startArea;
totalArea.yMax = endArea.yMax;
if (AnimancerGUI.TryUseClickEvent(totalArea, 1))
ShowContextMenu();
}
/************************************************************************************************************************/
/// <summary>Draws the <see cref="CustomGUI{T}.Value"/> and returns the area used.</summary>
private Rect DoValueGUI()
{
_ValueGUI ??= CustomGUIFactory.GetOrCreateForType(Value.ValueType);
_ValueGUI.Label = Label;
_ValueGUI.Value = Value.Value;
_ValueGUI.DoGUI();
Value.Value = _ValueGUI.Value;
return GUILayoutUtility.GetLastRect();
}
/************************************************************************************************************************/
/// <summary>Draws a foldout for the parameter details and returns true if expanded.</summary>
private bool DoFoldoutGUI(out Rect totalArea)
{
var area = AnimancerGUI.LayoutSingleLineRect();
totalArea = area;
GUILayout.Space(-AnimancerGUI.LineHeight - AnimancerGUI.StandardSpacing);
var indented = EditorGUI.IndentedRect(area);
area.xMax = indented.xMin;
EditorGUIUtility.AddCursorRect(area, MouseCursor.Arrow);
return AnimancerGUI.DoHashedFoldoutGUI(area, ExpandedNames, Label.text);
}
/************************************************************************************************************************/
/// <summary>Draws the details of the parameter.</summary>
private void DoDetailsGUI()
{
EditorGUILayout.LabelField("Type", Value.ValueType.GetNameCS(false));
_DelegateGUI ??= CustomGUIFactory.GetOrCreateForType(typeof(MulticastDelegate));
_DelegateGUI.SetLabel("On Value Changed");
_DelegateGUI.Value = Value.GetOnValueChanged();
_DelegateGUI.DoGUI();
}
/************************************************************************************************************************/
/// <summary>Shows a context menu for the parameter.</summary>
private void ShowContextMenu()
{
var menu = new GenericMenu();
menu.AddItem(new("Log Interactions"), Value.LogContext != null, () =>
{
Value.LogContext = Value.LogContext is null
? ""
: null;
});
menu.AddItem(new("Inspector Control Only"), Value.InspectorControlOnly, () =>
{
Value.InspectorControlOnly = !Value.InspectorControlOnly;
});
AnimancerEditorUtilities.AddDocumentationLink(
menu,
"Parameters Documentation",
Strings.DocsURLs.Parameters);
menu.ShowAsContext();
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,127 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// Delegate for validating and responding to <see cref="DragAndDrop"/> operations.
/// </summary>
/// <remarks>
///
/// <strong>Example:</strong>
/// <code>
/// private DragAndDropHandler&lt;AnimationClip&gt; _AnimationDropHandler;
///
/// void OnGUI(Rect area)
/// {
/// _AnimationDropHandler ??= (clip, isDrop) =>
/// {
/// if (clip.legacy)// Reject legacy animations
/// return false;
///
/// if (isDrop)// Only act when dropping.
/// Debug.Log(clip + " was dropped");
///
/// return true;// Drag or drop is accepted.
/// };
///
/// _AnimationDropHandler.Handle(area);
/// }
/// </code></remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DragAndDropHandler_1
public delegate bool DragAndDropHandler<T>(
T dragging,
bool isDrop)
where T : class;
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGUI
public static partial class AnimancerGUI
{
/************************************************************************************************************************/
/// <summary>Handles the current event.</summary>
/// <remarks>See <see cref="DragAndDropHandler{T}"/> for a usage example.</remarks>
public static bool Handle<T>(
this DragAndDropHandler<T> handler,
Rect area,
DragAndDropVisualMode mode = DragAndDropVisualMode.Link)
where T : class
{
var currentEvent = Event.current;
bool isDrop;
switch (currentEvent.type)
{
case EventType.DragUpdated:
isDrop = false;
break;
case EventType.DragPerform:
isDrop = true;
break;
default:
return false;
}
if (!area.Contains(currentEvent.mousePosition))
return false;
return handler.Handle(DragAndDrop.objectReferences, isDrop, mode);
}
/************************************************************************************************************************/
/// <summary>Handles the current event.</summary>
/// <remarks>See <see cref="DragAndDropHandler{T}"/> for a usage example.</remarks>
public static bool Handle<T>(
this DragAndDropHandler<T> handler,
IEnumerable dragging,
bool isDrop,
DragAndDropVisualMode mode = DragAndDropVisualMode.Link)
where T : class
{
if (dragging == null)
return false;
var droppedAny = false;
foreach (var obj in dragging)
{
if (obj is not T t ||
!handler(t, isDrop))
continue;
Deselect();
Event.current.Use();
if (isDrop)
{
droppedAny = true;
}
else
{
DragAndDrop.visualMode = mode;
return true;
}
}
if (!droppedAny)
return false;
DragAndDrop.AcceptDrag();
return true;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e7b86b4f7872d7944b4379dd41e5ac2a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,172 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using Animancer.Units;
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws manual controls for the <see cref="AnimancerGraph.PlayableGraph"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphControls
[Serializable, InternalSerializableType]
public class AnimancerGraphControls : AnimancerSettingsGroup
{
/************************************************************************************************************************/
/// <summary>Draws manual controls for the <see cref="AnimancerGraph.PlayableGraph"/>.</summary>
public static void DoGraphGUI(AnimancerGraph graph, out Rect area)
{
GUILayout.BeginVertical();
DoSpeedSliderGUI(graph);
DoAddAnimationGUI(graph);
GUILayout.EndVertical();
area = GUILayoutUtility.GetLastRect();
}
/************************************************************************************************************************/
/// <summary>Draws the <see cref="AnimancerGraphSpeedSlider"/>.</summary>
private static void DoSpeedSliderGUI(AnimancerGraph graph)
{
if (!AnimancerGraphSpeedSlider.Instance.IsOn)
return;
var area = LayoutSingleLineRect();
area = area.Expand(StandardSpacing, 0);
AnimancerGraphSpeedSlider.Instance.Graph = graph;
AnimancerGraphSpeedSlider.Instance.DoSpeedSlider(ref area, null);
}
/************************************************************************************************************************/
/// <summary>Draws a toggle to play and pause the graph.</summary>
public static void DoPlayPauseToggle(Rect area, AnimancerGraph graph, GUIStyle style = null)
{
EditorGUI.BeginChangeCheck();
var isGraphPlaying = AnimancerGUI.DoPlayPauseToggle(
area,
graph.IsGraphPlaying,
style);
if (EditorGUI.EndChangeCheck())
graph.IsGraphPlaying = isGraphPlaying;
}
/************************************************************************************************************************/
/// <summary>Draws a button to step time forward.</summary>
public static void DoFrameStepButton(Rect area, AnimancerGraph graph, GUIStyle style)
{
if (GUI.Button(area, AnimancerIcons.StepForwardIcon, style))
graph.Evaluate(FrameStep);
}
/************************************************************************************************************************/
#region Add Animation
/************************************************************************************************************************/
private static void DoAddAnimationGUI(AnimancerGraph graph)
{
if (!AnimancerGraphDrawer.ShowAddAnimation)
return;
var label = "Add Animation";
var area = LayoutSingleLineRect(SpacingMode.Before);
var labelArea = StealFromLeft(ref area, EditorStyles.label.CalculateWidth(label), StandardSpacing);
var objectArea = StealFromRight(ref area, area.width * 0.35f, StandardSpacing);
var clipArea = area;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
GUI.Label(labelArea, label);
var sourceClip = EditorGUI.ObjectField(clipArea, null, typeof(AnimationClip), false);
var source = EditorGUI.ObjectField(objectArea, null, typeof(Object), false);
EditorGUI.indentLevel = indentLevel;
if (sourceClip is AnimationClip sourceClipTyped)
{
graph.Layers[0].Play(sourceClipTyped);
return;
}
if (source == null)
return;
if (source is ITransition transition)
{
graph.Layers[0].Play(transition);
return;
}
var transitionAsset = TryCreateTransitionAttribute.TryCreateTransitionAsset(source);
if (transitionAsset != null)
{
var state = graph.Layers[0].Play(transitionAsset);
if (!EditorUtility.IsPersistent(transitionAsset))
{
state.SetDebugName(source);
Object.DestroyImmediate(transitionAsset);
}
return;
}
using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
{
clips.GatherFromSource(source);
foreach (var clip in clips)
graph.Layers[0].Play(clip);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Settings
/************************************************************************************************************************/
/// <inheritdoc/>
public override string DisplayName
=> "Graph Controls";
/// <inheritdoc/>
public override int Index
=> 2;
/************************************************************************************************************************/
[SerializeField]
[Seconds(Rule = Validate.Value.IsNotNegative)]
[DefaultValue(0.02f)]
[Tooltip("The amount of time that will be added by a single frame step")]
private float _FrameStep = 0.02f;
/// <summary>The amount of time that will be added by a single frame step (in seconds).</summary>
public static float FrameStep
=> AnimancerSettingsGroup<AnimancerGraphControls>.Instance._FrameStep;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,602 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using UnityEngine.Playables;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="IAnimancerComponent.Graph"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphDrawer
///
public class AnimancerGraphDrawer
{
/************************************************************************************************************************/
/// <summary>The currently drawing instance.</summary>
public static AnimancerGraphDrawer Current { get; private set; }
/// <summary>A lazy list of information about the layers currently being displayed.</summary>
private readonly List<AnimancerLayerDrawer>
LayerDrawers = new();
/// <summary>The number of elements in <see cref="LayerDrawers"/> that are currently being used.</summary>
private int _LayerCount;
/************************************************************************************************************************/
/// <summary>Draws the GUI of the <see cref="IAnimancerComponent.Graph"/> if there is only one target.</summary>
public void DoGUI(IAnimancerComponent[] targets)
{
if (targets.Length != 1)
return;
DoGUI(targets[0]);
}
/************************************************************************************************************************/
/// <summary>Draws the GUI of the <see cref="IAnimancerComponent.Graph"/>.</summary>
public void DoGUI(IAnimancerComponent target)
{
Current = this;
DoNativeAnimatorControllerGUI(target);
if (!target.IsGraphInitialized)
{
DoGraphNotInitializedGUI(target);
return;
}
GUILayout.BeginVertical();
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
EditorGUI.BeginChangeCheck();
var graph = target.Graph;
// Gather the during the layout event and use the same ones during subsequent events to avoid GUI errors
// in case they change (they shouldn't, but this is also more efficient).
if (Event.current.type == EventType.Layout)
{
AnimancerLayerDrawer.GatherLayerEditors(graph, LayerDrawers, out _LayerCount);
GatherMainObjectUsage(graph);
}
AnimancerGraphControls.DoGraphGUI(graph, out var area);
CheckContextMenu(area, graph);
for (int i = 0; i < _LayerCount; i++)
LayerDrawers[i].DoGUI();
DoOrphanStatesGUI(graph);
GUILayout.Space(StandardSpacing);
DoLayerWeightWarningGUI(target);
ParameterDictionaryDrawer.DoParametersGUI(graph);
NamedEventDictionaryDrawer.DoEventsGUI(graph);
if (ShowInternalDetails)
DoInternalDetailsGUI(graph);
if (EditorGUI.EndChangeCheck() && !graph.IsGraphPlaying)
graph.Evaluate();
DoMultipleAnimationSystemWarningGUI(target);
EditorGUIUtility.hierarchyMode = hierarchyMode;
GUILayout.EndVertical();
AnimancerLayerDrawer.HandleDragAndDropToPlay(GUILayoutUtility.GetLastRect(), graph);
Current = null;
}
/************************************************************************************************************************/
/// <summary>Draws a GUI for the <see cref="Animator.runtimeAnimatorController"/> if there is one.</summary>
private void DoNativeAnimatorControllerGUI(IAnimancerComponent target)
{
if (!EditorApplication.isPlaying &&
!target.IsGraphInitialized)
return;
var animator = target.Animator;
if (animator == null)
return;
var controller = animator.runtimeAnimatorController;
if (controller == null)
return;
BeginVerticalBox(GUI.skin.box);
var label = "Native Animator Controller";
EditorGUI.BeginChangeCheck();
controller = DoObjectFieldGUI(label, controller, false);
if (EditorGUI.EndChangeCheck())
animator.runtimeAnimatorController = controller;
if (controller is AnimatorController editorController)
{
var layers = editorController.layers;
for (int i = 0; i < layers.Length; i++)
{
var layer = layers[i];
var runtimeState = animator.IsInTransition(i) ?
animator.GetNextAnimatorStateInfo(i) :
animator.GetCurrentAnimatorStateInfo(i);
var states = layer.stateMachine.states;
var editorState = GetState(states, runtimeState.shortNameHash);
var area = LayoutSingleLineRect(SpacingMode.Before);
var weight = i == 0 ? 1 : animator.GetLayerWeight(i);
string stateName;
if (editorState != null)
{
stateName = editorState.GetCachedName();
var isLooping = editorState.motion != null && editorState.motion.isLooping;
AnimancerStateDrawer<ClipState>.DoTimeHighlightBarGUI(
area,
true,
weight,
runtimeState.normalizedTime * runtimeState.length,
runtimeState.speed,
runtimeState.length,
isLooping);
}
else
{
stateName = "State Not Found";
}
DoWeightLabel(ref area, weight, weight);
EditorGUI.LabelField(area, layer.name, stateName);
}
}
EndVerticalBox(GUI.skin.box);
}
/************************************************************************************************************************/
/// <summary>Returns the state with the specified <see cref="AnimatorState.nameHash"/>.</summary>
private static AnimatorState GetState(ChildAnimatorState[] states, int nameHash)
{
for (int i = 0; i < states.Length; i++)
{
var state = states[i].state;
if (state.nameHash == nameHash)
{
return state;
}
}
return null;
}
/************************************************************************************************************************/
private void DoGraphNotInitializedGUI(IAnimancerComponent target)
{
if (!EditorApplication.isPlaying ||
target.Animator == null ||
EditorUtility.IsPersistent(target.Animator))
return;
EditorGUILayout.HelpBox("Animancer is not initialized." +
" It will be initialized automatically when something uses it, such as playing an animation.",
MessageType.Info);
if (TryUseClickEventInLastRect(1))
{
var menu = new GenericMenu();
menu.AddItem(new("Initialize"), false, () => target.Graph.Evaluate());
AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers);
menu.ShowAsContext();
}
}
/************************************************************************************************************************/
private readonly AnimancerLayerDrawer OrphanStatesDrawer = new();
private void DoOrphanStatesGUI(AnimancerGraph graph)
{
var states = OrphanStatesDrawer.ActiveStates;
states.Clear();
foreach (var state in graph.States)
if (state.Parent == null)
states.Add(state);
if (states.Count > 0)
{
ApplySortStatesByName(states);
OrphanStatesDrawer.DoStatesGUI("Orphans", states);
}
}
/************************************************************************************************************************/
private void DoLayerWeightWarningGUI(IAnimancerComponent target)
{
if (_LayerCount == 0)
{
EditorGUILayout.HelpBox(
"No layers have been created, which likely means no animations have been played yet.",
MessageType.Warning);
if (GUILayout.Button("Create Base Layer"))
target.Graph.Layers.Count = 1;
return;
}
if (!target.gameObject.activeInHierarchy ||
!target.enabled ||
(target.Animator != null && target.Animator.runtimeAnimatorController != null))
return;
if (_LayerCount == 1)
{
var layer = LayerDrawers[0].Value;
if (layer.Weight == 0)
EditorGUILayout.HelpBox(
layer + " is at 0 weight, which likely means no animations have been played yet.",
MessageType.Warning);
return;
}
for (int i = 0; i < _LayerCount; i++)
{
var layer = LayerDrawers[i].Value;
if (layer.Weight == 1 &&
!layer.IsAdditive &&
layer._Mask == null &&
Mathf.Approximately(layer.GetTotalChildWeight(), 1))
return;
}
EditorGUILayout.HelpBox(
"There are no Override layers at weight 1, which will likely give undesirable results." +
" Click here for more information.",
MessageType.Warning);
if (TryUseClickEventInLastRect())
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Layers + "#blending");
}
/************************************************************************************************************************/
private void DoMultipleAnimationSystemWarningGUI(IAnimancerComponent target)
{
const string OnlyOneSystemWarning =
"This is not supported. Each object can only be controlled by one system at a time.";
using (ListPool<IAnimancerComponent>.Instance.Acquire(out var animancers))
{
target.gameObject.GetComponents(animancers);
if (animancers.Count > 1)
{
for (int i = 0; i < animancers.Count; i++)
{
var other = animancers[i];
if (other != target && other.Animator == target.Animator)
{
EditorGUILayout.HelpBox(
$"There are multiple {nameof(IAnimancerComponent)}s trying to control the target" +
$" {nameof(Animator)}. {OnlyOneSystemWarning}",
MessageType.Warning);
break;
}
}
}
}
if (target.Animator.TryGetComponent<Animation>(out _))
{
EditorGUILayout.HelpBox(
$"There is a Legacy {nameof(Animation)} component on the same object as the target" +
$" {nameof(Animator)}. {OnlyOneSystemWarning}",
MessageType.Warning);
}
}
/************************************************************************************************************************/
private static readonly BoolPref
ArePreUpdatablesExpanded = new(KeyPrefix + nameof(ArePreUpdatablesExpanded), false),
ArePostUpdatablesExpanded = new(KeyPrefix + nameof(ArePostUpdatablesExpanded), false),
AreDisposablesExpanded = new(KeyPrefix + nameof(AreDisposablesExpanded), false);
/// <summary>Draws a box describing the internal details of the `graph`.</summary>
private void DoInternalDetailsGUI(AnimancerGraph graph)
{
EditorGUI.indentLevel++;
DoGroupDetailsGUI(graph.PreUpdatables, "Pre-Updatables", ArePreUpdatablesExpanded);
DoGroupDetailsGUI(graph.PostUpdatables, "Post-Updatables", ArePostUpdatablesExpanded);
DoGroupDetailsGUI(graph.Disposables, "Disposables", AreDisposablesExpanded);
EditorGUI.indentLevel--;
}
/// <summary>Draws the `items`.</summary>
private static void DoGroupDetailsGUI<T>(IReadOnlyList<T> items, string groupName, BoolPref isExpanded)
{
var count = items.Count;
isExpanded.Value = DoLabelFoldoutFieldGUI(groupName, count.ToStringCached(), isExpanded);
EditorGUI.indentLevel++;
if (isExpanded)
for (int i = 0; i < count; i++)
DoDetailsGUI(items[i]);
EditorGUI.indentLevel--;
}
/// <summary>Draws the details of the `item`.</summary>
private static void DoDetailsGUI(object item)
{
if (item is AnimancerNode node)
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
var field = new FastObjectField();
field.Set(node, node.GetPath(), FastObjectField.GetIcon(node));
field.Draw(area);
return;
}
var gui = CustomGUIFactory.GetOrCreateForObject(item);
if (gui != null)
{
gui.DoGUI();
return;
}
EditorGUILayout.LabelField(item.ToString());
}
/************************************************************************************************************************/
#region Main Object Lookup
/************************************************************************************************************************/
private readonly Dictionary<Object, bool>
MainObjectDuplicateUsage = new();
/************************************************************************************************************************/
/// <summary>Is the given `mainObject` used as the <see cref="AnimancerState.MainObject"/> of multiple states?</summary>
public bool IsMainObjectUsedMultipleTimes(Object mainObject)
=> MainObjectDuplicateUsage.TryGetValue(mainObject, out var duplicate)
&& duplicate;
/************************************************************************************************************************/
private void GatherMainObjectUsage(AnimancerGraph graph)
{
MainObjectDuplicateUsage.Clear();
var layers = graph.Layers;
var layerCount = layers.Count;
for (int iLayer = 0; iLayer < layerCount; iLayer++)
{
var layer = layers[iLayer];
var childCount = layer.ChildCount;
for (int iState = 0; iState < childCount; iState++)
{
var state = layer.GetChild(iState);
var mainObject = state.MainObject;
if (mainObject == null)
continue;
if (MainObjectDuplicateUsage.TryGetValue(mainObject, out var duplicate))
{
if (!duplicate)
MainObjectDuplicateUsage[mainObject] = true;
}
else
{
MainObjectDuplicateUsage.Add(mainObject, false);
}
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
/// <summary>
/// Checks if the current event is a context menu click within the `clickArea`
/// and opens a context menu with various functions for the `graph`.
/// </summary>
private void CheckContextMenu(Rect clickArea, AnimancerGraph graph)
{
if (!TryUseClickEvent(clickArea, 1))
return;
var menu = new GenericMenu();
menu.AddDisabledItem(new(graph._PlayableGraph.GetEditorName() ?? "Unnamed Graph"), false);
menu.AddDisabledItem(new("Frame ID: " + graph.FrameID), false);
AddDisposablesFunctions(menu, graph.Disposables);
AddUpdateModeFunctions(menu, graph);
AnimancerNodeBase.AddContextMenuIK(menu, graph);
AddRootFunctions(menu, graph);
menu.AddSeparator("");
AddDisplayOptions(menu);
menu.AddItem(new("Log Details Of Everything"), false,
() => Debug.Log(graph.GetDescription(), graph.Component as Object));
AddPlayableGraphVisualizerFunction(menu, "", graph._PlayableGraph);
AnimancerEditorUtilities.AddDocumentationLink(menu, "Inspector Documentation", Strings.DocsURLs.Inspector);
menu.ShowAsContext();
}
/************************************************************************************************************************/
/// <summary>Adds functions for controlling the `graph`.</summary>
public static void AddRootFunctions(GenericMenu menu, AnimancerGraph graph)
{
menu.AddFunction("Add Layer",
graph.Layers.Count < AnimancerLayerList.DefaultCapacity,
() => graph.Layers.Count++);
menu.AddFunction("Remove Layer",
graph.Layers.Count > 0,
() => graph.Layers.Count--);
menu.AddItem(new("Keep Children Connected ?"),
graph.KeepChildrenConnected,
() => graph.SetKeepChildrenConnected(!graph.KeepChildrenConnected));
}
/************************************************************************************************************************/
/// <summary>Adds menu functions to set the <see cref="DirectorUpdateMode"/>.</summary>
private void AddUpdateModeFunctions(GenericMenu menu, AnimancerGraph graph)
{
var modes = Enum.GetValues(typeof(DirectorUpdateMode));
for (int i = 0; i < modes.Length; i++)
{
var mode = (DirectorUpdateMode)modes.GetValue(i);
menu.AddItem(new("Update Mode/" + mode), graph.UpdateMode == mode,
() => graph.UpdateMode = mode);
}
}
/************************************************************************************************************************/
/// <summary>Adds disabled items for each disposable.</summary>
private void AddDisposablesFunctions(GenericMenu menu, List<IDisposable> disposables)
{
var prefix = $"{nameof(AnimancerGraph.Disposables)}: {disposables.Count}";
if (disposables.Count == 0)
{
menu.AddDisabledItem(new(prefix), false);
}
else
{
prefix += "/";
for (int i = 0; i < disposables.Count; i++)
{
menu.AddDisabledItem(new(prefix + disposables[i]), false);
}
}
}
/************************************************************************************************************************/
/// <summary>Adds a menu function to open the Playable Graph Visualiser if it exists in the project.</summary>
public static void AddPlayableGraphVisualizerFunction(GenericMenu menu, string prefix, PlayableGraph graph)
{
var type = Type.GetType(
"GraphVisualizer.PlayableGraphVisualizerWindow, Unity.PlayableGraphVisualizer.Editor");
menu.AddFunction(prefix + "Playable Graph Visualizer", type != null, () =>
{
var window = EditorWindow.GetWindow(type);
var field = type.GetField("m_CurrentGraph", AnimancerReflection.AnyAccessBindings);
field?.SetValue(window, graph);
});
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Prefs
/************************************************************************************************************************/
internal const string
KeyPrefix = "Inspector/",
MenuPrefix = "Display Options/";
internal static readonly BoolPref
SortStatesByName = new(KeyPrefix, MenuPrefix + "Sort States By Name", true),
SeparateActiveFromInactiveStates = new(KeyPrefix, MenuPrefix + "Separate Active From Inactive States", false),
ShowInactiveStates = new(KeyPrefix, MenuPrefix + "Show Inactive States", true),
ShowSingleLayerHeader = new(KeyPrefix, MenuPrefix + "Show Single Layer Header", false),
ShowEvents = new(KeyPrefix, MenuPrefix + "Show Events", true),
ShowInternalDetails = new(KeyPrefix, MenuPrefix + "Show Internal Details", false),
ShowAddAnimation = new(KeyPrefix, MenuPrefix + "Show 'Add Animation' Field", false),
RepaintConstantly = new(KeyPrefix, MenuPrefix + "Repaint Constantly", true),
ScaleTimeBarByWeight = new(KeyPrefix, MenuPrefix + "Scale Time Bar by Weight", true),
VerifyAnimationBindings = new(KeyPrefix, MenuPrefix + "Verify Animation Bindings", true),
AutoNormalizeWeights = new(KeyPrefix, MenuPrefix + "Auto Normalize Weights", true),
UseNormalizedTimeSliders = new("Inspector", nameof(UseNormalizedTimeSliders), false);
/************************************************************************************************************************/
/// <summary>Adds functions to the `menu` for each of the Display Options.</summary>
public static void AddDisplayOptions(GenericMenu menu)
{
RepaintConstantly.AddToggleFunction(menu);
SortStatesByName.AddToggleFunction(menu);
SeparateActiveFromInactiveStates.AddToggleFunction(menu);
ShowInactiveStates.AddToggleFunction(menu);
ShowSingleLayerHeader.AddToggleFunction(menu);
ShowEvents.AddToggleFunction(menu);
ShowInternalDetails.AddToggleFunction(menu);
ShowAddAnimation.AddToggleFunction(menu);
ScaleTimeBarByWeight.AddToggleFunction(menu);
VerifyAnimationBindings.AddToggleFunction(menu);
AutoNormalizeWeights.AddToggleFunction(menu);
}
/************************************************************************************************************************/
/// <summary>Sorts the `states` if <see cref="SortStatesByName"/> is enabled.</summary>
public static void ApplySortStatesByName(List<AnimancerState> states)
{
if (SortStatesByName)
states.Sort((x, y) => x.ToString().CompareTo(y.ToString()));
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,57 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// <see cref="ToggledSpeedSlider"/> for <see cref="AnimancerGraph"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphSpeedSlider
public class AnimancerGraphSpeedSlider : ToggledSpeedSlider
{
/************************************************************************************************************************/
/// <summary>Singleton.</summary>
public static readonly AnimancerGraphSpeedSlider
Instance = new();
/// <summary>The target graph.</summary>
public AnimancerGraph Graph { get; set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerGraphSpeedSlider"/>.</summary>
public AnimancerGraphSpeedSlider()
: base(nameof(AnimancerGraphSpeedSlider) + ".Show")
{
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetSpeed(float speed)
{
if (Graph != null)
Graph.Speed = speed;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool DoToggleGUI(Rect area, GUIStyle style)
{
if (Graph != null)
Speed = Graph.Speed;
return base.DoToggleGUI(area, style);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,108 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] [Internal]
/// A utility for drawing empty [<see cref="SerializeReference"/>] <see cref="IInvokable"/> fields.
/// </summary>
/// <remarks>
/// Used to draw empty slots in <see cref="AnimancerEvent.Sequence.Serializable.Callbacks"/>
/// which don't actually have a <see cref="SerializedProperty"/> of their own because
/// the array is compacted to trim any <c>null</c> items from the end.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DummyInvokableDrawer
internal class DummyInvokableDrawer : ScriptableObject
{
/************************************************************************************************************************/
[SerializeReference, Polymorphic]
private IInvokable[] _Invokable;
/************************************************************************************************************************/
private static SerializedProperty _InvokableProperty;
/// <summary>[Editor-Only] A static dummy <see cref="IInvokable"/>.</summary>
private static SerializedProperty InvokableProperty
{
get
{
if (_InvokableProperty == null)
{
var instance = CreateInstance<DummyInvokableDrawer>();
instance.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
var serializedObject = new SerializedObject(instance);
_InvokableProperty = serializedObject.FindProperty(nameof(_Invokable));
AssemblyReloadEvents.beforeAssemblyReload += () =>
{
serializedObject.Dispose();
DestroyImmediate(instance);
};
}
return _InvokableProperty;
}
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] The GUI height required by <see cref="DoGUI"/>.</summary>
public static float Height
=> AnimancerGUI.LineHeight;
/************************************************************************************************************************/
private static int _LastControlID;
private static int _PropertyIndex;
/// <summary>[Editor-Only] Draws the <see cref="InvokableProperty"/> GUI.</summary>
public static bool DoGUI(
ref Rect area,
GUIContent label,
SerializedProperty property,
out object invokable)
{
var controlID = GUIUtility.GetControlID(FocusType.Passive);
if (_LastControlID >= controlID)
_PropertyIndex = 0;
_LastControlID = controlID;
var invokablesProperty = InvokableProperty;
if (invokablesProperty.arraySize <= _PropertyIndex)
invokablesProperty.arraySize = _PropertyIndex + 1;
var invokableProperty = invokablesProperty.GetArrayElementAtIndex(_PropertyIndex);
invokableProperty.prefabOverride = property.prefabOverride;
_PropertyIndex++;
label = EditorGUI.BeginProperty(area, label, property);
EditorGUI.PropertyField(area, invokableProperty, label, false);
EditorGUI.EndProperty();
invokable = invokableProperty.managedReferenceValue;
if (invokable == null)
return false;
invokableProperty.managedReferenceValue = null;
invokableProperty.serializedObject.ApplyModifiedProperties();
return true;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,166 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System.Runtime.CompilerServices;
using UnityEditor;
using UnityEngine;
using Sequence = Animancer.AnimancerEvent.Sequence;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="Sequence"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/EventSequenceDrawer
public class EventSequenceDrawer
{
/************************************************************************************************************************/
private static readonly ConditionalWeakTable<Sequence, EventSequenceDrawer>
SequenceToDrawer = new();
/// <summary>Returns a cached <see cref="EventSequenceDrawer"/> for the `events`.</summary>
/// <remarks>
/// The cache uses a <see cref="ConditionalWeakTable{TKey, TValue}"/> so it doesn't prevent the `events`
/// from being garbage collected.
/// </remarks>
public static EventSequenceDrawer Get(Sequence events)
{
if (events == null)
return null;
if (!SequenceToDrawer.TryGetValue(events, out var drawer))
SequenceToDrawer.Add(events, drawer = new());
return drawer;
}
/************************************************************************************************************************/
/// <summary>Calculates the number of vertical pixels required to draw the contents of the `events`.</summary>
public float CalculateHeight(Sequence events)
=> AnimancerGUI.CalculateHeight(CalculateLineCount(events));
/// <summary>Calculates the number of lines required to draw the contents of the `events`.</summary>
public int CalculateLineCount(Sequence events)
{
if (events == null)
return 0;
if (!IsExpanded)
return 1;
var count = 1;
for (int i = 0; i < events.Count; i++)
count += DelegateGUI.CalculateLineCount(events[i].callback);
count += DelegateGUI.CalculateLineCount(events.EndEvent.callback);
return count;
}
/************************************************************************************************************************/
/// <summary>Should the target's default be shown?</summary>
public bool IsExpanded { get; set; }
private static ConversionCache<int, string> _EventNumberCache;
private static float _LogButtonWidth = float.NaN;
/************************************************************************************************************************/
/// <summary>Draws the GUI for the `events`.</summary>
public void DoGUI(ref Rect area, Sequence events, GUIContent label)
{
using (var content = PooledGUIContent.Acquire(GetSummary(events)))
DoGUI(ref area, events, label, content);
}
/// <summary>Draws the GUI for the `events`.</summary>
public void DoGUI(ref Rect area, Sequence events, GUIContent label, GUIContent summary)
{
if (events == null)
return;
area.height = AnimancerGUI.LineHeight;
var headerArea = area;
const string LogLabel = "Log";
if (float.IsNaN(_LogButtonWidth))
_LogButtonWidth = EditorStyles.miniButton.CalculateWidth(LogLabel);
var logArea = AnimancerGUI.StealFromRight(ref headerArea, _LogButtonWidth);
if (GUI.Button(logArea, LogLabel, EditorStyles.miniButton))
Debug.Log(events.DeepToString());
IsExpanded = EditorGUI.Foldout(headerArea, IsExpanded, GUIContent.none, true);
EditorGUI.LabelField(headerArea, label, summary);
AnimancerGUI.NextVerticalArea(ref area);
if (!IsExpanded)
return;
var enabled = GUI.enabled;
GUI.enabled = false;
EditorGUI.indentLevel++;
for (int i = 0; i < events.Count; i++)
{
var name = events.GetName(i);
if (string.IsNullOrEmpty(name))
{
_EventNumberCache ??= new(index => $"Event {index}");
name = _EventNumberCache.Convert(i);
}
Draw(ref area, name, events[i]);
}
Draw(ref area, "End Event", events.EndEvent);
EditorGUI.indentLevel--;
GUI.enabled = enabled;
}
/************************************************************************************************************************/
private static readonly ConversionCache<int, string>
SummaryCache = new((count) => $"[{count}]"),
EndSummaryCache = new((count) => $"[{count}] + End");
/// <summary>Returns a summary of the `events`.</summary>
public static string GetSummary(Sequence events)
{
var cache = float.IsNaN(events.NormalizedEndTime) && AnimancerEvent.IsNullOrDummy(events.OnEnd)
? SummaryCache
: EndSummaryCache;
return cache.Convert(events.Count);
}
/************************************************************************************************************************/
private static ConversionCache<float, string> _EventTimeCache;
/// <summary>Draws the GUI for the `animancerEvent`.</summary>
public static void Draw(ref Rect area, string name, AnimancerEvent animancerEvent)
{
_EventTimeCache ??= new((time)
=> float.IsNaN(time) ? "Time = Auto" : $"Time = {time.ToStringCached()}x");
var timeText = _EventTimeCache.Convert(animancerEvent.normalizedTime);
using (var label = PooledGUIContent.Acquire(name))
using (var value = PooledGUIContent.Acquire(timeText))
DelegateGUI.DoGUI(ref area, label, animancerEvent.callback, value);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,195 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A GUI wrapper for drawing any object as a label with an icon.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/FastObjectField
///
public struct FastObjectField
{
/************************************************************************************************************************/
/// <summary>A <see cref="FastObjectField"/> representing <c>null</c>.</summary>
public static FastObjectField Null => new()
{
Text = "Null"
};
/************************************************************************************************************************/
/// <summary>The object passed into the last <c>Draw</c> call.</summary>
public object Value { get; private set; }
/// <summary>The text used for the label in the last <c>Draw</c> call.</summary>
public string Text { get; private set; }
/// <summary>The icon drawn in the last <c>Draw</c> call.</summary>
public Texture Icon { get; private set; }
/************************************************************************************************************************/
/// <summary>Sets the current details directly.</summary>
public void Set(object value, string text, Texture icon)
{
Value = value;
Text = text;
Icon = icon;
}
/************************************************************************************************************************/
/// <summary>Draws a field for the `value`.</summary>
public Rect Draw(Rect area, string label, object value, bool drawPing = true)
{
if (drawPing)
ObjectHighlightGUI.Draw(area, value);
if (!string.IsNullOrEmpty(label))
{
var labelWidth = EditorGUIUtility.labelWidth - area.x + AnimancerGUI.StandardSpacing + 1;
var labelArea = AnimancerGUI.StealFromLeft(ref area, labelWidth);
EditorGUI.LabelField(labelArea, label);
}
Draw(area, value, false);
return area;
}
/************************************************************************************************************************/
/// <summary>Draws a field for the `value`.</summary>
public void Draw(Rect area, object value, bool drawPing = true)
{
if (Value != value && Event.current.type == EventType.Layout)
SetValue(value);
Draw(area, drawPing);
}
/************************************************************************************************************************/
/// <summary>Draws a field for the <see cref="Value"/>.</summary>
public readonly void Draw(Rect area, bool drawPing = true)
{
HandleClick(area, drawPing);
if (Icon != null)
{
var iconArea = AnimancerGUI.StealFromLeft(ref area, area.height);
GUI.DrawTexture(iconArea, Icon);
}
GUI.Label(area, Text);
}
/************************************************************************************************************************/
private readonly void HandleClick(Rect area, bool drawPing)
{
var currentEvent = Event.current;
switch (currentEvent.rawType)
{
case EventType.MouseUp:
if (currentEvent.button == 0 &&
area.Contains(currentEvent.mousePosition))
{
ObjectHighlightGUI.Highlight(Value);
}
break;
case EventType.Repaint:
if (drawPing)
ObjectHighlightGUI.Draw(area, Value);
break;
}
}
/************************************************************************************************************************/
/// <summary>Sets the cached details.</summary>
public void SetValue(object value, string text, Texture icon = null)
{
Value = value;
Text = text;
Icon = icon;
}
/************************************************************************************************************************/
/// <summary>Sets the cached details based on the `value`.</summary>
public void SetValue(object value)
{
Value = value;
Text = GetText();
Icon = GetIcon();
}
/************************************************************************************************************************/
/// <summary>Returns a string based on the <see cref="Value"/>.</summary>
private readonly string GetText()
{
if (Value == null)
return "Null";
if (Value is Object obj)
{
if (obj == null)
return $"Null({obj.GetType().Name})";
return obj.GetCachedName();
}
if (Value is string str)
return $"\"{str}\"";
return Value.ToString();
}
/************************************************************************************************************************/
/// <summary>Returns an icon based on the type of the <see cref="Value"/>.</summary>
private readonly Texture GetIcon()
=> GetIcon(Value);
/// <summary>Returns an icon based on the type of the `value`.</summary>
public static Texture GetIcon(object value)
{
if (value == null)
return null;
if (value is Object obj)
return AssetPreview.GetMiniThumbnail(obj);
var type = value is AnimancerState
? typeof(AnimatorState)
: value.GetType();
return AssetPreview.GetMiniTypeThumbnail(type);
}
/************************************************************************************************************************/
/// <summary>Clears all cached details.</summary>
public void Clear()
{
Value = null;
Text = null;
Icon = null;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,81 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="NamedEventDictionary"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/NamedEventDictionaryDrawer
///
public static class NamedEventDictionaryDrawer
{
/************************************************************************************************************************/
private const string
KeyPrefix = AnimancerGraphDrawer.KeyPrefix;
private static readonly BoolPref
AreEventsExpanded = new(KeyPrefix + nameof(AreEventsExpanded), false);
/************************************************************************************************************************/
/// <summary>Draws the <see cref="AnimancerGraph.Events"/>.</summary>
public static void DoEventsGUI(AnimancerGraph graph)
{
if (!graph.HasEvents)
return;
EditorGUI.indentLevel++;
var events = graph.Events;
AreEventsExpanded.Value = AnimancerGUI.DoLabelFoldoutFieldGUI(
"Events",
events.Count.ToStringCached(),
AreEventsExpanded);
if (AreEventsExpanded)
{
EditorGUI.indentLevel++;
var sortedEvents = ListPool.Acquire<StringReference>();
sortedEvents.AddRange(events.Keys);
sortedEvents.Sort();
foreach (var item in sortedEvents)
DoEventGUI(item, events[item]);
ListPool.Release(sortedEvents);
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Draws an event.</summary>
public static void DoEventGUI(string name, Action action)
{
var gui = CustomGUIFactory.GetOrCreateForObject(action);
if (gui == null)
{
EditorGUILayout.LabelField(name, action.ToStringDetailed());
return;
}
gui.SetLabel(name);
gui.DoGUI();
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,79 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="ParameterDictionary"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ParameterDictionaryDrawer
///
public static class ParameterDictionaryDrawer
{
/************************************************************************************************************************/
private const string
KeyPrefix = AnimancerGraphDrawer.KeyPrefix;
private static readonly BoolPref
AreParametersExpanded = new(KeyPrefix + nameof(AreParametersExpanded), false);
/************************************************************************************************************************/
/// <summary>Draws the <see cref="AnimancerGraph.Parameters"/>.</summary>
public static void DoParametersGUI(AnimancerGraph graph)
{
if (!graph.HasParameters)
return;
EditorGUI.indentLevel++;
var parameters = graph.Parameters;
AreParametersExpanded.Value = AnimancerGUI.DoLabelFoldoutFieldGUI(
"Parameters",
parameters.Count.ToStringCached(),
AreParametersExpanded);
if (AreParametersExpanded)
{
EditorGUI.indentLevel++;
var sortedParameters = ListPool.Acquire<IParameter>();
sortedParameters.AddRange(parameters);
sortedParameters.Sort();
foreach (var item in sortedParameters)
DoParameterGUI(item);
ListPool.Release(sortedParameters);
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Draws the `parameter`.</summary>
private static void DoParameterGUI(IParameter parameter)
{
var gui = CustomGUIFactory.GetOrCreateForObject(parameter);
if (gui == null)
{
EditorGUILayout.LabelField(parameter.Key, parameter.Value.ToString());
return;
}
gui.SetLabel(parameter.Key);
gui.DoGUI();
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

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

View File

@@ -0,0 +1,274 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.IO;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A custom Inspector for <see cref="StringAsset"/> fields.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/StringAssetDrawer
[CustomPropertyDrawer(typeof(StringAsset), true)]
public class StringAssetDrawer : PropertyDrawer
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> LineHeight;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
label = EditorGUI.BeginProperty(area, label, property);
property.objectReferenceValue = DrawGUI(
area,
label,
property,
out var exitGUI);
if (exitGUI)
{
property.serializedObject.ApplyModifiedProperties();
GUIUtility.ExitGUI();
}
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
private static readonly Func<Object[]> GetCurrentPropertyValues =
() => _CurrentProperty != null ? Serialization.GetValues<Object>(_CurrentProperty) : null;
private static SerializedProperty _CurrentProperty;
/// <summary>Draws the GUI for a <see cref="StringAsset"/>.</summary>
public static Object DrawGUI(
Rect area,
GUIContent label,
SerializedProperty property,
out bool exitGUI)
{
var showMixedValue = EditorGUI.showMixedValue;
if (property != null && property.hasMultipleDifferentValues)
EditorGUI.showMixedValue = true;
_CurrentProperty = property;
var value = DrawGUI(
area,
label,
property?.objectReferenceValue,
property?.serializedObject?.targetObject,
out exitGUI,
GetCurrentPropertyValues);
_CurrentProperty = null;
EditorGUI.showMixedValue = showMixedValue;
return value;
}
/************************************************************************************************************************/
private static readonly int ButtonHash = "Button".GetHashCode();
private static readonly int ObjectFieldHash = "s_ObjectFieldHash".GetHashCode();
/// <summary>Draws the GUI for a <see cref="StringAsset"/>.</summary>
public static Object DrawGUI(
Rect area,
GUIContent label,
Object value,
Object context,
out bool exitGUI,
Func<Object[]> getAllValues = null)
{
var currentEvent = Event.current;
if (currentEvent.type == EventType.Repaint &&
!area.Contains(currentEvent.mousePosition) &&
!IsDraggingStringAsset())
{
GUIUtility.GetControlID(ButtonHash, FocusType.Passive);// Button.
GUIUtility.GetControlID(ButtonHash, FocusType.Passive);// Button.
GUIUtility.GetControlID(DragHint, FocusType.Passive, area);// DragAndDrop.
var iconSize = EditorGUIUtility.GetIconSize();
var newIconSize = LineHeight * 2 / 3;
EditorGUIUtility.SetIconSize(new(newIconSize, newIconSize));
var controlID = GUIUtility.GetControlID(ObjectFieldHash, FocusType.Keyboard, area);// Object.
var valueArea = EditorGUI.PrefixLabel(area, controlID, label);
using (var content = PooledGUIContent.Acquire())
{
if (value != null)
{
content.text = value.name;
content.image = AnimancerIcons.ScriptableObject;
}
else
{
content.text = "";
content.image = AssetPreview.GetMiniTypeThumbnail(typeof(StringAsset));
}
EditorStyles.objectField.Draw(valueArea, content, false, false, false, false);
}
EditorGUIUtility.SetIconSize(iconSize);
exitGUI = false;
return value;
}
if (value == null)
{
var buttonArea = StealFromRight(ref area, area.height, StandardSpacing);
var content = AnimancerIcons.AddIcon("Create and save a new String Asset");
if (GUI.Button(buttonArea, content, NoPaddingButtonStyle))
{
exitGUI = true;
return CreateNewInstance(label.text, context);
}
GUIUtility.GetControlID(ButtonHash, FocusType.Passive);
}
else
{
var clearArea = StealFromRight(ref area, area.height, StandardSpacing);
var copyArea = StealFromRight(ref area, area.height, StandardSpacing);
var content = AnimancerIcons.CopyIcon("Copy string to clipboard");
if (GUI.Button(copyArea, content, NoPaddingButtonStyle))
GUIUtility.systemCopyBuffer = value.name;
content = AnimancerIcons.ClearIcon("Clear reference");
if (GUI.Button(clearArea, content, NoPaddingButtonStyle))
value = null;
}
HandleDragAndDrop(area, currentEvent, value, getAllValues);
exitGUI = false;
return EditorGUI.ObjectField(area, label, value, typeof(StringAsset), false);
}
/************************************************************************************************************************/
private static readonly int DragHint = "Drag".GetHashCode();
private static void HandleDragAndDrop(
Rect area,
Event currentEvent,
Object value,
Func<Object[]> getAllValues)
{
var id = GUIUtility.GetControlID(DragHint, FocusType.Passive, area);
switch (currentEvent.type)
{
// Drag out of object field.
case EventType.MouseDrag:
if (GUIUtility.keyboardControl == id + 1 &&
currentEvent.button == 0 &&
area.Contains(currentEvent.mousePosition) &&
value != null)
{
var values = getAllValues?.Invoke() ?? new Object[] { value };
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = values;
DragAndDrop.StartDrag("Objects");
currentEvent.Use();
}
break;
}
}
/// <summary>Is a <see cref="StringAsset"/> currently being dragged?</summary>
private static bool IsDraggingStringAsset()
{
var dragging = DragAndDrop.objectReferences;
if (dragging.IsNullOrEmpty())
return false;
for (int i = 0; i < dragging.Length; i++)
if (dragging[i] is not StringAsset)
return false;
return true;
}
/************************************************************************************************************************/
private const string FolderPathKey = nameof(StringAsset) + ".FolderPath";
/// <summary>Asks where to save a new <see cref="StringAsset"/>.</summary>
private static Object CreateNewInstance(string name, Object targetObject)
{
var folderPath = GetSaveFolder(targetObject);
var path = EditorUtility.SaveFilePanelInProject(
"Create String Asset",
name,
"asset",
"Where yould you like to save the new String Asset?",
folderPath);
if (string.IsNullOrEmpty(path))
return null;
EditorPrefs.SetString(FolderPathKey, Path.GetDirectoryName(path));
var instance = ScriptableObject.CreateInstance<StringAsset>();
AssetDatabase.CreateAsset(instance, path);
return instance;
}
private static string GetSaveFolder(Object targetObject)
{
var getActiveFolderPath = typeof(ProjectWindowUtil).GetMethod(
"GetActiveFolderPath",
AnimancerReflection.StaticBindings,
null,
Type.EmptyTypes,
null);
if (getActiveFolderPath != null &&
getActiveFolderPath.ReturnType == typeof(string))
{
var activeFolderPath = getActiveFolderPath.Invoke(null, Array.Empty<object>())?.ToString();
if (!string.IsNullOrEmpty(activeFolderPath))
return activeFolderPath;
}
var folderPath = AssetDatabase.GetAssetPath(targetObject);
if (!string.IsNullOrEmpty(folderPath))
folderPath = Path.GetDirectoryName(folderPath);
if (string.IsNullOrEmpty(folderPath))
{
folderPath = EditorPrefs.GetString(FolderPathKey);
if (folderPath == null || !folderPath.StartsWith("Assets/"))
folderPath = "Assets/";
}
return folderPath;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,833 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using Animancer.Editor.Previews;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Draws a GUI box denoting a period of time.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TimelineGUI
///
public class TimelineGUI : IDisposable
{
/************************************************************************************************************************/
#region Fields
/************************************************************************************************************************/
private static readonly ConversionCache<float, string>
G2Cache = new(value =>
{
if (Math.Abs(value) <= 99)
return value.ToString("G2");
else
return ((int)value).ToString();
});
private static Texture _EventIcon;
/// <summary>The icon used for events.</summary>
public static Texture EventIcon
=> AnimancerIcons.Load(ref _EventIcon, "Animation.EventMarker");
private static readonly Color
FadeHighlightColor = new(0.35f, 0.5f, 1, 0.5f),
SelectedEventColor = new(0.3f, 0.55f, 0.95f),
UnselectedEventColor = AnimancerGUI.Grey(0.9f),
PreviewTimeColor = AnimancerStateDrawerColors.FadeLineColor,
BaseTimeColor = AnimancerGUI.Grey(0.5f, 0.75f);
private Rect _Area;
/// <summary>The pixel area in which this <see cref="TimelineGUI"/> is drawing.</summary>
public Rect Area => _Area;
private float _Speed, _Duration, _MinTime, _MaxTime, _StartTime, _EndTime, _FadeInEnd, _FadeOutEnd, _SecondsToPixels;
private bool _HasEndTime;
private readonly List<float>
EventTimes = new();
/// <summary>The height of the time ticks.</summary>
public float TickHeight { get; private set; }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Conversions
/************************************************************************************************************************/
/// <summary>Converts a number of seconds to a horizontal pixel position along the ruler.</summary>
/// <remarks>The value is rounded to the nearest integer.</remarks>
public float SecondsToPixels(float seconds) => AnimancerUtilities.Round((seconds - _MinTime) * _SecondsToPixels);
/// <summary>Converts a horizontal pixel position along the ruler to a number of seconds.</summary>
public float PixelsToSeconds(float pixels) => (pixels / _SecondsToPixels) + _MinTime;
/// <summary>Converts a number of seconds to a normalized time value.</summary>
public float SecondsToNormalized(float seconds) => seconds / _Duration;
/// <summary>Converts a normalized time value to a number of seconds.</summary>
public float NormalizedToSeconds(float normalized) => normalized * _Duration;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
private TimelineGUI() { }
private static readonly TimelineGUI Instance = new();
/// <summary>The currently drawing <see cref="TimelineGUI"/> (or null if none is drawing).</summary>
public static TimelineGUI Current { get; private set; }
/// <summary>Ends the area started by <see cref="BeginGUI"/>.</summary>
void IDisposable.Dispose()
{
Current = null;
GUI.EndClip();
}
/************************************************************************************************************************/
/// <summary>
/// Sets the `area` in which the ruler will be drawn and draws a <see cref="GUI.Box(Rect, string)"/> there.
/// The returned object must have <see cref="IDisposable.Dispose"/> called on it afterwards.
/// </summary>
private static IDisposable BeginGUI(Rect area)
{
if (Current != null)
throw new InvalidOperationException($"{nameof(TimelineGUI)} can't be used recursively.");
if (!EditorGUIUtility.hierarchyMode)
EditorGUI.indentLevel++;
area = EditorGUI.IndentedRect(area);
if (!EditorGUIUtility.hierarchyMode)
EditorGUI.indentLevel--;
GUI.Box(area, "");
GUI.BeginClip(area);
area.x = area.y = 0;
Instance._Area = area;
Instance.TickHeight = Mathf.Ceil(area.height * 0.3f);
return Current = Instance;
}
/************************************************************************************************************************/
/// <summary>Draws the ruler GUI and handles input events for the specified `context`.</summary>
public static void DoGUI(Rect area, SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
{
using (BeginGUI(area))
Current.DoGUI(context, out addEventNormalizedTime);
}
/************************************************************************************************************************/
/// <summary>Draws the ruler GUI and handles input events for the specified `context`.</summary>
private void DoGUI(SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
{
if (context.Property.hasMultipleDifferentValues)
{
GUI.Label(_Area, "Multi-editing events is not supported");
addEventNormalizedTime = float.NaN;
return;
}
var transition = context.TransitionContext.Transition;
_Speed = transition.Speed;
if (float.IsNaN(_Speed))
_Speed = 1;
_Duration = context.TransitionContext.MaximumLength;
if (_Duration <= 0)
_Duration = 1;
GatherEventTimes(context);
_StartTime = GetStartTime(transition.NormalizedStartTime, _Speed, _Duration);
_FadeInEnd = _StartTime + transition.FadeDuration * _Speed;
_FadeOutEnd = GetFadeOutEnd(_Speed, _EndTime, _Duration);
_MinTime = Mathf.Min(0, _StartTime);
_MinTime = Mathf.Min(_MinTime, _FadeOutEnd);
_MinTime = Mathf.Min(_MinTime, EventTimes[0]);
_MaxTime = Mathf.Max(_StartTime, _FadeOutEnd);
if (EventTimes.Count >= 2)
_MaxTime = Mathf.Max(_MaxTime, EventTimes[^2]);
if (_MaxTime < _Duration)
_MaxTime = _Duration;
_SecondsToPixels = _Area.width / (_MaxTime - _MinTime);
DoFadeHighlightGUI();
if (AnimancerUtilities.TryGetWrappedObject(transition, out ITransitionGUI gui))
gui.OnTimelineBackgroundGUI();
DoEventsGUI(context, out addEventNormalizedTime);
DoRulerGUI();
if (_Speed > 0)
{
if (_StartTime >= _EndTime)
GUI.Label(_Area, "Start Time is not before End Time");
}
else if (_Speed < 0)
{
if (_StartTime <= _EndTime)
GUI.Label(_Area, "Start Time is not after End Time");
}
gui?.OnTimelineForegroundGUI();
}
/************************************************************************************************************************/
/// <summary>Calculates the start time of the transition (in seconds).</summary>
public static float GetStartTime(float normalizedStartTime, float speed, float duration)
{
if (float.IsNaN(normalizedStartTime))
{
return speed < 0 ? duration : 0;
}
else
{
return normalizedStartTime * duration;
}
}
/// <summary>Calculates the end time of the fade out (in seconds).</summary>
public static float GetFadeOutEnd(float speed, float endTime, float duration)
{
if (speed < 0)
return endTime > 0 ? 0 : (endTime - AnimancerGraph.DefaultFadeDuration) * -speed;
else
return endTime < duration ? duration : endTime + AnimancerGraph.DefaultFadeDuration * speed;
}
/************************************************************************************************************************/
private static readonly Vector3[] QuadVertices = new Vector3[4];
/// <summary>Draws a polygon describing the start, end, and fade details.</summary>
private void DoFadeHighlightGUI()
{
if (Event.current.type != EventType.Repaint)
return;
var color = Handles.color;
Handles.color = FadeHighlightColor;
QuadVertices[0] = new(SecondsToPixels(_StartTime), _Area.y);
QuadVertices[1] = new(SecondsToPixels(_FadeInEnd), _Area.yMax + 1);
QuadVertices[2] = new(SecondsToPixels(_FadeOutEnd), _Area.yMax + 1);
QuadVertices[3] = new(SecondsToPixels(_EndTime), _Area.y);
Handles.DrawAAConvexPolygon(QuadVertices);
Handles.color = color;
}
/************************************************************************************************************************/
#region Events
/************************************************************************************************************************/
private void GatherEventTimes(SerializableEventSequenceDrawer.Context context)
{
EventTimes.Clear();
if (context.Times.Count > 0)
{
var depth = context.Times.Property.depth;
var time = context.Times.GetElement(0);
while (time.depth > depth)
{
EventTimes.Add(time.floatValue * _Duration);
time.Next(false);
}
_EndTime = EventTimes[^1];
if (!float.IsNaN(_EndTime))
{
_HasEndTime = true;
return;
}
}
_EndTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(_Speed) * _Duration;
_HasEndTime = false;
if (EventTimes.Count == 0)
EventTimes.Add(_EndTime);
else
EventTimes[^1] = _EndTime;
}
/************************************************************************************************************************/
private static readonly int EventHash = "Event".GetHashCode();
private static readonly List<int> EventControlIDs = new();
/// <summary>Draws the details of the <see cref="SerializableEventSequenceDrawer.Context.Callbacks"/>.</summary>
private void DoEventsGUI(SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
{
addEventNormalizedTime = float.NaN;
var currentEvent = Event.current;
EventControlIDs.Clear();
var selectedEventControlID = -1;
var baseControlID = GUIUtility.GetControlID(EventHash - 1, FocusType.Passive);
for (int i = 0; i < EventTimes.Count; i++)
{
var controlID = GUIUtility.GetControlID(EventHash + i, FocusType.Keyboard);
EventControlIDs.Add(controlID);
if (context.SelectedEvent == i)
selectedEventControlID = controlID;
}
EventControlIDs.Add(baseControlID);
switch (currentEvent.type)
{
case EventType.Repaint:
RepaintEventsGUI(context);
break;
case EventType.MouseDown:
OnMouseDown(currentEvent, context, ref addEventNormalizedTime);
break;
case EventType.MouseUp:
OnMouseUp(currentEvent, context);
break;
case EventType.MouseDrag:
if (_Duration <= 0)
break;
var hotControl = GUIUtility.hotControl;
if (hotControl == baseControlID)
{
SetPreviewTime(context, currentEvent);
GUIUtility.ExitGUI();
}
else
{
for (int i = 0; i < EventTimes.Count; i++)
{
if (hotControl == EventControlIDs[i])
{
if (context.Times.Count < 1)
context.Times.Count = 1;
var seconds = PixelsToSeconds(currentEvent.mousePosition.x);
if (currentEvent.control)
SnapToFrameRate(context, ref seconds);
var timeProperty = context.Times.GetElement(i);
var normalizedTime = seconds / _Duration;
timeProperty.floatValue = normalizedTime;
SerializableEventSequenceDrawer.SyncEventTimeChange(context, i, normalizedTime);
timeProperty.serializedObject.ApplyModifiedProperties();
timeProperty.serializedObject.Update();
GUIUtility.hotControl = EventControlIDs[context.SelectedEvent];
GUI.changed = true;
SetPreviewTime(context, currentEvent);
GUIUtility.ExitGUI();
}
}
}
break;
case EventType.KeyUp:
if (GUIUtility.keyboardControl != selectedEventControlID)
break;
var exitGUI = false;
switch (currentEvent.keyCode)
{
case KeyCode.Delete:
case KeyCode.Backspace:
SerializableEventSequenceDrawer.RemoveEvent(context, context.SelectedEvent);
exitGUI = true;
break;
case KeyCode.LeftArrow:
NudgeEventTime(context, Event.current.shift ? -10 : -1);
break;
case KeyCode.RightArrow:
NudgeEventTime(context, Event.current.shift ? 10 : 1);
break;
case KeyCode.Space:
RoundEventTime(context);
break;
default: return;// Don't call Use.
}
GUI.changed = true;
currentEvent.Use();
if (exitGUI)
GUIUtility.ExitGUI();
break;
}
}
/************************************************************************************************************************/
/// <summary>Snaps the `seconds` value to the nearest multiple of the <see cref="AnimationClip.frameRate"/>.</summary>
public void SnapToFrameRate(SerializableEventSequenceDrawer.Context context, ref float seconds)
{
if (AnimancerUtilities.TryGetFrameRate(context.TransitionContext.Transition, out var frameRate))
{
seconds = AnimancerUtilities.Round(seconds, 1f / frameRate);
}
}
/************************************************************************************************************************/
private void RepaintEventsGUI(SerializableEventSequenceDrawer.Context context)
{
var color = GUI.color;
for (int i = 0; i < EventTimes.Count; i++)
{
var currentColor = color;
// Read Only: currentColor *= new(0.9f, 0.9f, 0.9f, 0.5f * alpha);
if (context.SelectedEvent == i)
{
currentColor *= SelectedEventColor;
}
else
{
currentColor *= UnselectedEventColor;
}
if (i == EventTimes.Count - 1 && !_HasEndTime)
currentColor.a *= 0.65f;
GUI.color = currentColor;
var area = GetEventIconArea(i);
GUI.DrawTexture(area, EventIcon);
}
GUI.color = color;
}
/************************************************************************************************************************/
private void OnMouseDown(Event currentEvent, SerializableEventSequenceDrawer.Context context, ref float addEventNormalizedTime)
{
if (!_Area.Contains(currentEvent.mousePosition))
return;
var selectedEventControlID = 0;
var selectedEvent = -1;
for (int i = 0; i < EventControlIDs.Count; i++)
{
var area = i < EventTimes.Count ? GetEventIconArea(i) : _Area;
if (area.Contains(currentEvent.mousePosition))
{
selectedEventControlID = EventControlIDs[i];
selectedEvent = i;
break;
}
}
if (selectedEvent < 0 || selectedEvent >= EventTimes.Count)
{
SetPreviewTime(context, currentEvent);
selectedEvent = -1;
}
if (currentEvent.type == EventType.MouseDown &&
currentEvent.clickCount == 2)
{
addEventNormalizedTime = PixelsToSeconds(currentEvent.mousePosition.x);
addEventNormalizedTime = SecondsToNormalized(addEventNormalizedTime);
}
context.SelectedEvent = selectedEvent;
GUIUtility.keyboardControl = selectedEventControlID;
currentEvent.Use(selectedEventControlID);
}
/************************************************************************************************************************/
private void OnMouseUp(Event currentEvent, SerializableEventSequenceDrawer.Context context)
{
if (currentEvent.button == 1 &&
_Area.Contains(currentEvent.mousePosition))
{
currentEvent.Use(0);
ShowContextMenu(currentEvent, context);
}
}
/************************************************************************************************************************/
private void ShowContextMenu(Event currentEvent, SerializableEventSequenceDrawer.Context context)
{
context = context.Copy();
var time = SecondsToNormalized(PixelsToSeconds(currentEvent.mousePosition.x));
var hasSelectedEvent = context.SelectedEvent >= 0;
var menu = new GenericMenu();
AddContextFunction(menu, context, "Add Event (Double Click)", true,
() => SerializableEventSequenceDrawer.AddEvent(context, time, null));
AddContextFunction(menu, context, "Remove Event (Delete)", hasSelectedEvent,
() => SerializableEventSequenceDrawer.RemoveEvent(context, context.SelectedEvent));
AddCopyEventsFromAnimationClipsFunction(menu, context);
const string NudgePrefix = "Nudge Event Time/";
AddContextFunction(menu, context, NudgePrefix + "Left 1 Pixel (Left Arrow)", hasSelectedEvent,
() => NudgeEventTime(context, -1));
AddContextFunction(menu, context, NudgePrefix + "Left 10 Pixels (Shift + Left Arrow)", hasSelectedEvent,
() => NudgeEventTime(context, -10));
AddContextFunction(menu, context, NudgePrefix + "Right 1 Pixel (Right Arrow)", hasSelectedEvent,
() => NudgeEventTime(context, 1));
AddContextFunction(menu, context, NudgePrefix + "Right 10 Pixels (Shift + Right Arrow)", hasSelectedEvent,
() => NudgeEventTime(context, 10));
var canRoundTime = hasSelectedEvent;
if (canRoundTime)
{
time = context.Times.GetElement(context.SelectedEvent).floatValue;
canRoundTime = TryRoundValue(ref time);
}
AddContextFunction(menu, context, $"Round Event Time to {time}x (Space)", canRoundTime,
() => RoundEventTime(context));
menu.ShowAsContext();
}
/************************************************************************************************************************/
private static void AddContextFunction(
GenericMenu menu,
SerializableEventSequenceDrawer.Context context,
string label,
bool enabled,
Action function)
{
menu.AddFunction(label, enabled, () =>
{
using (context.SetAsCurrent())
{
function();
GUI.changed = true;
}
});
}
/************************************************************************************************************************/
private void AddCopyEventsFromAnimationClipsFunction(
GenericMenu menu,
SerializableEventSequenceDrawer.Context context)
{
var transition = context.TransitionContext.Transition;
var clips = ListPool<AnimationClip>.Instance.Acquire();
clips.GatherFromSource(transition);
var enabled = clips.Count > 0;
AddContextFunction(menu, context, "Copy Events from Animation Clip", enabled,
() =>
{
var names = ListPool<StringAsset>.Instance.Acquire();
var normalizedTimes = ListPool<float>.Instance.Acquire();
string createDirectory = null;
foreach (var clip in clips)
{
foreach (var animancerEvent in clip.events)
{
var name = StringReference.Get(animancerEvent.functionName);
var asset = StringAsset.Find(name, out _);
if (asset == null)
{
asset = StringAsset.Create(name, ref createDirectory, out _);
// If no directory is picked, cancel the rest of this function.
if (asset == null)
return;
}
names.Add(asset);
normalizedTimes.Add(animancerEvent.time / clip.length);
}
}
ListPool<AnimationClip>.Instance.Release(clips);
for (int i = 0; i < normalizedTimes.Count; i++)
SerializableEventSequenceDrawer.AddEvent(context, normalizedTimes[i], names[i]);
ListPool<StringAsset>.Instance.Release(names);
ListPool<float>.Instance.Release(normalizedTimes);
});
}
/************************************************************************************************************************/
private void SetPreviewTime(SerializableEventSequenceDrawer.Context context, Event currentEvent)
{
if (_Duration > 0)
{
var seconds = PixelsToSeconds(currentEvent.mousePosition.x);
if (currentEvent.control)
SnapToFrameRate(context, ref seconds);
TransitionPreviewWindow.PreviewNormalizedTime = seconds / _Duration;
}
}
/************************************************************************************************************************/
private Rect GetEventIconArea(int index)
{
var width = EventIcon.width;
var x = SecondsToPixels(EventTimes[index]) - width * 0.5f;
x = Mathf.Clamp(x, 0, _Area.width - width);
return new(x, _Area.y, width, EventIcon.height);
}
/************************************************************************************************************************/
private void NudgeEventTime(SerializableEventSequenceDrawer.Context context, float offsetPixels)
{
var index = context.SelectedEvent;
var time = context.Times.GetElement(index);
var value = time.floatValue;
value = NormalizedToSeconds(value);
value = SecondsToPixels(value);
value += offsetPixels;
value = PixelsToSeconds(value);
value = SecondsToNormalized(value);
time.floatValue = value;
SerializableEventSequenceDrawer.SyncEventTimeChange(context, index, value);
}
/************************************************************************************************************************/
private static void RoundEventTime(SerializableEventSequenceDrawer.Context context)
{
var index = context.SelectedEvent;
var time = context.Times.GetElement(index);
var value = time.floatValue;
if (TryRoundValue(ref value))
{
time.floatValue = value;
SerializableEventSequenceDrawer.SyncEventTimeChange(context, index, value);
}
}
private static bool TryRoundValue(ref float value)
{
var format = System.Globalization.NumberFormatInfo.InvariantInfo;
var text = value.ToString(format);
var dot = text.IndexOf('.');
if (dot < 0)
return false;
Round:
var newValue = (float)Math.Round(value, text.Length - dot - 2, MidpointRounding.AwayFromZero);
if (newValue == value)
{
dot--;
if (dot > 0)
goto Round;
}
if (value != newValue)
{
value = newValue;
return true;
}
else return false;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Ticks
/************************************************************************************************************************/
private static readonly List<float> TickTimes = new();
/// <summary>Draws ticks and labels for important times throughout the area.</summary>
private void DoRulerGUI()
{
if (Event.current.type != EventType.Repaint)
return;
var area = new Rect(SecondsToPixels(0), _Area.yMax - TickHeight, 0, TickHeight)
{
xMax = SecondsToPixels(_Duration)
};
EditorGUI.DrawRect(area, BaseTimeColor);
TickTimes.Clear();
TickTimes.Add(0);
TickTimes.Add(_StartTime);
TickTimes.Add(_FadeInEnd);
TickTimes.Add(_Duration);
TickTimes.AddRange(EventTimes);
TickTimes.Sort();
var previousTime = float.NaN;
area.x = float.NegativeInfinity;
for (int i = 0; i < TickTimes.Count; i++)
{
var time = TickTimes[i];
if (previousTime != time)
{
previousTime = time;
DoRulerLabelGUI(ref area, time);
}
}
DrawPreviewTime();
}
/************************************************************************************************************************/
private void DrawPreviewTime()
{
var state = TransitionPreviewWindow.GetCurrentState();
if (state == null)
return;
var normalizedTime = TransitionPreviewWindow.PreviewNormalizedTime;
DrawPreviewTime(normalizedTime, alpha: 1);
// Looping states show faded indicators at every other multiple of the loop.
if (!state.IsLooping)
return;
// Make sure the area is actually wide enough for it to not just be a solid bar.
if ((int)SecondsToPixels(0) > (int)SecondsToPixels(_Duration) - 4)
return;
// Go back to the first visible increment.
while (normalizedTime * _Duration >= _MinTime + _Duration)
normalizedTime -= 1;
// Draw every visible increment from there on.
while (normalizedTime * _Duration <= _MaxTime)
{
DrawPreviewTime(normalizedTime, alpha: 0.2f);
normalizedTime += 1;
}
}
private void DrawPreviewTime(float normalizedTime, float alpha)
{
var time = NormalizedToSeconds(normalizedTime);
var x = SecondsToPixels(time);
if (x >= 0 && x <= _Area.width)
{
var color = PreviewTimeColor;
color.a = alpha;
EditorGUI.DrawRect(new(x - 1, _Area.y, 2, _Area.height), color);
}
}
/************************************************************************************************************************/
private static GUIStyle _RulerLabelStyle;
private static ConversionCache<string, float> _TimeLabelWidthCache;
private void DoRulerLabelGUI(ref Rect previousArea, float time)
{
_RulerLabelStyle ??= new(GUI.skin.label)
{
padding = new(),
contentOffset = new(0, -2),
alignment = TextAnchor.UpperLeft,
fontSize = Mathf.CeilToInt(AnimancerGUI.LineHeight * 0.6f),
};
var text = G2Cache.Convert(time);
_TimeLabelWidthCache ??= ConversionCache.CreateWidthCache(_RulerLabelStyle);
var area = new Rect(
SecondsToPixels(time),
_Area.y,
_TimeLabelWidthCache.Convert(text),
_Area.height);
if (area.x > _Area.x)
{
var tickY = _Area.yMax - TickHeight;
EditorGUI.DrawRect(new(area.x, tickY, 1, TickHeight), AnimancerGUI.TextColor);
}
if (area.xMax > _Area.xMax)
area.x = _Area.xMax - area.width;
if (area.x < 0)
area.x = 0;
if (area.x > previousArea.xMax + 2)
{
GUI.Label(area, text, _RulerLabelStyle);
previousArea = area;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,126 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Utility for a toggle which can show and hide a speed slider.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ToggledSpeedSlider
public class ToggledSpeedSlider
{
/************************************************************************************************************************/
/// <summary>The content displayed on the toggle.</summary>
/// <remarks>The <see cref="GUIContent.text"/> is set by the <see cref="Speed"/>.</remarks>
public readonly GUIContent GUIContent = new();
/// <summary>Is the toggle currently on?</summary>
public readonly BoolPref IsOn;
/************************************************************************************************************************/
private float _Speed = float.NaN;
/// <summary>The current speed value.</summary>
public float Speed
{
get => _Speed;
set
{
if (_Speed == value)
return;
_Speed = value;
GUIContent.text = _Speed.ToString("0.0x");
OnSetSpeed(_Speed);
}
}
/// <summary>Called when the <see cref="Speed"/> is changed.</summary>
protected virtual void OnSetSpeed(float speed) { }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ToggledSpeedSlider"/>.</summary>
public ToggledSpeedSlider(string prefKey)
{
IsOn = new(prefKey);
}
/************************************************************************************************************************/
/// <summary>Draws a toggle to show or hide the speed slider.</summary>
public virtual bool DoToggleGUI(Rect area, GUIStyle style)
{
HandleResetClick(area);
style ??= GUI.skin.toggle;
IsOn.Value = GUI.Toggle(area, IsOn, GUIContent, style);
return IsOn;
}
/************************************************************************************************************************/
private static float _SpeedLabelWidth;
/// <summary>Draws a slider to control the <see cref="Speed"/>.</summary>
public float DoSpeedSlider(ref Rect area, GUIStyle backgroundStyle = null)
{
if (!IsOn)
return Speed;
var sliderArea = StealLineFromTop(ref area);
sliderArea = sliderArea.Expand(-StandardSpacing, 0);
if (backgroundStyle != null)
GUI.Label(sliderArea, GUIContent.none, backgroundStyle);
HandleResetClick(sliderArea);
var label = "Speed";
if (_SpeedLabelWidth == 0)
_SpeedLabelWidth = CalculateLabelWidth(label);
var labelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = _SpeedLabelWidth;
EditorGUI.BeginChangeCheck();
var speed = EditorGUI.Slider(sliderArea, label, Speed, 0.1f, 2);
if (EditorGUI.EndChangeCheck())
Speed = speed;
EditorGUIUtility.labelWidth = labelWidth;
return Speed;
}
/************************************************************************************************************************/
/// <summary>Handles Right or Middle Clicking in the `area` to reset the <see cref="Speed"/>.</summary>
public void HandleResetClick(Rect area)
{
if (!TryUseClickEvent(area, 1) && !TryUseClickEvent(area, 2))
return;
if (Speed != 1)
Speed = 1;
else
Speed = 0.5f;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,56 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only] A custom Inspector for <see cref="TransitionLibraryAsset"/> fields.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryDrawer
[CustomPropertyDrawer(typeof(TransitionLibraryAsset), true)]
public class TransitionLibraryDrawer : PropertyDrawer
{
/************************************************************************************************************************/
private const string EditLabel = "Edit";
private static float _EditButtonWidth;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> LineHeight;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
var library = property.objectReferenceValue as TransitionLibraryAsset;
if (library != null)
{
var style = EditorStyles.miniButton;
if (_EditButtonWidth <= 0)
_EditButtonWidth = style.CalculateWidth(EditLabel);
var editArea = StealFromRight(ref area, _EditButtonWidth, StandardSpacing);
if (GUI.Button(editArea, EditLabel, style))
TransitionLibraryWindow.Open(library);
}
EditorGUI.PropertyField(area, property, label);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a788bddb70bff9d41bac4c3a69207a4a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && ULT_EVENTS
using Animancer.Editor;
using UnityEditor;
[assembly: PolymorphicDrawerDetails(typeof(UltEvents.UltEventBase), SeparateHeader = true)]
namespace Animancer.Editor
{
/// <summary>[Editor-Only] <see cref="PropertyDrawer"/> for <see cref="UltEvent"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/UltEventDrawer
[CustomPropertyDrawer(typeof(UltEvent), true)]
public class UltEventDrawer : UltEvents.Editor.UltEventDrawer,
PropertyDrawers.IDiscardOnSelectionChange
{ }
}
#endif

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7b7d1c1b70200d5498e65acbbb6199e0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
using UnityEngine;
namespace Animancer.Units.Editor
{
/// <summary>[Editor-Only]
/// A <see cref="PropertyDrawer"/> for fields with an <see cref="AnimationSpeedAttributeDrawer"/>
/// which displays them using an 'x' suffix.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/AnimationSpeedAttributeDrawer
[CustomPropertyDrawer(typeof(AnimationSpeedAttribute), true)]
public class AnimationSpeedAttributeDrawer : UnitsAttributeDrawer
{
/************************************************************************************************************************/
/// <inheritdoc/>
protected override int GetLineCount(SerializedProperty property, GUIContent label)
=> 1;
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,281 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using Animancer.Editor;
using Animancer.Editor.Previews;
using System;
using UnityEditor;
using UnityEngine;
namespace Animancer.Units.Editor
{
/// <summary>[Editor-Only]
/// A <see cref="PropertyDrawer"/> for <see cref="float"/> fields with a <see cref="UnitsAttribute"/>
/// which displays them using 3 fields: Normalized, Seconds, and Frames.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#time-fields">
/// Time Fields</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/AnimationTimeAttributeDrawer
[CustomPropertyDrawer(typeof(AnimationTimeAttribute), true)]
public class AnimationTimeAttributeDrawer : UnitsAttributeDrawer
{
/************************************************************************************************************************/
/// <summary>
/// Should the <see cref="NextDefaultValue"/> and <see cref="NextValueIsOptional"/>
/// be used for the next field to be drawn?
/// </summary>
public static bool HasNextDefaultValue { get; private set; }
/// <summary>The default value to be used for the next field drawn by this attribute.</summary>
public static float NextDefaultValue { get; private set; } = float.NaN;
/// <summary>The default value to be used for the next field drawn by this attribute.</summary>
public static bool NextValueIsOptional { get; private set; }
/// <summary>Sets the <see cref="NextDefaultValue"/> and <see cref="NextValueIsOptional"/>.</summary>
public static void SetNextDefaultValue(float defaultValue, bool isOptional)
{
HasNextDefaultValue = true;
NextDefaultValue = defaultValue;
NextValueIsOptional = isOptional;
}
/// <summary>
/// Sets the <see cref="NextDefaultValue"/>
/// and the <see cref="NextValueIsOptional"/> is true if the value is not <see cref="float.NaN"/>.
/// </summary>
public static void SetNextDefaultValue(float defaultValue)
{
HasNextDefaultValue = true;
NextDefaultValue = defaultValue;
NextValueIsOptional = !float.IsNaN(defaultValue);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override int GetLineCount(SerializedProperty property, GUIContent label)
=> EditorGUIUtility.wideMode || TransitionDrawer.Context.Property == null
? 1
: 2;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginChangeCheck();
var nextDefaultValue = NextDefaultValue;
BeginProperty(area, property, ref label, out var value);
OnGUI(area, label, ref value);
EndProperty(area, property, ref value);
if (EditorGUI.EndChangeCheck())
{
var index = (int)AnimationTimeAttribute.Units.Normalized;
TransitionPreviewWindow.PreviewNormalizedTime =
GetDisplayValue(value, nextDefaultValue) * Attribute.Multipliers[index];
}
}
/************************************************************************************************************************/
/// <summary>Draws the GUI for this attribute.</summary>
public void OnGUI(Rect area, GUIContent label, ref float value)
{
try
{
Initialize();
var isOptional = Attribute.IsOptional;
var defaultValue = Attribute.DefaultValue;
try
{
if (HasNextDefaultValue)
{
Attribute.IsOptional = NextValueIsOptional;
Attribute.DefaultValue = NextDefaultValue;
}
var context = TransitionDrawer.Context;
if (context.Transition == null)
{
value = DoSpecialFloatField(area, label, value, DisplayConverters[Attribute.UnitIndex]);
return;
}
var length = context.MaximumLength;
if (length <= 0)
length = float.NaN;
AnimancerUtilities.TryGetFrameRate(context.Transition, out var frameRate);
var multipliers = CalculateMultipliers(length, frameRate);
if (multipliers == null)
{
EditorGUI.LabelField(area, label.text, $"Invalid {nameof(Validate)}.{nameof(Validate.Value)}");
return;
}
DoPreviewTimeButton(ref area, ref value, multipliers);
DoFieldGUI(area, label, ref value);
}
finally
{
Attribute.IsOptional = isOptional;
Attribute.DefaultValue = defaultValue;
}
}
finally
{
HasNextDefaultValue = false;
}
}
/************************************************************************************************************************/
private float[] CalculateMultipliers(float length, float frameRate)
{
switch ((AnimationTimeAttribute.Units)Attribute.UnitIndex)
{
case AnimationTimeAttribute.Units.Normalized:
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Normalized] = 1;
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Seconds] = length;
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Frames] = length * frameRate;
break;
case AnimationTimeAttribute.Units.Seconds:
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Normalized] = 1f / length;
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Seconds] = 1;
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Frames] = frameRate;
break;
case AnimationTimeAttribute.Units.Frames:
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Normalized] = 1f / length / frameRate;
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Seconds] = 1f / frameRate;
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Frames] = 1;
break;
default:
return null;
}
var settings = AnimancerSettingsGroup<AnimationTimeAttributeSettings>.Instance;
ApplyVisibilitySetting(settings.showNormalized, AnimationTimeAttribute.Units.Normalized);
ApplyVisibilitySetting(settings.showSeconds, AnimationTimeAttribute.Units.Seconds);
ApplyVisibilitySetting(settings.showFrames, AnimationTimeAttribute.Units.Frames);
void ApplyVisibilitySetting(bool show, AnimationTimeAttribute.Units setting)
{
if (show)
return;
var index = (int)setting;
if (Attribute.UnitIndex != index)
Attribute.Multipliers[index] = float.NaN;
}
return Attribute.Multipliers;
}
/************************************************************************************************************************/
private void DoPreviewTimeButton(
ref Rect area,
ref float value,
float[] multipliers)
{
if (!TransitionPreviewWindow.IsPreviewingCurrentProperty())
return;
var previewTime = TransitionPreviewWindow.PreviewNormalizedTime;
const string Tooltip =
"<22> Left Click = preview the current value of this field." +
"\n<> Right Click = set this field to use the current preview time.";
var displayValue = GetDisplayValue(value, NextDefaultValue);
var multiplier = multipliers[(int)AnimationTimeAttribute.Units.Normalized];
displayValue *= multiplier;
var isCurrent = Mathf.Approximately(displayValue, previewTime);
var buttonArea = area;
if (TransitionDrawer.DoPreviewButtonGUI(ref buttonArea, isCurrent, Tooltip))
{
if (Event.current.button != 1)
TransitionPreviewWindow.PreviewNormalizedTime = displayValue;
else
value = previewTime / multiplier;
}
// Only steal the button area for single line fields.
if (area.height <= AnimancerGUI.LineHeight)
area = buttonArea;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#region Settings
/************************************************************************************************************************/
/// <summary>[Editor-Only] Options to determine how <see cref="AnimationTimeAttribute"/> displays.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/AnimationTimeAttributeSettings
[Serializable, InternalSerializableType]
public class AnimationTimeAttributeSettings : AnimancerSettingsGroup
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string DisplayName
=> "Time Fields";
/// <inheritdoc/>
public override int Index
=> 5;
/************************************************************************************************************************/
/// <summary>Should time fields show approximations if the value is too long for the GUI?</summary>
/// <remarks>This setting is used by <see cref="CompactUnitConversionCache"/>.</remarks>
[Tooltip("Should time fields show approximations if the value is too long for the GUI?" +
" For example, '1.111111' could instead show '1.111~'.")]
public bool showApproximations = true;
/// <summary>Should the <see cref="AnimationTimeAttribute.Units.Normalized"/> field be shown?</summary>
/// <remarks>This setting is ignored for fields which directly store the normalized value.</remarks>
[Tooltip("Should the " + nameof(AnimationTimeAttribute.Units.Normalized) + " field be shown?")]
public bool showNormalized = true;
/// <summary>Should the <see cref="AnimationTimeAttribute.Units.Seconds"/> field be shown?</summary>
/// <remarks>This setting is ignored for fields which directly store the seconds value.</remarks>
[Tooltip("Should the " + nameof(AnimationTimeAttribute.Units.Seconds) + " field be shown?")]
public bool showSeconds = true;
/// <summary>Should the <see cref="AnimationTimeAttribute.Units.Frames"/> field be shown?</summary>
/// <remarks>This setting is ignored for fields which directly store the frame value.</remarks>
[Tooltip("Should the " + nameof(AnimationTimeAttribute.Units.Frames) + " field be shown?")]
public bool showFrames = true;
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
#endif

Some files were not shown because too many files have changed in this diff Show More