123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- // 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
|