AnimancerNodeDrawer.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR && UNITY_IMGUI
  3. using System;
  4. using UnityEditor;
  5. using UnityEngine;
  6. using UnityEngine.Playables;
  7. using static Animancer.Editor.AnimancerGUI;
  8. using Object = UnityEngine.Object;
  9. namespace Animancer.Editor
  10. {
  11. /// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerNode"/>.</summary>
  12. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerNodeDrawer_1
  13. ///
  14. public abstract class AnimancerNodeDrawer<T> : CustomGUI<T>
  15. where T : AnimancerNode
  16. {
  17. /************************************************************************************************************************/
  18. /// <summary>Extra padding for the left side of the labels.</summary>
  19. public const float ExtraLeftPadding = 3;
  20. /************************************************************************************************************************/
  21. /// <summary>Should the target node's details be expanded in the Inspector?</summary>
  22. public ref bool IsExpanded
  23. => ref Value._IsInspectorExpanded;
  24. /************************************************************************************************************************/
  25. /// <inheritdoc/>
  26. public override void DoGUI()
  27. {
  28. if (!Value.IsValid())
  29. return;
  30. GUILayout.BeginVertical();
  31. {
  32. DoHeaderGUI();
  33. DoDetailsGUI();
  34. }
  35. GUILayout.EndVertical();
  36. if (TryUseClickEvent(GUILayoutUtility.GetLastRect(), 1))
  37. OpenContextMenu();
  38. }
  39. /************************************************************************************************************************/
  40. /// <summary>Draws the name and other details of the <see cref="CustomGUI{T}.Value"/> in the GUI.</summary>
  41. protected virtual void DoHeaderGUI()
  42. {
  43. var area = LayoutSingleLineRect(SpacingMode.Before);
  44. DoLabelGUI(area);
  45. DoFoldoutGUI(area);
  46. }
  47. /************************************************************************************************************************/
  48. /// <summary>
  49. /// Draws a field for the <see cref="AnimancerState.MainObject"/> if it has one, otherwise just a simple text
  50. /// label.
  51. /// </summary>
  52. protected abstract void DoLabelGUI(Rect area);
  53. /// <summary>Draws a foldout arrow to expand/collapse the node details.</summary>
  54. protected abstract void DoFoldoutGUI(Rect area);
  55. /************************************************************************************************************************/
  56. private FastObjectField _DebugNameField;
  57. /// <summary>Draws the details of the <see cref="CustomGUI{T}.Value"/>.</summary>
  58. protected virtual void DoDetailsGUI()
  59. {
  60. if (!IsExpanded)
  61. return;
  62. var debugName = Value.DebugName;
  63. if (debugName == null)
  64. return;
  65. var area = LayoutSingleLineRect(SpacingMode.Before);
  66. area = EditorGUI.IndentedRect(area);
  67. _DebugNameField.Draw(area, "Debug Name", debugName);
  68. }
  69. /************************************************************************************************************************/
  70. private static readonly int FloatFieldHash = "EditorTextField".GetHashCode();
  71. /// <summary>
  72. /// Draws controls for <see cref="AnimancerState.IsPlaying"/>, <see cref="AnimancerNodeBase.Speed"/>, and
  73. /// <see cref="AnimancerNode.Weight"/>.
  74. /// </summary>
  75. protected void DoNodeDetailsGUI()
  76. {
  77. var area = LayoutSingleLineRect(SpacingMode.Before);
  78. area.xMin += EditorGUI.indentLevel * IndentSize + ExtraLeftPadding;
  79. var xMin = area.xMin;
  80. var labelWidth = EditorGUIUtility.labelWidth;
  81. var indentLevel = EditorGUI.indentLevel;
  82. EditorGUI.indentLevel = 0;
  83. // Is Playing.
  84. if (Value is AnimancerState state)
  85. {
  86. var buttonArea = StealFromLeft(ref area, LineHeight, StandardSpacing);
  87. state.IsPlaying = DoPlayPauseToggle(buttonArea, state.IsPlaying);
  88. }
  89. SplitHorizontally(area, "Speed", "Weight",
  90. out var speedWidth,
  91. out var weightWidth,
  92. out var speedRect,
  93. out var weightRect);
  94. // Speed.
  95. EditorGUIUtility.labelWidth = speedWidth;
  96. EditorGUI.BeginChangeCheck();
  97. var speed = EditorGUI.FloatField(speedRect, "Speed", Value.Speed);
  98. if (EditorGUI.EndChangeCheck())
  99. Value.Speed = speed;
  100. if (TryUseClickEvent(speedRect, 2))
  101. Value.Speed = Value.Speed != 1 ? 1 : 0;
  102. // Weight.
  103. EditorGUIUtility.labelWidth = weightWidth;
  104. EditorGUI.BeginChangeCheck();
  105. var weight = EditorGUI.FloatField(weightRect, "Weight", Value.Weight);
  106. if (EditorGUI.EndChangeCheck())
  107. SetWeight(Mathf.Max(weight, 0));
  108. if (TryUseClickEvent(weightRect, 2))
  109. SetWeight(Value.Weight != 1 ? 1 : 0);
  110. // Real Speed.
  111. // Mixer Synchronization changes the internal Playable Speed without setting the State Speed.
  112. speed = (float)Value._Playable.GetSpeed();
  113. if (Value.Speed != speed)
  114. {
  115. using (new EditorGUI.DisabledScope(true))
  116. {
  117. area = LayoutSingleLineRect(SpacingMode.Before);
  118. area.xMin = xMin;
  119. var label = BeginTightLabel("Real Speed");
  120. EditorGUIUtility.labelWidth = CalculateLabelWidth(label);
  121. EditorGUI.FloatField(area, label, speed);
  122. EndTightLabel();
  123. }
  124. }
  125. else// Add a dummy ID so that subsequent IDs don't change when the Real Speed appears or disappears.
  126. {
  127. GUIUtility.GetControlID(FloatFieldHash, FocusType.Keyboard);
  128. }
  129. EditorGUI.indentLevel = indentLevel;
  130. EditorGUIUtility.labelWidth = labelWidth;
  131. DoFadeDetailsGUI();
  132. }
  133. /************************************************************************************************************************/
  134. /// <summary>Indicates whether changing the <see cref="AnimancerNode.Weight"/> should normalize its siblings.</summary>
  135. protected virtual bool AutoNormalizeSiblingWeights
  136. => false;
  137. private void SetWeight(float weight)
  138. {
  139. if (weight < 0 ||
  140. weight > 1 ||
  141. Mathf.Approximately(Value.Weight, 1) ||
  142. !AutoNormalizeSiblingWeights)
  143. goto JustSetWeight;
  144. var parent = Value.Parent;
  145. if (parent == null)
  146. goto JustSetWeight;
  147. var totalWeight = 0f;
  148. var siblingCount = parent.ChildCount;
  149. for (int i = 0; i < siblingCount; i++)
  150. {
  151. var sibling = parent.GetChildNode(i);
  152. if (sibling.IsValid())
  153. totalWeight += sibling.Weight;
  154. }
  155. // If the weights weren't previously normalized, don't normalize them now.
  156. if (!Mathf.Approximately(totalWeight, 1))
  157. goto JustSetWeight;
  158. var siblingWeightMultiplier = (totalWeight - weight) / (totalWeight - Value.Weight);
  159. for (int i = 0; i < siblingCount; i++)
  160. {
  161. var sibling = parent.GetChildNode(i);
  162. if (sibling != Value && sibling.IsValid())
  163. sibling.Weight *= siblingWeightMultiplier;
  164. }
  165. JustSetWeight:
  166. Value.Weight = weight;
  167. }
  168. /************************************************************************************************************************/
  169. private float
  170. _FadeDuration = float.NaN,
  171. _TargetWeight = float.NaN;
  172. /// <summary>
  173. /// Draws the <see cref="AnimancerNode.FadeSpeed"/>
  174. /// and <see cref="AnimancerNode.TargetWeight"/>.
  175. /// </summary>
  176. private void DoFadeDetailsGUI()
  177. {
  178. var area = LayoutSingleLineRect(SpacingMode.Before);
  179. area = EditorGUI.IndentedRect(area);
  180. area.xMin += ExtraLeftPadding;
  181. var durationLabel = "Fade Duration";
  182. var targetLabel = "Target Weight";
  183. SplitHorizontally(
  184. area,
  185. durationLabel,
  186. targetLabel,
  187. out var durationWidth,
  188. out var weightWidth,
  189. out var durationRect,
  190. out var weightRect);
  191. var labelWidth = EditorGUIUtility.labelWidth;
  192. var indentLevel = EditorGUI.indentLevel;
  193. EditorGUI.indentLevel = 0;
  194. EditorGUI.BeginChangeCheck();
  195. var fade = Value.FadeGroup;
  196. var fadeDuration = DoFadeDurationGUI(durationWidth, durationRect, durationLabel, fade);
  197. var targetWeight = DoTargetWeightGUI(weightWidth, weightRect, targetLabel, fade);
  198. if (EditorGUI.EndChangeCheck())
  199. SetFade(targetWeight, fadeDuration);
  200. EditorGUI.indentLevel = indentLevel;
  201. EditorGUIUtility.labelWidth = labelWidth;
  202. }
  203. /************************************************************************************************************************/
  204. private float DoFadeDurationGUI(
  205. float labelWidth,
  206. Rect area,
  207. string label,
  208. FadeGroup fade)
  209. {
  210. EditorGUIUtility.labelWidth = labelWidth;
  211. var fadeDuration = fade != null ? fade.FadeDuration : _FadeDuration;
  212. fadeDuration = EditorGUI.DelayedFloatField(area, label, fadeDuration);
  213. if (fadeDuration > 0)
  214. {
  215. }
  216. else// NaN or Negative.
  217. {
  218. fadeDuration = _FadeDuration = float.NaN;
  219. }
  220. if (TryUseClickEvent(area, 2))
  221. {
  222. var defaultFadeDuration = AnimancerGraph.DefaultFadeDuration;
  223. if (fadeDuration != 0 || defaultFadeDuration == 0)
  224. {
  225. fadeDuration = 0;
  226. }
  227. else
  228. {
  229. var fadeDistance = Math.Abs(Value.Weight - Value.TargetWeight);
  230. if (fadeDistance != 0)
  231. {
  232. fadeDuration = fadeDistance / defaultFadeDuration;
  233. }
  234. else
  235. {
  236. fadeDuration = defaultFadeDuration;
  237. }
  238. }
  239. }
  240. return fadeDuration;
  241. }
  242. /************************************************************************************************************************/
  243. private float DoTargetWeightGUI(
  244. float labelWidth,
  245. Rect area,
  246. string label,
  247. FadeGroup fade)
  248. {
  249. EditorGUIUtility.labelWidth = labelWidth;
  250. var targetWeight = fade != null
  251. ? fade.TargetWeight
  252. : _TargetWeight.IsFinite()
  253. ? _TargetWeight
  254. : Value.Weight;
  255. targetWeight = EditorGUI.DelayedFloatField(area, label, targetWeight);
  256. if (targetWeight >= 0)
  257. {
  258. }
  259. else// NaN or Negative.
  260. {
  261. targetWeight = _TargetWeight = float.NaN;
  262. }
  263. if (TryUseClickEvent(area, 2))
  264. {
  265. if (targetWeight != Value.Weight)
  266. targetWeight = Value.Weight;
  267. else if (targetWeight != 1)
  268. targetWeight = 1;
  269. else
  270. targetWeight = 0;
  271. }
  272. return targetWeight;
  273. }
  274. /************************************************************************************************************************/
  275. /// <summary>Starts a fade or changes the details of an existing one.</summary>
  276. private void SetFade(float targetWeight, float fadeDuration)
  277. {
  278. _TargetWeight = targetWeight;
  279. _FadeDuration = fadeDuration;
  280. if (!targetWeight.IsFinite() ||
  281. !fadeDuration.IsFinite() ||
  282. targetWeight == Value.Weight ||
  283. fadeDuration <= 0)
  284. return;
  285. // If it's a state attached to a layer, start a proper cross fade.
  286. if (Value is AnimancerState state &&
  287. state.Parent is AnimancerLayer layer)
  288. {
  289. layer.Play(state, fadeDuration, FadeMode.FixedDuration);
  290. // That might not have started a fade if the state was already playing,
  291. // So just continue to verify its details.
  292. }
  293. var fade = Value.FadeGroup;
  294. if (fade != null && fade.FadeIn.Node == Value)
  295. {
  296. fade.TargetWeight = targetWeight;
  297. fade.FadeDuration = fadeDuration;
  298. return;
  299. }
  300. Value.StartFade(targetWeight, fadeDuration);
  301. }
  302. /************************************************************************************************************************/
  303. #region Context Menu
  304. /************************************************************************************************************************/
  305. /// <summary>
  306. /// The menu label prefix used for details about the <see cref="CustomGUI{T}.Value"/>.
  307. /// </summary>
  308. protected const string DetailsPrefix = "Details/";
  309. /// <summary>
  310. /// Checks if the current event is a context menu click within the `clickArea` and opens a context menu with various
  311. /// functions for the <see cref="CustomGUI{T}.Value"/>.
  312. /// </summary>
  313. protected void OpenContextMenu()
  314. {
  315. var menu = new GenericMenu();
  316. menu.AddDisabledItem(new(Value.ToString()));
  317. PopulateContextMenu(menu);
  318. menu.AddItem(new(DetailsPrefix + "Log Details"), false,
  319. () => Debug.Log(Value.GetDescription(), Value.Graph?.Component as Object));
  320. menu.AddItem(new(DetailsPrefix + "Log Details Of Everything"), false,
  321. () => Debug.Log(Value.Graph.GetDescription(), Value.Graph?.Component as Object));
  322. AnimancerGraphDrawer.AddPlayableGraphVisualizerFunction(menu, DetailsPrefix, Value.Graph._PlayableGraph);
  323. menu.ShowAsContext();
  324. }
  325. /// <summary>Adds functions relevant to the <see cref="CustomGUI{T}.Value"/>.</summary>
  326. protected abstract void PopulateContextMenu(GenericMenu menu);
  327. /************************************************************************************************************************/
  328. #endregion
  329. /************************************************************************************************************************/
  330. }
  331. }
  332. #endif