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 subFolders = new List(); //private List subFolderNames = new List(); //private List subFolderToggles = new List(); // 动画条目 [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 items = new List() { 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("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 folderToggles = new Dictionary(); //目录的toggle值 private Dictionary folderHasImages = new Dictionary(); //目录是否带图片 private Dictionary folderAsSubName = new Dictionary(); //末端目录名称 #region 树形 private Dictionary folderFoldouts = new Dictionary(); //文件夹展开状态 // 递归刷新:构建状态字典(包含根) 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(json); if (wrapper != null && wrapper.items != null) this.items = wrapper.items; } } // 包装类用于序列化 List [System.Serializable] private class AnimItemListWrapper { public List 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(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); } }