GenerateSpriteAnimationsTool.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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. using static Animancer.Editor.AnimancerGUI;
  10. namespace Animancer.Editor.Tools
  11. {
  12. /// <summary>[Editor-Only] [Pro-Only]
  13. /// A <see cref="SpriteModifierTool"/> for generating <see cref="AnimationClip"/>s from <see cref="Sprite"/>s.
  14. /// </summary>
  15. /// <remarks>
  16. /// <strong>Documentation:</strong>
  17. /// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/generate-sprite-animations">
  18. /// Generate Sprite Animations</see>
  19. /// </remarks>
  20. /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/GenerateSpriteAnimationsTool
  21. ///
  22. [Serializable]
  23. public class GenerateSpriteAnimationsTool : SpriteModifierTool
  24. {
  25. /************************************************************************************************************************/
  26. #region Tool
  27. /************************************************************************************************************************/
  28. [NonSerialized] private List<string> _Names;
  29. [NonSerialized] private Dictionary<string, List<Sprite>> _NameToSprites;
  30. [NonSerialized] private ReorderableList _Display;
  31. [NonSerialized] private bool _NamesAreDirty;
  32. [NonSerialized] private double _PreviewStartTime;
  33. [NonSerialized] private long _PreviewFrameIndex;
  34. [NonSerialized] private bool _RequiresRepaint;
  35. /************************************************************************************************************************/
  36. /// <inheritdoc/>
  37. public override int DisplayOrder => 3;
  38. /// <inheritdoc/>
  39. public override string Name => "Generate Sprite Animations";
  40. /// <inheritdoc/>
  41. public override string HelpURL => Strings.DocsURLs.GenerateSpriteAnimations;
  42. /// <inheritdoc/>
  43. public override string Instructions
  44. {
  45. get
  46. {
  47. if (Sprites.Count == 0)
  48. return "Select the Sprites you want to generate animations from.";
  49. return "Configure the animation settings then click Generate.";
  50. }
  51. }
  52. /************************************************************************************************************************/
  53. /// <inheritdoc/>
  54. public override void OnEnable(int index)
  55. {
  56. base.OnEnable(index);
  57. _Names = new();
  58. _NameToSprites = new();
  59. _Display = AnimancerToolsWindow.CreateReorderableList(
  60. _Names,
  61. "Animations to Generate",
  62. DrawDisplayElement);
  63. _Display.elementHeightCallback = CalculateDisplayElementHeight;
  64. _PreviewStartTime = EditorApplication.timeSinceStartup;
  65. }
  66. /************************************************************************************************************************/
  67. /// <inheritdoc/>
  68. public override void OnSelectionChanged()
  69. {
  70. _NameToSprites.Clear();
  71. _Names.Clear();
  72. _NamesAreDirty = true;
  73. }
  74. /************************************************************************************************************************/
  75. /// <inheritdoc/>
  76. public override void DoBodyGUI()
  77. {
  78. var property = GenerateSpriteAnimationsSettings.SerializedProperty;
  79. property.serializedObject.Update();
  80. using (var label = PooledGUIContent.Acquire("Settings"))
  81. EditorGUILayout.PropertyField(property, label, true);
  82. property.serializedObject.ApplyModifiedProperties();
  83. GenerateSpriteAnimationsSettings.Instance.FillDefaults();
  84. var sprites = Sprites;
  85. if (_NamesAreDirty)
  86. {
  87. _NamesAreDirty = false;
  88. GatherNameToSprites(sprites, _NameToSprites);
  89. _Names.AddRange(_NameToSprites.Keys);
  90. }
  91. using (new EditorGUI.DisabledScope(true))
  92. {
  93. var previewCurrentTime = EditorApplication.timeSinceStartup - _PreviewStartTime;
  94. _PreviewFrameIndex = (long)(previewCurrentTime * GenerateSpriteAnimationsSettings.FrameRate);
  95. _Display.DoLayoutList();
  96. GUILayout.BeginHorizontal();
  97. {
  98. GUILayout.FlexibleSpace();
  99. GUI.enabled = sprites.Count > 0;
  100. if (GUILayout.Button("Generate"))
  101. {
  102. Deselect();
  103. GenerateAnimationsBySpriteName(sprites);
  104. }
  105. }
  106. GUILayout.EndHorizontal();
  107. }
  108. EditorGUILayout.HelpBox("This function is also available via:" +
  109. "\n• The 'Assets/Create/Animancer' menu." +
  110. "\n• The Context Menu in the top right of the Inspector for Sprite and Texture assets",
  111. MessageType.Info);
  112. if (_RequiresRepaint)
  113. {
  114. _RequiresRepaint = false;
  115. AnimancerToolsWindow.Repaint();
  116. }
  117. }
  118. /************************************************************************************************************************/
  119. /// <summary>Calculates the height of an animation to generate.</summary>
  120. private float CalculateDisplayElementHeight(int index)
  121. {
  122. if (_NameToSprites.Count <= 0 || _Names.Count <= 0)
  123. return 0;
  124. var lineCount = _NameToSprites[_Names[index]].Count + 3;
  125. return (LineHeight + StandardSpacing) * lineCount;
  126. }
  127. /************************************************************************************************************************/
  128. /// <summary>Draws the details of an animation to generate.</summary>
  129. private void DrawDisplayElement(Rect area, int index, bool isActive, bool isFocused)
  130. {
  131. area.y = Mathf.Ceil(area.y + StandardSpacing * 0.5f);
  132. area.height = LineHeight;
  133. DrawAnimationHeader(ref area, index, out var sprites);
  134. DrawAnimationBody(ref area, sprites);
  135. }
  136. /************************************************************************************************************************/
  137. /// <summary>Draws the name and preview of an animation to generate.</summary>
  138. private void DrawAnimationHeader(ref Rect area, int index, out List<Sprite> sprites)
  139. {
  140. var width = area.width;
  141. var previewSize = 3 * LineHeight + 2 * StandardSpacing;
  142. var previewArea = StealFromRight(ref area, previewSize, StandardSpacing);
  143. previewArea.height = previewSize;
  144. // Name.
  145. var name = _Names[index];
  146. AnimancerToolsWindow.BeginChangeCheck();
  147. name = EditorGUI.TextField(area, name);
  148. if (AnimancerToolsWindow.EndChangeCheck())
  149. {
  150. _Names[index] = name;
  151. }
  152. NextVerticalArea(ref area);
  153. // Frame Count.
  154. sprites = _NameToSprites[name];
  155. var frame = (int)(_PreviewFrameIndex % sprites.Count);
  156. var enabled = GUI.enabled;
  157. GUI.enabled = false;
  158. EditorGUI.TextField(area, $"Frame {frame} / {sprites.Count}");
  159. NextVerticalArea(ref area);
  160. // Preview Time.
  161. GUI.enabled = true;
  162. var beforeControlID = GUIUtility.GetControlID(FocusType.Passive);
  163. var newFrame = EditorGUI.IntSlider(area, frame, 0, sprites.Count);
  164. var afterControlID = GUIUtility.GetControlID(FocusType.Passive);
  165. var hotControl = GUIUtility.hotControl;
  166. if (newFrame != frame ||
  167. (hotControl > beforeControlID && hotControl < afterControlID))
  168. {
  169. _PreviewStartTime = EditorApplication.timeSinceStartup;
  170. _PreviewStartTime -= newFrame / GenerateSpriteAnimationsSettings.FrameRate;
  171. _PreviewFrameIndex = newFrame;
  172. frame = newFrame % sprites.Count;
  173. }
  174. GUI.enabled = enabled;
  175. NextVerticalArea(ref area);
  176. area.width = width;
  177. // Preview.
  178. DrawSprite(previewArea, sprites[frame]);
  179. _RequiresRepaint = true;
  180. }
  181. /************************************************************************************************************************/
  182. /// <summary>Draws the sprite contents of an animation to generate.</summary>
  183. private void DrawAnimationBody(ref Rect area, List<Sprite> sprites)
  184. {
  185. var previewFrame = (int)(_PreviewFrameIndex % sprites.Count);
  186. for (int i = 0; i < sprites.Count; i++)
  187. {
  188. var sprite = sprites[i];
  189. var fieldArea = area;
  190. var thumbnailArea = StealFromLeft(
  191. ref fieldArea,
  192. fieldArea.height,
  193. StandardSpacing);
  194. AnimancerToolsWindow.BeginChangeCheck();
  195. sprite = DoObjectFieldGUI(fieldArea, "", sprite, false);
  196. if (AnimancerToolsWindow.EndChangeCheck())
  197. {
  198. sprites[i] = sprite;
  199. }
  200. if (i == previewFrame)
  201. EditorGUI.DrawRect(fieldArea, new(0.25f, 1, 0.25f, 0.1f));
  202. DrawSprite(thumbnailArea, sprite);
  203. NextVerticalArea(ref area);
  204. }
  205. }
  206. /************************************************************************************************************************/
  207. #endregion
  208. /************************************************************************************************************************/
  209. #region Methods
  210. /************************************************************************************************************************/
  211. /// <summary>Uses <see cref="GatherNameToSprites"/> and creates new animations from those groups.</summary>
  212. private static void GenerateAnimationsBySpriteName(List<Sprite> sprites)
  213. {
  214. if (sprites.Count == 0)
  215. return;
  216. sprites.Sort(NaturalCompare);
  217. var nameToSprites = new Dictionary<string, List<Sprite>>();
  218. GatherNameToSprites(sprites, nameToSprites);
  219. var pathToSprites = new Dictionary<string, List<Sprite>>();
  220. var message = StringBuilderPool.Instance.Acquire()
  221. .Append("Do you wish to generate the following animations?");
  222. const int MaxLines = 25;
  223. var line = 0;
  224. foreach (var nameToSpriteGroup in nameToSprites)
  225. {
  226. var path = AssetDatabase.GetAssetPath(nameToSpriteGroup.Value[0]);
  227. path = Path.GetDirectoryName(path);
  228. path = Path.Combine(path, nameToSpriteGroup.Key + ".anim");
  229. pathToSprites.Add(path, nameToSpriteGroup.Value);
  230. if (++line <= MaxLines)
  231. {
  232. message.AppendLine()
  233. .Append("- ")
  234. .Append(path)
  235. .Append(" (")
  236. .Append(nameToSpriteGroup.Value.Count)
  237. .Append(" frames)");
  238. }
  239. }
  240. if (line > MaxLines)
  241. {
  242. message.AppendLine()
  243. .Append("And ")
  244. .Append(line - MaxLines)
  245. .Append(" others.");
  246. }
  247. if (!EditorUtility.DisplayDialog("Generate Sprite Animations?", message.ReleaseToString(), "Generate", "Cancel"))
  248. return;
  249. foreach (var pathToSpriteGroup in pathToSprites)
  250. CreateAnimation(pathToSpriteGroup.Key, pathToSpriteGroup.Value.ToArray());
  251. AssetDatabase.SaveAssets();
  252. }
  253. /************************************************************************************************************************/
  254. private static char[] _Numbers, _TrimOther;
  255. /// <summary>Groups the `sprites` by name into the `nameToSptires`.</summary>
  256. private static void GatherNameToSprites(List<Sprite> sprites, Dictionary<string, List<Sprite>> nameToSprites)
  257. {
  258. for (int i = 0; i < sprites.Count; i++)
  259. {
  260. var sprite = sprites[i];
  261. var name = sprite.name;
  262. // Remove numbers from the end.
  263. _Numbers ??= new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
  264. name = name.TrimEnd(_Numbers);
  265. // Then remove other characters from the end.
  266. _TrimOther ??= new char[] { ' ', '_', '-' };
  267. name = name.TrimEnd(_TrimOther);
  268. // Doing both at once would turn "Attack2-0" (Attack 2 Frame 0) into "Attack" (losing the number).
  269. if (!nameToSprites.TryGetValue(name, out var spriteGroup))
  270. {
  271. spriteGroup = new();
  272. nameToSprites.Add(name, spriteGroup);
  273. }
  274. // Add the sprite to the group if it's not a duplicate.
  275. if (spriteGroup.Count == 0 || spriteGroup[^1] != sprite)
  276. spriteGroup.Add(sprite);
  277. }
  278. }
  279. /************************************************************************************************************************/
  280. /// <summary>Creates and saves a new <see cref="AnimationClip"/> that plays the `sprites`.</summary>
  281. private static void CreateAnimation(string path, params Sprite[] sprites)
  282. {
  283. var frameRate = GenerateSpriteAnimationsSettings.FrameRate;
  284. var hierarchyPath = GenerateSpriteAnimationsSettings.HierarchyPath;
  285. var type = GenerateSpriteAnimationsSettings.TargetType.Type ?? typeof(SpriteRenderer);
  286. var property = GenerateSpriteAnimationsSettings.PropertyName;
  287. if (string.IsNullOrWhiteSpace(property))
  288. property = "m_Sprite";
  289. var clip = new AnimationClip
  290. {
  291. frameRate = frameRate,
  292. };
  293. var spriteKeyFrames = new ObjectReferenceKeyframe[sprites.Length];
  294. for (int i = 0; i < spriteKeyFrames.Length; i++)
  295. {
  296. spriteKeyFrames[i] = new()
  297. {
  298. time = i / (float)frameRate,
  299. value = sprites[i]
  300. };
  301. }
  302. var spriteBinding = EditorCurveBinding.PPtrCurve(hierarchyPath, type, property);
  303. AnimationUtility.SetObjectReferenceCurve(clip, spriteBinding, spriteKeyFrames);
  304. AssetDatabase.CreateAsset(clip, path);
  305. }
  306. /************************************************************************************************************************/
  307. #endregion
  308. /************************************************************************************************************************/
  309. #region Menu Functions
  310. /************************************************************************************************************************/
  311. private const string GenerateAnimationsBySpriteNameFunctionName = "Generate Animations By Sprite Name";
  312. /************************************************************************************************************************/
  313. /// <summary>Should <see cref="GenerateAnimationsBySpriteName()"/> be enabled or greyed out?</summary>
  314. [MenuItem(Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
  315. private static bool ValidateGenerateAnimationsBySpriteName()
  316. {
  317. var selection = Selection.objects;
  318. for (int i = 0; i < selection.Length; i++)
  319. {
  320. var selected = selection[i];
  321. if (selected is Sprite || selected is Texture)
  322. return true;
  323. }
  324. return false;
  325. }
  326. /// <summary>Calls <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> with the selected <see cref="Sprite"/>s.</summary>
  327. [MenuItem(
  328. itemName: Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName,
  329. priority = Strings.AssetMenuOrder + 6)]
  330. private static void GenerateAnimationsBySpriteName()
  331. {
  332. var sprites = new List<Sprite>();
  333. var selection = Selection.objects;
  334. for (int i = 0; i < selection.Length; i++)
  335. {
  336. var selected = selection[i];
  337. if (selected is Sprite sprite)
  338. {
  339. sprites.Add(sprite);
  340. }
  341. else if (selected is Texture2D texture)
  342. {
  343. sprites.AddRange(LoadAllSpritesInTexture(texture));
  344. }
  345. }
  346. GenerateAnimationsBySpriteName(sprites);
  347. }
  348. /************************************************************************************************************************/
  349. private static List<Sprite> _CachedSprites;
  350. /// <summary>
  351. /// Returns a list of <see cref="Sprite"/>s which will be passed into
  352. /// <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> by <see cref="EditorApplication.delayCall"/>.
  353. /// </summary>
  354. private static List<Sprite> GetCachedSpritesToGenerateAnimations()
  355. {
  356. if (_CachedSprites == null)
  357. return _CachedSprites = new();
  358. // Delay the call in case multiple objects are selected.
  359. if (_CachedSprites.Count == 0)
  360. {
  361. EditorApplication.delayCall += () =>
  362. {
  363. GenerateAnimationsBySpriteName(_CachedSprites);
  364. _CachedSprites.Clear();
  365. };
  366. }
  367. return _CachedSprites;
  368. }
  369. /************************************************************************************************************************/
  370. /// <summary>
  371. /// Adds the <see cref="MenuCommand.context"/> to the <see cref="GetCachedSpritesToGenerateAnimations"/>.
  372. /// </summary>
  373. [MenuItem("CONTEXT/" + nameof(Sprite) + GenerateAnimationsBySpriteNameFunctionName)]
  374. private static void GenerateAnimationsFromSpriteByName(MenuCommand command)
  375. {
  376. GetCachedSpritesToGenerateAnimations().Add((Sprite)command.context);
  377. }
  378. /************************************************************************************************************************/
  379. /// <summary>Should <see cref="GenerateAnimationsFromTextureBySpriteName"/> be enabled or greyed out?</summary>
  380. [MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
  381. private static bool ValidateGenerateAnimationsFromTextureBySpriteName(MenuCommand command)
  382. {
  383. var importer = (TextureImporter)command.context;
  384. var sprites = LoadAllSpritesAtPath(importer.assetPath);
  385. return sprites.Length > 0;
  386. }
  387. /// <summary>
  388. /// Adds all <see cref="Sprite"/> sub-assets of the <see cref="MenuCommand.context"/> to the
  389. /// <see cref="GetCachedSpritesToGenerateAnimations"/>.
  390. /// </summary>
  391. [MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName)]
  392. private static void GenerateAnimationsFromTextureBySpriteName(MenuCommand command)
  393. {
  394. var cachedSprites = GetCachedSpritesToGenerateAnimations();
  395. var importer = (TextureImporter)command.context;
  396. cachedSprites.AddRange(LoadAllSpritesAtPath(importer.assetPath));
  397. }
  398. /************************************************************************************************************************/
  399. #endregion
  400. /************************************************************************************************************************/
  401. }
  402. /************************************************************************************************************************/
  403. #region Settings
  404. /************************************************************************************************************************/
  405. /// <summary>[Editor-Only] Settings for <see cref="GenerateSpriteAnimationsTool"/>.</summary>
  406. /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/GenerateSpriteAnimationsSettings
  407. [Serializable, InternalSerializableType]
  408. public class GenerateSpriteAnimationsSettings : AnimancerSettingsGroup
  409. {
  410. /************************************************************************************************************************/
  411. /// <summary>Gets or creates an instance.</summary>
  412. public static GenerateSpriteAnimationsSettings Instance
  413. => AnimancerSettingsGroup<GenerateSpriteAnimationsSettings>.Instance;
  414. /// <summary>The <see cref="UnityEditor.SerializedProperty"/> representing the <see cref="Instance"/>.</summary>
  415. public static SerializedProperty SerializedProperty
  416. => Instance.GetSerializedProperty(null);
  417. /************************************************************************************************************************/
  418. /// <inheritdoc/>
  419. public override string DisplayName
  420. => "Generate Sprite Animations Tool";
  421. /// <inheritdoc/>
  422. public override int Index
  423. => 6;
  424. /************************************************************************************************************************/
  425. [SerializeField]
  426. [Tooltip("The frame rate to use for new animations")]
  427. private float _FrameRate = 12;
  428. /// <summary>The frame rate to use for new animations.</summary>
  429. public static ref float FrameRate
  430. => ref Instance._FrameRate;
  431. /************************************************************************************************************************/
  432. [SerializeField]
  433. [Tooltip("The Transform Hierarchy path from the Animator to the object being animated" +
  434. " using forward slashes '/' between each object name")]
  435. private string _HierarchyPath;
  436. /// <summary>The Transform Hierarchy path from the <see cref="Animator"/> to the object being animated.</summary>
  437. public static ref string HierarchyPath
  438. => ref Instance._HierarchyPath;
  439. /************************************************************************************************************************/
  440. [SerializeField]
  441. [Tooltip("The type of component being animated. Defaults to " + nameof(SpriteRenderer) + " if not set." +
  442. " Use the type picker on the right or drag and drop a component onto it to set this field.")]
  443. private SerializableTypeReference _TargetType = new(typeof(SpriteRenderer));
  444. /// <summary>The type of component being animated. Defaults to <see cref="SpriteRenderer"/> if not set.</summary>
  445. public static ref SerializableTypeReference TargetType
  446. => ref Instance._TargetType;
  447. /************************************************************************************************************************/
  448. /// <summary>The default value for <see cref="PropertyName"/>.</summary>
  449. public const string DefaultPropertyName = "m_Sprite";
  450. [SerializeField]
  451. [Tooltip("The path of the property being animated. Defaults to " + DefaultPropertyName + " if not set.")]
  452. private string _PropertyName = DefaultPropertyName;
  453. /// <summary>The path of the property being animated.</summary>
  454. public static ref string PropertyName
  455. => ref Instance._PropertyName;
  456. /************************************************************************************************************************/
  457. /// <summary>Reverts any empty values to their defaults.</summary>
  458. public void FillDefaults()
  459. {
  460. if (string.IsNullOrWhiteSpace(_TargetType.QualifiedName))
  461. _TargetType = new(typeof(SpriteRenderer));
  462. if (string.IsNullOrWhiteSpace(_PropertyName))
  463. _PropertyName = DefaultPropertyName;
  464. }
  465. /************************************************************************************************************************/
  466. }
  467. /************************************************************************************************************************/
  468. #endregion
  469. /************************************************************************************************************************/
  470. }
  471. #endif