LiveInspectorTutorial.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
  3. using System;
  4. using System.Collections.Generic;
  5. using UnityEngine;
  6. #if UNITY_UGUI
  7. using UnityEngine.UI;
  8. #endif
  9. namespace Animancer.Samples.FineControl
  10. {
  11. /// <summary>Tracks user progress through a tutorial and displays appropriate instructions.</summary>
  12. /// <remarks>
  13. /// <strong>Sample:</strong>
  14. /// <see href="https://kybernetik.com.au/animancer/docs/samples/fine-control/live-inspector">
  15. /// Live Inspector</see>
  16. /// </remarks>
  17. /// https://kybernetik.com.au/animancer/api/Animancer.Samples.FineControl/LiveInspectorTutorial
  18. [AddComponentMenu(Strings.SamplesMenuPrefix + "Fine Control - Live Inspector Tutorial")]
  19. [AnimancerHelpUrl(typeof(LiveInspectorTutorial))]
  20. public class LiveInspectorTutorial : MonoBehaviour
  21. {
  22. /************************************************************************************************************************/
  23. #if UNITY_UGUI
  24. /************************************************************************************************************************/
  25. [SerializeField] private AnimancerComponent _Animancer;
  26. [SerializeField] private Text _Text;
  27. /************************************************************************************************************************/
  28. #if !UNITY_EDITOR
  29. /************************************************************************************************************************/
  30. protected virtual void Awake()
  31. {
  32. _Text.text = "This Sample only works in the Unity Editor";
  33. }
  34. /************************************************************************************************************************/
  35. #else
  36. /************************************************************************************************************************/
  37. private class TutorialStep
  38. {
  39. /************************************************************************************************************************/
  40. /// <summary>Instructions to display while waiting for the user to complete this step.</summary>
  41. public readonly string Instructions;
  42. /// <summary>Has the user completed this step?</summary>
  43. public readonly Func<bool> IsComplete;
  44. /************************************************************************************************************************/
  45. public TutorialStep(string instructions, Func<bool> isComplete)
  46. {
  47. Instructions = instructions;
  48. IsComplete = isComplete;
  49. }
  50. /************************************************************************************************************************/
  51. }
  52. /************************************************************************************************************************/
  53. private readonly List<TutorialStep> Steps = new();
  54. private int _CurrentStep;
  55. private bool _WasShowingAddAnimationField;
  56. private const string
  57. AutoNormalizeWeightsFunction = "Display Options/Auto Normalize Weights",
  58. ShowAddAnimationFieldFunction = "Display Options/Show 'Add Animation' Field",
  59. ShowAddAnimationFieldPref = "Animancer/Inspector/" + ShowAddAnimationFieldFunction,
  60. IdleName = "Humanoid-Idle",
  61. WalkName = "Humanoid-WalkForwards",
  62. DragAndDropAnimations =
  63. "\n\nYou can also Drag and Drop animations from the Project window into the preview area to add them.";
  64. /************************************************************************************************************************/
  65. protected virtual void Awake()
  66. {
  67. _WasShowingAddAnimationField = UnityEditor.EditorPrefs.GetBool(ShowAddAnimationFieldPref);
  68. // Select AnimancerComponent.
  69. Steps.Add(new(
  70. $"Select the {_Animancer.name} object in the Hierarchy",
  71. () => UnityEditor.Selection.activeGameObject == _Animancer.gameObject));
  72. // Initialize graph.
  73. Steps.Add(new(
  74. $"Right Click on the header of the {nameof(AnimancerComponent)} in the Inspector" +
  75. $" and select the 'Initialize Graph' function from the context menu.",
  76. () => _Animancer.IsGraphInitialized));
  77. // Show 'Add Animation' field.
  78. Steps.Add(new(
  79. $"Note how the character is now in a hunched over pose with their legs under the ground." +
  80. $" That's the default pose for a Humanoid Rig when it has no animations so let's add some." +
  81. $"\n" +
  82. $"\nMake sure the preview area down the bottom of the Inspector is expanded." +
  83. $"\n" +
  84. $"\nRight Click on the word 'States' in the preview area" +
  85. $" and select the '{ShowAddAnimationFieldFunction}' function from the context menu.",
  86. () => UnityEditor.EditorPrefs.GetBool(ShowAddAnimationFieldPref)));
  87. // Add idle animation.
  88. Steps.Add(new(
  89. $"Use the 'Add Animation' field at the top of the preview area" +
  90. $" to select the '{IdleName}' animation." +
  91. DragAndDropAnimations,
  92. () => IsCurrentState(IdleName)));
  93. // Add walk animation.
  94. Steps.Add(new(
  95. $"Use the 'Add Animation' field at the top of the preview area" +
  96. $" to select the '{WalkName}' animation." +
  97. DragAndDropAnimations,
  98. () => IsCurrentState(WalkName)));
  99. // Play idle animation.
  100. Steps.Add(new(
  101. $"Ctrl + Left Click on the '{IdleName}' animation to play it.",
  102. () => IsCurrentState(IdleName)));
  103. // Play walk animation as well.
  104. Steps.Add(new(
  105. $"With the '{IdleName}' animation still playing," +
  106. $" Left Click on the '{WalkName}' animation to expand its details." +
  107. $"\n" +
  108. $"\nClick the Play button in that state's details.",
  109. () => IsCurrentState(IdleName)
  110. && TryGetState(WalkName, out AnimancerState walk)
  111. && walk.Weight == 0
  112. && walk.IsPlaying));
  113. // Set weight.
  114. Steps.Add(new(
  115. $"Note how the preview now shows both animations playing," +
  116. $" but you can still only see the '{IdleName}' pose in the Game window." +
  117. $" That's because interacting directly with the details of a state only affects those specific details" +
  118. $" and the Weight of '{WalkName}' is still 0." +
  119. $"\n" +
  120. $"\nSet the Weight of '{WalkName}' to 0.4.",
  121. () => IsCurrentState(IdleName)
  122. && TryGetState(IdleName, out AnimancerState idle)
  123. && TryGetState(WalkName, out AnimancerState walk)
  124. && idle.Weight == 0.6f
  125. && walk.Weight == 0.4f));
  126. // Pause graph.
  127. Steps.Add(new(
  128. $"Now we have a blend of both animations playing at the same time." +
  129. $" This sort of thing is normally achieved using Mixers." +
  130. $"\n" +
  131. $"\nNote how setting the Weight of '{WalkName}' to 0.4" +
  132. $" automatically changed the Weight of '{IdleName}' to 0.6 so that they add up to a total of 1." +
  133. $" If you had set its Weight in code it would only affect that state" +
  134. $" so everything can have whatever values you want," +
  135. $" but that's usually inconvenient when fiddling with them in the Inspector." +
  136. $" This feature can be disabled via '{AutoNormalizeWeightsFunction}' if you don't want it." +
  137. $"\n" +
  138. $"\nClick the Pause button at the top of the preview area to pause the character.",
  139. () => !_Animancer.Graph.IsGraphPlaying));
  140. // Unpause graph.
  141. Steps.Add(new(
  142. $"The character is now paused, but nothing else is." +
  143. $" You can still Right Click in the Game window and drag to move the camera around." +
  144. $"\n" +
  145. $"\nClick the Play button at the top of the preview area to resume playing.",
  146. () => _Animancer.Graph.IsGraphPlaying));
  147. // Set speed.
  148. Steps.Add(new(
  149. $"Click the '1.0x' button at the top of the preview area next to the Play/Pause button" +
  150. $" to show the 'Speed' slider." +
  151. $"\n" +
  152. $"\nUse that slider to set the speed to 0.5.",
  153. () => _Animancer.Graph.Speed == 0.5f));
  154. ApplyCurrentStep();
  155. }
  156. /************************************************************************************************************************/
  157. protected virtual void Update()
  158. {
  159. if (_CurrentStep >= Steps.Count)
  160. return;
  161. if (!Steps[_CurrentStep].IsComplete())
  162. return;
  163. _CurrentStep++;
  164. ApplyCurrentStep();
  165. }
  166. /************************************************************************************************************************/
  167. protected virtual void OnDestroy()
  168. {
  169. if (!_WasShowingAddAnimationField &&
  170. UnityEditor.EditorPrefs.GetBool(ShowAddAnimationFieldPref))
  171. {
  172. UnityEditor.EditorPrefs.SetBool(ShowAddAnimationFieldPref, false);
  173. Debug.Log(
  174. ShowAddAnimationFieldFunction +
  175. " has been disabled as it was before starting this tutorial." +
  176. " Normally it would remember the value you set.");
  177. }
  178. }
  179. /************************************************************************************************************************/
  180. private void ApplyCurrentStep()
  181. {
  182. if (_CurrentStep < Steps.Count)
  183. {
  184. _Text.text = $"{_CurrentStep}/{Steps.Count} {Steps[_CurrentStep].Instructions}";
  185. }
  186. else
  187. {
  188. _Text.text = $"{Steps.Count}/{Steps.Count} Congratulations for completing this tutorial.";
  189. enabled = false;
  190. }
  191. }
  192. /************************************************************************************************************************/
  193. private bool IsCurrentState(string clipName)
  194. {
  195. AnimancerState state = _Animancer.States.Current;
  196. if (state == null)
  197. return false;
  198. AnimationClip clip = state.Clip;
  199. if (clip == null)
  200. return false;
  201. return clip.name == clipName;
  202. }
  203. /************************************************************************************************************************/
  204. private bool TryGetState(string clipName, out AnimancerState state)
  205. {
  206. foreach (AnimancerState otherState in _Animancer.States)
  207. {
  208. if (otherState.Clip.name == clipName)
  209. {
  210. state = otherState;
  211. return true;
  212. }
  213. }
  214. state = null;
  215. return false;
  216. }
  217. /************************************************************************************************************************/
  218. #endif
  219. /************************************************************************************************************************/
  220. #else
  221. /************************************************************************************************************************/
  222. protected virtual void Awake()
  223. {
  224. SampleReadMe.LogMissingUnityUIModuleError(this);
  225. }
  226. /************************************************************************************************************************/
  227. #endif
  228. /************************************************************************************************************************/
  229. }
  230. }