using UnityEditor; using UnityEngine; using System; using System.IO; using System.Linq; using System.Collections.Generic; public class DependencyOrganizerWindow : EditorWindow { private List draggedAssets = new List(); private Vector2 prefabScroll; private Vector2 depScroll; private Dictionary> dependenciesByType = new Dictionary>(); private Dictionary typeToggle = new Dictionary(); private Dictionary typeFoldout = new Dictionary(); private Dictionary filterTypeToggle = new Dictionary(); // 贴图路径 -> HashSet<材质路径> private Dictionary> textureUsedByMaterials = new Dictionary>(); private string targetRootFolder = string.Empty; private string lastOrganizeAbsPath = string.Empty; private SortedDictionary undoManifests = new SortedDictionary(); private const string UndoManifestDir = "Assets/Editor/DependencyOrganizerUndo"; private const string UndoManifestPrefix = "DependencyOrganizerWindow_undo_"; private Dictionary matsFoldout = new Dictionary(); [MenuItem("Tools/特效依赖整理工具")] public static void ShowWindow() { var w = GetWindow("特效依赖整理工具-by很喜欢乱来"); w.minSize = new Vector2(700, 700); } private void OnEnable() { LoadUndoManifests(); InitFilterTypeToggle(); } private void InitFilterTypeToggle() { string[] filterTypes = new string[] { "Textures", "Materials", "Models", "Shaders", "Scripts", "Prefabs", "Animations", "Animators", "Audio", "Physics", "Others" }; foreach (var t in filterTypes) { if (!filterTypeToggle.ContainsKey(t)) filterTypeToggle[t] = true; } } private void OnGUI() { GUILayout.Label("依赖整理工具-by很喜欢乱来", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "1. 拖入Prefab/Scene/ScriptableObject等资源。\n" + "2. 点击“收集依赖资源”,按类型和文件复选。\n" + "3. 支持折叠、全选/反选。\n" + "4. 选择目标根目录(支持拖入文件夹)。\n" + "5. 点击“整理”移动资源,自动避免冲突重命名。\n" + "6. 支持多版本撤销。\n" + "7. 支持导出UnityPackage。\n" + "8. 移动时显示进度条。\n" + "9. 显示资源大小预估。\n" + "10. 资源名点击定位,右键复制资源名。\n" + "11. 贴图资源显示被哪些材质引用,材质路径列表可点击定位。\n", MessageType.Info); EditorGUILayout.Space(); DrawFilterTypeToggle(); EditorGUILayout.Space(); DrawDragArea(); DrawPrefabList(); EditorGUILayout.Space(); if (GUILayout.Button("收集依赖资源", GUILayout.Height(30))) CollectDependencies(); EditorGUILayout.Space(); DrawDependenciesList(); EditorGUILayout.Space(); DrawSizeEstimate(); EditorGUILayout.Space(); DrawTargetFolderSelector(); EditorGUILayout.Space(); DrawActionButtons(); EditorGUILayout.Space(); DrawUndoHistory(); } private void DrawFilterTypeToggle() { EditorGUILayout.BeginHorizontal(); foreach (var key in new List(filterTypeToggle.Keys)) { bool val = filterTypeToggle[key]; bool newVal = GUILayout.Toggle(val, key, "Button", GUILayout.Height(22)); if (newVal != val) { filterTypeToggle[key] = newVal; } } EditorGUILayout.EndHorizontal(); } private void DrawDragArea() { Rect dropArea = GUILayoutUtility.GetRect(0, 60, GUILayout.ExpandWidth(true)); GUI.Box(dropArea, "拖入Prefab/Scene/ScriptableObject/Hierarchy中的GameObject"); var evt = Event.current; if ((evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform) && dropArea.Contains(evt.mousePosition)) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (evt.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); foreach (var obj in DragAndDrop.objectReferences) { string path = AssetDatabase.GetAssetPath(obj); if (obj is GameObject go) { bool isPrefab = !string.IsNullOrEmpty(path) && path.StartsWith("Assets"); if (isPrefab) { if (!draggedAssets.Contains(obj)) draggedAssets.Add(obj); } else { GameObject prefabRoot = PrefabUtility.GetCorrespondingObjectFromSource(go); if (prefabRoot != null && !draggedAssets.Contains(prefabRoot)) draggedAssets.Add(prefabRoot); else if (!draggedAssets.Contains(obj)) draggedAssets.Add(obj); } } else if (IsSupportedAsset(obj, path)) { if (!draggedAssets.Contains(obj)) draggedAssets.Add(obj); } } } evt.Use(); } } private bool IsSupportedAsset(UnityEngine.Object obj, string path) { if (obj == null || string.IsNullOrEmpty(path)) return false; return (obj is GameObject && path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) || path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase) || obj is ScriptableObject; } private void DrawPrefabList() { EditorGUILayout.LabelField("已拖入资源:"); if (draggedAssets.Count > 0) { if (GUILayout.Button("清空列表", GUILayout.Width(80))) { draggedAssets.Clear(); dependenciesByType.Clear(); typeToggle.Clear(); typeFoldout.Clear(); textureUsedByMaterials.Clear(); matsFoldout.Clear(); } } prefabScroll = EditorGUILayout.BeginScrollView(prefabScroll, GUILayout.Height(100)); for (int i = draggedAssets.Count - 1; i >= 0; i--) { EditorGUILayout.BeginHorizontal(); draggedAssets[i] = EditorGUILayout.ObjectField(draggedAssets[i], typeof(UnityEngine.Object), false); if (GUILayout.Button("移除", GUILayout.Width(60))) draggedAssets.RemoveAt(i); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); } private void CollectDependencies() { dependenciesByType.Clear(); typeToggle.Clear(); typeFoldout.Clear(); textureUsedByMaterials.Clear(); matsFoldout.Clear(); if (draggedAssets.Count == 0) { EditorUtility.DisplayDialog("提示", "请先拖入至少一个资源。", "确定"); return; } HashSet allDeps = new HashSet(); foreach (var obj in draggedAssets) { string origPath = AssetDatabase.GetAssetPath(obj); var deps = AssetDatabase.GetDependencies(origPath, true); foreach (var dep in deps) { if (!dep.StartsWith("Assets/") || dep.StartsWith("Assets/Editor/") || dep.Contains("Packages/")) continue; allDeps.Add(dep); } } // 构建贴图->材质引用映射 foreach (var dep in allDeps) { Type type = AssetDatabase.GetMainAssetTypeAtPath(dep); if (type == typeof(Material)) { Material mat = AssetDatabase.LoadAssetAtPath(dep); if (mat == null) continue; Shader shader = mat.shader; int propertyCount = ShaderUtil.GetPropertyCount(shader); for (int i = 0; i < propertyCount; i++) { if (ShaderUtil.GetPropertyType(shader, i) == ShaderUtil.ShaderPropertyType.TexEnv) { string propName = ShaderUtil.GetPropertyName(shader, i); Texture tex = mat.GetTexture(propName); if (tex != null) { string texPath = AssetDatabase.GetAssetPath(tex); if (!string.IsNullOrEmpty(texPath)) { if (!textureUsedByMaterials.TryGetValue(texPath, out var mats)) { mats = new HashSet(); textureUsedByMaterials[texPath] = mats; } mats.Add(dep); } } } } } } foreach (var dep in allDeps) { Type type = AssetDatabase.GetMainAssetTypeAtPath(dep); string folder = GetTargetFolder(type, dep); if (!dependenciesByType.ContainsKey(folder)) dependenciesByType[folder] = new Dictionary(); if (!dependenciesByType[folder].ContainsKey(dep)) dependenciesByType[folder][dep] = true; if (!typeToggle.ContainsKey(folder)) typeToggle[folder] = true; if (!typeFoldout.ContainsKey(folder)) typeFoldout[folder] = false; } } private void DrawDependenciesList() { if (dependenciesByType.Count == 0) return; float availableHeight = position.height - 550f; if (availableHeight < 150f) availableHeight = 150f; depScroll = EditorGUILayout.BeginScrollView(depScroll, GUILayout.Height(availableHeight)); foreach (var kvp in dependenciesByType) { string type = kvp.Key; var filesDict = kvp.Value; if (!filterTypeToggle.TryGetValue(type, out bool showType) || !showType) continue; if (!typeFoldout.ContainsKey(type)) typeFoldout[type] = false; if (!typeToggle.ContainsKey(type)) typeToggle[type] = true; EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); typeFoldout[type] = EditorGUILayout.Foldout(typeFoldout[type], $"{type} ({filesDict.Count})", true); if (GUILayout.Button("全选", GUILayout.Width(40))) { typeToggle[type] = true; foreach (var k in filesDict.Keys.ToList()) filesDict[k] = true; } if (GUILayout.Button("反选", GUILayout.Width(40))) { bool allSelected = filesDict.Values.All(v => v); foreach (var k in filesDict.Keys.ToList()) filesDict[k] = !allSelected; typeToggle[type] = filesDict.Values.Any(v => v); } EditorGUILayout.EndHorizontal(); bool newTypeToggle = EditorGUILayout.ToggleLeft("选择该类型全部文件", typeToggle[type]); if (newTypeToggle != typeToggle[type]) { typeToggle[type] = newTypeToggle; foreach (var k in filesDict.Keys.ToList()) filesDict[k] = newTypeToggle; } if (typeFoldout[type]) { EditorGUI.indentLevel++; foreach (var fileKey in filesDict.Keys.ToList()) { bool selected = filesDict[fileKey]; EditorGUILayout.BeginHorizontal(GUILayout.Height(22)); GUILayout.Space(6 * (EditorGUI.indentLevel - 1)); Rect toggleRect = GUILayoutUtility.GetRect(24, 22, GUILayout.Width(24)); bool newSelected = EditorGUI.Toggle(toggleRect, selected); if (newSelected != selected) filesDict[fileKey] = newSelected; if (!newSelected) GUI.enabled = false; string fileName = Path.GetFileName(fileKey); GUIContent fileNameContent = new GUIContent(fileName); Vector2 fileNameSize = EditorStyles.boldLabel.CalcSize(fileNameContent); float fileNameWidth = Mathf.Min(fileNameSize.x, 200); GUILayout.Space(6); Rect fileNameRect = GUILayoutUtility.GetRect(fileNameWidth, 22, GUILayout.Width(fileNameWidth)); GUI.Label(fileNameRect, fileName, EditorStyles.boldLabel); if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && fileNameRect.Contains(Event.current.mousePosition)) { var obj = AssetDatabase.LoadAssetAtPath(fileKey); if (obj != null) { EditorGUIUtility.PingObject(obj); Event.current.Use(); } } else if (Event.current.type == EventType.ContextClick && fileNameRect.Contains(Event.current.mousePosition)) { Event.current.Use(); GenericMenu menu = new GenericMenu(); menu.AddItem(new GUIContent("复制资源名称"), false, () => { GUIUtility.systemCopyBuffer = fileName; }); menu.ShowAsContext(); } GUILayout.Label(fileKey, EditorStyles.miniLabel, GUILayout.ExpandWidth(true)); if (GUILayout.Button("定位", GUILayout.Width(60))) { var obj = AssetDatabase.LoadAssetAtPath(fileKey); if (obj != null) EditorGUIUtility.PingObject(obj); } if (!newSelected) GUI.enabled = true; EditorGUILayout.EndHorizontal(); if (textureUsedByMaterials.TryGetValue(fileKey, out var mats) && mats.Count > 0) { if (!matsFoldout.ContainsKey(fileKey)) matsFoldout[fileKey] = false; Rect foldoutRect = GUILayoutUtility.GetRect(130, EditorGUIUtility.singleLineHeight); matsFoldout[fileKey] = EditorGUI.Foldout(foldoutRect, matsFoldout[fileKey], $"引用于 {mats.Count} 个材质", true); if (matsFoldout[fileKey]) { EditorGUI.indentLevel++; foreach (var matPath in mats) { Rect matRect = GUILayoutUtility.GetRect(20, 18); GUILayout.Space(6 * (EditorGUI.indentLevel - 1)); if (GUI.Button(matRect, matPath, EditorStyles.linkLabel)) { var matObj = AssetDatabase.LoadAssetAtPath(matPath); if (matObj != null) EditorGUIUtility.PingObject(matObj); } } EditorGUI.indentLevel--; } } } EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } EditorGUILayout.EndScrollView(); } private void DrawSizeEstimate() { long totalBytes = 0; foreach (var kvp in dependenciesByType) { foreach (var path in kvp.Value.Keys) { string absPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)); if (File.Exists(absPath)) { totalBytes += new FileInfo(absPath).Length; } } } EditorGUILayout.LabelField($"预估待移动资源大小: {totalBytes / 1024f / 1024f:F2} MB"); } private void DrawTargetFolderSelector() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("目标根目录:", GUILayout.Width(80)); GUIStyle boxStyle = GUI.skin.box; Rect dragRect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth - 100, EditorGUIUtility.singleLineHeight + 6); GUI.Box(dragRect, "", boxStyle); GUI.Label(dragRect, string.IsNullOrEmpty(targetRootFolder) ? "<拖入目标根目录文件夹>" : targetRootFolder, EditorStyles.label); Event evt = Event.current; if ((evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform) && dragRect.Contains(evt.mousePosition)) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (evt.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); if (DragAndDrop.objectReferences.Length > 0) { UnityEngine.Object obj = DragAndDrop.objectReferences[0]; string path = AssetDatabase.GetAssetPath(obj); if (AssetDatabase.IsValidFolder(path)) { targetRootFolder = path; evt.Use(); EditorGUIUtility.ExitGUI(); return; } } if (DragAndDrop.paths != null && DragAndDrop.paths.Length > 0) { string firstPath = DragAndDrop.paths[0]; if (Directory.Exists(firstPath)) { string absDataPath = Application.dataPath.Replace("/", "\\"); string absFirstPath = firstPath.Replace("/", "\\"); if (absFirstPath.StartsWith(absDataPath)) { string relativePath = "Assets" + absFirstPath.Substring(absDataPath.Length); targetRootFolder = relativePath.Replace("\\", "/"); evt.Use(); EditorGUIUtility.ExitGUI(); return; } } } evt.Use(); } else { evt.Use(); } } EditorGUILayout.EndHorizontal(); } private void DrawActionButtons() { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("整理", GUILayout.Height(30))) MoveSelectedDependencies(); if (GUILayout.Button("导出 UnityPackage", GUILayout.Height(30))) ExportPackage(); if (GUILayout.Button("撤销", GUILayout.Height(30))) ShowUndoMenu(); if (!string.IsNullOrEmpty(lastOrganizeAbsPath) && GUILayout.Button("打开生成路径", GUILayout.Height(30))) EditorUtility.RevealInFinder(lastOrganizeAbsPath); EditorGUILayout.EndHorizontal(); } private void DrawUndoHistory() { if (undoManifests.Count == 0) { EditorGUILayout.LabelField("暂无撤销历史"); return; } EditorGUILayout.LabelField("撤销历史记录:"); foreach (var kvp in undoManifests.Reverse()) { string label = DateTimeOffset.FromUnixTimeMilliseconds(kvp.Key).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); EditorGUILayout.LabelField(label); } } private void ShowUndoMenu() { if (undoManifests.Count == 0) { EditorUtility.DisplayDialog("提示", "没有可用的撤销记录。", "确定"); return; } GenericMenu menu = new GenericMenu(); foreach (var kvp in undoManifests.Reverse()) { string label = DateTimeOffset.FromUnixTimeMilliseconds(kvp.Key).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); string path = kvp.Value; menu.AddItem(new GUIContent(label), false, () => { UndoMoveAssets(path); }); } menu.ShowAsContext(); } private void UndoMoveAssets(string manifestPath) { if (!File.Exists(manifestPath)) { EditorUtility.DisplayDialog("错误", "撤销文件不存在。", "确定"); RemoveManifestByPath(manifestPath); return; } string json = File.ReadAllText(manifestPath); var manifest = JsonUtility.FromJson(json); if (manifest == null || manifest.entries == null) { EditorUtility.DisplayDialog("错误", "撤销文件格式错误。", "确定"); return; } AssetDatabase.StartAssetEditing(); try { foreach (var e in manifest.entries.Reverse()) { if (AssetDatabase.MoveAsset(e.destPath, e.originalPath).Length == 0) Debug.Log($"已恢复 {e.destPath}"); else Debug.LogWarning($"恢复失败或路径冲突: {e.destPath}"); } } finally { AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); } File.Delete(manifestPath); RemoveManifestByPath(manifestPath); EditorUtility.DisplayDialog("撤销完成", "资源已恢复。", "确定"); } private void RemoveManifestByPath(string manifestPath) { var keysToRemove = undoManifests.Where(kvp => kvp.Value == manifestPath).Select(kvp => kvp.Key).ToList(); foreach (var key in keysToRemove) undoManifests.Remove(key); } private void LoadUndoManifests() { undoManifests.Clear(); if (!AssetDatabase.IsValidFolder(UndoManifestDir)) return; string fullDir = Path.Combine(Application.dataPath, "Editor/DependencyOrganizerUndo"); if (!Directory.Exists(fullDir)) return; foreach (var file in Directory.GetFiles(fullDir, UndoManifestPrefix + "*.json")) { try { string fileName = Path.GetFileNameWithoutExtension(file); string timestampStr = fileName.Substring(UndoManifestPrefix.Length); if (long.TryParse(timestampStr, out long ts)) undoManifests[ts] = file; } catch { } } } private void MoveSelectedDependencies() { if (draggedAssets.Count == 0) { EditorUtility.DisplayDialog("提示", "请先拖入至少一个资源。", "确定"); return; } if (dependenciesByType.Count == 0) { EditorUtility.DisplayDialog("提示", "请先收集依赖资源。", "确定"); return; } if (!AssetDatabase.IsValidFolder(UndoManifestDir)) AssetDatabase.CreateFolder("Assets/Editor", "DependencyOrganizerUndo"); long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); string manifestFile = $"{UndoManifestDir}/{UndoManifestPrefix}{timestamp}.json"; List moves = new List(); List<(string originalPath, string destPath)> movesToDo = new List<(string, string)>(); AssetDatabase.StartAssetEditing(); try { foreach (var obj in draggedAssets) { string origPath = AssetDatabase.GetAssetPath(obj); string root; if (!string.IsNullOrEmpty(targetRootFolder) && AssetDatabase.IsValidFolder(targetRootFolder)) { root = targetRootFolder; } else { string exportBase = "Assets/export"; if (!AssetDatabase.IsValidFolder(exportBase)) AssetDatabase.CreateFolder("Assets", "export"); string name = Path.GetFileNameWithoutExtension(origPath); root = $"{exportBase}/{name}"; if (!AssetDatabase.IsValidFolder(root)) AssetDatabase.CreateFolder(exportBase, name); } lastOrganizeAbsPath = Path.GetFullPath(root); var deps = AssetDatabase.GetDependencies(origPath, true); foreach (var dep in deps) { if (!dep.StartsWith("Assets/") || dep.StartsWith("Assets/Editor/") || dep.Contains("Packages/")) continue; if (movesToDo.Any(m => m.destPath == dep) || dep.StartsWith(root + "/")) continue; Type type = AssetDatabase.GetMainAssetTypeAtPath(dep); string folder = GetTargetFolder(type, dep); if (!dependenciesByType.TryGetValue(folder, out var filesDict)) continue; if (!filesDict.TryGetValue(dep, out bool moveThis) || !moveThis) continue; string dstDir = $"{root}/{folder}"; if (!AssetDatabase.IsValidFolder(dstDir)) AssetDatabase.CreateFolder(root, folder); string dst = GetUniqueAssetPath($"{dstDir}/{Path.GetFileName(dep)}"); movesToDo.Add((dep, dst)); } } int total = movesToDo.Count; for (int i = 0; i < total; i++) { var (src, dst) = movesToDo[i]; EditorUtility.DisplayProgressBar("移动资源", $"正在移动 {Path.GetFileName(src)} ({i + 1}/{total})", (float)i / total); string err = AssetDatabase.MoveAsset(src, dst); if (string.IsNullOrEmpty(err)) moves.Add(new MoveEntry(src, dst)); else Debug.LogWarning($"移动失败: {src} -> {dst},错误:{err}"); } } finally { EditorUtility.ClearProgressBar(); AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); } File.WriteAllText(manifestFile, JsonUtility.ToJson(new MoveManifest { entries = moves.ToArray() }, true)); AssetDatabase.ImportAsset(manifestFile); LoadUndoManifests(); EditorUtility.DisplayDialog("整理完成", $"共移动 {moves.Count} 个资源,存放于 {(!string.IsNullOrEmpty(targetRootFolder) ? targetRootFolder : "Assets/export")}", "确定"); } private string GetTargetFolder(Type type, string path) { string ext = Path.GetExtension(path).ToLowerInvariant(); if (new[] { ".cs" }.Contains(ext)) return "Scripts"; if (new[] { ".fbx", ".obj", ".blend" }.Contains(ext)) return "Models"; if (new[] { ".anim" }.Contains(ext) || type == typeof(AnimationClip)) return "Animations"; if (new[] { ".controller" }.Contains(ext) || type == typeof(UnityEditor.Animations.AnimatorController)) return "Animators"; if (new[] { ".shader", ".shadergraph" }.Contains(ext) || type == typeof(Shader)) return "Shaders"; if (new[] { ".png", ".jpg", ".jpeg", ".tga", ".psd", ".exr" }.Contains(ext) || type == typeof(Texture2D)) return "Textures"; if (new[] { ".wav", ".mp3", ".ogg" }.Contains(ext) || type == typeof(AudioClip)) return "Audio"; if (ext == ".mat" || type == typeof(Material)) return "Materials"; if (ext == ".physicmaterial" || type == typeof(PhysicMaterial)) return "Physics"; if (ext == ".prefab" || type == typeof(GameObject)) return "Prefabs"; return "Others"; } private string GetUniqueAssetPath(string desiredPath) { string path = desiredPath; string dir = Path.GetDirectoryName(desiredPath); string filename = Path.GetFileNameWithoutExtension(desiredPath); string ext = Path.GetExtension(desiredPath); int count = 1; while (AssetPathExists(path)) { path = $"{dir}/{filename}_{count}{ext}"; count++; } return path; } private bool AssetPathExists(string path) { return AssetDatabase.LoadAssetAtPath(path) != null; } private void ExportPackage() { string exportRel = !string.IsNullOrEmpty(targetRootFolder) && AssetDatabase.IsValidFolder(targetRootFolder) ? targetRootFolder : "Assets/export"; if (!AssetDatabase.IsValidFolder(exportRel)) { EditorUtility.DisplayDialog("错误", $"导出目录无效:{exportRel}", "确定"); return; } var guids = AssetDatabase.FindAssets("", new[] { exportRel }); var paths = guids.Select(AssetDatabase.GUIDToAssetPath).ToArray(); string save = EditorUtility.SaveFilePanel("保存 UnityPackage", Application.dataPath, "Package.unitypackage", "unitypackage"); if (string.IsNullOrEmpty(save)) return; AssetDatabase.ExportPackage(paths, save, ExportPackageOptions.Default); EditorUtility.DisplayDialog("导出成功", $"已导出 {paths.Length} 个资源到 {save}", "确定"); } [Serializable] private class MoveEntry { public string originalPath; public string destPath; public MoveEntry(string o, string d) { originalPath = o; destPath = d; } } [Serializable] private class MoveManifest { public MoveEntry[] entries; } }