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.
This commit is contained in:
2026-05-22 13:34:47 +08:00
parent e7b44e1d60
commit f1c0b65737
4 changed files with 416 additions and 112 deletions

View File

@@ -60,6 +60,11 @@ namespace BaseGames.Editor
// Tab ③
private List<SelectionEntry> _selectionEntries;
private Vector2 _selectionScrollPos;
private bool _selFilterPrefab = true;
private bool _selFilterScene = true;
private bool _selFilterSO = true;
private bool _selFilterTexture;
private bool _selFilterAudio;
// 共用
private int _targetGroupIndex;
@@ -67,6 +72,7 @@ namespace BaseGames.Editor
private string _newGroupName = "New Group";
private string _newLabel = "";
private bool _overwriteAddress;
private bool _applyRulesOnRegister = true;
// ── 样式(惰性初始化)────────────────────────────────────────────────
private GUIStyle _headerStyle;
@@ -81,7 +87,7 @@ namespace BaseGames.Editor
public static void OpenWindow()
{
var win = GetWindow<AddressableBatchTool>(Title);
win.minSize = new Vector2(600, 460);
win.minSize = new Vector2(920, 520);
win.Show();
}
@@ -144,9 +150,11 @@ namespace BaseGames.Editor
// 列表表头
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
EditorGUILayout.LabelField("常量名", _boldStyle, GUILayout.Width(200));
EditorGUILayout.LabelField("地址 Key", _boldStyle, GUILayout.Width(180));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(100));
EditorGUILayout.LabelField("常量名", _boldStyle, GUILayout.Width(180));
EditorGUILayout.LabelField("地址 Key", _boldStyle, GUILayout.Width(160));
EditorGUILayout.LabelField("期望分组", _boldStyle, GUILayout.Width(110));
EditorGUILayout.LabelField("期望标签", _boldStyle, GUILayout.Width(140));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80));
EditorGUILayout.LabelField("匹配资产", _boldStyle);
}
@@ -160,24 +168,30 @@ namespace BaseGames.Editor
{
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(20)))
{
EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(200));
EditorGUILayout.LabelField(entry.AddressKey, GUILayout.Width(180));
EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(180));
EditorGUILayout.LabelField(entry.AddressKey, GUILayout.Width(160));
EditorGUILayout.LabelField(
AddressableRules.GetExpectedGroup(entry.AddressKey) ?? "Default",
GUILayout.Width(110));
EditorGUILayout.LabelField(
FormatLabels(AddressableRules.GetExpectedLabels(entry.AddressKey)),
GUILayout.Width(140));
if (entry.IsRegistered)
{
EditorGUILayout.LabelField("✅ 已注册", _okStyle, GUILayout.Width(100));
EditorGUILayout.LabelField("✅ 已注册", _okStyle, GUILayout.Width(80));
EditorGUILayout.LabelField(entry.ExistingAssetPath ?? "—");
}
else if (entry.FoundAssetPath != null)
{
EditorGUILayout.LabelField("⚠ 未注册", _warnStyle, GUILayout.Width(100));
EditorGUILayout.LabelField("⚠ 未注册", _warnStyle, GUILayout.Width(80));
EditorGUILayout.LabelField(entry.FoundAssetPath, GUILayout.ExpandWidth(true));
if (GUILayout.Button("注册", GUILayout.Width(50)))
RegisterKeyEntry(entry);
}
else
{
EditorGUILayout.LabelField("❌ 未找到", _warnStyle, GUILayout.Width(100));
EditorGUILayout.LabelField("❌ 未找到", _warnStyle, GUILayout.Width(80));
entry.ManualAsset = (UnityEngine.Object)EditorGUILayout.ObjectField(
entry.ManualAsset, typeof(UnityEngine.Object), false);
if (entry.ManualAsset != null)
@@ -247,6 +261,8 @@ namespace BaseGames.Editor
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("⚡ 全量扫描 _Game/", GUILayout.Width(150)))
QuickScanGameFolder();
GUILayout.FlexibleSpace();
GUI.enabled = !string.IsNullOrEmpty(_folderPath);
if (GUILayout.Button("扫描文件夹", GUILayout.Width(100)))
@@ -266,9 +282,14 @@ namespace BaseGames.Editor
EditorGUILayout.Space(4);
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.ExpandWidth(true));
EditorGUILayout.LabelField("预计地址", _boldStyle, GUILayout.Width(200));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80));
EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.Width(180));
EditorGUILayout.LabelField("地址", _boldStyle, GUILayout.Width(150));
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField("分组(规则)", _boldStyle, GUILayout.Width(120));
EditorGUILayout.LabelField("标签(规则)", _boldStyle, GUILayout.Width(150));
}
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(70));
}
_folderScrollPos = EditorGUILayout.BeginScrollView(_folderScrollPos, GUILayout.ExpandHeight(true));
@@ -276,11 +297,16 @@ namespace BaseGames.Editor
{
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18)))
{
EditorGUILayout.LabelField(entry.AssetPath, GUILayout.ExpandWidth(true));
entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(200));
EditorGUILayout.LabelField(entry.AssetPath, GUILayout.Width(180));
entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(150));
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField(entry.PredictedGroup ?? "Default", GUILayout.Width(120));
EditorGUILayout.LabelField(entry.PredictedLabels ?? "—", GUILayout.Width(150));
}
var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册";
var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel;
EditorGUILayout.LabelField(label, style, GUILayout.Width(80));
EditorGUILayout.LabelField(label, style, GUILayout.Width(70));
}
}
EditorGUILayout.EndScrollView();
@@ -296,6 +322,18 @@ namespace BaseGames.Editor
EditorGUILayout.LabelField("在 Project 窗口中选中资产或文件夹,然后点击「读取选中项」。", EditorStyles.wordWrappedMiniLabel);
EditorGUILayout.Space(4);
// 资产类型筛选(与 Tab ② 一致,防止误注册不该 Addressable 的文件类型)
EditorGUILayout.LabelField("资产类型筛选", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
_selFilterPrefab = GUILayout.Toggle(_selFilterPrefab, "Prefab", GUILayout.Width(70));
_selFilterScene = GUILayout.Toggle(_selFilterScene, "Scene", GUILayout.Width(70));
_selFilterSO = GUILayout.Toggle(_selFilterSO, "SO/Asset", GUILayout.Width(80));
_selFilterTexture = GUILayout.Toggle(_selFilterTexture, "Texture", GUILayout.Width(70));
_selFilterAudio = GUILayout.Toggle(_selFilterAudio, "Audio", GUILayout.Width(70));
}
EditorGUILayout.Space(4);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
@@ -316,9 +354,14 @@ namespace BaseGames.Editor
EditorGUILayout.Space(4);
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.ExpandWidth(true));
EditorGUILayout.LabelField("注册地址", _boldStyle, GUILayout.Width(200));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80));
EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.Width(180));
EditorGUILayout.LabelField("注册地址", _boldStyle, GUILayout.Width(150));
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField("分组(规则)", _boldStyle, GUILayout.Width(120));
EditorGUILayout.LabelField("标签(规则)", _boldStyle, GUILayout.Width(150));
}
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(70));
}
_selectionScrollPos = EditorGUILayout.BeginScrollView(_selectionScrollPos, GUILayout.ExpandHeight(true));
@@ -326,11 +369,16 @@ namespace BaseGames.Editor
{
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18)))
{
EditorGUILayout.LabelField(entry.AssetPath, GUILayout.ExpandWidth(true));
entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(200));
EditorGUILayout.LabelField(entry.AssetPath, GUILayout.Width(180));
entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(150));
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField(entry.PredictedGroup ?? "Default", GUILayout.Width(120));
EditorGUILayout.LabelField(entry.PredictedLabels ?? "—", GUILayout.Width(150));
}
var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册";
var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel;
EditorGUILayout.LabelField(label, style, GUILayout.Width(80));
EditorGUILayout.LabelField(label, style, GUILayout.Width(70));
}
}
EditorGUILayout.EndScrollView();
@@ -347,22 +395,32 @@ namespace BaseGames.Editor
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("目标分组", GUILayout.Width(70));
if (_groupNames != null && _groupNames.Length > 0)
// 自动规则模式下,目标分组由规则决定,手动选择无效
GUI.enabled = !_applyRulesOnRegister;
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField("(由 AddressableRules 自动决定)",
EditorStyles.miniLabel, GUILayout.Width(200));
}
else if (_groupNames != null && _groupNames.Length > 0)
{
_targetGroupIndex = EditorGUILayout.Popup(_targetGroupIndex,
_groupNames, GUILayout.Width(200));
}
GUI.enabled = true;
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("标签", GUILayout.Width(30));
EditorGUILayout.LabelField("附加标签", GUILayout.Width(52));
_newLabel = EditorGUILayout.TextField(_newLabel, GUILayout.Width(120));
GUILayout.Label("留空则不添加标签", EditorStyles.miniLabel);
GUILayout.Label("可在规则标签基础上追加", EditorStyles.miniLabel);
GUILayout.FlexibleSpace();
}
using (new EditorGUILayout.HorizontalScope())
{
_overwriteAddress = GUILayout.Toggle(_overwriteAddress, "已注册的资产也覆盖地址");
_overwriteAddress = GUILayout.Toggle(_overwriteAddress, "已注册的资产也覆盖地址");
GUILayout.Space(16);
_applyRulesOnRegister = GUILayout.Toggle(_applyRulesOnRegister, "自动应用分组/标签规则");
GUILayout.FlexibleSpace();
if (GUILayout.Button("新建分组…", GUILayout.Width(100)))
ShowCreateGroupDialog();
@@ -469,7 +527,7 @@ namespace BaseGames.Editor
_folderEntries = new List<FolderEntry>();
if (!AssetDatabase.IsValidFolder(_folderPath)) return;
var settings = AddressableAssetSettingsDefaultObject.Settings;
var settings = AddressableAssetSettingsDefaultObject.Settings;
var registeredGuids = CollectRegisteredGuids(settings);
var filters = BuildSearchFilter();
@@ -479,23 +537,45 @@ namespace BaseGames.Editor
string absFolder = Path.GetFullPath(_folderPath);
// 收集所有文件
var allFiles = new List<string>();
foreach (string filter in filters)
allFiles.AddRange(Directory.GetFiles(absFolder, filter, option));
try
{
foreach (string absPath in Directory.GetFiles(absFolder, filter, option))
for (int i = 0; i < allFiles.Count; i++)
{
string absPath = allFiles[i];
if (i % 20 == 0)
EditorUtility.DisplayProgressBar("扫描文件夹",
Path.GetFileName(absPath),
(float)i / allFiles.Count);
string relPath = "Assets" + absPath.Substring(Application.dataPath.Length).Replace('\\', '/');
string guid = AssetDatabase.AssetPathToGUID(relPath);
if (!IsAddressableAssetPath(relPath)) continue;
if (ShouldExclude(relPath)) continue;
string guid = AssetDatabase.AssetPathToGUID(relPath);
if (string.IsNullOrEmpty(guid)) continue;
string addr = BuildAddress(relPath);
_folderEntries.Add(new FolderEntry
{
AssetPath = relPath,
Guid = guid,
Address = BuildAddress(relPath),
Address = addr,
AlreadyRegistered = registeredGuids.Contains(guid),
PredictedGroup = AddressableRules.GetExpectedGroup(addr),
PredictedLabels = FormatLabels(AddressableRules.GetExpectedLabels(addr)),
});
}
}
finally
{
EditorUtility.ClearProgressBar();
}
// 去重(多个 filter 可能匹配同一文件)
_folderEntries = _folderEntries
@@ -504,6 +584,34 @@ namespace BaseGames.Editor
.ToList();
}
/// <summary>
/// 返回 true 表示该文件应从 Addressable 注册中排除。
/// 规范来源AddressablesLabelSpec §5.2(禁止注册项)。
/// </summary>
private static bool ShouldExclude(string relPath)
{
string lowerPath = relPath.Replace('\\', '/').ToLowerInvariant();
string fileName = Path.GetFileNameWithoutExtension(relPath);
// 测试场景(放在 Scenes/Testings/ 下)
if (lowerPath.Contains("/scenes/testings/")) return true;
// 事件频道 ScriptableObjectEVT_ 前缀)
if (fileName.StartsWith("EVT_", StringComparison.Ordinal)) return true;
// Sprite Atlas — 随依赖它的 Prefab 隐式打包,不单独注册
if (Path.GetExtension(relPath) == ".spriteatlas") return true;
// Material — 随 Prefab 依赖关系打包,不单独注册
if (Path.GetExtension(relPath) == ".mat") return true;
// HitBox / HurtBox 等碰撞盒子 Prefab子 Prefab不独立寻址
if (fileName.StartsWith("HitBox_", StringComparison.OrdinalIgnoreCase)) return true;
if (fileName.StartsWith("HurtBox_", StringComparison.OrdinalIgnoreCase)) return true;
return false;
}
private void RegisterAllFolderEntries()
{
int count = 0;
@@ -554,12 +662,29 @@ namespace BaseGames.Editor
private void AddSelectionEntry(string guid, string path, HashSet<string> registeredGuids)
{
if (!IsAddressableAssetPath(path)) return;
if (ShouldExclude(path)) return;
// 资产类型筛选(与 Tab ③ 筛选 Toggle 联动)
string ext = Path.GetExtension(path).ToLowerInvariant();
bool isMatch = (_selFilterPrefab && ext == ".prefab")
|| (_selFilterScene && ext == ".unity")
|| (_selFilterSO && ext == ".asset")
|| (_selFilterTexture && (ext == ".png" || ext == ".jpg" || ext == ".tga"))
|| (_selFilterAudio && (ext == ".mp3" || ext == ".wav" || ext == ".ogg"));
// 若没有任何类型 Toggle 被勾选,则接受所有类型(兜底行为,避免全部筛空)
bool anyToggled = _selFilterPrefab || _selFilterScene || _selFilterSO || _selFilterTexture || _selFilterAudio;
if (anyToggled && !isMatch) return;
string addr = BuildAddress(path);
_selectionEntries.Add(new SelectionEntry
{
AssetPath = path,
Guid = guid,
Address = BuildAddress(path),
Address = addr,
AlreadyRegistered = registeredGuids.Contains(guid),
PredictedGroup = AddressableRules.GetExpectedGroup(addr),
PredictedLabels = FormatLabels(AddressableRules.GetExpectedLabels(addr)),
});
}
@@ -584,8 +709,24 @@ namespace BaseGames.Editor
if (string.IsNullOrEmpty(guid)) return;
var settings = AddressableAssetSettingsDefaultObject.Settings;
var group = groupNameOverride != null
? GetOrCreateGroup(settings, groupNameOverride)
// 重复地址检查:同一 address 已注册到不同 GUID 时提示确认
var existingByAddress = FindEntryByAddress(settings, address);
if (existingByAddress != null && existingByAddress.guid != guid)
{
bool proceed = EditorUtility.DisplayDialog(
"⚠ 地址已存在",
$"地址 \"{address}\" 已注册到:\n{existingByAddress.AssetPath}\n\n" +
$"继续会将该地址重新指向当前资产GUID: {guid})。是否继续?",
"继续", "取消");
if (!proceed) return;
}
// Determine target group: explicit override → rules → manual selection
string effectiveGroup = groupNameOverride
?? (_applyRulesOnRegister ? AddressableRules.GetExpectedGroup(address) : null);
var group = effectiveGroup != null
? GetOrCreateGroup(settings, effectiveGroup)
: GetTargetGroup(settings);
if (group == null) return;
@@ -601,6 +742,28 @@ namespace BaseGames.Editor
if (!string.IsNullOrWhiteSpace(_newLabel))
entry.SetLabel(_newLabel.Trim(), true, true);
// Apply rules-based labels
if (_applyRulesOnRegister)
{
foreach (var lbl in AddressableRules.GetExpectedLabels(address))
{
EnsureLabelExists(settings, lbl);
entry.SetLabel(lbl, true, true);
}
}
}
private static AddressableAssetEntry FindEntryByAddress(AddressableAssetSettings settings, string address)
{
if (settings == null) return null;
foreach (var group in settings.groups)
{
if (group == null) continue;
foreach (var e in group.entries)
if (e != null && e.address == address) return e;
}
return null;
}
private AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
@@ -636,8 +799,36 @@ namespace BaseGames.Editor
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
}
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
{
if (!settings.GetLabels().Contains(label))
{
settings.AddLabel(label, true);
Debug.Log($"[AddressableBatch] 已创建标签:{label}");
}
}
private static string FormatLabels(string[] labels)
=> labels.Length > 0 ? string.Join(", ", labels) : "—";
// ══ 创建分组 ══════════════════════════════════════════════════════════
private void QuickScanGameFolder()
{
_folderPath = "Assets/_Game";
_folderAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(_folderPath);
_includeSubfolders = true;
_filterPrefab = true;
_filterScene = true;
_filterSO = true;
_filterAudio = true;
_filterTexture = false;
_addressFormat = AddressFormat.FileName;
_applyRulesOnRegister = true;
_tab = 1;
ScanFolder();
}
private void ShowCreateGroupDialog()
{
_newGroupName = EditorInputDialog.Show("新建 Addressable 分组", "请输入分组名称:", _newGroupName);
@@ -684,15 +875,29 @@ namespace BaseGames.Editor
private string BuildAddress(string assetPath)
{
string fileName = Path.GetFileNameWithoutExtension(assetPath);
string fileName = Path.GetFileNameWithoutExtension(assetPath);
string relativePath = MakeRelativePath(assetPath, _folderPath);
return _addressFormat switch
{
AddressFormat.FileName => fileName,
AddressFormat.FullAssetPath => assetPath,
AddressFormat.RelativeToFolder => MakeRelativePath(assetPath, _folderPath),
AddressFormat.PrefixPlusFileName => _addressPrefix + fileName,
AddressFormat.PrefixPlusRelativePath=> _addressPrefix + MakeRelativePath(assetPath, _folderPath),
_ => fileName,
AddressFormat.FileName => fileName,
AddressFormat.FullAssetPath => assetPath,
AddressFormat.RelativeToFolder => relativePath,
// 前缀拼接:前缀以 '/' 结尾时直接连接(如 "Config/" + "FootstepCatalog"
// 前缀不以 '/' 结尾时用 '_' 连接(如 "Room_Forest" + "_01"
AddressFormat.PrefixPlusFileName =>
string.IsNullOrEmpty(_addressPrefix)
? fileName
: (_addressPrefix.EndsWith("/")
? _addressPrefix + fileName
: _addressPrefix + "_" + fileName),
AddressFormat.PrefixPlusRelativePath =>
string.IsNullOrEmpty(_addressPrefix)
? relativePath
: (_addressPrefix.EndsWith("/")
? _addressPrefix + relativePath
: _addressPrefix + "_" + relativePath),
_ => fileName,
};
}
@@ -716,6 +921,29 @@ namespace BaseGames.Editor
return list;
}
/// <summary>
/// 判断路径是否为可寻址资产排除脚本、程序集定义、Shader、Sprite Atlas、Material 等文件)。
/// </summary>
private static bool IsAddressableAssetPath(string path)
{
string ext = Path.GetExtension(path);
if (string.IsNullOrEmpty(ext)) return false;
// 排除代码 / 元数据类文件
return ext != ".cs"
&& ext != ".asmdef"
&& ext != ".asmref"
&& ext != ".shader"
&& ext != ".hlsl"
&& ext != ".cginc"
&& ext != ".glsl"
&& ext != ".json"
&& ext != ".xml"
&& ext != ".txt"
&& ext != ".md"
&& ext != ".spriteatlas" // 随依赖它的 Prefab 隐式打包
&& ext != ".mat"; // Material 随 Prefab 依赖打包
}
/// <summary>从 AddressKey如 "ENM_GruntWarrior")派生搜索名("GruntWarrior")。</summary>
private static string DeriveName(string key)
{
@@ -740,27 +968,6 @@ namespace BaseGames.Editor
return string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 判断路径是否为可寻址资产排除脚本、程序集定义、Shader 等代码文件)。
/// </summary>
private static bool IsAddressableAssetPath(string path)
{
string ext = Path.GetExtension(path);
if (string.IsNullOrEmpty(ext)) return false;
// 排除代码 / 元数据类文件
return ext != ".cs"
&& ext != ".asmdef"
&& ext != ".asmref"
&& ext != ".shader"
&& ext != ".hlsl"
&& ext != ".cginc"
&& ext != ".glsl"
&& ext != ".json"
&& ext != ".xml"
&& ext != ".txt"
&& ext != ".md";
}
private void InitStyles()
{
if (_stylesInitialized) return;
@@ -790,6 +997,8 @@ namespace BaseGames.Editor
public string Guid;
public string Address;
public bool AlreadyRegistered;
public string PredictedGroup;
public string PredictedLabels;
}
private class SelectionEntry
@@ -798,6 +1007,8 @@ namespace BaseGames.Editor
public string Guid;
public string Address;
public bool AlreadyRegistered;
public string PredictedGroup;
public string PredictedLabels;
}
private enum AddressFormat