- 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.
1081 lines
50 KiB
C#
1081 lines
50 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using UnityEditor;
|
||
using UnityEditor.AddressableAssets;
|
||
using UnityEditor.AddressableAssets.Settings;
|
||
using UnityEngine;
|
||
using BaseGames.Core.Assets;
|
||
|
||
namespace BaseGames.Editor
|
||
{
|
||
/// <summary>
|
||
/// Addressable 批量注册工具
|
||
/// 菜单:BaseGames → Tools → Addressable Batch Tool (Alt+Shift+A)
|
||
///
|
||
/// 三种工作流:
|
||
/// ① 同步 AddressKeys — 读取 AddressKeys.cs 中的常量,按名称在 Project 中搜索对应资产并自动注册
|
||
/// ② 文件夹批量注册 — 拖入文件夹,将其下所有指定类型的资产注册到选定分组,地址格式可配置
|
||
/// ③ 选中资产注册 — 在 Project 窗口选中资产后,一键批量注册到选定分组
|
||
/// </summary>
|
||
public class AddressableBatchTool : EditorWindow
|
||
{
|
||
// ── 常量 ─────────────────────────────────────────────────────────────
|
||
private const string Title = "Addressable 批量工具";
|
||
private const string MenuPath = "BaseGames/Addressables/Addressable Batch Tool";
|
||
private const string PrefsKey = "AddressableBatch.";
|
||
|
||
// ── 状态 ─────────────────────────────────────────────────────────────
|
||
private int _tab;
|
||
private string[] _tabNames = { "① 同步 AddressKeys", "② 文件夹批量注册", "③ 选中资产注册" };
|
||
|
||
// Tab ①
|
||
private List<KeySyncEntry> _keyEntries;
|
||
private Vector2 _keyScrollPos;
|
||
private bool _onlyShowMissing = true;
|
||
private bool _autoSearch = true; // 自动按名称搜索资产
|
||
private bool _autoGroupByPrefix = true; // 按 Key 前缀自动选/建分组
|
||
|
||
// Key 前缀 → 分组名称映射(Tab ① 自动分组用)
|
||
// 规范数据统一来自 AddressableRules,此处不再声明本地副本。
|
||
|
||
// Tab ②
|
||
private DefaultAsset _folderAsset;
|
||
private string _folderPath;
|
||
private bool _includeSubfolders = true;
|
||
private AddressFormat _addressFormat = AddressFormat.FileName;
|
||
private string _addressPrefix = "";
|
||
private string[] _assetTypeFilters = { "*.prefab", "*.unity", "*.asset" };
|
||
private bool _filterPrefab = true;
|
||
private bool _filterScene = true;
|
||
private bool _filterSO = true;
|
||
private bool _filterTexture;
|
||
private bool _filterAudio;
|
||
private List<FolderEntry> _folderEntries;
|
||
private Vector2 _folderScrollPos;
|
||
|
||
// 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;
|
||
private string[] _groupNames;
|
||
private string _newGroupName = "New Group";
|
||
private string _newLabel = "";
|
||
private bool _overwriteAddress;
|
||
private bool _applyRulesOnRegister = true;
|
||
|
||
// ── 样式(惰性初始化)────────────────────────────────────────────────
|
||
private GUIStyle _headerStyle;
|
||
private GUIStyle _okStyle;
|
||
private GUIStyle _warnStyle;
|
||
private GUIStyle _boldStyle;
|
||
private bool _stylesInitialized;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
[MenuItem(MenuPath, priority = 200)]
|
||
[MenuItem("BaseGames/Addressables/Open Addressable Batch Tool", priority = 250)]
|
||
public static void OpenWindow()
|
||
{
|
||
var win = GetWindow<AddressableBatchTool>(Title);
|
||
win.minSize = new Vector2(920, 520);
|
||
win.Show();
|
||
}
|
||
|
||
// ══ GUI ══════════════════════════════════════════════════════════════
|
||
|
||
private void OnGUI()
|
||
{
|
||
InitStyles();
|
||
|
||
if (AddressableAssetSettingsDefaultObject.Settings == null)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
"Addressable Settings 未初始化。\n" +
|
||
"请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。",
|
||
MessageType.Error);
|
||
return;
|
||
}
|
||
|
||
RefreshGroupNames();
|
||
|
||
EditorGUILayout.Space(4);
|
||
_tab = GUILayout.Toolbar(_tab, _tabNames, GUILayout.Height(28));
|
||
EditorGUILayout.Space(4);
|
||
|
||
switch (_tab)
|
||
{
|
||
case 0: DrawSyncTab(); break;
|
||
case 1: DrawFolderTab(); break;
|
||
case 2: DrawSelectionTab(); break;
|
||
}
|
||
|
||
EditorGUILayout.Space(4);
|
||
DrawSharedOptions();
|
||
}
|
||
|
||
// ══ Tab ① 同步 AddressKeys ═══════════════════════════════════════════
|
||
|
||
private void DrawSyncTab()
|
||
{
|
||
EditorGUILayout.LabelField("根据 AddressKeys.cs 中的常量,自动搜索匹配资产并注册到 Addressables。", EditorStyles.wordWrappedMiniLabel);
|
||
EditorGUILayout.Space(4);
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
_onlyShowMissing = GUILayout.Toggle(_onlyShowMissing, "仅显示未注册项", GUILayout.Width(140));
|
||
_autoSearch = GUILayout.Toggle(_autoSearch, "自动按文件名搜索", GUILayout.Width(140));
|
||
_autoGroupByPrefix = GUILayout.Toggle(_autoGroupByPrefix, "按前缀自动分组", GUILayout.Width(120));
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("刷新列表", GUILayout.Width(80)))
|
||
RefreshKeyEntries();
|
||
if (GUILayout.Button("注册所有已匹配项", GUILayout.Width(120)))
|
||
RegisterAllMatchedKeys();
|
||
}
|
||
|
||
if (_keyEntries == null)
|
||
RefreshKeyEntries();
|
||
|
||
EditorGUILayout.Space(4);
|
||
|
||
// 列表表头
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
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);
|
||
}
|
||
|
||
var displayList = _onlyShowMissing
|
||
? _keyEntries.Where(e => !e.IsRegistered).ToList()
|
||
: _keyEntries;
|
||
|
||
_keyScrollPos = EditorGUILayout.BeginScrollView(_keyScrollPos, GUILayout.ExpandHeight(true));
|
||
|
||
foreach (var entry in displayList)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(20)))
|
||
{
|
||
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(80));
|
||
EditorGUILayout.LabelField(entry.ExistingAssetPath ?? "—");
|
||
}
|
||
else if (entry.FoundAssetPath != null)
|
||
{
|
||
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(80));
|
||
entry.ManualAsset = (UnityEngine.Object)EditorGUILayout.ObjectField(
|
||
entry.ManualAsset, typeof(UnityEngine.Object), false);
|
||
if (entry.ManualAsset != null)
|
||
{
|
||
if (GUILayout.Button("注册", GUILayout.Width(50)))
|
||
RegisterKeyEntryManual(entry);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
EditorGUILayout.Space(4);
|
||
var total = _keyEntries.Count;
|
||
var registered = _keyEntries.Count(e => e.IsRegistered);
|
||
var matched = _keyEntries.Count(e => !e.IsRegistered && e.FoundAssetPath != null);
|
||
EditorGUILayout.LabelField(
|
||
$"总计 {total} 个 Key | 已注册 {registered} | 已搜索到但未注册 {matched} | 未找到 {total - registered - matched}",
|
||
EditorStyles.miniLabel);
|
||
}
|
||
|
||
// ══ Tab ② 文件夹批量注册 ═════════════════════════════════════════════
|
||
|
||
private void DrawFolderTab()
|
||
{
|
||
EditorGUILayout.LabelField("将指定文件夹中所有符合条件的资产批量注册到 Addressables。", EditorStyles.wordWrappedMiniLabel);
|
||
EditorGUILayout.Space(4);
|
||
|
||
// 文件夹选择
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
_folderAsset = (DefaultAsset)EditorGUILayout.ObjectField(
|
||
"目标文件夹", _folderAsset, typeof(DefaultAsset), false);
|
||
if (_folderAsset != null)
|
||
_folderPath = AssetDatabase.GetAssetPath(_folderAsset);
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(_folderPath) && !AssetDatabase.IsValidFolder(_folderPath))
|
||
{
|
||
EditorGUILayout.HelpBox("请拖入一个文件夹(蓝色图标),不是文件。", MessageType.Warning);
|
||
_folderPath = null;
|
||
}
|
||
|
||
_includeSubfolders = EditorGUILayout.Toggle("包含子文件夹", _includeSubfolders);
|
||
|
||
// 资产类型筛选
|
||
EditorGUILayout.LabelField("资产类型筛选", EditorStyles.boldLabel);
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
_filterPrefab = GUILayout.Toggle(_filterPrefab, "Prefab", GUILayout.Width(70));
|
||
_filterScene = GUILayout.Toggle(_filterScene, "Scene", GUILayout.Width(70));
|
||
_filterSO = GUILayout.Toggle(_filterSO, "SO/Asset", GUILayout.Width(80));
|
||
_filterTexture = GUILayout.Toggle(_filterTexture, "Texture", GUILayout.Width(70));
|
||
_filterAudio = GUILayout.Toggle(_filterAudio, "Audio", GUILayout.Width(70));
|
||
}
|
||
|
||
// 地址格式
|
||
_addressFormat = (AddressFormat)EditorGUILayout.EnumPopup("地址格式", _addressFormat);
|
||
if (_addressFormat == AddressFormat.PrefixPlusFileName ||
|
||
_addressFormat == AddressFormat.PrefixPlusRelativePath)
|
||
{
|
||
_addressPrefix = EditorGUILayout.TextField("地址前缀", _addressPrefix);
|
||
}
|
||
|
||
EditorGUILayout.Space(4);
|
||
|
||
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)))
|
||
ScanFolder();
|
||
GUI.enabled = _folderEntries != null && _folderEntries.Count > 0;
|
||
if (GUILayout.Button("注册所有", GUILayout.Width(100)))
|
||
RegisterAllFolderEntries();
|
||
GUI.enabled = true;
|
||
}
|
||
|
||
if (_folderEntries == null || _folderEntries.Count == 0)
|
||
{
|
||
EditorGUILayout.HelpBox("拖入文件夹后点击「扫描文件夹」。", MessageType.Info);
|
||
return;
|
||
}
|
||
|
||
EditorGUILayout.Space(4);
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
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));
|
||
foreach (var entry in _folderEntries)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18)))
|
||
{
|
||
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(70));
|
||
}
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
int newCount = _folderEntries.Count(e => !e.AlreadyRegistered);
|
||
EditorGUILayout.LabelField($"共 {_folderEntries.Count} 个资产,{newCount} 个待注册", EditorStyles.miniLabel);
|
||
}
|
||
|
||
// ══ Tab ③ 选中资产注册 ════════════════════════════════════════════════
|
||
|
||
private void DrawSelectionTab()
|
||
{
|
||
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();
|
||
if (GUILayout.Button("读取选中项", GUILayout.Width(110)))
|
||
LoadSelection();
|
||
GUI.enabled = _selectionEntries != null && _selectionEntries.Count > 0;
|
||
if (GUILayout.Button("注册所有", GUILayout.Width(100)))
|
||
RegisterAllSelectionEntries();
|
||
GUI.enabled = true;
|
||
}
|
||
|
||
if (_selectionEntries == null || _selectionEntries.Count == 0)
|
||
{
|
||
EditorGUILayout.HelpBox("在 Project 窗口选中资产后点击「读取选中项」。", MessageType.Info);
|
||
return;
|
||
}
|
||
|
||
EditorGUILayout.Space(4);
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
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));
|
||
foreach (var entry in _selectionEntries)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18)))
|
||
{
|
||
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(70));
|
||
}
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
int newCount = _selectionEntries.Count(e => !e.AlreadyRegistered);
|
||
EditorGUILayout.LabelField($"共 {_selectionEntries.Count} 项,{newCount} 待注册", EditorStyles.miniLabel);
|
||
}
|
||
|
||
// ══ 共用选项区 ════════════════════════════════════════════════════════
|
||
|
||
private void DrawSharedOptions()
|
||
{
|
||
EditorGUILayout.LabelField("── 注册选项 ──", EditorStyles.boldLabel);
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
EditorGUILayout.LabelField("目标分组", GUILayout.Width(70));
|
||
// 自动规则模式下,目标分组由规则决定,手动选择无效
|
||
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(52));
|
||
_newLabel = EditorGUILayout.TextField(_newLabel, GUILayout.Width(120));
|
||
GUILayout.Label("(可在规则标签基础上追加)", EditorStyles.miniLabel);
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
_overwriteAddress = GUILayout.Toggle(_overwriteAddress, "已注册的资产也覆盖地址");
|
||
GUILayout.Space(16);
|
||
_applyRulesOnRegister = GUILayout.Toggle(_applyRulesOnRegister, "自动应用分组/标签规则");
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("新建分组…", GUILayout.Width(100)))
|
||
ShowCreateGroupDialog();
|
||
}
|
||
}
|
||
|
||
// ══ 逻辑:Tab ① ══════════════════════════════════════════════════════
|
||
|
||
private void RefreshKeyEntries()
|
||
{
|
||
_keyEntries = new List<KeySyncEntry>();
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (settings == null) return;
|
||
|
||
// 收集所有已注册地址 → 地址字符串 → 资产路径
|
||
var registeredMap = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
foreach (var group in settings.groups)
|
||
{
|
||
if (group == null) continue;
|
||
foreach (var e in group.entries)
|
||
if (e != null) registeredMap[e.address] = e.AssetPath;
|
||
}
|
||
|
||
// 遍历 AddressKeys 常量
|
||
var fields = typeof(AddressKeys)
|
||
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
|
||
.Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string));
|
||
|
||
foreach (var field in fields)
|
||
{
|
||
var key = (string)field.GetRawConstantValue();
|
||
var entry = new KeySyncEntry { FieldName = field.Name, AddressKey = key };
|
||
|
||
if (registeredMap.TryGetValue(key, out string existingPath))
|
||
{
|
||
entry.IsRegistered = true;
|
||
entry.ExistingAssetPath = existingPath;
|
||
}
|
||
else if (_autoSearch)
|
||
{
|
||
// 从地址 key 派生搜索名:取最后一段,去掉前缀(ENM_、VFX_ 等)
|
||
string searchName = DeriveName(key);
|
||
string[] guids = AssetDatabase.FindAssets(searchName);
|
||
if (guids.Length > 0)
|
||
{
|
||
// 优先取名称完全匹配的;排除文件夹、脚本及程序集定义文件
|
||
string best = guids
|
||
.Select(AssetDatabase.GUIDToAssetPath)
|
||
.Where(p => !AssetDatabase.IsValidFolder(p) && IsAddressableAssetPath(p))
|
||
.OrderBy(p => ExactNameMatch(p, searchName) ? 0 : 1)
|
||
.FirstOrDefault();
|
||
entry.FoundAssetPath = best;
|
||
entry.FoundGuid = best != null ? AssetDatabase.AssetPathToGUID(best) : null;
|
||
}
|
||
}
|
||
|
||
_keyEntries.Add(entry);
|
||
}
|
||
}
|
||
|
||
private void RegisterKeyEntry(KeySyncEntry entry)
|
||
{
|
||
if (entry.FoundAssetPath == null) return;
|
||
string groupOverride = _autoGroupByPrefix ? DeriveGroupName(entry.AddressKey) : null;
|
||
Register(entry.FoundGuid, entry.AddressKey, groupOverride);
|
||
entry.IsRegistered = true;
|
||
entry.ExistingAssetPath = entry.FoundAssetPath;
|
||
entry.FoundAssetPath = null;
|
||
SaveSettings();
|
||
}
|
||
|
||
private void RegisterKeyEntryManual(KeySyncEntry entry)
|
||
{
|
||
string path = AssetDatabase.GetAssetPath(entry.ManualAsset);
|
||
string guid = AssetDatabase.AssetPathToGUID(path);
|
||
string groupOverride = _autoGroupByPrefix ? DeriveGroupName(entry.AddressKey) : null;
|
||
Register(guid, entry.AddressKey, groupOverride);
|
||
entry.IsRegistered = true;
|
||
entry.ExistingAssetPath = path;
|
||
entry.ManualAsset = null;
|
||
SaveSettings();
|
||
}
|
||
|
||
private void RegisterAllMatchedKeys()
|
||
{
|
||
int count = 0;
|
||
foreach (var entry in _keyEntries.Where(e => !e.IsRegistered && e.FoundAssetPath != null))
|
||
{
|
||
string groupOverride = _autoGroupByPrefix ? DeriveGroupName(entry.AddressKey) : null;
|
||
Register(entry.FoundGuid, entry.AddressKey, groupOverride);
|
||
entry.IsRegistered = true;
|
||
entry.ExistingAssetPath = entry.FoundAssetPath;
|
||
entry.FoundAssetPath = null;
|
||
count++;
|
||
}
|
||
Debug.Log($"[AddressableBatch] 已注册 {count} 个 AddressKeys 条目。");
|
||
SaveSettings();
|
||
}
|
||
|
||
// ══ 逻辑:Tab ② ══════════════════════════════════════════════════════
|
||
|
||
private void ScanFolder()
|
||
{
|
||
_folderEntries = new List<FolderEntry>();
|
||
if (!AssetDatabase.IsValidFolder(_folderPath)) return;
|
||
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
var registeredGuids = CollectRegisteredGuids(settings);
|
||
|
||
var filters = BuildSearchFilter();
|
||
var option = _includeSubfolders
|
||
? SearchOption.AllDirectories
|
||
: SearchOption.TopDirectoryOnly;
|
||
|
||
string absFolder = Path.GetFullPath(_folderPath);
|
||
|
||
// 收集所有文件
|
||
var allFiles = new List<string>();
|
||
foreach (string filter in filters)
|
||
allFiles.AddRange(Directory.GetFiles(absFolder, filter, option));
|
||
|
||
try
|
||
{
|
||
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('\\', '/');
|
||
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 = addr,
|
||
AlreadyRegistered = registeredGuids.Contains(guid),
|
||
PredictedGroup = AddressableRules.GetExpectedGroup(addr),
|
||
PredictedLabels = FormatLabels(AddressableRules.GetExpectedLabels(addr)),
|
||
});
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
}
|
||
|
||
// 去重(多个 filter 可能匹配同一文件)
|
||
_folderEntries = _folderEntries
|
||
.GroupBy(e => e.Guid)
|
||
.Select(g => g.First())
|
||
.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;
|
||
|
||
// 事件频道 ScriptableObject(EVT_ 前缀)
|
||
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;
|
||
foreach (var entry in _folderEntries)
|
||
{
|
||
if (entry.AlreadyRegistered && !_overwriteAddress) continue;
|
||
Register(entry.Guid, entry.Address);
|
||
entry.AlreadyRegistered = true;
|
||
count++;
|
||
}
|
||
Debug.Log($"[AddressableBatch] 文件夹批量注册完成,共注册 {count} 个资产。");
|
||
SaveSettings();
|
||
}
|
||
|
||
// ══ 逻辑:Tab ③ ══════════════════════════════════════════════════════
|
||
|
||
private void LoadSelection()
|
||
{
|
||
_selectionEntries = new List<SelectionEntry>();
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
var registeredGuids = CollectRegisteredGuids(settings);
|
||
|
||
foreach (string guid in Selection.assetGUIDs)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
|
||
if (AssetDatabase.IsValidFolder(path))
|
||
{
|
||
// 展开文件夹
|
||
foreach (string sub in AssetDatabase.FindAssets("", new[] { path }))
|
||
{
|
||
string subPath = AssetDatabase.GUIDToAssetPath(sub);
|
||
if (!AssetDatabase.IsValidFolder(subPath))
|
||
AddSelectionEntry(sub, subPath, registeredGuids);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
AddSelectionEntry(guid, path, registeredGuids);
|
||
}
|
||
}
|
||
|
||
_selectionEntries = _selectionEntries
|
||
.GroupBy(e => e.Guid)
|
||
.Select(g => g.First())
|
||
.ToList();
|
||
}
|
||
|
||
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 = addr,
|
||
AlreadyRegistered = registeredGuids.Contains(guid),
|
||
PredictedGroup = AddressableRules.GetExpectedGroup(addr),
|
||
PredictedLabels = FormatLabels(AddressableRules.GetExpectedLabels(addr)),
|
||
});
|
||
}
|
||
|
||
private void RegisterAllSelectionEntries()
|
||
{
|
||
int count = 0;
|
||
foreach (var entry in _selectionEntries)
|
||
{
|
||
if (entry.AlreadyRegistered && !_overwriteAddress) continue;
|
||
Register(entry.Guid, entry.Address);
|
||
entry.AlreadyRegistered = true;
|
||
count++;
|
||
}
|
||
Debug.Log($"[AddressableBatch] 选中资产注册完成,共注册 {count} 个资产。");
|
||
SaveSettings();
|
||
}
|
||
|
||
// ══ 核心注册 API ═════════════════════════════════════════════════════
|
||
|
||
private void Register(string guid, string address, string groupNameOverride = null)
|
||
{
|
||
if (string.IsNullOrEmpty(guid)) return;
|
||
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
|
||
// 重复地址检查:同一 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;
|
||
|
||
AddressableAssetEntry entry = settings.FindAssetEntry(guid) ??
|
||
settings.CreateOrMoveEntry(guid, group, false, false);
|
||
|
||
if (entry == null) return;
|
||
|
||
if (_overwriteAddress || entry.address != address)
|
||
entry.address = address;
|
||
|
||
settings.MoveEntry(entry, group, false, false);
|
||
|
||
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)
|
||
{
|
||
var existing = settings.groups.FirstOrDefault(g => g != null && g.name == groupName);
|
||
if (existing != null) return existing;
|
||
|
||
var template = settings.GroupTemplateObjects.FirstOrDefault() as AddressableAssetGroupTemplate;
|
||
var newGroup = settings.CreateGroup(groupName, false, false, true,
|
||
template != null ? new List<AddressableAssetGroupSchema>(template.SchemaObjects) : null);
|
||
|
||
if (newGroup != null)
|
||
{
|
||
RefreshGroupNames();
|
||
Debug.Log($"[AddressableBatch] 已自动创建分组:{groupName}");
|
||
}
|
||
return newGroup ?? settings.DefaultGroup;
|
||
}
|
||
|
||
private AddressableAssetGroup GetTargetGroup(AddressableAssetSettings settings)
|
||
{
|
||
RefreshGroupNames();
|
||
if (_groupNames == null || _groupNames.Length == 0) return settings.DefaultGroup;
|
||
string name = _groupNames[Mathf.Clamp(_targetGroupIndex, 0, _groupNames.Length - 1)];
|
||
return settings.groups.FirstOrDefault(g => g != null && g.name == name)
|
||
?? settings.DefaultGroup;
|
||
}
|
||
|
||
private static void SaveSettings()
|
||
{
|
||
AssetDatabase.SaveAssets();
|
||
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
|
||
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);
|
||
if (string.IsNullOrWhiteSpace(_newGroupName)) return;
|
||
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
var template = settings.GroupTemplateObjects.FirstOrDefault() as AddressableAssetGroupTemplate;
|
||
var newGroup = settings.CreateGroup(_newGroupName.Trim(), false, false, true,
|
||
template != null ? new List<AddressableAssetGroupSchema>(template.SchemaObjects) : null);
|
||
|
||
if (newGroup != null)
|
||
{
|
||
Debug.Log($"[AddressableBatch] 已创建分组:{newGroup.name}");
|
||
RefreshGroupNames();
|
||
_targetGroupIndex = Array.IndexOf(_groupNames, newGroup.name);
|
||
}
|
||
}
|
||
|
||
// ══ 辅助 ══════════════════════════════════════════════════════════════
|
||
|
||
private void RefreshGroupNames()
|
||
{
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (settings == null) { _groupNames = Array.Empty<string>(); return; }
|
||
_groupNames = settings.groups
|
||
.Where(g => g != null)
|
||
.Select(g => g.name)
|
||
.ToArray();
|
||
_targetGroupIndex = Mathf.Clamp(_targetGroupIndex, 0, Mathf.Max(0, _groupNames.Length - 1));
|
||
}
|
||
|
||
private static HashSet<string> CollectRegisteredGuids(AddressableAssetSettings settings)
|
||
{
|
||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||
if (settings == null) return set;
|
||
foreach (var group in settings.groups)
|
||
{
|
||
if (group == null) continue;
|
||
foreach (var e in group.entries)
|
||
if (e != null) set.Add(e.guid);
|
||
}
|
||
return set;
|
||
}
|
||
|
||
private string BuildAddress(string assetPath)
|
||
{
|
||
string fileName = Path.GetFileNameWithoutExtension(assetPath);
|
||
string relativePath = MakeRelativePath(assetPath, _folderPath);
|
||
|
||
return _addressFormat switch
|
||
{
|
||
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,
|
||
};
|
||
}
|
||
|
||
private static string MakeRelativePath(string assetPath, string baseFolderPath)
|
||
{
|
||
if (string.IsNullOrEmpty(baseFolderPath)) return assetPath;
|
||
return assetPath.StartsWith(baseFolderPath)
|
||
? assetPath.Substring(baseFolderPath.Length).TrimStart('/')
|
||
: assetPath;
|
||
}
|
||
|
||
private List<string> BuildSearchFilter()
|
||
{
|
||
var list = new List<string>();
|
||
if (_filterPrefab) list.Add("*.prefab");
|
||
if (_filterScene) list.Add("*.unity");
|
||
if (_filterSO) list.Add("*.asset");
|
||
if (_filterTexture) { list.Add("*.png"); list.Add("*.jpg"); list.Add("*.tga"); }
|
||
if (_filterAudio) { list.Add("*.mp3"); list.Add("*.wav"); list.Add("*.ogg"); }
|
||
if (list.Count == 0) list.Add("*.*");
|
||
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)
|
||
{
|
||
// 取最后一个 '/' 之后的部分(Config/FootstepCatalog → FootstepCatalog)
|
||
int slash = key.LastIndexOf('/');
|
||
string last = slash >= 0 ? key.Substring(slash + 1) : key;
|
||
|
||
// 去掉前缀(ENM_, VFX_, PROJ_ 等):找第一个 '_' 并截断前缀
|
||
int underscore = last.IndexOf('_');
|
||
return underscore >= 0 && underscore < last.Length - 1
|
||
? last.Substring(underscore + 1)
|
||
: last;
|
||
}
|
||
|
||
/// <summary>根据 AddressKey 前缀返回建议分组名,未匹配时返回 null(回退到手动选定分组)。</summary>
|
||
private static string DeriveGroupName(string key)
|
||
=> AddressableRules.GetExpectedGroup(key);
|
||
|
||
private static bool ExactNameMatch(string assetPath, string searchName)
|
||
{
|
||
string name = Path.GetFileNameWithoutExtension(assetPath);
|
||
return string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private void InitStyles()
|
||
{
|
||
if (_stylesInitialized) return;
|
||
_headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 };
|
||
_okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(0.2f, 0.8f, 0.2f) } };
|
||
_warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(1f, 0.6f, 0.1f) } };
|
||
_boldStyle = new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Bold };
|
||
_stylesInitialized = true;
|
||
}
|
||
|
||
// ══ 数据结构 ══════════════════════════════════════════════════════════
|
||
|
||
private class KeySyncEntry
|
||
{
|
||
public string FieldName;
|
||
public string AddressKey;
|
||
public bool IsRegistered;
|
||
public string ExistingAssetPath;
|
||
public string FoundAssetPath;
|
||
public string FoundGuid;
|
||
public UnityEngine.Object ManualAsset;
|
||
}
|
||
|
||
private class FolderEntry
|
||
{
|
||
public string AssetPath;
|
||
public string Guid;
|
||
public string Address;
|
||
public bool AlreadyRegistered;
|
||
public string PredictedGroup;
|
||
public string PredictedLabels;
|
||
}
|
||
|
||
private class SelectionEntry
|
||
{
|
||
public string AssetPath;
|
||
public string Guid;
|
||
public string Address;
|
||
public bool AlreadyRegistered;
|
||
public string PredictedGroup;
|
||
public string PredictedLabels;
|
||
}
|
||
|
||
private enum AddressFormat
|
||
{
|
||
[InspectorName("文件名(推荐)")] FileName,
|
||
[InspectorName("完整 Asset 路径")] FullAssetPath,
|
||
[InspectorName("相对于选定文件夹")] RelativeToFolder,
|
||
[InspectorName("前缀 + 文件名")] PrefixPlusFileName,
|
||
[InspectorName("前缀 + 相对路径")] PrefixPlusRelativePath,
|
||
}
|
||
}
|
||
|
||
// ── 轻量输入对话框(避免依赖 EditorInputDialog 插件)─────────────────────
|
||
internal static class EditorInputDialog
|
||
{
|
||
/// <summary>弹出单行文本输入对话框。返回用户输入,取消则返回原始默认值。</summary>
|
||
public static string Show(string title, string message, string defaultValue = "")
|
||
{
|
||
string result = defaultValue;
|
||
|
||
// 通过简单的 EditorWindow 实现
|
||
var win = ScriptableObject.CreateInstance<InputDialogWindow>();
|
||
win.Init(title, message, defaultValue, v => { result = v; });
|
||
win.ShowModal();
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
internal class InputDialogWindow : EditorWindow
|
||
{
|
||
private string _title;
|
||
private string _message;
|
||
private string _value;
|
||
private Action<string> _onConfirm;
|
||
|
||
public void Init(string title, string message, string defaultValue, Action<string> onConfirm)
|
||
{
|
||
titleContent = new GUIContent(title);
|
||
_title = title;
|
||
_message = message;
|
||
_value = defaultValue;
|
||
_onConfirm = onConfirm;
|
||
minSize = maxSize = new Vector2(340, 110);
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
EditorGUILayout.Space(8);
|
||
EditorGUILayout.LabelField(_message);
|
||
GUI.SetNextControlName("input");
|
||
_value = EditorGUILayout.TextField(_value);
|
||
EditorGUI.FocusTextInControl("input");
|
||
|
||
EditorGUILayout.Space(8);
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("取消", GUILayout.Width(70)))
|
||
Close();
|
||
if (GUILayout.Button("确认", GUILayout.Width(70)))
|
||
{
|
||
_onConfirm?.Invoke(_value);
|
||
Close();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|