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