Files
zeling_v2/Assets/_Game/Scripts/Editor/Addressables/AddressReferenceGraphWindow.cs
Joywayer f1c0b65737 feat: Enhance Addressable tools with improved scanning and filtering features
- Updated AddressReferenceGraphWindow to scan for AddressKeys in the _Game directory and added a warning for missing directories.
- Enhanced AddressableBatchTool with new filters for asset types (Prefab, Scene, ScriptableObject, Texture, Audio) and improved UI layout for better usability.
- Introduced automatic application of grouping and labeling rules during registration in AddressableBatchTool.
- Added functionality to quickly scan the _Game folder and improved address building logic.
- Updated AddressableRuleSyncWindow to include handling for custom labels and improved reporting of issues.
- Enhanced AddressableRules with a whitelist for known labels and refined grouping and labeling logic based on asset prefixes.
2026-05-22 13:34:47 +08:00

308 lines
14 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 System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEngine;
namespace BaseGames.Editor
{
/// <summary>
/// Addressable Key 引用关系图窗口(架构 13_AssetPoolModule §11
/// 菜单BaseGames/Tools/Asset Reference Graph
///
/// 功能:
/// - 扫描所有 .cs 文件中对 AddressKeys.X 的引用
/// - 列出每个 Key声明位置、引用文件列表、是否存在于 Addressables
/// - 孤儿 Key有声明无引用标红显示
/// - 无效 Key有引用但不存在于 Addressables标橙显示
/// - 一键导出 CSV
/// </summary>
public class AddressReferenceGraphWindow : EditorWindow
{
// ── State ──────────────────────────────────────────────────────────
private List<KeyEntry> _entries;
private Vector2 _scrollPos;
private string _searchFilter = "";
private bool _showOrphansOnly;
private bool _showMissingOnly;
// ── Colors ─────────────────────────────────────────────────────────
private static readonly Color ColOrphan = new Color(0.90f, 0.15f, 0.15f, 0.80f); // 孤儿 Key无引用
private static readonly Color ColMissing = new Color(0.95f, 0.55f, 0.10f, 0.80f); // 无效 Key不在 Addressables
private static readonly Color ColOk = new Color(0.20f, 0.75f, 0.30f, 0.80f); // 正常
[MenuItem("BaseGames/Addressables/Asset Reference Graph")]
public static void OpenWindow()
{
var win = GetWindow<AddressReferenceGraphWindow>("Asset Reference Graph");
win.minSize = new Vector2(900, 500);
win.Show();
}
// ── GUI ────────────────────────────────────────────────────────────
private void OnGUI()
{
DrawToolbar();
if (_entries == null)
{
EditorGUILayout.HelpBox("点击上方「扫描」按钮分析 AddressKeys 引用关系。", MessageType.Info);
return;
}
DrawFilterRow();
DrawResults();
}
// ── Toolbar ───────────────────────────────────────────────────────
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(60)))
RunScan();
if (_entries != null && GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(70)))
ExportCsv();
GUILayout.FlexibleSpace();
if (_entries != null)
{
int orphans = _entries.Count(e => e.ReferenceCount == 0);
int missing = _entries.Count(e => !e.ExistsInAddressables);
EditorGUILayout.LabelField(
$"共 {_entries.Count} 个 Key | 孤儿:{orphans} | 未在 Addressables{missing}",
EditorStyles.toolbarButton);
}
EditorGUILayout.EndHorizontal();
}
// ── 过滤行 ────────────────────────────────────────────────────────
private void DrawFilterRow()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("搜索:", GUILayout.Width(40));
_searchFilter = EditorGUILayout.TextField(_searchFilter, GUILayout.ExpandWidth(true));
_showOrphansOnly = EditorGUILayout.ToggleLeft("仅显示孤儿", _showOrphansOnly, GUILayout.Width(90));
_showMissingOnly = EditorGUILayout.ToggleLeft("仅显示缺失", _showMissingOnly, GUILayout.Width(90));
EditorGUILayout.EndHorizontal();
}
// ── 结果列表 ──────────────────────────────────────────────────────
private void DrawResults()
{
var filtered = _entries.AsEnumerable();
if (_showOrphansOnly)
filtered = filtered.Where(e => e.ReferenceCount == 0);
if (_showMissingOnly)
filtered = filtered.Where(e => !e.ExistsInAddressables);
if (!string.IsNullOrEmpty(_searchFilter))
filtered = filtered.Where(e =>
e.FieldName.IndexOf(_searchFilter, System.StringComparison.OrdinalIgnoreCase) >= 0);
var list = filtered.ToList();
// 表头
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
EditorGUILayout.LabelField("状态", GUILayout.Width(50));
EditorGUILayout.LabelField("Key 名称", GUILayout.Width(280));
EditorGUILayout.LabelField("地址值", GUILayout.Width(300));
EditorGUILayout.LabelField("引用数", GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
foreach (var entry in list)
{
bool isOrphan = entry.ReferenceCount == 0;
bool isMissing = !entry.ExistsInAddressables;
Color statusColor = isOrphan ? ColOrphan : (isMissing ? ColMissing : ColOk);
string statusIcon = isOrphan ? "⊘" : (isMissing ? "⚠" : "✓");
var prevBg = GUI.backgroundColor;
GUI.backgroundColor = statusColor * 0.6f;
EditorGUILayout.BeginHorizontal("box");
GUI.backgroundColor = prevBg;
EditorGUILayout.LabelField(statusIcon, GUILayout.Width(50));
EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(280));
// 地址值可点击 → Ping Addressable asset
if (GUILayout.Button(entry.Value,
isOrphan ? EditorStyles.label : EditorStyles.miniButtonMid,
GUILayout.Width(300)))
{
PingAddressableAsset(entry.Value);
}
EditorGUILayout.LabelField(
$"{entry.ReferenceCount}",
GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
// 展开:显示引用文件列表
if (entry.ReferenceCount > 0 && entry.ReferencedInFiles != null)
{
foreach (var file in entry.ReferencedInFiles)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(60);
EditorGUILayout.LabelField($" ↳ {file}", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
}
}
EditorGUILayout.EndScrollView();
}
// ── 扫描逻辑 ──────────────────────────────────────────────────────
private void RunScan()
{
_entries = new List<KeyEntry>();
// 1. 收集所有 AddressKeys 常量
var keyFields = typeof(BaseGames.Core.Assets.AddressKeys)
.GetFields(System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.Static
| System.Reflection.BindingFlags.FlattenHierarchy)
.Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string));
var keyDict = new Dictionary<string, KeyEntry>();
foreach (var f in keyFields)
{
var value = (string)f.GetRawConstantValue();
keyDict[f.Name] = new KeyEntry
{
FieldName = f.Name,
Value = value,
ExistsInAddressables = false,
ReferencedInFiles = new List<string>()
};
}
// 2. 检查 Addressables直接内联不依赖 Verification 程序集)
var registeredAddressValues = GetRegisteredAddressableAddresses();
foreach (var kv in keyDict)
kv.Value.ExistsInAddressables = registeredAddressValues.Contains(kv.Value.Value);
// 3. 扫描 _Game/ 下所有 .cs 文件中对 AddressKeys 的引用
string gameDir = Path.Combine(Application.dataPath, "_Game");
if (!Directory.Exists(gameDir))
{
Debug.LogWarning($"[AddressReferenceGraph] 未找到游戏目录:{gameDir}");
foreach (var kv in keyDict) _entries.Add(kv.Value);
return;
}
var csFiles = Directory.GetFiles(gameDir, "*.cs", SearchOption.AllDirectories);
foreach (var file in csFiles)
{
string content;
try { content = File.ReadAllText(file); }
catch { continue; }
foreach (var kv in keyDict)
{
// 匹配 AddressKeys.FieldName单词边界避免前缀误匹配
if (Regex.IsMatch(content, $@"\bAddressKeys\.{Regex.Escape(kv.Key)}\b"))
{
string relativePath = "Assets" + file.Substring(Application.dataPath.Length).Replace('\\', '/');
kv.Value.ReferencedInFiles.Add(relativePath);
}
}
}
foreach (var kv in keyDict)
_entries.Add(kv.Value);
_entries.Sort((a, b) =>
{
// 孤儿排最前,其次缺失,最后正常
int aScore = a.ReferenceCount == 0 ? 0 : (!a.ExistsInAddressables ? 1 : 2);
int bScore = b.ReferenceCount == 0 ? 0 : (!b.ExistsInAddressables ? 1 : 2);
return aScore != bScore ? aScore.CompareTo(bScore) : string.Compare(a.FieldName, b.FieldName);
});
Debug.Log($"[AddressReferenceGraph] 扫描完成:{_entries.Count} 个 Key" +
$"{_entries.Count(e => e.ReferenceCount == 0)} 孤儿," +
$"{_entries.Count(e => !e.ExistsInAddressables)} 未在 Addressables。");
}
// ── CSV 导出 ──────────────────────────────────────────────────────
private void ExportCsv()
{
string path = EditorUtility.SaveFilePanel("导出 CSV", "", "AddressKeyReport", "csv");
if (string.IsNullOrEmpty(path)) return;
using var writer = new StreamWriter(path, false, System.Text.Encoding.UTF8);
writer.WriteLine("FieldName,Value,ExistsInAddressables,ReferenceCount,ReferencedFiles");
foreach (var e in _entries)
{
string files = e.ReferencedInFiles != null
? string.Join(" | ", e.ReferencedInFiles)
: "";
writer.WriteLine($"{e.FieldName},{e.Value},{e.ExistsInAddressables},{e.ReferenceCount},{files}");
}
Debug.Log($"[AddressReferenceGraph] CSV 已导出:{path}");
}
// ── Addressables 辅助(独立实现,不依赖 Verification 程序集)─────────
private static HashSet<string> GetRegisteredAddressableAddresses()
{
var addresses = new HashSet<string>(StringComparer.Ordinal);
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) return addresses;
foreach (var group in settings.groups)
{
if (group == null) continue;
foreach (var entry in group.entries)
if (entry != null) addresses.Add(entry.address);
}
return addresses;
}
// ── Ping Addressable ──────────────────────────────────────────────
private static void PingAddressableAsset(string address)
{
#if UNITY_EDITOR
var guids = AssetDatabase.FindAssets($"\"{address}\"");
if (guids.Length > 0)
{
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(AssetDatabase.GUIDToAssetPath(guids[0]));
if (obj != null) EditorGUIUtility.PingObject(obj);
}
#endif
}
// ── Data ──────────────────────────────────────────────────────────
private class KeyEntry
{
public string FieldName;
public string Value;
public bool ExistsInAddressables;
public List<string> ReferencedInFiles;
public int ReferenceCount => ReferencedInFiles?.Count ?? 0;
}
}
}