DirectionalAnimationSet.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using UnityEngine;
  6. using Object = UnityEngine.Object;
  7. namespace Animancer
  8. {
  9. /// <summary>A set of up/right/down/left animations.</summary>
  10. /// <remarks>
  11. /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">Directional Animation Sets</see>
  12. /// </remarks>
  13. /// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimationSet
  14. ///
  15. [CreateAssetMenu(menuName = Strings.MenuPrefix + "Directional Animation Set/4 Directions", order = Strings.AssetMenuOrder + 10)]
  16. [HelpURL(Strings.DocsURLs.APIDocumentation + "/" + nameof(DirectionalAnimationSet))]
  17. public class DirectionalAnimationSet : ScriptableObject, IAnimationClipSource
  18. {
  19. /************************************************************************************************************************/
  20. [SerializeField]
  21. private AnimationClip _Up;
  22. /// <summary>[<see cref="SerializeField"/>] The animation facing up (0, 1).</summary>
  23. /// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
  24. public AnimationClip Up
  25. {
  26. get => _Up;
  27. set
  28. {
  29. AssertCanSetClips();
  30. _Up = value;
  31. AnimancerUtilities.SetDirty(this);
  32. }
  33. }
  34. /************************************************************************************************************************/
  35. [SerializeField]
  36. private AnimationClip _Right;
  37. /// <summary>[<see cref="SerializeField"/>] The animation facing right (1, 0).</summary>
  38. /// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
  39. public AnimationClip Right
  40. {
  41. get => _Right;
  42. set
  43. {
  44. AssertCanSetClips();
  45. _Right = value;
  46. AnimancerUtilities.SetDirty(this);
  47. }
  48. }
  49. /************************************************************************************************************************/
  50. [SerializeField]
  51. private AnimationClip _Down;
  52. /// <summary>[<see cref="SerializeField"/>] The animation facing down (0, -1).</summary>
  53. /// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
  54. public AnimationClip Down
  55. {
  56. get => _Down;
  57. set
  58. {
  59. AssertCanSetClips();
  60. _Down = value;
  61. AnimancerUtilities.SetDirty(this);
  62. }
  63. }
  64. /************************************************************************************************************************/
  65. [SerializeField]
  66. private AnimationClip _Left;
  67. /// <summary>[<see cref="SerializeField"/>] The animation facing left (-1, 0).</summary>
  68. /// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
  69. public AnimationClip Left
  70. {
  71. get => _Left;
  72. set
  73. {
  74. AssertCanSetClips();
  75. _Left = value;
  76. AnimancerUtilities.SetDirty(this);
  77. }
  78. }
  79. /************************************************************************************************************************/
  80. #if UNITY_ASSERTIONS
  81. private bool _AllowSetClips;
  82. #endif
  83. /// <summary>[Assert-Only] Determines whether the <see cref="AnimationClip"/> properties are allowed to be set.</summary>
  84. [System.Diagnostics.Conditional(Strings.Assertions)]
  85. public void AllowSetClips(bool allow = true)
  86. {
  87. #if UNITY_ASSERTIONS
  88. _AllowSetClips = allow;
  89. #endif
  90. }
  91. /// <summary>[Assert-Only] Throws an <see cref="ArgumentException"/> if <see cref="AllowSetClips"/> was not called.</summary>
  92. [System.Diagnostics.Conditional(Strings.Assertions)]
  93. public void AssertCanSetClips()
  94. {
  95. #if UNITY_ASSERTIONS
  96. AnimancerUtilities.Assert(_AllowSetClips, $"{nameof(AllowSetClips)}() must be called before attempting to set any of" +
  97. $" the animations in a {nameof(DirectionalAnimationSet)} to ensure that they are not changed accidentally.");
  98. #endif
  99. }
  100. /************************************************************************************************************************/
  101. /// <summary>Returns the animation closest to the specified `direction`.</summary>
  102. public virtual AnimationClip GetClip(Vector2 direction)
  103. {
  104. if (direction.x >= 0)
  105. {
  106. if (direction.y >= 0)
  107. return direction.x > direction.y ? _Right : _Up;
  108. else
  109. return direction.x > -direction.y ? _Right : _Down;
  110. }
  111. else
  112. {
  113. if (direction.y >= 0)
  114. return direction.x < -direction.y ? _Left : _Up;
  115. else
  116. return direction.x < direction.y ? _Left : _Down;
  117. }
  118. }
  119. /************************************************************************************************************************/
  120. #region Directions
  121. /************************************************************************************************************************/
  122. /// <summary>The number of animations in this set.</summary>
  123. public virtual int ClipCount => 4;
  124. /************************************************************************************************************************/
  125. /// <summary>Up, Right, Down, or Left.</summary>
  126. /// <remarks>
  127. /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">Directional Animation Sets</see>
  128. /// </remarks>
  129. /// https://kybernetik.com.au/animancer/api/Animancer/Direction
  130. ///
  131. public enum Direction
  132. {
  133. /// <summary><see cref="Vector2.up"/>.</summary>
  134. Up,
  135. /// <summary><see cref="Vector2.right"/>.</summary>
  136. Right,
  137. /// <summary><see cref="Vector2.down"/>.</summary>
  138. Down,
  139. /// <summary><see cref="Vector2.left"/>.</summary>
  140. Left,
  141. }
  142. /************************************************************************************************************************/
  143. /// <summary>Returns the name of the specified `direction`.</summary>
  144. protected virtual string GetDirectionName(int direction) => ((Direction)direction).ToString();
  145. /************************************************************************************************************************/
  146. /// <summary>Returns the animation associated with the specified `direction`.</summary>
  147. public AnimationClip GetClip(Direction direction)
  148. {
  149. switch (direction)
  150. {
  151. case Direction.Up: return _Up;
  152. case Direction.Right: return _Right;
  153. case Direction.Down: return _Down;
  154. case Direction.Left: return _Left;
  155. default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
  156. }
  157. }
  158. /// <summary>Returns the animation associated with the specified `direction`.</summary>
  159. public virtual AnimationClip GetClip(int direction) => GetClip((Direction)direction);
  160. /************************************************************************************************************************/
  161. /// <summary>Sets the animation associated with the specified `direction`.</summary>
  162. public void SetClip(Direction direction, AnimationClip clip)
  163. {
  164. switch (direction)
  165. {
  166. case Direction.Up: Up = clip; break;
  167. case Direction.Right: Right = clip; break;
  168. case Direction.Down: Down = clip; break;
  169. case Direction.Left: Left = clip; break;
  170. default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
  171. }
  172. }
  173. /// <summary>Sets the animation associated with the specified `direction`.</summary>
  174. public virtual void SetClip(int direction, AnimationClip clip) => SetClip((Direction)direction, clip);
  175. /************************************************************************************************************************/
  176. #region Conversion
  177. /************************************************************************************************************************/
  178. /// <summary>Returns a vector representing the specified `direction`.</summary>
  179. public static Vector2 DirectionToVector(Direction direction)
  180. {
  181. switch (direction)
  182. {
  183. case Direction.Up: return Vector2.up;
  184. case Direction.Right: return Vector2.right;
  185. case Direction.Down: return Vector2.down;
  186. case Direction.Left: return Vector2.left;
  187. default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
  188. }
  189. }
  190. /// <summary>Returns a vector representing the specified `direction`.</summary>
  191. public virtual Vector2 GetDirection(int direction) => DirectionToVector((Direction)direction);
  192. /************************************************************************************************************************/
  193. /// <summary>Returns the direction closest to the specified `vector`.</summary>
  194. public static Direction VectorToDirection(Vector2 vector)
  195. {
  196. if (vector.x >= 0)
  197. {
  198. if (vector.y >= 0)
  199. return vector.x > vector.y ? Direction.Right : Direction.Up;
  200. else
  201. return vector.x > -vector.y ? Direction.Right : Direction.Down;
  202. }
  203. else
  204. {
  205. if (vector.y >= 0)
  206. return vector.x < -vector.y ? Direction.Left : Direction.Up;
  207. else
  208. return vector.x < vector.y ? Direction.Left : Direction.Down;
  209. }
  210. }
  211. /************************************************************************************************************************/
  212. /// <summary>Returns a copy of the `vector` pointing in the closest direction this set type has an animation for.</summary>
  213. public static Vector2 SnapVectorToDirection(Vector2 vector)
  214. {
  215. var magnitude = vector.magnitude;
  216. var direction = VectorToDirection(vector);
  217. vector = DirectionToVector(direction) * magnitude;
  218. return vector;
  219. }
  220. /// <summary>Returns a copy of the `vector` pointing in the closest direction this set has an animation for.</summary>
  221. public virtual Vector2 Snap(Vector2 vector) => SnapVectorToDirection(vector);
  222. /************************************************************************************************************************/
  223. #endregion
  224. /************************************************************************************************************************/
  225. #region Collections
  226. /************************************************************************************************************************/
  227. /// <summary>Adds all animations from this set to the `clips`, starting from the specified `index`.</summary>
  228. public void AddClips(AnimationClip[] clips, int index)
  229. {
  230. var count = ClipCount;
  231. for (int i = 0; i < count; i++)
  232. clips[index + i] = GetClip(i);
  233. }
  234. /// <summary>[<see cref="IAnimationClipSource"/>] Adds all animations from this set to the `clips`.</summary>
  235. public void GetAnimationClips(List<AnimationClip> clips)
  236. {
  237. var count = ClipCount;
  238. for (int i = 0; i < count; i++)
  239. clips.Add(GetClip(i));
  240. }
  241. /************************************************************************************************************************/
  242. /// <summary>
  243. /// Adds unit vectors corresponding to each of the animations in this set to the `directions`, starting from
  244. /// the specified `index`.
  245. /// </summary>
  246. public void AddDirections(Vector2[] directions, int index)
  247. {
  248. var count = ClipCount;
  249. for (int i = 0; i < count; i++)
  250. directions[index + i] = GetDirection(i);
  251. }
  252. /************************************************************************************************************************/
  253. /// <summary>Calls <see cref="AddClips"/> and <see cref="AddDirections"/>.</summary>
  254. public void AddClipsAndDirections(AnimationClip[] clips, Vector2[] directions, int index)
  255. {
  256. AddClips(clips, index);
  257. AddDirections(directions, index);
  258. }
  259. /************************************************************************************************************************/
  260. #endregion
  261. /************************************************************************************************************************/
  262. #endregion
  263. /************************************************************************************************************************/
  264. #region Editor Functions
  265. /************************************************************************************************************************/
  266. #if UNITY_EDITOR
  267. /************************************************************************************************************************/
  268. [UnityEditor.CustomEditor(typeof(DirectionalAnimationSet), true), UnityEditor.CanEditMultipleObjects]
  269. private class Editor : Animancer.Editor.ScriptableObjectEditor { }
  270. /************************************************************************************************************************/
  271. /// <summary>[Editor-Only]
  272. /// Attempts to assign the `clip` to one of this set's fields based on its name and returns the direction index
  273. /// of that field (or -1 if it was unable to determine the direction).
  274. /// </summary>
  275. public virtual int SetClipByName(AnimationClip clip)
  276. {
  277. var name = clip.name;
  278. int bestDirection = -1;
  279. int bestDirectionIndex = -1;
  280. var directionCount = ClipCount;
  281. for (int i = 0; i < directionCount; i++)
  282. {
  283. var index = name.LastIndexOf(GetDirectionName(i));
  284. if (bestDirectionIndex < index)
  285. {
  286. bestDirectionIndex = index;
  287. bestDirection = i;
  288. }
  289. }
  290. if (bestDirection >= 0)
  291. SetClip(bestDirection, clip);
  292. return bestDirection;
  293. }
  294. /************************************************************************************************************************/
  295. [UnityEditor.MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet) + "/Find Animations")]
  296. private static void FindSimilarAnimations(UnityEditor.MenuCommand command)
  297. {
  298. var set = (DirectionalAnimationSet)command.context;
  299. UnityEditor.Undo.RecordObject(set, "Find Animations");
  300. var directory = UnityEditor.AssetDatabase.GetAssetPath(set);
  301. directory = Path.GetDirectoryName(directory);
  302. var guids = UnityEditor.AssetDatabase.FindAssets(
  303. $"{set.name} t:{nameof(AnimationClip)}",
  304. new string[] { directory });
  305. for (int i = 0; i < guids.Length; i++)
  306. {
  307. var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]);
  308. var clip = UnityEditor.AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
  309. if (clip == null)
  310. continue;
  311. set.SetClipByName(clip);
  312. }
  313. }
  314. /************************************************************************************************************************/
  315. [UnityEditor.MenuItem(Strings.CreateMenuPrefix + "Directional Animation Set/From Selection",
  316. priority = Strings.AssetMenuOrder + 12)]
  317. private static void CreateDirectionalAnimationSet()
  318. {
  319. var nameToAnimations = new Dictionary<string, List<AnimationClip>>();
  320. var selection = UnityEditor.Selection.objects;
  321. for (int i = 0; i < selection.Length; i++)
  322. {
  323. var clip = selection[i] as AnimationClip;
  324. if (clip == null)
  325. continue;
  326. var name = clip.name;
  327. for (Direction direction = 0; direction < (Direction)4; direction++)
  328. {
  329. name = name.Replace(direction.ToString(), "");
  330. }
  331. if (!nameToAnimations.TryGetValue(name, out var clips))
  332. {
  333. clips = new List<AnimationClip>();
  334. nameToAnimations.Add(name, clips);
  335. }
  336. clips.Add(clip);
  337. }
  338. if (nameToAnimations.Count == 0)
  339. throw new InvalidOperationException("No clips are selected");
  340. var sets = new List<Object>();
  341. foreach (var nameAndAnimations in nameToAnimations)
  342. {
  343. var set = nameAndAnimations.Value.Count <= 4 ?
  344. CreateInstance<DirectionalAnimationSet>() :
  345. CreateInstance<DirectionalAnimationSet8>();
  346. set.AllowSetClips();
  347. for (int i = 0; i < nameAndAnimations.Value.Count; i++)
  348. {
  349. set.SetClipByName(nameAndAnimations.Value[i]);
  350. }
  351. var path = UnityEditor.AssetDatabase.GetAssetPath(nameAndAnimations.Value[0]);
  352. path = $"{Path.GetDirectoryName(path)}/{nameAndAnimations.Key}.asset";
  353. UnityEditor.AssetDatabase.CreateAsset(set, path);
  354. sets.Add(set);
  355. }
  356. UnityEditor.Selection.objects = sets.ToArray();
  357. }
  358. /************************************************************************************************************************/
  359. [UnityEditor.MenuItem("CONTEXT/" + nameof(DirectionalAnimationSet) + "/Toggle Looping")]
  360. private static void ToggleLooping(UnityEditor.MenuCommand command)
  361. {
  362. var set = (DirectionalAnimationSet)command.context;
  363. var count = set.ClipCount;
  364. for (int i = 0; i < count; i++)
  365. {
  366. var clip = set.GetClip(i);
  367. if (clip == null)
  368. continue;
  369. var isLooping = !clip.isLooping;
  370. for (i = 0; i < count; i++)
  371. {
  372. clip = set.GetClip(i);
  373. if (clip == null)
  374. continue;
  375. Animancer.Editor.AnimancerEditorUtilities.SetLooping(clip, isLooping);
  376. }
  377. break;
  378. }
  379. }
  380. /************************************************************************************************************************/
  381. #endif
  382. /************************************************************************************************************************/
  383. #endregion
  384. /************************************************************************************************************************/
  385. }
  386. }