AnimancerStateDrawer.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using UnityEditor;
  5. using UnityEngine;
  6. using Object = UnityEngine.Object;
  7. using static Animancer.Editor.AnimancerPlayableDrawer;
  8. namespace Animancer.Editor
  9. {
  10. /// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
  11. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1
  12. ///
  13. public class AnimancerStateDrawer<T> : AnimancerNodeDrawer<T> where T : AnimancerState
  14. {
  15. /************************************************************************************************************************/
  16. /// <summary>
  17. /// Creates a new <see cref="AnimancerStateDrawer{T}"/> to manage the Inspector GUI for the `target`.
  18. /// </summary>
  19. public AnimancerStateDrawer(T target) => Target = target;
  20. /************************************************************************************************************************/
  21. /// <summary>The <see cref="GUIStyle"/> used for the area encompassing this drawer is <c>null</c>.</summary>
  22. protected override GUIStyle RegionStyle => null;
  23. /************************************************************************************************************************/
  24. /// <summary>Determines whether the <see cref="AnimancerState.MainObject"/> field can occupy the whole line.</summary>
  25. private bool IsAssetUsedAsKey =>
  26. string.IsNullOrEmpty(Target.DebugName) &&
  27. (Target.Key == null || ReferenceEquals(Target.Key, Target.MainObject));
  28. /************************************************************************************************************************/
  29. /// <inheritdoc/>
  30. protected override bool AutoNormalizeSiblingWeights => AutoNormalizeWeights;
  31. /************************************************************************************************************************/
  32. /// <summary>
  33. /// Draws the state's main label: an <see cref="Object"/> field if it has a
  34. /// <see cref="AnimancerState.MainObject"/>, otherwise just a simple text label.
  35. /// <para></para>
  36. /// Also shows a bar to indicate its progress.
  37. /// </summary>
  38. protected override void DoLabelGUI(Rect area)
  39. {
  40. string label;
  41. if (!string.IsNullOrEmpty(Target.DebugName))
  42. {
  43. label = Target.DebugName;
  44. }
  45. else if (IsAssetUsedAsKey)
  46. {
  47. label = "";
  48. }
  49. else
  50. {
  51. var key = Target.Key;
  52. if (key is string str)
  53. label = $"\"{str}\"";
  54. else
  55. label = key.ToString();
  56. }
  57. HandleLabelClick(area);
  58. AnimancerGUI.DoWeightLabel(ref area, Target.Weight);
  59. AnimationBindings.DoBindingMatchGUI(ref area, Target);
  60. var mainObject = Target.MainObject;
  61. if (!(mainObject is null))
  62. {
  63. EditorGUI.BeginChangeCheck();
  64. mainObject = EditorGUI.ObjectField(area, label, mainObject, typeof(Object), false);
  65. if (EditorGUI.EndChangeCheck())
  66. Target.MainObject = mainObject;
  67. }
  68. else if (!string.IsNullOrEmpty(Target.DebugName))
  69. {
  70. EditorGUI.LabelField(area, Target.DebugName);
  71. }
  72. else
  73. {
  74. EditorGUI.LabelField(area, label, Target.ToString());
  75. }
  76. // Highlight a section of the label based on the time like a loading bar.
  77. area.width -= 18;// Remove the area for the Object Picker icon to line the bar up with the field.
  78. DoTimeHighlightBarGUI(area, Target.IsPlaying, Target.EffectiveWeight, Target.Time, Target.Length, Target.IsLooping);
  79. }
  80. /************************************************************************************************************************/
  81. /// <summary>Draws a progress bar to show the animation time.</summary>
  82. public static void DoTimeHighlightBarGUI(Rect area, bool isPlaying, float weight, float time, float length, bool isLooping)
  83. {
  84. var color = GUI.color;
  85. if (ScaleTimeBarByWeight)
  86. {
  87. var height = area.height;
  88. area.height = 1 + (area.height - 1) * Mathf.Clamp01(weight);
  89. area.y += height - area.height;
  90. }
  91. // Green = Playing, Yelow = Paused.
  92. GUI.color = isPlaying ? new Color(0.15f, 0.7f, 0.15f, 0.35f) : new Color(0.7f, 0.7f, 0.15f, 0.35f);
  93. area = EditorGUI.IndentedRect(area);
  94. var wrappedTime = GetWrappedTime(time, length, isLooping);
  95. if (length > 0)
  96. area.width *= Mathf.Clamp01(wrappedTime / length);
  97. GUI.DrawTexture(area, Texture2D.whiteTexture);
  98. GUI.color = color;
  99. }
  100. /************************************************************************************************************************/
  101. /// <summary>Handles Ctrl + Click on the label to CrossFade the animation.</summary>
  102. private void HandleLabelClick(Rect area)
  103. {
  104. var currentEvent = Event.current;
  105. if (currentEvent.type != EventType.MouseUp ||
  106. !currentEvent.control ||
  107. !area.Contains(currentEvent.mousePosition))
  108. return;
  109. currentEvent.Use();
  110. Target.Root.UnpauseGraph();
  111. var fadeDuration = Target.CalculateEditorFadeDuration(AnimancerPlayable.DefaultFadeDuration);
  112. Target.Root.Play(Target, fadeDuration);
  113. }
  114. /************************************************************************************************************************/
  115. /// <inheritdoc/>
  116. protected override void DoFoldoutGUI(Rect area)
  117. {
  118. float foldoutWidth;
  119. if (IsAssetUsedAsKey)
  120. {
  121. foldoutWidth = EditorGUI.indentLevel * AnimancerGUI.IndentSize;
  122. }
  123. else
  124. {
  125. foldoutWidth = EditorGUIUtility.labelWidth;
  126. }
  127. area.xMin -= 2;
  128. area.width = foldoutWidth;
  129. var hierarchyMode = EditorGUIUtility.hierarchyMode;
  130. EditorGUIUtility.hierarchyMode = true;
  131. IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);
  132. EditorGUIUtility.hierarchyMode = hierarchyMode;
  133. }
  134. /************************************************************************************************************************/
  135. /// <summary>
  136. /// Gets the current <see cref="AnimancerState.Time"/>.
  137. /// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
  138. /// </summary>
  139. private float GetWrappedTime(out float length) => GetWrappedTime(Target.Time, length = Target.Length, Target.IsLooping);
  140. /// <summary>
  141. /// Gets the current <see cref="AnimancerState.Time"/>.
  142. /// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
  143. /// </summary>
  144. private static float GetWrappedTime(float time, float length, bool isLooping)
  145. {
  146. var wrappedTime = time;
  147. if (isLooping)
  148. {
  149. wrappedTime = AnimancerUtilities.Wrap(wrappedTime, length);
  150. if (wrappedTime == 0 && time != 0)
  151. wrappedTime = length;
  152. }
  153. return wrappedTime;
  154. }
  155. /************************************************************************************************************************/
  156. /// <inheritdoc/>
  157. protected override void DoDetailsGUI()
  158. {
  159. if (!IsExpanded)
  160. return;
  161. EditorGUI.indentLevel++;
  162. DoTimeSliderGUI();
  163. DoNodeDetailsGUI();
  164. DoOnEndGUI();
  165. EditorGUI.indentLevel--;
  166. }
  167. /************************************************************************************************************************/
  168. /// <summary>Draws a slider for controlling the current <see cref="AnimancerState.Time"/>.</summary>
  169. private void DoTimeSliderGUI()
  170. {
  171. if (Target.Length <= 0)
  172. return;
  173. var time = GetWrappedTime(out var length);
  174. if (length == 0)
  175. return;
  176. var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before);
  177. var normalized = DoNormalizedTimeToggle(ref area);
  178. string label;
  179. float max;
  180. if (normalized)
  181. {
  182. label = "Normalized Time";
  183. time /= length;
  184. max = 1;
  185. }
  186. else
  187. {
  188. label = "Time";
  189. max = length;
  190. }
  191. DoLoopCounterGUI(ref area, length);
  192. EditorGUI.BeginChangeCheck();
  193. label = AnimancerGUI.BeginTightLabel(label);
  194. time = EditorGUI.Slider(area, label, time, 0, max);
  195. AnimancerGUI.EndTightLabel();
  196. if (AnimancerGUI.TryUseClickEvent(area, 2))
  197. time = 0;
  198. if (EditorGUI.EndChangeCheck())
  199. {
  200. if (normalized)
  201. Target.NormalizedTime = time;
  202. else
  203. Target.Time = time;
  204. }
  205. }
  206. /************************************************************************************************************************/
  207. private bool DoNormalizedTimeToggle(ref Rect area)
  208. {
  209. using (ObjectPool.Disposable.AcquireContent(out var label, "N"))
  210. {
  211. var style = AnimancerGUI.MiniButton;
  212. var width = style.CalculateWidth(label);
  213. var toggleArea = AnimancerGUI.StealFromRight(ref area, width);
  214. UseNormalizedTimeSliders.Value = GUI.Toggle(toggleArea, UseNormalizedTimeSliders, label, style);
  215. }
  216. return UseNormalizedTimeSliders;
  217. }
  218. /************************************************************************************************************************/
  219. private static ConversionCache<int, string> _LoopCounterCache;
  220. private void DoLoopCounterGUI(ref Rect area, float length)
  221. {
  222. if (_LoopCounterCache == null)
  223. _LoopCounterCache = new ConversionCache<int, string>((x) => "x" + x);
  224. string label;
  225. var normalizedTime = Target.Time / length;
  226. if (float.IsNaN(normalizedTime))
  227. {
  228. label = "NaN";
  229. }
  230. else
  231. {
  232. var loops = Mathf.FloorToInt(Target.Time / length);
  233. label = _LoopCounterCache.Convert(loops);
  234. }
  235. var width = AnimancerGUI.CalculateLabelWidth(label);
  236. var labelArea = AnimancerGUI.StealFromRight(ref area, width);
  237. GUI.Label(labelArea, label);
  238. }
  239. /************************************************************************************************************************/
  240. private void DoOnEndGUI()
  241. {
  242. if (!Target.HasEvents)
  243. return;
  244. var events = Target.Events;
  245. var drawer = EventSequenceDrawer.Get(events);
  246. var area = GUILayoutUtility.GetRect(0, drawer.CalculateHeight(events) + AnimancerGUI.StandardSpacing);
  247. area.yMin += AnimancerGUI.StandardSpacing;
  248. using (ObjectPool.Disposable.AcquireContent(out var label, "Events"))
  249. drawer.Draw(ref area, events, label);
  250. }
  251. /************************************************************************************************************************/
  252. #region Context Menu
  253. /************************************************************************************************************************/
  254. /// <inheritdoc/>
  255. protected override void PopulateContextMenu(GenericMenu menu)
  256. {
  257. AddContextMenuFunctions(menu);
  258. menu.AddFunction("Play",
  259. !Target.IsPlaying || Target.Weight != 1,
  260. () =>
  261. {
  262. Target.Root.UnpauseGraph();
  263. Target.Root.Play(Target);
  264. });
  265. AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade (Ctrl + Click)",
  266. Target.Weight != 1,
  267. Target, (duration) =>
  268. {
  269. Target.Root.UnpauseGraph();
  270. Target.Root.Play(Target, duration);
  271. });
  272. menu.AddSeparator("");
  273. menu.AddItem(new GUIContent("Destroy State"), false, () => Target.Destroy());
  274. menu.AddSeparator("");
  275. AddDisplayOptions(menu);
  276. AnimancerEditorUtilities.AddDocumentationLink(menu, "State Documentation", Strings.DocsURLs.States);
  277. }
  278. /************************************************************************************************************************/
  279. /// <summary>Adds the details of this state to the `menu`.</summary>
  280. protected virtual void AddContextMenuFunctions(GenericMenu menu)
  281. {
  282. menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}{nameof(AnimancerState.Key)}: {Target.Key}"));
  283. var length = Target.Length;
  284. if (!float.IsNaN(length))
  285. menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}{nameof(AnimancerState.Length)}: {length}"));
  286. menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}Playable Path: {Target.GetPath()}"));
  287. var mainAsset = Target.MainObject;
  288. if (mainAsset != null)
  289. {
  290. var assetPath = AssetDatabase.GetAssetPath(mainAsset);
  291. if (assetPath != null)
  292. menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}Asset Path: {assetPath.Replace("/", "->")}"));
  293. }
  294. if (Target.HasEvents)
  295. {
  296. var events = Target.Events;
  297. for (int i = 0; i < events.Count; i++)
  298. {
  299. var index = i;
  300. AddEventFunctions(menu, "Event " + index, events[index],
  301. () => events.SetCallback(index, AnimancerEvent.DummyCallback),
  302. () => events.Remove(index));
  303. }
  304. AddEventFunctions(menu, "End Event", events.EndEvent,
  305. () => events.EndEvent = new AnimancerEvent(float.NaN, null), null);
  306. }
  307. }
  308. /************************************************************************************************************************/
  309. private void AddEventFunctions(GenericMenu menu, string name, AnimancerEvent animancerEvent,
  310. GenericMenu.MenuFunction clearEvent, GenericMenu.MenuFunction removeEvent)
  311. {
  312. name = $"Events/{name}/";
  313. menu.AddDisabledItem(new GUIContent($"{name}{nameof(AnimancerState.NormalizedTime)}: {animancerEvent.normalizedTime}"));
  314. bool canInvoke;
  315. if (animancerEvent.callback == null)
  316. {
  317. menu.AddDisabledItem(new GUIContent(name + "Callback: null"));
  318. canInvoke = false;
  319. }
  320. else if (animancerEvent.callback == AnimancerEvent.DummyCallback)
  321. {
  322. menu.AddDisabledItem(new GUIContent(name + "Callback: Dummy"));
  323. canInvoke = false;
  324. }
  325. else
  326. {
  327. var label = name +
  328. (animancerEvent.callback.Target != null ? ("Target: " + animancerEvent.callback.Target) : "Target: null");
  329. var targetObject = animancerEvent.callback.Target as Object;
  330. menu.AddFunction(label,
  331. targetObject != null,
  332. () => Selection.activeObject = targetObject);
  333. menu.AddDisabledItem(new GUIContent(
  334. $"{name}Declaring Type: {animancerEvent.callback.Method.DeclaringType.GetNameCS()}"));
  335. menu.AddDisabledItem(new GUIContent(
  336. $"{name}Method: {animancerEvent.callback.Method}"));
  337. canInvoke = true;
  338. }
  339. if (clearEvent != null)
  340. menu.AddFunction(name + "Clear", canInvoke || !float.IsNaN(animancerEvent.normalizedTime), clearEvent);
  341. if (removeEvent != null)
  342. menu.AddFunction(name + "Remove", true, removeEvent);
  343. menu.AddFunction(name + "Invoke", canInvoke, () => animancerEvent.Invoke(Target));
  344. }
  345. /************************************************************************************************************************/
  346. #endregion
  347. /************************************************************************************************************************/
  348. }
  349. }
  350. #endif