Files
5.24BuYu/Assets/MyGame/Framework/Editor/AnimatorClipCreate.cs
T
2026-10-20 18:30:03 +08:00

530 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using System.Linq;
public class AnimationClipCreator2D : EditorWindow
{
private Vector2 scrollDirectoryPos; //文件夹滚动条
private Vector2 scrollAnimationClipPos; //动画滚动条
// 全局设置
private string rootFolderPath = "Assets/SpriteGroups"; // 源根目录
private string saveFolderPath = "Assets/Animations"; // 输出根目录
private string fixedSpritePrefix = "Sprite"; // 固定序列帧前缀
private int defaultNumCount = 5; // 序号补零位数
private bool showItems = true;
//// 已发现的子文件夹
//private List<string> subFolders = new List<string>();
//private List<string> subFolderNames = new List<string>();
//private List<bool> subFolderToggles = new List<bool>();
// 动画条目
[System.Serializable]
private class AnimItem
{
public string animName = "Idle";
public int startFrame = 0;
public int endFrame = 10;
public float frameRate = 12f;
public bool overrideNumCount = false;
public int numCount = 5;
}
private List<AnimItem> items = new List<AnimItem>()
{
new AnimItem(){ animName="Idle", startFrame=0, endFrame=10, frameRate=12f },
new AnimItem(){ animName="Run", startFrame=0, endFrame=7, frameRate=12f },
};
// EditorPrefs keys
private const string PREF_ROOT = "AnimCreator_RootPath";
private const string PREF_SAVE = "AnimCreator_SavePath";
private const string PREF_PREFIX = "AnimCreator_FixedPrefix";
private const string PREF_NUMCOUNT = "AnimCreator_NumCount";
[MenuItem("Tools/2D Animation Clip Creator (Folders)")]
public static void ShowWindow()
{
var window = GetWindow<AnimationClipCreator2D>("Animation Clip Creator");
window.LoadPrefs();
window.LoadAnimItems(); // 加载动画条目
}
private void OnGUI()
{
GUILayout.Label("批量创建 AnimationClip(子文件夹驱动,固定 PNG 前缀)", EditorStyles.boldLabel);
rootFolderPath = EditorGUILayout.TextField("序列帧根目录", rootFolderPath);
saveFolderPath = EditorGUILayout.TextField("输出动画根目录", saveFolderPath);
fixedSpritePrefix = EditorGUILayout.TextField("固定序列帧前缀", fixedSpritePrefix);
defaultNumCount = EditorGUILayout.IntField("默认补零位数", defaultNumCount);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("刷新子文件夹"))
{
RefreshFolderData();
}
EditorGUILayout.LabelField($"已发现子文件夹数量: {folderHasImages.Count}");
EditorGUILayout.EndHorizontal();
GUILayout.Label("选择要生成的子文件夹:", EditorStyles.boldLabel);
// 模式切换选项
displayMode = (DisplayMode)EditorGUILayout.EnumPopup("显示模式", displayMode);
// 子文件夹选择列表
scrollDirectoryPos = EditorGUILayout.BeginScrollView(scrollDirectoryPos, GUILayout.MaxHeight(400));
if (folderHasImages.Count > 0)
{
GUILayout.Space(10);
if (displayMode == DisplayMode.Grid)
{
DrawGridMode();
}
else
{
DrawTreeMode();
}
}
EditorGUILayout.EndScrollView();
// 动画条目列表
showItems = EditorGUILayout.BeginFoldoutHeaderGroup(showItems, $"动画条目列表 ({items.Count})");
scrollAnimationClipPos = EditorGUILayout.BeginScrollView(scrollAnimationClipPos);
if (showItems)
{
for (int i = 0; i < items.Count; i++)
{
var it = items[i];
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
it.animName = EditorGUILayout.TextField("动画名字", it.animName);
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
items.RemoveAt(i);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
break;
}
EditorGUILayout.EndHorizontal();
it.startFrame = EditorGUILayout.IntField("开始帧", it.startFrame);
it.endFrame = EditorGUILayout.IntField("结束帧(含)", it.endFrame);
it.frameRate = EditorGUILayout.FloatField("帧率", it.frameRate);
it.overrideNumCount = EditorGUILayout.Toggle("覆盖补零位数", it.overrideNumCount);
if (it.overrideNumCount)
it.numCount = EditorGUILayout.IntField("补零位数", it.numCount);
// 预览:用第一个子文件夹示例
if (folderHasImages.Count > 0)
{
string folderName = "";
foreach (var item in folderHasImages.Keys)
{
folderName=item;
}
int nc = it.overrideNumCount ? it.numCount : defaultNumCount;
string sampleIndex = it.startFrame.ToString("D" + nc);
string samplePng = $"{fixedSpritePrefix}{sampleIndex}.png";
string sampleOut = $"{Path.GetFileName(folderName)}_{it.animName}.anim";
EditorGUILayout.HelpBox(
$"示例 PNG 路径:\n{rootFolderPath}/{folderName}/{samplePng}\n" +
$"输出动画文件名:\n{sampleOut}\n" +
$"输出动画完整路径:\n{saveFolderPath}/{folderName}/{sampleOut}",
MessageType.None
);
}
EditorGUILayout.EndVertical();
}
if (GUILayout.Button("添加新动画条目"))
{
items.Add(new AnimItem()
{
animName = "NewAnim",
startFrame = 0,
endFrame = 10,
frameRate = 12f,
overrideNumCount = false,
numCount = defaultNumCount
});
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUILayout.EndScrollView();
GUILayout.Space(8);
if (GUILayout.Button("批量创建 AnimationClips", GUILayout.Height(30)))
{
SavePrefs();
SaveAnimItems();
CreateAnimationClips();
}
}
// 定义模式枚举
private enum DisplayMode { Grid, Tree }
private DisplayMode displayMode = DisplayMode.Grid;
// 状态字典(按完整路径存)
private Dictionary<string, bool> folderToggles = new Dictionary<string, bool>(); //目录的toggle值
private Dictionary<string, bool> folderHasImages = new Dictionary<string, bool>(); //目录是否带图片
private Dictionary<string, string> folderAsSubName = new Dictionary<string, string>(); //末端目录名称
#region 树形
private Dictionary<string, bool> folderFoldouts = new Dictionary<string, bool>(); //文件夹展开状态
// 递归刷新:构建状态字典(包含根)
private void RefreshFolderData()
{
folderFoldouts.Clear();
folderAsSubName.Clear();
folderToggles.Clear();
folderHasImages.Clear();
if (!Directory.Exists(rootFolderPath))
{
EditorUtility.DisplayDialog("错误", "根目录不存在!", "确定");
return;
}
// 加入根目录本身
EnsureNode(rootFolderPath);
// 递归加入所有子目录
CollectNodesRecursive(rootFolderPath);
Repaint();
}
#endregion
// 平铺模式(横向自动换行)
private void DrawGridMode()
{
if (folderHasImages.Count > 0)
{
float viewWidth = EditorGUIUtility.currentViewWidth - 20;
float itemWidth = 140f;
float spacing = 10f;
float currentRowWidth = 0f;
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
foreach (var folder in folderHasImages.Keys) //随便找一个列表的键来便利所有的目录
{
if (currentRowWidth + (itemWidth + spacing) > viewWidth)
{
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
currentRowWidth = 0f;
}
string folderPath = folder;
//folderToggles[folderPath] = EditorGUILayout.ToggleLeft(subFolderNames[i], folderToggles[folderPath]);
EditorGUILayout.BeginVertical("box", GUILayout.Width(itemWidth));
folderToggles[folderPath] = EditorGUILayout.ToggleLeft(Path.GetFileName(folderPath), folderToggles[folderPath]);
//subFolderToggles[i] = EditorGUILayout.ToggleLeft(subFolderNames[i], subFolderToggles[i]);
string status = folderHasImages[folderPath] ? " 有图片" : " 无图片";
EditorGUILayout.LabelField(status, EditorStyles.wordWrappedLabel);
EditorGUILayout.EndVertical();
currentRowWidth += itemWidth + spacing;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
}
// 将一个目录加入字典(若已存在则保留原值)
private void EnsureNode(string path)
{
string norm = path.Replace('\\', '/');
if (!folderFoldouts.ContainsKey(norm))
folderFoldouts[norm] = true; // 默认展开
if (!folderHasImages.ContainsKey(norm))
{
// 当前目录是否有 .png
bool hasPng = Directory.GetFiles(norm, "*.png").Length > 0;
folderHasImages[norm] = hasPng;
}
string fileName = Path.GetFileName(norm);
if (!folderAsSubName.ContainsKey(fileName))
folderAsSubName[norm] = fileName; // 有图默认勾选,无图默认不勾选
if (!folderToggles.ContainsKey(norm))
folderToggles[norm] = folderHasImages[norm]; // 有图默认勾选,无图默认不勾选
}
private void CollectNodesRecursive(string current)
{
var dirs = Directory.GetDirectories(current);
foreach (var d in dirs)
{
string norm = d.Replace('\\', '/');
EnsureNode(norm);
CollectNodesRecursive(norm);
}
}
// 树形绘制(从根开始)
private void DrawTreeMode()
{
if (!Directory.Exists(rootFolderPath))
{
EditorGUILayout.HelpBox("根目录不存在", MessageType.Error);
return;
}
GUILayout.Label("选择要生成的子文件夹:", EditorStyles.boldLabel);
DrawFolderTreeNode(rootFolderPath, 0);
}
// 单节点绘制(折叠箭头 + 名字 + Toggle + 状态),递归子节点
private void DrawFolderTreeNode(string path, int depth)
{
string norm = path.Replace('\\', '/');
string name = Path.GetFileName(norm);
if (string.IsNullOrEmpty(name)) name = norm; // 根可能没有文件名
// 保证节点存在
EnsureNode(norm);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(depth * 18f); // 缩进
// 折叠箭头 + 名字(true 参数启用丰富样式)
folderFoldouts[norm] = EditorGUILayout.Foldout(folderFoldouts[norm], name, true);
// Toggle(同一行显示,宽度固定避免布局抖动)
folderToggles[norm] = EditorGUILayout.Toggle(folderToggles[norm], GUILayout.Width(20));
// 状态文本(自动换行)
string status = folderHasImages[norm] ? " 有图片" : " 无图片";
EditorGUILayout.LabelField(status, EditorStyles.wordWrappedLabel);
EditorGUILayout.EndHorizontal();
// 展开时绘制子目录
if (folderFoldouts[norm])
{
var dirs = Directory.GetDirectories(norm);
foreach (var d in dirs)
{
DrawFolderTreeNode(d, depth + 1);
}
}
}
private void OnDisable()
{
SavePrefs();
SaveAnimItems();
}
private const string PREF_ITEMS = "AnimCreator_AnimItems";
// 保存动画条目列表
private void SaveAnimItems()
{
string json = JsonUtility.ToJson(new AnimItemListWrapper { items = this.items });
EditorPrefs.SetString(PREF_ITEMS, json);
}
// 加载动画条目列表
private void LoadAnimItems()
{
if (EditorPrefs.HasKey(PREF_ITEMS))
{
string json = EditorPrefs.GetString(PREF_ITEMS);
var wrapper = JsonUtility.FromJson<AnimItemListWrapper>(json);
if (wrapper != null && wrapper.items != null)
this.items = wrapper.items;
}
}
// 包装类用于序列化 List<AnimItem>
[System.Serializable]
private class AnimItemListWrapper
{
public List<AnimItem> items;
}
private void CreateAnimationClips()
{
if (folderHasImages.Count == 0)
{
EditorUtility.DisplayDialog("错误", "没有发现子文件夹!", "确定");
return;
}
// 1. 先筛选出所有选中的目录(避免循环中重复判断)
var selectedFolders = folderToggles.Where(kv => kv.Value).Select(kv => kv.Key).ToList();
if (selectedFolders.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先选中要处理的文件夹!", "确定");
return;
}
// 2. 计算总任务量(选中目录数 × 每个目录的动画数),让进度更平滑
int totalTasks = selectedFolders.Count * items.Count;
int completedTasks = 0;
int created = 0;
try
{
foreach (var folderPath in selectedFolders)
{
string relativePath = folderPath.Substring(rootFolderPath.Length + 1);
string outFolder = Path.Combine(saveFolderPath, relativePath).Replace('\\', '/');
// 创建输出目录(优化:统一刷新,避免多次调用 AssetDatabase.Refresh())
if (!Directory.Exists(outFolder))
{
Directory.CreateDirectory(outFolder);
}
foreach (var it in items)
{
// 3. 支持取消操作(用户点击进度条的“取消”按钮)
if (EditorUtility.DisplayCancelableProgressBar(
"批量创建动画",
$"正在处理:{folderPath}\n动画:{it.animName}\n进度:{completedTasks + 1}/{totalTasks}",
(float)completedTasks / totalTasks))
{
EditorUtility.DisplayDialog("提示", "操作已取消!", "确定");
return; // 退出方法,不再继续处理
}
// 跳过无效帧范围的动画
if (it.endFrame < it.startFrame)
{
Debug.LogError($"动画 {it.animName} 的结束帧必须 >= 开始帧,跳过。");
completedTasks++; // 即使跳过,也计入已完成任务量(避免进度卡住)
continue;
}
int numCount = it.overrideNumCount ? it.numCount : defaultNumCount;
var clip = new AnimationClip { frameRate = Mathf.Max(1f, it.frameRate) };
var binding = new EditorCurveBinding
{
type = typeof(SpriteRenderer),
path = "",
propertyName = "m_Sprite"
};
int len = it.endFrame - it.startFrame + 1;
var keyframes = new ObjectReferenceKeyframe[len];
int validCount = 0;
for (int i = 0; i < len; i++)
{
int frameIndex = it.startFrame + i;
string indexStr = frameIndex.ToString("D" + numCount);
string pngName = $"{fixedSpritePrefix}{indexStr}.png";
string spritePath = $"{folderPath}/{pngName}";
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spritePath);
if (sprite == null)
{
Debug.LogWarning($"未找到图片: {spritePath}");
continue;
}
keyframes[i] = new ObjectReferenceKeyframe
{
time = i / clip.frameRate,
value = sprite
};
validCount++;
}
if (validCount == 0)
{
Debug.LogError($"动画 {it.animName} 在文件夹 {folderPath} 未找到任何有效序列帧,跳过。");
completedTasks++;
continue;
}
AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes);
// 优化输出文件名(避免路径过长,用相对路径简化)
string folderName = Path.GetFileName(folderPath);
string outName = $"{folderName}_{it.animName}.anim";
string outPath = Path.Combine(outFolder, outName).Replace('\\', '/');
// 确保路径以 Assets/ 开头(Unity 资源路径规范)
if (!outPath.StartsWith("Assets/"))
{
outPath = outPath.Replace(Application.dataPath, "Assets");
}
AssetDatabase.CreateAsset(clip, outPath);
created++;
completedTasks++; // 完成一个动画,更新任务计数
Debug.Log($"已创建动画片段:{outPath}");
}
}
// 统一刷新资源数据库(优化性能,避免多次刷新)
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("完成", $"成功创建 {created} 个动画片段!", "确定");
}
catch (System.Exception e)
{
// 异常处理:提示错误并关闭进度条
EditorUtility.DisplayDialog("错误", $"创建动画失败:{e.Message}\n{e.StackTrace}", "确定");
}
finally
{
// 4. 必须关闭进度条(无论成功/失败/取消)
EditorUtility.ClearProgressBar();
}
}
// 保存设置到 EditorPrefs
private void SavePrefs()
{
EditorPrefs.SetString(PREF_ROOT, rootFolderPath);
EditorPrefs.SetString(PREF_SAVE, saveFolderPath);
EditorPrefs.SetString(PREF_PREFIX, fixedSpritePrefix);
EditorPrefs.SetInt(PREF_NUMCOUNT, defaultNumCount);
}
// 加载设置
private void LoadPrefs()
{
if (EditorPrefs.HasKey(PREF_ROOT)) rootFolderPath = EditorPrefs.GetString(PREF_ROOT);
if (EditorPrefs.HasKey(PREF_SAVE)) saveFolderPath = EditorPrefs.GetString(PREF_SAVE);
if (EditorPrefs.HasKey(PREF_PREFIX)) fixedSpritePrefix = EditorPrefs.GetString(PREF_PREFIX);
if (EditorPrefs.HasKey(PREF_NUMCOUNT)) defaultNumCount = EditorPrefs.GetInt(PREF_NUMCOUNT);
}
}