chore: initial commit
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbc29ff53b377cc498adb3c6cad4ec5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1649432a594f0b149b2190e2160b59e6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34f989a643e362047ad721eee5385571
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfb75952401f7664aac294342b0a33e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f416ad99b4587c543a7988a4aba85fec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f5aa603bfca9f446ad64081df30c975
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user