612 lines
25 KiB
C#
612 lines
25 KiB
C#
// 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
|
|
|