using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEngine; /// /// 资源依赖分析器 /// 用于查找某个 Prefab / 资源 关联的所有依赖资源,并标记其在 Addressables 中的归属(Group、Address、Labels), /// 供 AA 分包调查使用。 /// 菜单:Tools → 资源依赖分析器 /// public class AssetDependencyAnalyzer : EditorWindow { // ============================ 日志开关 ============================ // 启用: Player Settings → Scripting Define Symbols 添加 ENABLE_EDITOR_LOG // 关闭: 删除该符号。未定义时所有 Log/LogError 调用被编译器自动移除,零开销 [System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")] private static void Log(string msg) { Debug.Log(msg); } [System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")] private static void LogError(string msg) { Debug.LogError(msg); } #region 内部类 [Serializable] public class DependencyInfo { public string assetPath; public string guid; public Type type; public string typeName; public bool isInAddressables; public string aaAddress; public string aaGroup; public List aaLabels = new List(); public long fileSizeBytes; public string fileSizeReadable; } #endregion #region 字段 private Vector2 leftScroll; private Vector2 rightScroll; private Vector2 statsScroll; // 输入:单个目标 private UnityEngine.Object targetAsset; // 批量目标 private List batchTargets = new List(); private bool useBatchMode = false; // 收集结果 private List dependencies = new List(); private Dictionary dependencyMap = new Dictionary(); // 反向查找(谁引用了这个资源) private readonly Dictionary> reverseRefMap = new Dictionary>(); // 过滤 private enum FilterMode { All, InAddressables, NotInAddressables } private FilterMode filterMode = FilterMode.All; private string typeFilter = "全部"; private string groupFilter = "全部"; private string labelFilter = "全部"; private string pathKeyword = ""; // 展示 private bool showStats = true; private bool showFilter = true; private bool showBatchList = true; // 缓存 private AddressableAssetSettings aaSettings; private List allGroups = new List(); private List allLabels = new List(); private List allTypes = new List(); private bool includeScripts = false; private bool deepAnalyze = true; #endregion #region 菜单与初始化 [MenuItem("Tools/资源依赖分析器", false, 200)] public static void OpenWindow() { var window = GetWindow("资源依赖分析器"); window.minSize = new Vector2(1000, 600); window.Show(); } private void OnEnable() { RefreshAACache(); } private void RefreshAACache() { aaSettings = AddressableAssetSettingsDefaultObject.Settings; allGroups.Clear(); allLabels.Clear(); if (aaSettings != null) { foreach (var g in aaSettings.groups) { if (g != null) allGroups.Add(g.Name); } allLabels.AddRange(aaSettings.GetLabels()); } } #endregion #region 主GUI private void OnGUI() { DrawToolbar(); EditorGUILayout.Space(4); DrawTargetSelector(); if (showFilter) { EditorGUILayout.Space(4); DrawFilterBar(); } if (showStats && dependencies.Count > 0) { EditorGUILayout.Space(4); DrawStats(); } EditorGUILayout.Space(4); DrawDependencyList(); } #endregion #region 工具栏 private void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); if (GUILayout.Button("分析依赖", EditorStyles.toolbarButton, GUILayout.Width(80))) { AnalyzeAll(); } if (GUILayout.Button("反向查找引用", EditorStyles.toolbarButton, GUILayout.Width(100))) { ReverseLookupSelected(); } if (GUILayout.Button("导出报告", EditorStyles.toolbarButton, GUILayout.Width(80))) { ExportReport(); } if (GUILayout.Button("复制AA地址", EditorStyles.toolbarButton, GUILayout.Width(90))) { CopySelectedAddresses(); } if (GUILayout.Button("加入AA(选中)", EditorStyles.toolbarButton, GUILayout.Width(110))) { AddSelectedToAddressables(); } GUILayout.FlexibleSpace(); if (GUILayout.Button("刷新AA缓存", EditorStyles.toolbarButton, GUILayout.Width(90))) { RefreshAACache(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); showFilter = GUILayout.Toggle(showFilter, "过滤", EditorStyles.toolbarButton, GUILayout.Width(50)); showStats = GUILayout.Toggle(showStats, "统计", EditorStyles.toolbarButton, GUILayout.Width(50)); showBatchList = GUILayout.Toggle(showBatchList, "批量列表", EditorStyles.toolbarButton, GUILayout.Width(70)); useBatchMode = GUILayout.Toggle(useBatchMode, "批量模式", EditorStyles.toolbarButton, GUILayout.Width(70)); GUILayout.FlexibleSpace(); includeScripts = GUILayout.Toggle(includeScripts, "包含脚本", GUILayout.Width(80)); deepAnalyze = GUILayout.Toggle(deepAnalyze, "递归依赖", GUILayout.Width(80)); EditorGUILayout.EndHorizontal(); } #endregion #region 目标选择 private void DrawTargetSelector() { EditorGUILayout.BeginVertical("box"); if (!useBatchMode) { EditorGUILayout.LabelField("目标资源(Prefab / 贴图 / 音频 / 材质等,可拖拽)", EditorStyles.boldLabel); var newObj = EditorGUILayout.ObjectField("目标", targetAsset, typeof(UnityEngine.Object), false); if (newObj != targetAsset) { targetAsset = newObj; } EditorGUILayout.HelpBox( "操作说明:\n" + "1. 拖拽或选择目标资源(通常是 Prefab)。\n" + "2. 点击「分析依赖」递归收集所有依赖资源。\n" + "3. 通过过滤栏查看 AA 内 / 非 AA 资源,便于分包决策。\n" + "4. 选中非 AA 资源后点击「加入AA」可直接添加到指定 Group。\n" + "5. 「反向查找引用」会扫描所有 AA 条目,找出谁引用了当前选中资源。", MessageType.Info); } else { EditorGUILayout.LabelField("批量目标列表(可拖拽多个资源到下方区域)", EditorStyles.boldLabel); // 拖拽区域 Rect dropRect = GUILayoutUtility.GetRect(0, 80, GUILayout.ExpandWidth(true)); GUI.Box(dropRect, "拖拽资源到此处添加到批量列表"); HandleDragDrop(dropRect); // 列表显示 if (showBatchList && batchTargets.Count > 0) { leftScroll = EditorGUILayout.BeginScrollView(leftScroll, GUILayout.Height(120)); for (int i = 0; i < batchTargets.Count; i++) { EditorGUILayout.BeginHorizontal(); var obj = EditorGUILayout.ObjectField(batchTargets[i], typeof(UnityEngine.Object), false); if (obj != batchTargets[i]) batchTargets[i] = obj; if (GUILayout.Button("×", GUILayout.Width(20))) { batchTargets.RemoveAt(i); break; } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("清空列表", GUILayout.Width(80))) { batchTargets.Clear(); } EditorGUILayout.LabelField($"共 {batchTargets.Count} 个目标", EditorStyles.miniLabel); EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndVertical(); } private void HandleDragDrop(Rect dropRect) { var evt = Event.current; if (!dropRect.Contains(evt.mousePosition)) return; if (evt.type == EventType.DragUpdated) { DragAndDrop.visualMode = DragAndDropVisualMode.Generic; evt.Use(); } else if (evt.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); foreach (var path in DragAndDrop.paths) { var obj = AssetDatabase.LoadAssetAtPath(path); if (obj != null && !batchTargets.Contains(obj)) { batchTargets.Add(obj); } } evt.Use(); } } #endregion #region 过滤栏 private void DrawFilterBar() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); filterMode = (FilterMode)EditorGUILayout.EnumPopup("归属", filterMode, GUILayout.Width(180)); // 类型过滤 var typeOptions = new List { "全部" }; typeOptions.AddRange(allTypes); int typeIdx = Mathf.Max(0, typeOptions.IndexOf(typeFilter)); typeIdx = EditorGUILayout.Popup("类型", typeIdx, typeOptions.ToArray(), GUILayout.Width(180)); typeFilter = typeOptions[typeIdx]; // Group过滤 var groupOptions = new List { "全部", "(非AA)" }; groupOptions.AddRange(allGroups); int gIdx = Mathf.Max(0, groupOptions.IndexOf(groupFilter)); gIdx = EditorGUILayout.Popup("Group", gIdx, groupOptions.ToArray(), GUILayout.Width(180)); groupFilter = groupOptions[gIdx]; EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); var labelOptions = new List { "全部", "(无标签)" }; labelOptions.AddRange(allLabels); int lIdx = Mathf.Max(0, labelOptions.IndexOf(labelFilter)); lIdx = EditorGUILayout.Popup("Label", lIdx, labelOptions.ToArray(), GUILayout.Width(180)); labelFilter = labelOptions[lIdx]; pathKeyword = EditorGUILayout.TextField("路径关键字", pathKeyword, GUILayout.Width(220)); if (GUILayout.Button("清空过滤", GUILayout.Width(80))) { filterMode = FilterMode.All; typeFilter = "全部"; groupFilter = "全部"; labelFilter = "全部"; pathKeyword = ""; } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } #endregion #region 统计 private void DrawStats() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("统计", EditorStyles.boldLabel); int totalCount = dependencies.Count; int aaCount = dependencies.Count(d => d.isInAddressables); int nonAACount = totalCount - aaCount; long totalSize = dependencies.Sum(d => d.fileSizeBytes); EditorGUILayout.LabelField($"总依赖数: {totalCount} AA内: {aaCount} 非AA: {nonAACount} 总大小: {FormatSize(totalSize)}"); // 按类型统计 statsScroll = EditorGUILayout.BeginScrollView(statsScroll, GUILayout.Height(140)); var byType = dependencies.GroupBy(d => d.typeName).OrderByDescending(g => g.Count()); EditorGUILayout.LabelField("按类型统计:", EditorStyles.miniBoldLabel); foreach (var grp in byType) { long s = grp.Sum(d => d.fileSizeBytes); int aaInGroup = grp.Count(d => d.isInAddressables); EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 (AA内 {aaInGroup}) 大小 {FormatSize(s)}", EditorStyles.miniLabel); } // 按 Group 统计 EditorGUILayout.Space(4); EditorGUILayout.LabelField("按 AA Group 统计:", EditorStyles.miniBoldLabel); var byGroup = dependencies.Where(d => d.isInAddressables).GroupBy(d => d.aaGroup).OrderByDescending(g => g.Count()); foreach (var grp in byGroup) { long s = grp.Sum(d => d.fileSizeBytes); EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 大小 {FormatSize(s)}", EditorStyles.miniLabel); } // 非 AA 资源按目录统计 EditorGUILayout.Space(4); EditorGUILayout.LabelField("非AA资源按目录统计:", EditorStyles.miniBoldLabel); var nonAAByDir = dependencies .Where(d => !d.isInAddressables) .GroupBy(d => Path.GetDirectoryName(d.assetPath)?.Replace('\\', '/')) .OrderByDescending(g => g.Count()); foreach (var grp in nonAAByDir) { long s = grp.Sum(d => d.fileSizeBytes); EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 大小 {FormatSize(s)}", EditorStyles.miniLabel); } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } #endregion #region 依赖列表 private void DrawDependencyList() { if (dependencies.Count == 0) { EditorGUILayout.HelpBox("尚未分析。请选择目标资源后点击「分析依赖」。", MessageType.Info); return; } var filtered = ApplyFilter(dependencies); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"依赖列表 (过滤后: {filtered.Count} / {dependencies.Count})", EditorStyles.boldLabel); GUILayout.FlexibleSpace(); if (GUILayout.Button("全选非AA", GUILayout.Width(80))) { SelectAllNonAA(); } EditorGUILayout.EndHorizontal(); rightScroll = EditorGUILayout.BeginScrollView(rightScroll); // 表头 EditorGUILayout.BeginHorizontal("box"); EditorGUILayout.LabelField("", GUILayout.Width(20)); EditorGUILayout.LabelField("资源路径", GUILayout.Width(300)); EditorGUILayout.LabelField("类型", GUILayout.Width(80)); EditorGUILayout.LabelField("大小", GUILayout.Width(70)); EditorGUILayout.LabelField("AA归属", GUILayout.Width(120)); EditorGUILayout.LabelField("Address", GUILayout.Width(150)); EditorGUILayout.LabelField("Labels", GUILayout.Width(150)); EditorGUILayout.EndHorizontal(); foreach (var info in filtered) { DrawDependencyRow(info); } EditorGUILayout.EndScrollView(); } private List GetFilteredCache() { return ApplyFilter(dependencies); } private void SelectAllNonAA() { var filtered = GetFilteredCache(); var nonAA = filtered.Where(d => !d.isInAddressables).Select(d => d.assetPath).ToList(); var objs = new List(); foreach (var p in nonAA) { var o = AssetDatabase.LoadAssetAtPath(p); if (o != null) objs.Add(o); } Selection.objects = objs.ToArray(); Log($"[依赖分析器] 已选中 {objs.Count} 个非AA资源到 Project 视图"); } private void DrawDependencyRow(DependencyInfo info) { EditorGUILayout.BeginHorizontal(GUIStyle.none); // 复选框(仅展示,不维护勾选状态,使用 Selection.paths 作为统一选择机制) bool selected = Selection.objects != null && Selection.objects.Any(o => o != null && AssetDatabase.GetAssetPath(o) == info.assetPath); bool newSel = EditorGUILayout.Toggle(selected, GUILayout.Width(20)); if (newSel != selected) { var obj = AssetDatabase.LoadAssetAtPath(info.assetPath); if (obj != null) { var list = Selection.objects != null ? Selection.objects.ToList() : new List(); if (newSel && !list.Contains(obj)) list.Add(obj); if (!newSel && list.Contains(obj)) list.Remove(obj); Selection.objects = list.ToArray(); } } // 路径(可点击定位) if (GUILayout.Button(info.assetPath, EditorStyles.label, GUILayout.Width(300))) { var obj = AssetDatabase.LoadAssetAtPath(info.assetPath); if (obj != null) { EditorGUIUtility.PingObject(obj); Selection.activeObject = obj; } } EditorGUILayout.LabelField(info.typeName, GUILayout.Width(80)); EditorGUILayout.LabelField(info.fileSizeReadable, GUILayout.Width(70)); // AA 归属(彩色显示) var oldColor = GUI.color; GUI.color = info.isInAddressables ? new Color(0.6f, 1f, 0.6f) : new Color(1f, 0.6f, 0.6f); EditorGUILayout.LabelField(info.isInAddressables ? info.aaGroup : "非AA", GUILayout.Width(120)); GUI.color = oldColor; EditorGUILayout.LabelField(info.aaAddress ?? "-", GUILayout.Width(150)); EditorGUILayout.LabelField(info.aaLabels.Count > 0 ? string.Join(",", info.aaLabels) : "-", GUILayout.Width(150)); EditorGUILayout.EndHorizontal(); } private List ApplyFilter(List source) { var result = source; switch (filterMode) { case FilterMode.InAddressables: result = result.Where(d => d.isInAddressables).ToList(); break; case FilterMode.NotInAddressables: result = result.Where(d => !d.isInAddressables).ToList(); break; } if (typeFilter != "全部") result = result.Where(d => d.typeName == typeFilter).ToList(); if (groupFilter == "(非AA)") result = result.Where(d => !d.isInAddressables).ToList(); else if (groupFilter != "全部") result = result.Where(d => d.isInAddressables && d.aaGroup == groupFilter).ToList(); if (labelFilter == "(无标签)") result = result.Where(d => d.isInAddressables && d.aaLabels.Count == 0).ToList(); else if (labelFilter != "全部") result = result.Where(d => d.isInAddressables && d.aaLabels.Contains(labelFilter)).ToList(); if (!string.IsNullOrEmpty(pathKeyword)) result = result.Where(d => d.assetPath.IndexOf(pathKeyword, StringComparison.OrdinalIgnoreCase) >= 0).ToList(); return result; } #endregion #region 分析逻辑 private void AnalyzeAll() { dependencies.Clear(); dependencyMap.Clear(); allTypes.Clear(); RefreshAACache(); var targetPaths = new HashSet(); if (useBatchMode) { foreach (var obj in batchTargets) { if (obj == null) continue; string p = AssetDatabase.GetAssetPath(obj); if (!string.IsNullOrEmpty(p)) targetPaths.Add(p); } } else { if (targetAsset != null) { string p = AssetDatabase.GetAssetPath(targetAsset); if (!string.IsNullOrEmpty(p)) targetPaths.Add(p); } } if (targetPaths.Count == 0) { EditorUtility.DisplayDialog("提示", "请先选择目标资源。", "确定"); return; } try { EditorUtility.DisplayProgressBar("分析依赖", "正在收集依赖资源...", 0f); int idx = 0; foreach (string path in targetPaths) { EditorUtility.DisplayProgressBar("分析依赖", $"处理 {path}", (float)idx / targetPaths.Count); AnalyzeSingle(path); idx++; } // 收集类型列表 allTypes = dependencies.Select(d => d.typeName).Distinct().OrderBy(t => t).ToList(); EditorUtility.ClearProgressBar(); Log($"[依赖分析器] 分析完成,共 {dependencies.Count} 个依赖资源"); } catch (Exception e) { EditorUtility.ClearProgressBar(); LogError($"[依赖分析器] 分析失败: {e}"); } } private void AnalyzeSingle(string assetPath) { if (string.IsNullOrEmpty(assetPath)) return; // 自身也作为一项 AddDependencyInfo(assetPath); if (!deepAnalyze) return; string[] deps = AssetDatabase.GetDependencies(assetPath, true); foreach (var dep in deps) { AddDependencyInfo(dep); } } private void AddDependencyInfo(string assetPath) { if (dependencyMap.ContainsKey(assetPath)) return; if (!includeScripts && assetPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) return; // 过滤 Editor 仅资源 if (assetPath.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)) return; var info = new DependencyInfo { assetPath = assetPath, guid = AssetDatabase.AssetPathToGUID(assetPath) }; var mainAsset = AssetDatabase.LoadAssetAtPath(assetPath); if (mainAsset != null) { info.type = mainAsset.GetType(); info.typeName = info.type.Name; } else { info.typeName = Path.GetExtension(assetPath).TrimStart('.'); } // 文件大小 try { string fullPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, assetPath); if (File.Exists(fullPath)) { var fi = new FileInfo(fullPath); info.fileSizeBytes = fi.Length; info.fileSizeReadable = FormatSize(info.fileSizeBytes); } else { info.fileSizeBytes = 0; info.fileSizeReadable = "-"; } } catch { info.fileSizeBytes = 0; info.fileSizeReadable = "-"; } // AA 信息 if (aaSettings != null && !string.IsNullOrEmpty(info.guid)) { var entry = aaSettings.FindAssetEntry(info.guid); if (entry != null) { info.isInAddressables = true; info.aaAddress = entry.address; info.aaGroup = entry.parentGroup != null ? entry.parentGroup.Name : "(未知)"; info.aaLabels = entry.labels.ToList(); } } dependencies.Add(info); dependencyMap[assetPath] = info; } #endregion #region 反向查找 private void ReverseLookupSelected() { if (aaSettings == null) { EditorUtility.DisplayDialog("提示", "Addressables 未初始化。", "确定"); return; } var selected = Selection.objects; if (selected == null || selected.Length == 0) { EditorUtility.DisplayDialog("提示", "请先在 Project 视图中选择目标资源。", "确定"); return; } reverseRefMap.Clear(); var targetGuids = new HashSet(); foreach (var obj in selected) { string p = AssetDatabase.GetAssetPath(obj); if (!string.IsNullOrEmpty(p)) { string g = AssetDatabase.AssetPathToGUID(p); if (!string.IsNullOrEmpty(g)) targetGuids.Add(g); } } try { EditorUtility.DisplayProgressBar("反向查找", "正在遍历 AA 条目...", 0f); var allEntries = aaSettings.groups.SelectMany(g => g.entries).ToList(); int idx = 0; int matched = 0; foreach (var entry in allEntries) { idx++; if (idx % 50 == 0) EditorUtility.DisplayProgressBar("反向查找", $"扫描 {idx}/{allEntries.Count}", (float)idx / allEntries.Count); string entryPath = AssetDatabase.GUIDToAssetPath(entry.guid); if (string.IsNullOrEmpty(entryPath)) continue; string[] deps = AssetDatabase.GetDependencies(entryPath, true); foreach (var dep in deps) { string depGuid = AssetDatabase.AssetPathToGUID(dep); if (targetGuids.Contains(depGuid)) { if (!reverseRefMap.ContainsKey(dep)) reverseRefMap[dep] = new List(); if (!reverseRefMap[dep].Contains(entryPath)) reverseRefMap[dep].Add(entryPath); matched++; } } } EditorUtility.ClearProgressBar(); var sb = new StringBuilder(); sb.AppendLine($"反向查找完成,命中 {matched} 处引用。"); sb.AppendLine(); foreach (var kv in reverseRefMap) { sb.AppendLine($"【目标】{kv.Key}"); foreach (var refPath in kv.Value) { string refGuid = AssetDatabase.AssetPathToGUID(refPath); var refEntry = aaSettings.FindAssetEntry(refGuid); string aaInfo = refEntry != null ? $"[AA] {refEntry.parentGroup.Name} / {refEntry.address}" : "[非AA]"; sb.AppendLine($" ← {refPath} {aaInfo}"); } sb.AppendLine(); } string savePath = EditorUtility.SaveFilePanel("保存反向查找报告", Application.dataPath, "ReverseLookupReport", "txt"); if (!string.IsNullOrEmpty(savePath)) { File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8); EditorUtility.DisplayDialog("完成", $"报告已保存到:\n{savePath}", "确定"); } else { Log(sb.ToString()); } } catch (Exception e) { EditorUtility.ClearProgressBar(); LogError($"[依赖分析器] 反向查找失败: {e}"); } } #endregion #region 导出报告 private void ExportReport() { if (dependencies.Count == 0) { EditorUtility.DisplayDialog("提示", "请先分析依赖。", "确定"); return; } string savePath = EditorUtility.SaveFilePanel("导出依赖报告", Application.dataPath, "DependencyReport", "csv"); if (string.IsNullOrEmpty(savePath)) return; var sb = new StringBuilder(); sb.AppendLine("AssetPath,GUID,Type,Size,IsInAA,AAGroup,AAAddress,AALabels"); foreach (var info in dependencies) { sb.AppendLine(string.Join(",", EscapeCsv(info.assetPath), info.guid, info.typeName, info.fileSizeBytes, info.isInAddressables, EscapeCsv(info.aaGroup ?? ""), EscapeCsv(info.aaAddress ?? ""), EscapeCsv(string.Join(";", info.aaLabels)))); } File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8); EditorUtility.DisplayDialog("完成", $"报告已导出到:\n{savePath}", "确定"); Log($"[依赖分析器] 报告已导出: {savePath}"); } private string EscapeCsv(string s) { if (string.IsNullOrEmpty(s)) return ""; if (s.Contains(",") || s.Contains("\"") || s.Contains("\n")) { return "\"" + s.Replace("\"", "\"\"") + "\""; } return s; } #endregion #region 复制地址 private void CopySelectedAddresses() { var filtered = GetFilteredCache(); var selected = filtered.Where(d => Selection.objects != null && Selection.objects.Any(o => o != null && AssetDatabase.GetAssetPath(o) == d.assetPath)) .ToList(); if (selected.Count == 0) { EditorUtility.DisplayDialog("提示", "请先在下方列表勾选资源。", "确定"); return; } var sb = new StringBuilder(); foreach (var info in selected) { sb.AppendLine(info.aaAddress ?? info.assetPath); } GUIUtility.systemCopyBuffer = sb.ToString(); Log($"[依赖分析器] 已复制 {selected.Count} 条地址到剪贴板"); } #endregion #region 加入AA private void AddSelectedToAddressables() { if (aaSettings == null) { EditorUtility.DisplayDialog("错误", "Addressables 未初始化。", "确定"); return; } var filtered = GetFilteredCache(); var selected = filtered.Where(d => Selection.objects != null && Selection.objects.Any(o => o != null && AssetDatabase.GetAssetPath(o) == d.assetPath)) .ToList(); if (selected.Count == 0) { EditorUtility.DisplayDialog("提示", "请先在下方列表勾选资源。", "确定"); return; } // 选择 Group var groupNames = aaSettings.groups.Where(g => g != null).Select(g => g.Name).ToArray(); int choice = EditorUtility.DisplayDialogComplex( "选择目标 Group", $"将 {selected.Count} 个资源添加到哪个 Group?", groupNames.Length > 0 ? groupNames[0] : "Default", groupNames.Length > 1 ? groupNames[1] : "Cancel", groupNames.Length > 2 ? "其他..." : "Cancel"); if (choice == 2) { // 弹出菜单选择 var menu = new GenericMenu(); foreach (var gn in groupNames) { var captured = gn; menu.AddItem(new GUIContent(gn), false, () => DoAddToGroup(selected, captured)); } menu.ShowAsContext(); return; } else if (choice >= 0 && choice < groupNames.Length) { DoAddToGroup(selected, groupNames[choice]); } } private void DoAddToGroup(List list, string groupName) { var group = aaSettings.groups.FirstOrDefault(g => g != null && g.Name == groupName); if (group == null) { EditorUtility.DisplayDialog("错误", $"找不到 Group: {groupName}", "确定"); return; } int count = 0; var errors = new List(); foreach (var info in list) { if (string.IsNullOrEmpty(info.guid)) { errors.Add($"{info.assetPath}: GUID 为空"); continue; } var entry = aaSettings.FindAssetEntry(info.guid); if (entry == null) entry = aaSettings.CreateOrMoveEntry(info.guid, group); if (entry == null) { errors.Add($"{info.assetPath}: 创建条目失败"); continue; } // 默认 address 用文件名 if (string.IsNullOrEmpty(entry.address) || entry.address == info.guid) entry.address = Path.GetFileNameWithoutExtension(info.assetPath); count++; } aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, null, true); AssetDatabase.SaveAssets(); // 重新分析刷新 AnalyzeAll(); string msg = $"成功: {count} 个"; if (errors.Count > 0) msg += $"\n失败 {errors.Count} 个:\n" + string.Join("\n", errors); EditorUtility.DisplayDialog("加入AA完成", msg, "确定"); } #endregion #region 工具 private static string FormatSize(long bytes) { if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return (bytes / 1024.0).ToString("F1") + " KB"; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024.0 * 1024)).ToString("F2") + " MB"; return (bytes / (1024.0 * 1024 * 1024)).ToString("F2") + " GB"; } #endregion }