123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
- #if UNITY_EDITOR
- using System;
- using System.Collections.Generic;
- using System.IO;
- using UnityEditor;
- using UnityEditorInternal;
- using UnityEngine;
- using static Animancer.Editor.AnimancerGUI;
- namespace Animancer.Editor.Tools
- {
- /// <summary>[Editor-Only] [Pro-Only]
- /// A <see cref="SpriteModifierTool"/> for generating <see cref="AnimationClip"/>s from <see cref="Sprite"/>s.
- /// </summary>
- /// <remarks>
- /// <strong>Documentation:</strong>
- /// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/generate-sprite-animations">
- /// Generate Sprite Animations</see>
- /// </remarks>
- /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/GenerateSpriteAnimationsTool
- ///
- [Serializable]
- public class GenerateSpriteAnimationsTool : SpriteModifierTool
- {
- /************************************************************************************************************************/
- #region Tool
- /************************************************************************************************************************/
- [NonSerialized] private List<string> _Names;
- [NonSerialized] private Dictionary<string, List<Sprite>> _NameToSprites;
- [NonSerialized] private ReorderableList _Display;
- [NonSerialized] private bool _NamesAreDirty;
- [NonSerialized] private double _PreviewStartTime;
- [NonSerialized] private long _PreviewFrameIndex;
- [NonSerialized] private bool _RequiresRepaint;
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override int DisplayOrder => 3;
- /// <inheritdoc/>
- public override string Name => "Generate Sprite Animations";
- /// <inheritdoc/>
- public override string HelpURL => Strings.DocsURLs.GenerateSpriteAnimations;
- /// <inheritdoc/>
- public override string Instructions
- {
- get
- {
- if (Sprites.Count == 0)
- return "Select the Sprites you want to generate animations from.";
- return "Configure the animation settings then click Generate.";
- }
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void OnEnable(int index)
- {
- base.OnEnable(index);
- _Names = new();
- _NameToSprites = new();
- _Display = AnimancerToolsWindow.CreateReorderableList(
- _Names,
- "Animations to Generate",
- DrawDisplayElement);
- _Display.elementHeightCallback = CalculateDisplayElementHeight;
- _PreviewStartTime = EditorApplication.timeSinceStartup;
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void OnSelectionChanged()
- {
- _NameToSprites.Clear();
- _Names.Clear();
- _NamesAreDirty = true;
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void DoBodyGUI()
- {
- var property = GenerateSpriteAnimationsSettings.SerializedProperty;
- property.serializedObject.Update();
- using (var label = PooledGUIContent.Acquire("Settings"))
- EditorGUILayout.PropertyField(property, label, true);
- property.serializedObject.ApplyModifiedProperties();
- GenerateSpriteAnimationsSettings.Instance.FillDefaults();
- var sprites = Sprites;
- if (_NamesAreDirty)
- {
- _NamesAreDirty = false;
- GatherNameToSprites(sprites, _NameToSprites);
- _Names.AddRange(_NameToSprites.Keys);
- }
- using (new EditorGUI.DisabledScope(true))
- {
- var previewCurrentTime = EditorApplication.timeSinceStartup - _PreviewStartTime;
- _PreviewFrameIndex = (long)(previewCurrentTime * GenerateSpriteAnimationsSettings.FrameRate);
- _Display.DoLayoutList();
- GUILayout.BeginHorizontal();
- {
- GUILayout.FlexibleSpace();
- GUI.enabled = sprites.Count > 0;
- if (GUILayout.Button("Generate"))
- {
- Deselect();
- GenerateAnimationsBySpriteName(sprites);
- }
- }
- GUILayout.EndHorizontal();
- }
- EditorGUILayout.HelpBox("This function is also available via:" +
- "\n• The 'Assets/Create/Animancer' menu." +
- "\n• The Context Menu in the top right of the Inspector for Sprite and Texture assets",
- MessageType.Info);
- if (_RequiresRepaint)
- {
- _RequiresRepaint = false;
- AnimancerToolsWindow.Repaint();
- }
- }
- /************************************************************************************************************************/
- /// <summary>Calculates the height of an animation to generate.</summary>
- private float CalculateDisplayElementHeight(int index)
- {
- if (_NameToSprites.Count <= 0 || _Names.Count <= 0)
- return 0;
- var lineCount = _NameToSprites[_Names[index]].Count + 3;
- return (LineHeight + StandardSpacing) * lineCount;
- }
- /************************************************************************************************************************/
- /// <summary>Draws the details of an animation to generate.</summary>
- private void DrawDisplayElement(Rect area, int index, bool isActive, bool isFocused)
- {
- area.y = Mathf.Ceil(area.y + StandardSpacing * 0.5f);
- area.height = LineHeight;
- DrawAnimationHeader(ref area, index, out var sprites);
- DrawAnimationBody(ref area, sprites);
- }
- /************************************************************************************************************************/
- /// <summary>Draws the name and preview of an animation to generate.</summary>
- private void DrawAnimationHeader(ref Rect area, int index, out List<Sprite> sprites)
- {
- var width = area.width;
- var previewSize = 3 * LineHeight + 2 * StandardSpacing;
- var previewArea = StealFromRight(ref area, previewSize, StandardSpacing);
- previewArea.height = previewSize;
- // Name.
- var name = _Names[index];
- AnimancerToolsWindow.BeginChangeCheck();
- name = EditorGUI.TextField(area, name);
- if (AnimancerToolsWindow.EndChangeCheck())
- {
- _Names[index] = name;
- }
- NextVerticalArea(ref area);
- // Frame Count.
- sprites = _NameToSprites[name];
- var frame = (int)(_PreviewFrameIndex % sprites.Count);
- var enabled = GUI.enabled;
- GUI.enabled = false;
- EditorGUI.TextField(area, $"Frame {frame} / {sprites.Count}");
- NextVerticalArea(ref area);
- // Preview Time.
- GUI.enabled = true;
- var beforeControlID = GUIUtility.GetControlID(FocusType.Passive);
- var newFrame = EditorGUI.IntSlider(area, frame, 0, sprites.Count);
- var afterControlID = GUIUtility.GetControlID(FocusType.Passive);
- var hotControl = GUIUtility.hotControl;
- if (newFrame != frame ||
- (hotControl > beforeControlID && hotControl < afterControlID))
- {
- _PreviewStartTime = EditorApplication.timeSinceStartup;
- _PreviewStartTime -= newFrame / GenerateSpriteAnimationsSettings.FrameRate;
- _PreviewFrameIndex = newFrame;
- frame = newFrame % sprites.Count;
- }
- GUI.enabled = enabled;
- NextVerticalArea(ref area);
- area.width = width;
- // Preview.
- DrawSprite(previewArea, sprites[frame]);
- _RequiresRepaint = true;
- }
- /************************************************************************************************************************/
- /// <summary>Draws the sprite contents of an animation to generate.</summary>
- private void DrawAnimationBody(ref Rect area, List<Sprite> sprites)
- {
- var previewFrame = (int)(_PreviewFrameIndex % sprites.Count);
- for (int i = 0; i < sprites.Count; i++)
- {
- var sprite = sprites[i];
- var fieldArea = area;
- var thumbnailArea = StealFromLeft(
- ref fieldArea,
- fieldArea.height,
- StandardSpacing);
- AnimancerToolsWindow.BeginChangeCheck();
- sprite = DoObjectFieldGUI(fieldArea, "", sprite, false);
- if (AnimancerToolsWindow.EndChangeCheck())
- {
- sprites[i] = sprite;
- }
- if (i == previewFrame)
- EditorGUI.DrawRect(fieldArea, new(0.25f, 1, 0.25f, 0.1f));
- DrawSprite(thumbnailArea, sprite);
- NextVerticalArea(ref area);
- }
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Methods
- /************************************************************************************************************************/
- /// <summary>Uses <see cref="GatherNameToSprites"/> and creates new animations from those groups.</summary>
- private static void GenerateAnimationsBySpriteName(List<Sprite> sprites)
- {
- if (sprites.Count == 0)
- return;
- sprites.Sort(NaturalCompare);
- var nameToSprites = new Dictionary<string, List<Sprite>>();
- GatherNameToSprites(sprites, nameToSprites);
- var pathToSprites = new Dictionary<string, List<Sprite>>();
- var message = StringBuilderPool.Instance.Acquire()
- .Append("Do you wish to generate the following animations?");
- const int MaxLines = 25;
- var line = 0;
- foreach (var nameToSpriteGroup in nameToSprites)
- {
- var path = AssetDatabase.GetAssetPath(nameToSpriteGroup.Value[0]);
- path = Path.GetDirectoryName(path);
- path = Path.Combine(path, nameToSpriteGroup.Key + ".anim");
- pathToSprites.Add(path, nameToSpriteGroup.Value);
- if (++line <= MaxLines)
- {
- message.AppendLine()
- .Append("- ")
- .Append(path)
- .Append(" (")
- .Append(nameToSpriteGroup.Value.Count)
- .Append(" frames)");
- }
- }
- if (line > MaxLines)
- {
- message.AppendLine()
- .Append("And ")
- .Append(line - MaxLines)
- .Append(" others.");
- }
- if (!EditorUtility.DisplayDialog("Generate Sprite Animations?", message.ReleaseToString(), "Generate", "Cancel"))
- return;
- foreach (var pathToSpriteGroup in pathToSprites)
- CreateAnimation(pathToSpriteGroup.Key, pathToSpriteGroup.Value.ToArray());
- AssetDatabase.SaveAssets();
- }
- /************************************************************************************************************************/
- private static char[] _Numbers, _TrimOther;
- /// <summary>Groups the `sprites` by name into the `nameToSptires`.</summary>
- private static void GatherNameToSprites(List<Sprite> sprites, Dictionary<string, List<Sprite>> nameToSprites)
- {
- for (int i = 0; i < sprites.Count; i++)
- {
- var sprite = sprites[i];
- var name = sprite.name;
- // Remove numbers from the end.
- _Numbers ??= new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
- name = name.TrimEnd(_Numbers);
- // Then remove other characters from the end.
- _TrimOther ??= new char[] { ' ', '_', '-' };
- name = name.TrimEnd(_TrimOther);
- // Doing both at once would turn "Attack2-0" (Attack 2 Frame 0) into "Attack" (losing the number).
- if (!nameToSprites.TryGetValue(name, out var spriteGroup))
- {
- spriteGroup = new();
- nameToSprites.Add(name, spriteGroup);
- }
- // Add the sprite to the group if it's not a duplicate.
- if (spriteGroup.Count == 0 || spriteGroup[^1] != sprite)
- spriteGroup.Add(sprite);
- }
- }
- /************************************************************************************************************************/
- /// <summary>Creates and saves a new <see cref="AnimationClip"/> that plays the `sprites`.</summary>
- private static void CreateAnimation(string path, params Sprite[] sprites)
- {
- var frameRate = GenerateSpriteAnimationsSettings.FrameRate;
- var hierarchyPath = GenerateSpriteAnimationsSettings.HierarchyPath;
- var type = GenerateSpriteAnimationsSettings.TargetType.Type ?? typeof(SpriteRenderer);
- var property = GenerateSpriteAnimationsSettings.PropertyName;
- if (string.IsNullOrWhiteSpace(property))
- property = "m_Sprite";
- var clip = new AnimationClip
- {
- frameRate = frameRate,
- };
- var spriteKeyFrames = new ObjectReferenceKeyframe[sprites.Length];
- for (int i = 0; i < spriteKeyFrames.Length; i++)
- {
- spriteKeyFrames[i] = new()
- {
- time = i / (float)frameRate,
- value = sprites[i]
- };
- }
- var spriteBinding = EditorCurveBinding.PPtrCurve(hierarchyPath, type, property);
- AnimationUtility.SetObjectReferenceCurve(clip, spriteBinding, spriteKeyFrames);
- AssetDatabase.CreateAsset(clip, path);
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Menu Functions
- /************************************************************************************************************************/
- private const string GenerateAnimationsBySpriteNameFunctionName = "Generate Animations By Sprite Name";
- /************************************************************************************************************************/
- /// <summary>Should <see cref="GenerateAnimationsBySpriteName()"/> be enabled or greyed out?</summary>
- [MenuItem(Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
- private static bool ValidateGenerateAnimationsBySpriteName()
- {
- var selection = Selection.objects;
- for (int i = 0; i < selection.Length; i++)
- {
- var selected = selection[i];
- if (selected is Sprite || selected is Texture)
- return true;
- }
- return false;
- }
- /// <summary>Calls <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> with the selected <see cref="Sprite"/>s.</summary>
- [MenuItem(
- itemName: Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName,
- priority = Strings.AssetMenuOrder + 6)]
- private static void GenerateAnimationsBySpriteName()
- {
- var sprites = new List<Sprite>();
- var selection = Selection.objects;
- for (int i = 0; i < selection.Length; i++)
- {
- var selected = selection[i];
- if (selected is Sprite sprite)
- {
- sprites.Add(sprite);
- }
- else if (selected is Texture2D texture)
- {
- sprites.AddRange(LoadAllSpritesInTexture(texture));
- }
- }
- GenerateAnimationsBySpriteName(sprites);
- }
- /************************************************************************************************************************/
- private static List<Sprite> _CachedSprites;
- /// <summary>
- /// Returns a list of <see cref="Sprite"/>s which will be passed into
- /// <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> by <see cref="EditorApplication.delayCall"/>.
- /// </summary>
- private static List<Sprite> GetCachedSpritesToGenerateAnimations()
- {
- if (_CachedSprites == null)
- return _CachedSprites = new();
- // Delay the call in case multiple objects are selected.
- if (_CachedSprites.Count == 0)
- {
- EditorApplication.delayCall += () =>
- {
- GenerateAnimationsBySpriteName(_CachedSprites);
- _CachedSprites.Clear();
- };
- }
- return _CachedSprites;
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Adds the <see cref="MenuCommand.context"/> to the <see cref="GetCachedSpritesToGenerateAnimations"/>.
- /// </summary>
- [MenuItem("CONTEXT/" + nameof(Sprite) + GenerateAnimationsBySpriteNameFunctionName)]
- private static void GenerateAnimationsFromSpriteByName(MenuCommand command)
- {
- GetCachedSpritesToGenerateAnimations().Add((Sprite)command.context);
- }
- /************************************************************************************************************************/
- /// <summary>Should <see cref="GenerateAnimationsFromTextureBySpriteName"/> be enabled or greyed out?</summary>
- [MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
- private static bool ValidateGenerateAnimationsFromTextureBySpriteName(MenuCommand command)
- {
- var importer = (TextureImporter)command.context;
- var sprites = LoadAllSpritesAtPath(importer.assetPath);
- return sprites.Length > 0;
- }
- /// <summary>
- /// Adds all <see cref="Sprite"/> sub-assets of the <see cref="MenuCommand.context"/> to the
- /// <see cref="GetCachedSpritesToGenerateAnimations"/>.
- /// </summary>
- [MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName)]
- private static void GenerateAnimationsFromTextureBySpriteName(MenuCommand command)
- {
- var cachedSprites = GetCachedSpritesToGenerateAnimations();
- var importer = (TextureImporter)command.context;
- cachedSprites.AddRange(LoadAllSpritesAtPath(importer.assetPath));
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- #region Settings
- /************************************************************************************************************************/
- /// <summary>[Editor-Only] Settings for <see cref="GenerateSpriteAnimationsTool"/>.</summary>
- /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/GenerateSpriteAnimationsSettings
- [Serializable, InternalSerializableType]
- public class GenerateSpriteAnimationsSettings : AnimancerSettingsGroup
- {
- /************************************************************************************************************************/
- /// <summary>Gets or creates an instance.</summary>
- public static GenerateSpriteAnimationsSettings Instance
- => AnimancerSettingsGroup<GenerateSpriteAnimationsSettings>.Instance;
- /// <summary>The <see cref="UnityEditor.SerializedProperty"/> representing the <see cref="Instance"/>.</summary>
- public static SerializedProperty SerializedProperty
- => Instance.GetSerializedProperty(null);
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override string DisplayName
- => "Generate Sprite Animations Tool";
- /// <inheritdoc/>
- public override int Index
- => 6;
- /************************************************************************************************************************/
- [SerializeField]
- [Tooltip("The frame rate to use for new animations")]
- private float _FrameRate = 12;
- /// <summary>The frame rate to use for new animations.</summary>
- public static ref float FrameRate
- => ref Instance._FrameRate;
- /************************************************************************************************************************/
- [SerializeField]
- [Tooltip("The Transform Hierarchy path from the Animator to the object being animated" +
- " using forward slashes '/' between each object name")]
- private string _HierarchyPath;
- /// <summary>The Transform Hierarchy path from the <see cref="Animator"/> to the object being animated.</summary>
- public static ref string HierarchyPath
- => ref Instance._HierarchyPath;
- /************************************************************************************************************************/
- [SerializeField]
- [Tooltip("The type of component being animated. Defaults to " + nameof(SpriteRenderer) + " if not set." +
- " Use the type picker on the right or drag and drop a component onto it to set this field.")]
- private SerializableTypeReference _TargetType = new(typeof(SpriteRenderer));
- /// <summary>The type of component being animated. Defaults to <see cref="SpriteRenderer"/> if not set.</summary>
- public static ref SerializableTypeReference TargetType
- => ref Instance._TargetType;
- /************************************************************************************************************************/
- /// <summary>The default value for <see cref="PropertyName"/>.</summary>
- public const string DefaultPropertyName = "m_Sprite";
- [SerializeField]
- [Tooltip("The path of the property being animated. Defaults to " + DefaultPropertyName + " if not set.")]
- private string _PropertyName = DefaultPropertyName;
- /// <summary>The path of the property being animated.</summary>
- public static ref string PropertyName
- => ref Instance._PropertyName;
- /************************************************************************************************************************/
- /// <summary>Reverts any empty values to their defaults.</summary>
- public void FillDefaults()
- {
- if (string.IsNullOrWhiteSpace(_TargetType.QualifiedName))
- _TargetType = new(typeof(SpriteRenderer));
- if (string.IsNullOrWhiteSpace(_PropertyName))
- _PropertyName = DefaultPropertyName;
- }
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- }
- #endif
|