DirectionalAnimations3D.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. using UnityEngine;
  3. namespace Animancer
  4. {
  5. /// <summary>A <see cref="DirectionalAnimations3D{T}"/> using <see cref="int"/> as the group type.</summary>
  6. ///
  7. /// <remarks>
  8. /// <strong>Sample:</strong>
  9. /// <see href="https://kybernetik.com.au/animancer/docs/samples/sprites/character-3d">
  10. /// Directional Character 3D</see>
  11. /// </remarks>
  12. ///
  13. /// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D
  14. ///
  15. [AddComponentMenu(Strings.MenuPrefix + "Directional Animations 3D")]
  16. [AnimancerHelpUrl(typeof(DirectionalAnimations3D))]
  17. public class DirectionalAnimations3D : DirectionalAnimations3D<int> { }
  18. /************************************************************************************************************************/
  19. /// <summary>
  20. /// A component which manages a screen-facing billboard and plays animations from a
  21. /// <see cref="DirectionalAnimationSet"/> to make it look like a <see cref="Sprite"/>
  22. /// based character is facing a particular direction in 3D space.
  23. /// </summary>
  24. ///
  25. /// <remarks>
  26. /// <strong>Sample:</strong>
  27. /// <see href="https://kybernetik.com.au/animancer/docs/samples/sprites/character-3d">
  28. /// Directional Character 3D</see>
  29. /// </remarks>
  30. ///
  31. /// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D_1
  32. ///
  33. [AnimancerHelpUrl(typeof(DirectionalAnimations3D<>))]
  34. public class DirectionalAnimations3D<TGroup> : MonoBehaviour
  35. {
  36. /************************************************************************************************************************/
  37. #region Fields and Properties
  38. /************************************************************************************************************************/
  39. [SerializeField]
  40. [Tooltip("The object to rotate according to the " + nameof(Mode))]
  41. private Transform _Transform;
  42. /// <summary>[<see cref="SerializeField"/>]
  43. /// The object to rotate according to the <see cref="Mode"/>.
  44. /// </summary>
  45. /// <remarks>Uses this <see cref="Component.transform"/> by default.</remarks>
  46. public ref Transform Transform
  47. => ref _Transform;
  48. /************************************************************************************************************************/
  49. [SerializeField]
  50. [Tooltip("The " + nameof(UnityEngine.Camera) + " to make the " + nameof(Transform) + " face towards" +
  51. "\n\nLeave this null to automatically use the Main Camera")]
  52. private Transform _Camera;
  53. /// <summary>[<see cref="SerializeField"/>]
  54. /// The <see cref="UnityEngine.Camera"/> to make the <see cref="Transform"/> face towards.
  55. /// </summary>
  56. /// <remarks>
  57. /// Leave this <c>null</c> to automatically use the <see cref="Camera.main"/>.
  58. /// </remarks>
  59. public Transform Camera
  60. {
  61. get
  62. {
  63. if (_Camera == null)
  64. {
  65. var camera = UnityEngine.Camera.main;
  66. if (camera != null)
  67. _Camera = camera.transform;
  68. }
  69. return _Camera;
  70. }
  71. set => _Camera = value;
  72. }
  73. /************************************************************************************************************************/
  74. [SerializeField]
  75. [Tooltip("The " + nameof(AnimancerComponent) + " to play animations on")]
  76. private AnimancerComponent _Animancer;
  77. /// <summary>[<see cref="SerializeField"/>]
  78. /// The <see cref="AnimancerComponent"/> to play animations on.
  79. /// </summary>
  80. public ref AnimancerComponent Animancer
  81. => ref _Animancer;
  82. /************************************************************************************************************************/
  83. [SerializeField]
  84. [Tooltip("The " + nameof(DirectionalAnimationSet) + " to play animations from" +
  85. " (Forwards in 3D space corresponds to the Up animation)")]
  86. private DirectionalAnimationSet _Animations;
  87. /// <summary>[<see cref="SerializeField"/>]
  88. /// The animations to choose between based on the <see cref="Forward"/> direction.
  89. /// </summary>
  90. /// <remarks>Forwards in 3D space corresponds to the Up animation.</remarks>
  91. public ref DirectionalAnimationSet Animations
  92. => ref _Animations;
  93. /************************************************************************************************************************/
  94. [SerializeField]
  95. [Tooltip("The World-Space direction this character is facing used to select which animation to play")]
  96. private Vector3 _Forward = Vector3.forward;
  97. /// <summary>[<see cref="SerializeField"/>]
  98. /// The World-Space direction this character is facing used to select which animation to play.
  99. /// </summary>
  100. public Vector3 Forward
  101. {
  102. get => _Forward;
  103. set
  104. {
  105. _Forward = value;
  106. if (!enabled)
  107. PlayCurrentAnimation(TimeSynchronizer.CurrentGroup);
  108. }
  109. }
  110. /************************************************************************************************************************/
  111. /// <summary>Functions used to face the <see cref="Transform"/> towards the <see cref="Camera"/>.</summary>
  112. public enum BillboardMode
  113. {
  114. /// <summary>Don't control the <see cref="Transform"/>.</summary>
  115. None,
  116. /// <summary>Copy the <see cref="Camera"/> 's rotation.</summary>
  117. MatchRotation,
  118. /// <summary>Face the <see cref="Camera"/>'s position.</summary>
  119. FacePosition,
  120. /// <summary>As <see cref="MatchRotation"/>, but only rotate around the Y axis.</summary>
  121. UprightMatchRotation,
  122. /// <summary>As <see cref="FacePosition"/>, but only rotate around the Y axis.</summary>
  123. UprightFacePosition,
  124. /// <summary>
  125. /// As <see cref="UprightMatchRotation"/>,
  126. /// and also scale on the Y axis to maintain the same screen size
  127. /// regardless of the <see cref="Camera"/>'s Euler X Angle.</summary>
  128. /// <remarks>Only use this mode with an Orthographic Camera</remarks>
  129. UprightMatchRotationStretched,
  130. /// <summary>
  131. /// As <see cref="UprightFacePosition"/>,
  132. /// and also scale on the Y axis to maintain the same screen size
  133. /// regardless of the <see cref="Camera"/>'s Euler X Angle.</summary>
  134. /// <remarks>Only use this mode with an Orthographic Camera</remarks>
  135. UprightFacePositionStretched,
  136. }
  137. [SerializeField]
  138. [Tooltip("The function used to face the " + nameof(Transform) + " towards the " + nameof(Camera) + ":" +
  139. "\n• None - Don't control the " + nameof(Transform) +
  140. "\n• Match Rotation - Copy the " + nameof(Camera) + "'s rotation" +
  141. "\n• Face Position - Face the " + nameof(Camera) + "'s position" +
  142. "\n• Upright - As above, but only rotate around the Y axis" +
  143. "\n• Stretched - As above, and also scale on the Y axis to maintain the same screen size" +
  144. " regardless of the " + nameof(Camera) + "'s Euler X Angle (only use with an Orthographic Camera)")]
  145. private BillboardMode _Mode = BillboardMode.UprightMatchRotation;
  146. /// <summary>[<see cref="SerializeField"/>]
  147. /// The function used to face the <see cref="Transform"/> towards the <see cref="Camera"/>.
  148. /// </summary>
  149. public BillboardMode Mode
  150. {
  151. get => _Mode;
  152. set
  153. {
  154. _Mode = value;
  155. ResetScaleIfNotStretched();
  156. }
  157. }
  158. /************************************************************************************************************************/
  159. /// <summary>
  160. /// Maintains the <see cref="AnimancerState.NormalizedTime"/> when swapping between animations.
  161. /// </summary>
  162. public readonly TimeSynchronizer<TGroup>
  163. TimeSynchronizer = new(default, true);
  164. /************************************************************************************************************************/
  165. #endregion
  166. /************************************************************************************************************************/
  167. #region Methods
  168. /************************************************************************************************************************/
  169. /// <summary>
  170. /// Finds missing references,
  171. /// samples the current animation,
  172. /// and resets the scale to 1 if not using a stretched mode.
  173. /// </summary>
  174. protected virtual void OnValidate()
  175. {
  176. gameObject.GetComponentInParentOrChildren(ref _Transform);
  177. gameObject.GetComponentInParentOrChildren(ref _Animancer);
  178. if (TryGetCurrentAnimation(out var animation))
  179. AnimancerUtilities.EditModeSampleAnimation(animation, _Animancer);
  180. ResetScaleIfNotStretched();
  181. }
  182. /************************************************************************************************************************/
  183. /// <summary>
  184. /// Finds missing references,
  185. /// samples the current animation,
  186. /// and resets the scale to 1 if not using a stretched mode.
  187. /// </summary>
  188. protected virtual void OnDrawGizmosSelected()
  189. {
  190. if (TryGetCurrentAnimation(out var animation))
  191. AnimancerUtilities.EditModeSampleAnimation(animation, _Animancer);
  192. if (_Transform == null)
  193. return;
  194. var position = _Transform.position;
  195. var length = 1f;
  196. var renderer = GetComponentInChildren<Renderer>();
  197. if (renderer != null)
  198. {
  199. var bounds = renderer.bounds;
  200. position.y += bounds.extents.y;
  201. length = bounds.extents.magnitude;
  202. }
  203. Gizmos.color = new(0.75f, 0.75f, 1, 1);
  204. Gizmos.DrawRay(position, Forward.normalized * length);
  205. }
  206. /************************************************************************************************************************/
  207. /// <summary>
  208. /// Applies the <see cref="Mode"/> then plays the appropriate animation
  209. /// based on the current rotation and <see cref="Forward"/> direction.
  210. /// </summary>
  211. protected virtual void Update()
  212. {
  213. UpdateTransform();
  214. PlayCurrentAnimation(TimeSynchronizer.CurrentGroup);
  215. }
  216. /************************************************************************************************************************/
  217. /// <summary>Applies the <see cref="Mode"/>.</summary>
  218. public void UpdateTransform()
  219. {
  220. switch (_Mode)
  221. {
  222. default:
  223. case BillboardMode.None:
  224. break;
  225. case BillboardMode.MatchRotation:
  226. _Transform.rotation = Camera.rotation;
  227. break;
  228. case BillboardMode.FacePosition:
  229. _Transform.rotation = Quaternion.LookRotation(_Transform.position - Camera.position);
  230. break;
  231. case BillboardMode.UprightMatchRotation:
  232. _Transform.eulerAngles = new(0, Camera.eulerAngles.y, 0);
  233. break;
  234. case BillboardMode.UprightFacePosition:
  235. var direction = _Transform.position - Camera.position;
  236. _Transform.eulerAngles = new(
  237. 0,
  238. Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg,
  239. 0);
  240. break;
  241. case BillboardMode.UprightMatchRotationStretched:
  242. var eulerAngles = Camera.eulerAngles;
  243. _Transform.eulerAngles = new(0, eulerAngles.y, 0);
  244. StretchHeight(eulerAngles.x);
  245. break;
  246. case BillboardMode.UprightFacePositionStretched:
  247. StretchHeight(Camera.eulerAngles.x);
  248. goto case BillboardMode.UprightFacePosition;
  249. }
  250. }
  251. /************************************************************************************************************************/
  252. /// <summary>
  253. /// Scales the <see cref="Transform"/> on the Y axis to maintain the same screen size
  254. /// regardless of the <see cref="Camera"/>'s Euler X Angle.
  255. /// </summary>
  256. /// <remarks>This calculation only makes sense with an orthographic camera.</remarks>
  257. private void StretchHeight(float eulerX)
  258. {
  259. if (eulerX > 180)
  260. eulerX -= 360;
  261. else if (eulerX < -180)
  262. eulerX += 360;
  263. _Transform.localScale = new(
  264. 1,
  265. 1 / Mathf.Cos(eulerX * Mathf.Deg2Rad),
  266. 1);
  267. }
  268. /// <summary>
  269. /// Resets the <see cref="Transform.localScale"/> to 1 if not using a stretched <see cref="Mode"/>.
  270. /// </summary>
  271. private void ResetScaleIfNotStretched()
  272. {
  273. if (_Transform == null)
  274. return;
  275. switch (_Mode)
  276. {
  277. case BillboardMode.UprightMatchRotationStretched:
  278. case BillboardMode.UprightFacePositionStretched:
  279. break;
  280. default:
  281. _Transform.localScale = Vector3.one;
  282. break;
  283. }
  284. }
  285. /************************************************************************************************************************/
  286. /// <summary>
  287. /// Sets the <see cref="Animations"/> and plays the appropriate animation
  288. /// based on the current rotation and <see cref="Forward"/> direction.
  289. /// </summary>
  290. public void SetAnimations(DirectionalAnimationSet animations, TGroup group = default)
  291. {
  292. _Animations = animations;
  293. PlayCurrentAnimation(group);
  294. }
  295. /************************************************************************************************************************/
  296. /// <summary>
  297. /// Plays the appropriate animation based on the current rotation and <see cref="Forward"/> direction.
  298. /// </summary>
  299. /// <remarks>
  300. /// If the `group` is the same as the previous, the new animation will be given the same
  301. /// <see cref="AnimancerState.NormalizedTime"/> as the previous.
  302. /// </remarks>
  303. public void PlayCurrentAnimation(TGroup group)
  304. {
  305. if (TryGetCurrentAnimation(out var animation))
  306. {
  307. TimeSynchronizer.StoreTime(_Animancer);
  308. _Animancer.Play(animation);
  309. TimeSynchronizer.SyncTime(_Animancer, group);
  310. }
  311. }
  312. /************************************************************************************************************************/
  313. /// <summary>
  314. /// Tries to get an appropriate animation based on the current rotation and <see cref="Forward"/> direction.
  315. /// </summary>
  316. private bool TryGetCurrentAnimation(out AnimationClip animation)
  317. {
  318. if (_Animations == null ||
  319. _Forward == default)
  320. {
  321. animation = null;
  322. return false;
  323. }
  324. var localForward = _Transform.InverseTransformDirection(_Forward);
  325. var horizontalForward = new Vector2(localForward.x, localForward.z);
  326. animation = _Animations.GetClip(horizontalForward);
  327. return true;
  328. }
  329. /************************************************************************************************************************/
  330. #endregion
  331. /************************************************************************************************************************/
  332. }
  333. }