1
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de66b70d0c2dd9d4aa8cc22dfe238d4a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,529 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7783783f6a178d744b5598f36a97169b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,196 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEditorInternal;
|
||||
using System.Collections.Generic;
|
||||
using AnimatorController = UnityEditor.Animations.AnimatorController;
|
||||
|
||||
public class AnimatorLinkWindow : EditorWindow
|
||||
{
|
||||
private AnimatorController animatorController;
|
||||
private List<AnimationClip> clips = new List<AnimationClip>();
|
||||
private ReorderableList reorderableList;
|
||||
|
||||
// 排版参数
|
||||
private int statesPerColumn = 5;
|
||||
private float horizontalSpacing = 200f;
|
||||
private float verticalSpacing = 80f;
|
||||
|
||||
[MenuItem("Tools/Animator Link Window")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
GetWindow<AnimatorLinkWindow>("Animator Linker");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
reorderableList = new ReorderableList(clips, typeof(AnimationClip), true, true, true, true);
|
||||
|
||||
reorderableList.drawHeaderCallback = (Rect rect) =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Animation Clips (可拖拽排序)");
|
||||
};
|
||||
|
||||
reorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
|
||||
{
|
||||
clips[index] = (AnimationClip)EditorGUI.ObjectField(
|
||||
new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight),
|
||||
clips[index], typeof(AnimationClip), false);
|
||||
};
|
||||
|
||||
reorderableList.onAddCallback = (ReorderableList list) =>
|
||||
{
|
||||
clips.Add(null);
|
||||
};
|
||||
|
||||
reorderableList.onRemoveCallback = (ReorderableList list) =>
|
||||
{
|
||||
clips.RemoveAt(list.index);
|
||||
};
|
||||
}
|
||||
private Vector2 scrollPos;
|
||||
private void OnGUI()
|
||||
{
|
||||
|
||||
GUILayout.Label("Animator Controller", EditorStyles.boldLabel);
|
||||
animatorController = (AnimatorController)EditorGUILayout.ObjectField(
|
||||
"Controller", animatorController, typeof(AnimatorController), false);
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// 排版参数输入框
|
||||
statesPerColumn = EditorGUILayout.IntField("每列多少个状态", statesPerColumn);
|
||||
horizontalSpacing = EditorGUILayout.FloatField("同列水平间隔", horizontalSpacing);
|
||||
verticalSpacing = EditorGUILayout.FloatField("列与列垂直间隔", verticalSpacing);
|
||||
// 开始滚动区域
|
||||
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||||
GUILayout.Space(10);
|
||||
|
||||
reorderableList.DoLayoutList();
|
||||
EditorGUILayout.EndScrollView(); // 结束滚动区域
|
||||
|
||||
Rect dropArea = GUILayoutUtility.GetRect(0.0f, 50.0f, GUILayout.ExpandWidth(true));
|
||||
GUI.Box(dropArea, "拖拽 AnimationClip 到这里 (支持多选)");
|
||||
HandleDragAndDrop(dropArea);
|
||||
|
||||
GUILayout.Space(10);
|
||||
if (GUILayout.Button("生成状态并连线"))
|
||||
{
|
||||
GenerateStatesAndTransitions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void HandleDragAndDrop(Rect dropArea)
|
||||
{
|
||||
Event evt = Event.current;
|
||||
if (!dropArea.Contains(evt.mousePosition)) return;
|
||||
|
||||
switch (evt.type)
|
||||
{
|
||||
case EventType.DragUpdated:
|
||||
case EventType.DragPerform:
|
||||
if (DragAndDrop.objectReferences.Length > 0)
|
||||
{
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||||
|
||||
if (evt.type == EventType.DragPerform)
|
||||
{
|
||||
DragAndDrop.AcceptDrag();
|
||||
foreach (Object obj in DragAndDrop.objectReferences)
|
||||
{
|
||||
if (obj is AnimationClip clip)
|
||||
{
|
||||
clips.Add(clip);
|
||||
}
|
||||
}
|
||||
Debug.Log($"已添加 {DragAndDrop.objectReferences.Length} 个动画");
|
||||
}
|
||||
}
|
||||
Event.current.Use();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateStatesAndTransitions()
|
||||
{
|
||||
if (animatorController == null)
|
||||
{
|
||||
Debug.LogError("请先指定 AnimatorController");
|
||||
return;
|
||||
}
|
||||
if (clips.Count == 0)
|
||||
{
|
||||
Debug.LogError("请至少添加一个 AnimationClip");
|
||||
return;
|
||||
}
|
||||
|
||||
var rootStateMachine = animatorController.layers[0].stateMachine;
|
||||
|
||||
// 判断是否已有状态
|
||||
if (rootStateMachine.states.Length > 0)
|
||||
{
|
||||
int option = EditorUtility.DisplayDialogComplex(
|
||||
"已有状态",
|
||||
"当前 AnimatorController 已经有状态,是否清空后重新生成?",
|
||||
"清空并重新生成",
|
||||
"从尾部继续添加",
|
||||
"取消"
|
||||
);
|
||||
|
||||
if (option == 0)
|
||||
{
|
||||
// 清空
|
||||
rootStateMachine.states = new ChildAnimatorState[0];
|
||||
}
|
||||
else if (option == 2)
|
||||
{
|
||||
// 取消
|
||||
return;
|
||||
}
|
||||
// option == 1 表示从尾部继续添加,不清空
|
||||
}
|
||||
|
||||
AnimatorState prevState = null;
|
||||
if (rootStateMachine.states.Length > 0)
|
||||
{
|
||||
// 如果已有状态,从最后一个开始连线
|
||||
prevState = rootStateMachine.states[rootStateMachine.states.Length - 1].state;
|
||||
}
|
||||
|
||||
int existingCount = rootStateMachine.states.Length;
|
||||
|
||||
for (int i = 0; i < clips.Count; i++)
|
||||
{
|
||||
var clip = clips[i];
|
||||
if (clip == null) continue;
|
||||
|
||||
// ✅ 计算偏移量:已有状态数量 + 新的索引
|
||||
int index = existingCount + i;
|
||||
int col = index / statesPerColumn;
|
||||
int row = index % statesPerColumn;
|
||||
|
||||
var state = rootStateMachine.AddState(
|
||||
clip.name,
|
||||
new Vector3(col * horizontalSpacing, row * verticalSpacing, 0)
|
||||
);
|
||||
state.motion = clip;
|
||||
|
||||
if (prevState != null)
|
||||
{
|
||||
var transition = prevState.AddTransition(state);
|
||||
transition.hasExitTime = true;
|
||||
transition.exitTime = 1f;
|
||||
transition.duration = 0f;
|
||||
}
|
||||
|
||||
prevState = state;
|
||||
}
|
||||
|
||||
|
||||
Debug.Log("生成完成:已按顺序创建状态并排版连线");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e16de956fbdac1249b91b66baeece38b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Game.Hotfix.Editor.Animation",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"Game.Hotfix",
|
||||
"CameraShake",
|
||||
"spine-unity",
|
||||
"Coffee.UIParticle",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager",
|
||||
"Unity.TextMeshPro",
|
||||
"UnityEngine.UI",
|
||||
"Unity.InputSystem",
|
||||
"Unity.VisualScripting.Core"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 590ad1417b4e4fd449f888bc3f2127a3
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user