GenerateSpriteAnimationsTool.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using UnityEditor;
  7. using UnityEditorInternal;
  8. using UnityEngine;
  9. namespace Animancer.Editor.Tools
  10. {
  11. /// <summary>[Editor-Only] [Pro-Only]
  12. /// A <see cref="SpriteModifierTool"/> for generating <see cref="AnimationClip"/>s from <see cref="Sprite"/>s.
  13. /// </summary>
  14. /// <remarks>
  15. /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/tools/generate-sprite-animations">Generate Sprite Animations</see>
  16. /// </remarks>
  17. /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/GenerateSpriteAnimationsTool
  18. ///
  19. [Serializable]
  20. public class GenerateSpriteAnimationsTool : SpriteModifierTool
  21. {
  22. /************************************************************************************************************************/
  23. #region Tool
  24. /************************************************************************************************************************/
  25. [NonSerialized] private List<string> _Names;
  26. [NonSerialized] private Dictionary<string, List<Sprite>> _NameToSprites;
  27. [NonSerialized] private ReorderableList _Display;
  28. [NonSerialized] private bool _NamesAreDirty;
  29. /************************************************************************************************************************/
  30. /// <inheritdoc/>
  31. public override int DisplayOrder => 3;
  32. /// <inheritdoc/>
  33. public override string Name => "Generate Sprite Animations";
  34. /// <inheritdoc/>
  35. public override string HelpURL => Strings.DocsURLs.GenerateSpriteAnimations;
  36. /// <inheritdoc/>
  37. public override string Instructions
  38. {
  39. get
  40. {
  41. if (Sprites.Count == 0)
  42. return "Select the Sprites you want to generate animations from.";
  43. return "Click Generate.";
  44. }
  45. }
  46. /************************************************************************************************************************/
  47. /// <inheritdoc/>
  48. public override void OnEnable(int index)
  49. {
  50. base.OnEnable(index);
  51. _Names = new List<string>();
  52. _NameToSprites = new Dictionary<string, List<Sprite>>();
  53. _Display = AnimancerToolsWindow.CreateReorderableList(_Names, "Animations to Generate", (area, elementIndex, isActive, isFocused) =>
  54. {
  55. area.y = Mathf.Ceil(area.y + EditorGUIUtility.standardVerticalSpacing * 0.5f);
  56. area.height = EditorGUIUtility.singleLineHeight;
  57. var name = _Names[elementIndex];
  58. var sprites = _NameToSprites[name];
  59. AnimancerToolsWindow.BeginChangeCheck();
  60. name = EditorGUI.TextField(area, name);
  61. if (AnimancerToolsWindow.EndChangeCheck())
  62. {
  63. _Names[elementIndex] = name;
  64. }
  65. for (int i = 0; i < sprites.Count; i++)
  66. {
  67. area.y += area.height + EditorGUIUtility.standardVerticalSpacing;
  68. var sprite = sprites[i];
  69. AnimancerToolsWindow.BeginChangeCheck();
  70. sprite = (Sprite)EditorGUI.ObjectField(area, sprite, typeof(Sprite), false);
  71. if (AnimancerToolsWindow.EndChangeCheck())
  72. {
  73. sprites[i] = sprite;
  74. }
  75. }
  76. });
  77. _Display.elementHeightCallback = (elementIndex) =>
  78. {
  79. var lineCount = _NameToSprites[_Names[elementIndex]].Count + 1;
  80. return
  81. EditorGUIUtility.singleLineHeight * lineCount +
  82. EditorGUIUtility.standardVerticalSpacing * lineCount;
  83. };
  84. }
  85. /************************************************************************************************************************/
  86. /// <inheritdoc/>
  87. public override void OnSelectionChanged()
  88. {
  89. _NameToSprites.Clear();
  90. _Names.Clear();
  91. _NamesAreDirty = true;
  92. }
  93. /************************************************************************************************************************/
  94. /// <inheritdoc/>
  95. public override void DoBodyGUI()
  96. {
  97. EditorGUILayout.PropertyField(AnimancerSettings.NewAnimationFrameRate);
  98. var sprites = Sprites;
  99. if (_NamesAreDirty)
  100. {
  101. _NamesAreDirty = false;
  102. GatherNameToSprites(sprites, _NameToSprites);
  103. _Names.AddRange(_NameToSprites.Keys);
  104. }
  105. using (new EditorGUI.DisabledScope(true))
  106. {
  107. _Display.DoLayoutList();
  108. GUILayout.BeginHorizontal();
  109. {
  110. GUILayout.FlexibleSpace();
  111. GUI.enabled = sprites.Count > 0;
  112. if (GUILayout.Button("Generate"))
  113. {
  114. AnimancerGUI.Deselect();
  115. GenerateAnimationsBySpriteName(sprites);
  116. }
  117. }
  118. GUILayout.EndHorizontal();
  119. }
  120. EditorGUILayout.HelpBox("This function is also available via:" +
  121. "\n - The 'Assets/Create/Animancer' menu." +
  122. "\n - The Cog icon in the top right of the Inspector for Sprite and Texture assets",
  123. MessageType.Info);
  124. }
  125. /************************************************************************************************************************/
  126. #endregion
  127. /************************************************************************************************************************/
  128. #region Methods
  129. /************************************************************************************************************************/
  130. /// <summary>Uses <see cref="GatherNameToSprites"/> and creates new animations from those groups.</summary>
  131. private static void GenerateAnimationsBySpriteName(List<Sprite> sprites)
  132. {
  133. if (sprites.Count == 0)
  134. return;
  135. sprites.Sort(NaturalCompare);
  136. var nameToSprites = new Dictionary<string, List<Sprite>>();
  137. GatherNameToSprites(sprites, nameToSprites);
  138. var pathToSprites = new Dictionary<string, List<Sprite>>();
  139. var message = ObjectPool.AcquireStringBuilder()
  140. .Append("Do you wish to generate the following animations?");
  141. const int MaxLines = 25;
  142. var line = 0;
  143. foreach (var nameToSpriteGroup in nameToSprites)
  144. {
  145. var path = AssetDatabase.GetAssetPath(nameToSpriteGroup.Value[0]);
  146. path = Path.GetDirectoryName(path);
  147. path = Path.Combine(path, nameToSpriteGroup.Key + ".anim");
  148. pathToSprites.Add(path, nameToSpriteGroup.Value);
  149. if (++line <= MaxLines)
  150. {
  151. message.AppendLine()
  152. .Append("- ")
  153. .Append(path)
  154. .Append(" (")
  155. .Append(nameToSpriteGroup.Value.Count)
  156. .Append(" frames)");
  157. }
  158. }
  159. if (line > MaxLines)
  160. {
  161. message.AppendLine()
  162. .Append("And ")
  163. .Append(line - MaxLines)
  164. .Append(" others.");
  165. }
  166. if (!EditorUtility.DisplayDialog("Generate Sprite Animations?", message.ReleaseToString(), "Generate", "Cancel"))
  167. return;
  168. foreach (var pathToSpriteGroup in pathToSprites)
  169. CreateAnimation(pathToSpriteGroup.Key, pathToSpriteGroup.Value.ToArray());
  170. AssetDatabase.SaveAssets();
  171. }
  172. /************************************************************************************************************************/
  173. private static char[] _Numbers, _TrimOther;
  174. /// <summary>Groups the `sprites` by name into the `nameToSptires`.</summary>
  175. private static void GatherNameToSprites(List<Sprite> sprites, Dictionary<string, List<Sprite>> nameToSprites)
  176. {
  177. for (int i = 0; i < sprites.Count; i++)
  178. {
  179. var sprite = sprites[i];
  180. var name = sprite.name;
  181. // Remove numbers from the end.
  182. if (_Numbers == null)
  183. _Numbers = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
  184. name = name.TrimEnd(_Numbers);
  185. // Then remove other characters from the end.
  186. if (_TrimOther == null)
  187. _TrimOther = new char[] { ' ', '_', '-' };
  188. name = name.TrimEnd(_TrimOther);
  189. // Doing both at once would turn "Attack2-0" (Attack 2 Frame 0) into "Attack" (losing the number).
  190. if (!nameToSprites.TryGetValue(name, out var spriteGroup))
  191. {
  192. spriteGroup = new List<Sprite>();
  193. nameToSprites.Add(name, spriteGroup);
  194. }
  195. // Add the sprite to the group if it's not a duplicate.
  196. if (spriteGroup.Count == 0 || spriteGroup[spriteGroup.Count - 1] != sprite)
  197. spriteGroup.Add(sprite);
  198. }
  199. }
  200. /************************************************************************************************************************/
  201. /// <summary>Creates and saves a new <see cref="AnimationClip"/> that plays the `sprites`.</summary>
  202. private static void CreateAnimation(string path, params Sprite[] sprites)
  203. {
  204. var frameRate = AnimancerSettings.NewAnimationFrameRate.floatValue;
  205. var clip = new AnimationClip
  206. {
  207. frameRate = frameRate,
  208. };
  209. var spriteKeyFrames = new ObjectReferenceKeyframe[sprites.Length];
  210. for (int i = 0; i < spriteKeyFrames.Length; i++)
  211. {
  212. spriteKeyFrames[i] = new ObjectReferenceKeyframe
  213. {
  214. time = i / (float)frameRate,
  215. value = sprites[i]
  216. };
  217. }
  218. var spriteBinding = EditorCurveBinding.PPtrCurve("", typeof(SpriteRenderer), "m_Sprite");
  219. AnimationUtility.SetObjectReferenceCurve(clip, spriteBinding, spriteKeyFrames);
  220. AssetDatabase.CreateAsset(clip, path);
  221. }
  222. /************************************************************************************************************************/
  223. #endregion
  224. /************************************************************************************************************************/
  225. #region Menu Functions
  226. /************************************************************************************************************************/
  227. private const string GenerateAnimationsBySpriteNameFunctionName = "Generate Animations By Sprite Name";
  228. /************************************************************************************************************************/
  229. /// <summary>Should <see cref="GenerateAnimationsBySpriteName()"/> be enabled or greyed out?</summary>
  230. [MenuItem(Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
  231. private static bool ValidateGenerateAnimationsBySpriteName()
  232. {
  233. var selection = Selection.objects;
  234. for (int i = 0; i < selection.Length; i++)
  235. {
  236. var selected = selection[i];
  237. if (selected is Sprite || selected is Texture)
  238. return true;
  239. }
  240. return false;
  241. }
  242. /// <summary>Calls <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> with the selected <see cref="Sprite"/>s.</summary>
  243. [MenuItem(Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName, priority = Strings.AssetMenuOrder + 13)]
  244. private static void GenerateAnimationsBySpriteName()
  245. {
  246. var sprites = new List<Sprite>();
  247. var selection = Selection.objects;
  248. for (int i = 0; i < selection.Length; i++)
  249. {
  250. var selected = selection[i];
  251. if (selected is Sprite sprite)
  252. {
  253. sprites.Add(sprite);
  254. }
  255. else if (selected is Texture2D texture)
  256. {
  257. sprites.AddRange(LoadAllSpritesInTexture(texture));
  258. }
  259. }
  260. GenerateAnimationsBySpriteName(sprites);
  261. }
  262. /************************************************************************************************************************/
  263. private static List<Sprite> _CachedSprites;
  264. /// <summary>
  265. /// Returns a list of <see cref="Sprite"/>s which will be passed into
  266. /// <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> by <see cref="EditorApplication.delayCall"/>.
  267. /// </summary>
  268. private static List<Sprite> GetCachedSpritesToGenerateAnimations()
  269. {
  270. if (_CachedSprites == null)
  271. return _CachedSprites = new List<Sprite>();
  272. // Delay the call in case multiple objects are selected.
  273. if (_CachedSprites.Count == 0)
  274. {
  275. EditorApplication.delayCall += () =>
  276. {
  277. GenerateAnimationsBySpriteName(_CachedSprites);
  278. _CachedSprites.Clear();
  279. };
  280. }
  281. return _CachedSprites;
  282. }
  283. /************************************************************************************************************************/
  284. /// <summary>
  285. /// Adds the <see cref="MenuCommand.context"/> to the <see cref="GetCachedSpritesToGenerateAnimations"/>.
  286. /// </summary>
  287. [MenuItem("CONTEXT/" + nameof(Sprite) + GenerateAnimationsBySpriteNameFunctionName)]
  288. private static void GenerateAnimationsFromSpriteByName(MenuCommand command)
  289. {
  290. GetCachedSpritesToGenerateAnimations().Add((Sprite)command.context);
  291. }
  292. /************************************************************************************************************************/
  293. /// <summary>Should <see cref="GenerateAnimationsFromTextureBySpriteName"/> be enabled or greyed out?</summary>
  294. [MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
  295. private static bool ValidateGenerateAnimationsFromTextureBySpriteName(MenuCommand command)
  296. {
  297. var importer = (TextureImporter)command.context;
  298. var sprites = LoadAllSpritesAtPath(importer.assetPath);
  299. return sprites.Length > 0;
  300. }
  301. /// <summary>
  302. /// Adds all <see cref="Sprite"/> sub-assets of the <see cref="MenuCommand.context"/> to the
  303. /// <see cref="GetCachedSpritesToGenerateAnimations"/>.
  304. /// </summary>
  305. [MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName)]
  306. private static void GenerateAnimationsFromTextureBySpriteName(MenuCommand command)
  307. {
  308. var cachedSprites = GetCachedSpritesToGenerateAnimations();
  309. var importer = (TextureImporter)command.context;
  310. cachedSprites.AddRange(LoadAllSpritesAtPath(importer.assetPath));
  311. }
  312. /************************************************************************************************************************/
  313. #endregion
  314. /************************************************************************************************************************/
  315. }
  316. }
  317. #endif