AnimancerGraphDrawer.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR && UNITY_IMGUI
  3. using System;
  4. using System.Collections.Generic;
  5. using UnityEditor;
  6. using UnityEditor.Animations;
  7. using UnityEngine;
  8. using UnityEngine.Playables;
  9. using static Animancer.Editor.AnimancerGUI;
  10. using Object = UnityEngine.Object;
  11. namespace Animancer.Editor
  12. {
  13. /// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="IAnimancerComponent.Graph"/>.</summary>
  14. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphDrawer
  15. ///
  16. public class AnimancerGraphDrawer
  17. {
  18. /************************************************************************************************************************/
  19. /// <summary>The currently drawing instance.</summary>
  20. public static AnimancerGraphDrawer Current { get; private set; }
  21. /// <summary>A lazy list of information about the layers currently being displayed.</summary>
  22. private readonly List<AnimancerLayerDrawer>
  23. LayerDrawers = new();
  24. /// <summary>The number of elements in <see cref="LayerDrawers"/> that are currently being used.</summary>
  25. private int _LayerCount;
  26. /************************************************************************************************************************/
  27. /// <summary>Draws the GUI of the <see cref="IAnimancerComponent.Graph"/> if there is only one target.</summary>
  28. public void DoGUI(IAnimancerComponent[] targets)
  29. {
  30. if (targets.Length != 1)
  31. return;
  32. DoGUI(targets[0]);
  33. }
  34. /************************************************************************************************************************/
  35. /// <summary>Draws the GUI of the <see cref="IAnimancerComponent.Graph"/>.</summary>
  36. public void DoGUI(IAnimancerComponent target)
  37. {
  38. Current = this;
  39. DoNativeAnimatorControllerGUI(target);
  40. if (!target.IsGraphInitialized)
  41. {
  42. DoGraphNotInitializedGUI(target);
  43. return;
  44. }
  45. GUILayout.BeginVertical();
  46. var hierarchyMode = EditorGUIUtility.hierarchyMode;
  47. EditorGUIUtility.hierarchyMode = true;
  48. EditorGUI.BeginChangeCheck();
  49. var graph = target.Graph;
  50. // Gather the during the layout event and use the same ones during subsequent events to avoid GUI errors
  51. // in case they change (they shouldn't, but this is also more efficient).
  52. if (Event.current.type == EventType.Layout)
  53. {
  54. AnimancerLayerDrawer.GatherLayerEditors(graph, LayerDrawers, out _LayerCount);
  55. GatherMainObjectUsage(graph);
  56. }
  57. AnimancerGraphControls.DoGraphGUI(graph, out var area);
  58. CheckContextMenu(area, graph);
  59. for (int i = 0; i < _LayerCount; i++)
  60. LayerDrawers[i].DoGUI();
  61. DoOrphanStatesGUI(graph);
  62. GUILayout.Space(StandardSpacing);
  63. DoLayerWeightWarningGUI(target);
  64. ParameterDictionaryDrawer.DoParametersGUI(graph);
  65. NamedEventDictionaryDrawer.DoEventsGUI(graph);
  66. if (ShowInternalDetails)
  67. DoInternalDetailsGUI(graph);
  68. if (EditorGUI.EndChangeCheck() && !graph.IsGraphPlaying)
  69. graph.Evaluate();
  70. DoMultipleAnimationSystemWarningGUI(target);
  71. EditorGUIUtility.hierarchyMode = hierarchyMode;
  72. GUILayout.EndVertical();
  73. AnimancerLayerDrawer.HandleDragAndDropToPlay(GUILayoutUtility.GetLastRect(), graph);
  74. Current = null;
  75. }
  76. /************************************************************************************************************************/
  77. /// <summary>Draws a GUI for the <see cref="Animator.runtimeAnimatorController"/> if there is one.</summary>
  78. private void DoNativeAnimatorControllerGUI(IAnimancerComponent target)
  79. {
  80. if (!EditorApplication.isPlaying &&
  81. !target.IsGraphInitialized)
  82. return;
  83. var animator = target.Animator;
  84. if (animator == null)
  85. return;
  86. var controller = animator.runtimeAnimatorController;
  87. if (controller == null)
  88. return;
  89. BeginVerticalBox(GUI.skin.box);
  90. var label = "Native Animator Controller";
  91. EditorGUI.BeginChangeCheck();
  92. controller = DoObjectFieldGUI(label, controller, false);
  93. if (EditorGUI.EndChangeCheck())
  94. animator.runtimeAnimatorController = controller;
  95. if (controller is AnimatorController editorController)
  96. {
  97. var layers = editorController.layers;
  98. for (int i = 0; i < layers.Length; i++)
  99. {
  100. var layer = layers[i];
  101. var runtimeState = animator.IsInTransition(i) ?
  102. animator.GetNextAnimatorStateInfo(i) :
  103. animator.GetCurrentAnimatorStateInfo(i);
  104. var states = layer.stateMachine.states;
  105. var editorState = GetState(states, runtimeState.shortNameHash);
  106. var area = LayoutSingleLineRect(SpacingMode.Before);
  107. var weight = i == 0 ? 1 : animator.GetLayerWeight(i);
  108. string stateName;
  109. if (editorState != null)
  110. {
  111. stateName = editorState.GetCachedName();
  112. var isLooping = editorState.motion != null && editorState.motion.isLooping;
  113. AnimancerStateDrawer<ClipState>.DoTimeHighlightBarGUI(
  114. area,
  115. true,
  116. weight,
  117. runtimeState.normalizedTime * runtimeState.length,
  118. runtimeState.speed,
  119. runtimeState.length,
  120. isLooping);
  121. }
  122. else
  123. {
  124. stateName = "State Not Found";
  125. }
  126. DoWeightLabel(ref area, weight, weight);
  127. EditorGUI.LabelField(area, layer.name, stateName);
  128. }
  129. }
  130. EndVerticalBox(GUI.skin.box);
  131. }
  132. /************************************************************************************************************************/
  133. /// <summary>Returns the state with the specified <see cref="AnimatorState.nameHash"/>.</summary>
  134. private static AnimatorState GetState(ChildAnimatorState[] states, int nameHash)
  135. {
  136. for (int i = 0; i < states.Length; i++)
  137. {
  138. var state = states[i].state;
  139. if (state.nameHash == nameHash)
  140. {
  141. return state;
  142. }
  143. }
  144. return null;
  145. }
  146. /************************************************************************************************************************/
  147. private void DoGraphNotInitializedGUI(IAnimancerComponent target)
  148. {
  149. if (!EditorApplication.isPlaying ||
  150. target.Animator == null ||
  151. EditorUtility.IsPersistent(target.Animator))
  152. return;
  153. EditorGUILayout.HelpBox("Animancer is not initialized." +
  154. " It will be initialized automatically when something uses it, such as playing an animation.",
  155. MessageType.Info);
  156. if (TryUseClickEventInLastRect(1))
  157. {
  158. var menu = new GenericMenu();
  159. menu.AddItem(new("Initialize"), false, () => target.Graph.Evaluate());
  160. AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers);
  161. menu.ShowAsContext();
  162. }
  163. }
  164. /************************************************************************************************************************/
  165. private readonly AnimancerLayerDrawer OrphanStatesDrawer = new();
  166. private void DoOrphanStatesGUI(AnimancerGraph graph)
  167. {
  168. var states = OrphanStatesDrawer.ActiveStates;
  169. states.Clear();
  170. foreach (var state in graph.States)
  171. if (state.Parent == null)
  172. states.Add(state);
  173. if (states.Count > 0)
  174. {
  175. ApplySortStatesByName(states);
  176. OrphanStatesDrawer.DoStatesGUI("Orphans", states);
  177. }
  178. }
  179. /************************************************************************************************************************/
  180. private void DoLayerWeightWarningGUI(IAnimancerComponent target)
  181. {
  182. if (_LayerCount == 0)
  183. {
  184. EditorGUILayout.HelpBox(
  185. "No layers have been created, which likely means no animations have been played yet.",
  186. MessageType.Warning);
  187. if (GUILayout.Button("Create Base Layer"))
  188. target.Graph.Layers.Count = 1;
  189. return;
  190. }
  191. if (!target.gameObject.activeInHierarchy ||
  192. !target.enabled ||
  193. (target.Animator != null && target.Animator.runtimeAnimatorController != null))
  194. return;
  195. if (_LayerCount == 1)
  196. {
  197. var layer = LayerDrawers[0].Value;
  198. if (layer.Weight == 0)
  199. EditorGUILayout.HelpBox(
  200. layer + " is at 0 weight, which likely means no animations have been played yet.",
  201. MessageType.Warning);
  202. return;
  203. }
  204. for (int i = 0; i < _LayerCount; i++)
  205. {
  206. var layer = LayerDrawers[i].Value;
  207. if (layer.Weight == 1 &&
  208. !layer.IsAdditive &&
  209. layer._Mask == null &&
  210. Mathf.Approximately(layer.GetTotalChildWeight(), 1))
  211. return;
  212. }
  213. EditorGUILayout.HelpBox(
  214. "There are no Override layers at weight 1, which will likely give undesirable results." +
  215. " Click here for more information.",
  216. MessageType.Warning);
  217. if (TryUseClickEventInLastRect())
  218. EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Layers + "#blending");
  219. }
  220. /************************************************************************************************************************/
  221. private void DoMultipleAnimationSystemWarningGUI(IAnimancerComponent target)
  222. {
  223. const string OnlyOneSystemWarning =
  224. "This is not supported. Each object can only be controlled by one system at a time.";
  225. using (ListPool<IAnimancerComponent>.Instance.Acquire(out var animancers))
  226. {
  227. target.gameObject.GetComponents(animancers);
  228. if (animancers.Count > 1)
  229. {
  230. for (int i = 0; i < animancers.Count; i++)
  231. {
  232. var other = animancers[i];
  233. if (other != target && other.Animator == target.Animator)
  234. {
  235. EditorGUILayout.HelpBox(
  236. $"There are multiple {nameof(IAnimancerComponent)}s trying to control the target" +
  237. $" {nameof(Animator)}. {OnlyOneSystemWarning}",
  238. MessageType.Warning);
  239. break;
  240. }
  241. }
  242. }
  243. }
  244. if (target.Animator.TryGetComponent<Animation>(out _))
  245. {
  246. EditorGUILayout.HelpBox(
  247. $"There is a Legacy {nameof(Animation)} component on the same object as the target" +
  248. $" {nameof(Animator)}. {OnlyOneSystemWarning}",
  249. MessageType.Warning);
  250. }
  251. }
  252. /************************************************************************************************************************/
  253. private static readonly BoolPref
  254. ArePreUpdatablesExpanded = new(KeyPrefix + nameof(ArePreUpdatablesExpanded), false),
  255. ArePostUpdatablesExpanded = new(KeyPrefix + nameof(ArePostUpdatablesExpanded), false),
  256. AreDisposablesExpanded = new(KeyPrefix + nameof(AreDisposablesExpanded), false);
  257. /// <summary>Draws a box describing the internal details of the `graph`.</summary>
  258. private void DoInternalDetailsGUI(AnimancerGraph graph)
  259. {
  260. EditorGUI.indentLevel++;
  261. DoGroupDetailsGUI(graph.PreUpdatables, "Pre-Updatables", ArePreUpdatablesExpanded);
  262. DoGroupDetailsGUI(graph.PostUpdatables, "Post-Updatables", ArePostUpdatablesExpanded);
  263. DoGroupDetailsGUI(graph.Disposables, "Disposables", AreDisposablesExpanded);
  264. EditorGUI.indentLevel--;
  265. }
  266. /// <summary>Draws the `items`.</summary>
  267. private static void DoGroupDetailsGUI<T>(IReadOnlyList<T> items, string groupName, BoolPref isExpanded)
  268. {
  269. var count = items.Count;
  270. isExpanded.Value = DoLabelFoldoutFieldGUI(groupName, count.ToStringCached(), isExpanded);
  271. EditorGUI.indentLevel++;
  272. if (isExpanded)
  273. for (int i = 0; i < count; i++)
  274. DoDetailsGUI(items[i]);
  275. EditorGUI.indentLevel--;
  276. }
  277. /// <summary>Draws the details of the `item`.</summary>
  278. private static void DoDetailsGUI(object item)
  279. {
  280. if (item is AnimancerNode node)
  281. {
  282. var area = LayoutSingleLineRect(SpacingMode.Before);
  283. area = EditorGUI.IndentedRect(area);
  284. var field = new FastObjectField();
  285. field.Set(node, node.GetPath(), FastObjectField.GetIcon(node));
  286. field.Draw(area);
  287. return;
  288. }
  289. var gui = CustomGUIFactory.GetOrCreateForObject(item);
  290. if (gui != null)
  291. {
  292. gui.DoGUI();
  293. return;
  294. }
  295. EditorGUILayout.LabelField(item.ToString());
  296. }
  297. /************************************************************************************************************************/
  298. #region Main Object Lookup
  299. /************************************************************************************************************************/
  300. private readonly Dictionary<Object, bool>
  301. MainObjectDuplicateUsage = new();
  302. /************************************************************************************************************************/
  303. /// <summary>Is the given `mainObject` used as the <see cref="AnimancerState.MainObject"/> of multiple states?</summary>
  304. public bool IsMainObjectUsedMultipleTimes(Object mainObject)
  305. => MainObjectDuplicateUsage.TryGetValue(mainObject, out var duplicate)
  306. && duplicate;
  307. /************************************************************************************************************************/
  308. private void GatherMainObjectUsage(AnimancerGraph graph)
  309. {
  310. MainObjectDuplicateUsage.Clear();
  311. var layers = graph.Layers;
  312. var layerCount = layers.Count;
  313. for (int iLayer = 0; iLayer < layerCount; iLayer++)
  314. {
  315. var layer = layers[iLayer];
  316. var childCount = layer.ChildCount;
  317. for (int iState = 0; iState < childCount; iState++)
  318. {
  319. var state = layer.GetChild(iState);
  320. var mainObject = state.MainObject;
  321. if (mainObject == null)
  322. continue;
  323. if (MainObjectDuplicateUsage.TryGetValue(mainObject, out var duplicate))
  324. {
  325. if (!duplicate)
  326. MainObjectDuplicateUsage[mainObject] = true;
  327. }
  328. else
  329. {
  330. MainObjectDuplicateUsage.Add(mainObject, false);
  331. }
  332. }
  333. }
  334. }
  335. /************************************************************************************************************************/
  336. #endregion
  337. /************************************************************************************************************************/
  338. #region Context Menu
  339. /************************************************************************************************************************/
  340. /// <summary>
  341. /// Checks if the current event is a context menu click within the `clickArea`
  342. /// and opens a context menu with various functions for the `graph`.
  343. /// </summary>
  344. private void CheckContextMenu(Rect clickArea, AnimancerGraph graph)
  345. {
  346. if (!TryUseClickEvent(clickArea, 1))
  347. return;
  348. var menu = new GenericMenu();
  349. menu.AddDisabledItem(new(graph._PlayableGraph.GetEditorName() ?? "Unnamed Graph"), false);
  350. menu.AddDisabledItem(new("Frame ID: " + graph.FrameID), false);
  351. AddDisposablesFunctions(menu, graph.Disposables);
  352. AddUpdateModeFunctions(menu, graph);
  353. AnimancerNodeBase.AddContextMenuIK(menu, graph);
  354. AddRootFunctions(menu, graph);
  355. menu.AddSeparator("");
  356. AddDisplayOptions(menu);
  357. menu.AddItem(new("Log Details Of Everything"), false,
  358. () => Debug.Log(graph.GetDescription(), graph.Component as Object));
  359. AddPlayableGraphVisualizerFunction(menu, "", graph._PlayableGraph);
  360. AnimancerEditorUtilities.AddDocumentationLink(menu, "Inspector Documentation", Strings.DocsURLs.Inspector);
  361. menu.ShowAsContext();
  362. }
  363. /************************************************************************************************************************/
  364. /// <summary>Adds functions for controlling the `graph`.</summary>
  365. public static void AddRootFunctions(GenericMenu menu, AnimancerGraph graph)
  366. {
  367. menu.AddFunction("Add Layer",
  368. graph.Layers.Count < AnimancerLayerList.DefaultCapacity,
  369. () => graph.Layers.Count++);
  370. menu.AddFunction("Remove Layer",
  371. graph.Layers.Count > 0,
  372. () => graph.Layers.Count--);
  373. menu.AddItem(new("Keep Children Connected ?"),
  374. graph.KeepChildrenConnected,
  375. () => graph.SetKeepChildrenConnected(!graph.KeepChildrenConnected));
  376. }
  377. /************************************************************************************************************************/
  378. /// <summary>Adds menu functions to set the <see cref="DirectorUpdateMode"/>.</summary>
  379. private void AddUpdateModeFunctions(GenericMenu menu, AnimancerGraph graph)
  380. {
  381. var modes = Enum.GetValues(typeof(DirectorUpdateMode));
  382. for (int i = 0; i < modes.Length; i++)
  383. {
  384. var mode = (DirectorUpdateMode)modes.GetValue(i);
  385. menu.AddItem(new("Update Mode/" + mode), graph.UpdateMode == mode,
  386. () => graph.UpdateMode = mode);
  387. }
  388. }
  389. /************************************************************************************************************************/
  390. /// <summary>Adds disabled items for each disposable.</summary>
  391. private void AddDisposablesFunctions(GenericMenu menu, List<IDisposable> disposables)
  392. {
  393. var prefix = $"{nameof(AnimancerGraph.Disposables)}: {disposables.Count}";
  394. if (disposables.Count == 0)
  395. {
  396. menu.AddDisabledItem(new(prefix), false);
  397. }
  398. else
  399. {
  400. prefix += "/";
  401. for (int i = 0; i < disposables.Count; i++)
  402. {
  403. menu.AddDisabledItem(new(prefix + disposables[i]), false);
  404. }
  405. }
  406. }
  407. /************************************************************************************************************************/
  408. /// <summary>Adds a menu function to open the Playable Graph Visualiser if it exists in the project.</summary>
  409. public static void AddPlayableGraphVisualizerFunction(GenericMenu menu, string prefix, PlayableGraph graph)
  410. {
  411. var type = Type.GetType(
  412. "GraphVisualizer.PlayableGraphVisualizerWindow, Unity.PlayableGraphVisualizer.Editor");
  413. menu.AddFunction(prefix + "Playable Graph Visualizer", type != null, () =>
  414. {
  415. var window = EditorWindow.GetWindow(type);
  416. var field = type.GetField("m_CurrentGraph", AnimancerReflection.AnyAccessBindings);
  417. field?.SetValue(window, graph);
  418. });
  419. }
  420. /************************************************************************************************************************/
  421. #endregion
  422. /************************************************************************************************************************/
  423. #region Prefs
  424. /************************************************************************************************************************/
  425. internal const string
  426. KeyPrefix = "Inspector/",
  427. MenuPrefix = "Display Options/";
  428. internal static readonly BoolPref
  429. SortStatesByName = new(KeyPrefix, MenuPrefix + "Sort States By Name", true),
  430. SeparateActiveFromInactiveStates = new(KeyPrefix, MenuPrefix + "Separate Active From Inactive States", false),
  431. ShowInactiveStates = new(KeyPrefix, MenuPrefix + "Show Inactive States", true),
  432. ShowSingleLayerHeader = new(KeyPrefix, MenuPrefix + "Show Single Layer Header", false),
  433. ShowEvents = new(KeyPrefix, MenuPrefix + "Show Events", true),
  434. ShowInternalDetails = new(KeyPrefix, MenuPrefix + "Show Internal Details", false),
  435. ShowAddAnimation = new(KeyPrefix, MenuPrefix + "Show 'Add Animation' Field", false),
  436. RepaintConstantly = new(KeyPrefix, MenuPrefix + "Repaint Constantly", true),
  437. ScaleTimeBarByWeight = new(KeyPrefix, MenuPrefix + "Scale Time Bar by Weight", true),
  438. VerifyAnimationBindings = new(KeyPrefix, MenuPrefix + "Verify Animation Bindings", true),
  439. AutoNormalizeWeights = new(KeyPrefix, MenuPrefix + "Auto Normalize Weights", true),
  440. UseNormalizedTimeSliders = new("Inspector", nameof(UseNormalizedTimeSliders), false);
  441. /************************************************************************************************************************/
  442. /// <summary>Adds functions to the `menu` for each of the Display Options.</summary>
  443. public static void AddDisplayOptions(GenericMenu menu)
  444. {
  445. RepaintConstantly.AddToggleFunction(menu);
  446. SortStatesByName.AddToggleFunction(menu);
  447. SeparateActiveFromInactiveStates.AddToggleFunction(menu);
  448. ShowInactiveStates.AddToggleFunction(menu);
  449. ShowSingleLayerHeader.AddToggleFunction(menu);
  450. ShowEvents.AddToggleFunction(menu);
  451. ShowInternalDetails.AddToggleFunction(menu);
  452. ShowAddAnimation.AddToggleFunction(menu);
  453. ScaleTimeBarByWeight.AddToggleFunction(menu);
  454. VerifyAnimationBindings.AddToggleFunction(menu);
  455. AutoNormalizeWeights.AddToggleFunction(menu);
  456. }
  457. /************************************************************************************************************************/
  458. /// <summary>Sorts the `states` if <see cref="SortStatesByName"/> is enabled.</summary>
  459. public static void ApplySortStatesByName(List<AnimancerState> states)
  460. {
  461. if (SortStatesByName)
  462. states.Sort((x, y) => x.ToString().CompareTo(y.ToString()));
  463. }
  464. /************************************************************************************************************************/
  465. #endregion
  466. /************************************************************************************************************************/
  467. }
  468. }
  469. #endif