feat: Addressables rules/sync tools, UI fixes, AddressKeys update
- Add AddressableRules.cs: single source of truth for prefix->group and prefix->label rules - Add AddressableRuleSyncWindow.cs: scan/fix/export-CSV tool (BaseGames > Addressables > Rule Sync) - AddressableBatchTool.cs: delegate DeriveGroupName to AddressableRules, remove duplicate PrefixGroupMap - AddressKeys.cs: add Labels constants (Preload, Poolable, Enemy, BGM, SFX, Charms, Config, Weapon) - Docs/Standards/AddressablesLabelSpec.md: new label naming & assignment spec - Docs/Standards/AssetFolderSpec.md: update Addressables group strategy section - SplashScreenController.cs: fix MainMenu loading flow - BootFlowSetupWizard.cs / SceneScaffoldTools.cs: scene scaffold fixes - PlayerInputActions: set UI/Point to Pass-Through type - Persistent.unity: add BootSequencer to auto-load MainMenu on play - EditorBuildSettings.asset: register scenes for build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -57,14 +57,29 @@ namespace BaseGames.Core.Assets
|
||||
/// Addressable 标签常量(用于批量加载)。
|
||||
/// 注意:这里是标签名称而非资产地址,不会被 AddressKeyValidator 校验。
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Addressable Label 常量(用于批量加载与预热)。
|
||||
/// 代码中引用 Label 时必须使用此处的常量,禁止硬编码字符串。
|
||||
/// 完整说明见 Docs/Standards/AddressablesLabelSpec.md。
|
||||
/// </summary>
|
||||
public static class Labels
|
||||
{
|
||||
public const string Enemy = "Enemy";
|
||||
public const string Poolable = "Poolable";
|
||||
public const string BGM = "BGM";
|
||||
public const string Charms = "Charms";
|
||||
/// <summary>游戏启动时预热下载的资产标签(BootSequencer 使用)。</summary>
|
||||
/// <summary>游戏启动时通过 DownloadDependenciesAsync 预热下载依赖(BootSequencer 使用)。</summary>
|
||||
public const string Preload = "Preload";
|
||||
/// <summary>纳入 GlobalObjectPool 对象池管理的 Prefab(VFX、投射物、收集物等)。</summary>
|
||||
public const string Poolable = "Poolable";
|
||||
/// <summary>所有敌人顶级 Prefab,用于区域 Spawner 批量加载。</summary>
|
||||
public const string Enemy = "Enemy";
|
||||
/// <summary>背景音乐 AudioClip / FMOD bank 引用 SO,AudioManager 批量建立 BGM 索引。</summary>
|
||||
public const string BGM = "BGM";
|
||||
/// <summary>音效 AudioClip / SFX 配置 SO,AudioManager 批量建立 SFX 索引。</summary>
|
||||
public const string SFX = "SFX";
|
||||
/// <summary>所有护身符配置 SO(CHM_*.asset),EquipmentManager 批量加载护身符列表。</summary>
|
||||
public const string Charms = "Charms";
|
||||
/// <summary>运行时动态加载的配置类 SO(Inspector 直接引用的 SO 不加此标签)。</summary>
|
||||
public const string Config = "Config";
|
||||
/// <summary>所有武器 Prefab(WPN_*.prefab),玩家换形态时批量加载武器列表。</summary>
|
||||
public const string Weapon = "Weapon";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
Assets/_Game/Scripts/Core/BootSequencer.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/BootSequencer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da575c82357778a4baa083a9afcb1b8a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -40,18 +40,7 @@ namespace BaseGames.Editor
|
||||
private bool _autoGroupByPrefix = true; // 按 Key 前缀自动选/建分组
|
||||
|
||||
// Key 前缀 → 分组名称映射(Tab ① 自动分组用)
|
||||
private static readonly (string Prefix, string GroupName)[] PrefixGroupMap =
|
||||
{
|
||||
("Scene_", "Scenes"),
|
||||
("PLY_", "Player"),
|
||||
("ENM_", "Enemies"),
|
||||
("PROJ_", "Projectiles"),
|
||||
("VFX_", "VFX"),
|
||||
("UI_", "UI"),
|
||||
("COL_", "Collectibles"),
|
||||
("WPN_", "Weapons"),
|
||||
("Config/", "Config"),
|
||||
};
|
||||
// 规范数据统一来自 AddressableRules,此处不再声明本地副本。
|
||||
|
||||
// Tab ②
|
||||
private DefaultAsset _folderAsset;
|
||||
@@ -743,12 +732,7 @@ namespace BaseGames.Editor
|
||||
|
||||
/// <summary>根据 AddressKey 前缀返回建议分组名,未匹配时返回 null(回退到手动选定分组)。</summary>
|
||||
private static string DeriveGroupName(string key)
|
||||
{
|
||||
foreach (var (prefix, groupName) in PrefixGroupMap)
|
||||
if (key.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return groupName;
|
||||
return null;
|
||||
}
|
||||
=> AddressableRules.GetExpectedGroup(key);
|
||||
|
||||
private static bool ExactNameMatch(string assetPath, string searchName)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
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;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Addressable 规则同步窗口。
|
||||
///
|
||||
/// 功能:
|
||||
/// 1. 扫描所有已注册的 Addressable 资产
|
||||
/// 2. 根据 <see cref="AddressableRules"/> 中的规则计算期望分组与期望标签
|
||||
/// 3. 对比实际值,显示所有不符合规范的条目(分组错误 / 标签缺失 / 标签多余)
|
||||
/// 4. 一键自动修复全部问题
|
||||
/// 5. 导出 CSV 报告供存档或 Code Review
|
||||
///
|
||||
/// 菜单:BaseGames → Addressables → Rule Sync
|
||||
/// </summary>
|
||||
public class AddressableRuleSyncWindow : EditorWindow
|
||||
{
|
||||
// ── 内部数据结构 ───────────────────────────────────────────────────────
|
||||
|
||||
private enum IssueKind { None, WrongGroup, MissingLabel, ExtraLabel }
|
||||
|
||||
private class EntryReport
|
||||
{
|
||||
public string Address;
|
||||
public string AssetPath;
|
||||
public string CurrentGroup;
|
||||
public string ExpectedGroup; // null = 规则未覆盖,维持现状
|
||||
public string[] CurrentLabels;
|
||||
public string[] ExpectedLabels;
|
||||
public string[] MissingLabels; // 应有但没有
|
||||
public string[] ExtraLabels; // 有但不应有
|
||||
public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup;
|
||||
public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0;
|
||||
public bool IsOk => GroupOk && LabelsOk;
|
||||
}
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private List<EntryReport> _reports = new();
|
||||
private Vector2 _scrollPos;
|
||||
private bool _showOk = false;
|
||||
private bool _scanned = false;
|
||||
private string _searchFilter = "";
|
||||
|
||||
// ── 样式(惰性初始化)────────────────────────────────────────────────
|
||||
|
||||
private GUIStyle _okStyle;
|
||||
private GUIStyle _warnStyle;
|
||||
private GUIStyle _errorStyle;
|
||||
private GUIStyle _boldStyle;
|
||||
private GUIStyle _rowEven;
|
||||
private GUIStyle _rowOdd;
|
||||
private bool _stylesReady;
|
||||
|
||||
// ── 颜色 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly Color ColOk = new(0.20f, 0.78f, 0.35f, 1f);
|
||||
private static readonly Color ColWarn = new(0.95f, 0.75f, 0.10f, 1f);
|
||||
private static readonly Color ColError = new(0.90f, 0.25f, 0.20f, 1f);
|
||||
private static readonly Color ColRowEven = new(0.22f, 0.22f, 0.22f, 0.4f);
|
||||
|
||||
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Addressables/Rule Sync", priority = 110)]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync");
|
||||
win.minSize = new Vector2(900, 520);
|
||||
win.Show();
|
||||
}
|
||||
|
||||
// ── GUI ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EnsureStyles();
|
||||
|
||||
if (AddressableAssetSettingsDefaultObject.Settings == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"Addressable Settings 未初始化。\n" +
|
||||
"请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。",
|
||||
MessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawToolbar();
|
||||
DrawStats();
|
||||
DrawTable();
|
||||
DrawFooter();
|
||||
}
|
||||
|
||||
// ── 工具栏 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
Scan();
|
||||
|
||||
GUILayout.Space(8);
|
||||
_showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80));
|
||||
GUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.LabelField("搜索:", GUILayout.Width(42));
|
||||
_searchFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField,
|
||||
GUILayout.Width(200));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = _scanned && _reports.Any(r => !r.IsOk);
|
||||
if (GUILayout.Button("✦ 修复所有问题", EditorStyles.toolbarButton, GUILayout.Width(120)))
|
||||
FixAll();
|
||||
GUI.enabled = _scanned;
|
||||
if (GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
ExportCsv();
|
||||
GUI.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 统计行 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawStats()
|
||||
{
|
||||
if (!_scanned) return;
|
||||
|
||||
int total = _reports.Count;
|
||||
int ok = _reports.Count(r => r.IsOk);
|
||||
int issues = total - ok;
|
||||
int wrongGrp = _reports.Count(r => !r.GroupOk);
|
||||
int misLabel = _reports.Count(r => r.MissingLabels.Length > 0);
|
||||
int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0);
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label($"共 {total} 条目", EditorStyles.miniLabel);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"✅ 正常 {ok}", ColOk);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"⚠ 问题 {issues}", issues > 0 ? ColWarn : ColOk);
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 标签多余 {extLabel}",
|
||||
EditorStyles.miniLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
EditorGUILayout.Space(2);
|
||||
}
|
||||
|
||||
// ── 主表格 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawTable()
|
||||
{
|
||||
// 表头
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
GUILayout.Label("Address", _boldStyle, GUILayout.Width(200));
|
||||
GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(130));
|
||||
GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(130));
|
||||
GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(140));
|
||||
GUILayout.Label("多余标签", _boldStyle, GUILayout.Width(120));
|
||||
GUILayout.Label("状态", _boldStyle, GUILayout.Width(80));
|
||||
}
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
|
||||
|
||||
if (!_scanned)
|
||||
{
|
||||
EditorGUILayout.HelpBox("点击「扫描」按钮开始分析已注册的 Addressable 资产。", MessageType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = _reports
|
||||
.Where(r => _showOk || !r.IsOk)
|
||||
.Where(r => string.IsNullOrEmpty(_searchFilter)
|
||||
|| r.Address.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
.ToList();
|
||||
|
||||
if (display.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
_showOk ? "没有匹配搜索条件的条目。" : "✅ 所有资产均符合规范!",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
for (int i = 0; i < display.Count; i++)
|
||||
DrawRow(display[i], i);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawRow(EntryReport r, int idx)
|
||||
{
|
||||
var bg = idx % 2 == 0 ? _rowEven : GUIStyle.none;
|
||||
using (new EditorGUILayout.HorizontalScope(bg, GUILayout.Height(20)))
|
||||
{
|
||||
// Address(点击可 Ping)
|
||||
if (GUILayout.Button(r.Address, EditorStyles.linkLabel, GUILayout.Width(200)))
|
||||
PingAsset(r.AssetPath);
|
||||
|
||||
// 当前分组
|
||||
var grpColor = r.GroupOk ? ColOk : ColError;
|
||||
DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(130));
|
||||
|
||||
// 期望分组
|
||||
var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)";
|
||||
var expGrpColor = r.GroupOk ? ColOk : ColWarn;
|
||||
DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(130));
|
||||
|
||||
// 缺失标签
|
||||
var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—";
|
||||
DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(140));
|
||||
|
||||
// 多余标签
|
||||
var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—";
|
||||
DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(120));
|
||||
|
||||
// 状态 + 单条修复按钮
|
||||
if (r.IsOk)
|
||||
{
|
||||
DrawColoredLabel("✅ 正常", ColOk, GUILayout.Width(80));
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawColoredLabel("⚠ 需修复", ColWarn, GUILayout.Width(60));
|
||||
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
|
||||
FixEntry(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 底栏 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.HelpBox(
|
||||
"规则来源:Docs/Standards/AddressablesLabelSpec.md §3 分组规则:AssetFolderSpec.md §8.1\n" +
|
||||
"「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产(请用 Addressable Batch Tool)。",
|
||||
MessageType.None);
|
||||
}
|
||||
|
||||
// ── 扫描逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Scan()
|
||||
{
|
||||
_reports.Clear();
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return;
|
||||
|
||||
// 收集所有全局标签供"多余标签"判断
|
||||
var allKnownLabels = new HashSet<string>(settings.GetLabels(), StringComparer.Ordinal);
|
||||
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group == null) continue;
|
||||
foreach (var entry in group.entries)
|
||||
{
|
||||
if (entry == null) continue;
|
||||
|
||||
var address = entry.address;
|
||||
var expectedGroup = AddressableRules.GetExpectedGroup(address);
|
||||
var expectedLbls = AddressableRules.GetExpectedLabels(address);
|
||||
var currentLbls = entry.labels.ToArray();
|
||||
|
||||
var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray();
|
||||
var extra = currentLbls.Except(expectedLbls, StringComparer.Ordinal).ToArray();
|
||||
|
||||
_reports.Add(new EntryReport
|
||||
{
|
||||
Address = address,
|
||||
AssetPath = entry.AssetPath,
|
||||
CurrentGroup = group.name,
|
||||
ExpectedGroup = expectedGroup,
|
||||
CurrentLabels = currentLbls,
|
||||
ExpectedLabels = expectedLbls,
|
||||
MissingLabels = missing,
|
||||
ExtraLabels = extra,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 问题项排前面,正常项排后面;同类按 Address 字母序
|
||||
_reports = _reports
|
||||
.OrderBy(r => r.IsOk)
|
||||
.ThenBy(r => r.Address, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
_scanned = true;
|
||||
Repaint();
|
||||
Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," +
|
||||
$"{_reports.Count(r => !r.IsOk)} 个需要修复。");
|
||||
}
|
||||
|
||||
// ── 修复逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void FixAll()
|
||||
{
|
||||
var issues = _reports.Where(r => !r.IsOk).ToList();
|
||||
if (issues.Count == 0) return;
|
||||
|
||||
int fixedCount = 0;
|
||||
foreach (var r in issues)
|
||||
{
|
||||
if (FixEntry(r)) fixedCount++;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
Scan(); // 修复后重新扫描以更新结果
|
||||
Debug.Log($"[AddressableRuleSync] 修复完成:共处理 {fixedCount} 个条目。");
|
||||
}
|
||||
|
||||
private bool FixEntry(EntryReport r)
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return false;
|
||||
|
||||
var entry = FindEntry(settings, r.Address);
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogWarning($"[AddressableRuleSync] 找不到条目:{r.Address}");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// 修复分组
|
||||
if (!r.GroupOk && r.ExpectedGroup != null)
|
||||
{
|
||||
var targetGroup = GetOrCreateGroup(settings, r.ExpectedGroup);
|
||||
if (targetGroup != null && entry.parentGroup != targetGroup)
|
||||
{
|
||||
settings.MoveEntry(entry, targetGroup, false, false);
|
||||
r.CurrentGroup = r.ExpectedGroup;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加缺失标签
|
||||
foreach (var lbl in r.MissingLabels)
|
||||
{
|
||||
EnsureLabelExists(settings, lbl);
|
||||
entry.SetLabel(lbl, true, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 移除多余标签
|
||||
foreach (var lbl in r.ExtraLabels)
|
||||
{
|
||||
entry.SetLabel(lbl, false, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ── 导出 CSV ──────────────────────────────────────────────────────────
|
||||
|
||||
private void ExportCsv()
|
||||
{
|
||||
if (_reports.Count == 0) return;
|
||||
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"导出 Addressable Rule 报告", "", "AddressableRuleReport.csv", "csv");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Address,CurrentGroup,ExpectedGroup,GroupOk,MissingLabels,ExtraLabels,Status");
|
||||
|
||||
foreach (var r in _reports)
|
||||
{
|
||||
var status = r.IsOk ? "OK" : "ISSUE";
|
||||
sb.AppendLine(
|
||||
$"\"{r.Address}\"," +
|
||||
$"\"{r.CurrentGroup}\"," +
|
||||
$"\"{r.ExpectedGroup ?? "(uncovered)"}\"," +
|
||||
$"{r.GroupOk}," +
|
||||
$"\"{string.Join(";", r.MissingLabels)}\"," +
|
||||
$"\"{string.Join(";", r.ExtraLabels)}\"," +
|
||||
$"{status}");
|
||||
}
|
||||
|
||||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||||
Debug.Log($"[AddressableRuleSync] CSV 报告已导出:{path}");
|
||||
}
|
||||
|
||||
// ── 辅助方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static AddressableAssetEntry FindEntry(AddressableAssetSettings settings, string address)
|
||||
{
|
||||
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 static 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)
|
||||
Debug.Log($"[AddressableRuleSync] 已自动创建分组:{groupName}");
|
||||
|
||||
return newGroup ?? settings.DefaultGroup;
|
||||
}
|
||||
|
||||
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
|
||||
{
|
||||
var labels = settings.GetLabels();
|
||||
if (!labels.Contains(label))
|
||||
{
|
||||
settings.AddLabel(label, true);
|
||||
Debug.Log($"[AddressableRuleSync] 已创建标签:{label}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveSettings()
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
|
||||
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
|
||||
}
|
||||
|
||||
private static void PingAsset(string assetPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetPath)) return;
|
||||
var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||||
}
|
||||
|
||||
// ── 样式初始化 ────────────────────────────────────────────────────────
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_stylesReady) return;
|
||||
|
||||
_boldStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
|
||||
_okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColOk } };
|
||||
_warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColWarn } };
|
||||
_errorStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColError } };
|
||||
|
||||
_rowEven = new GUIStyle();
|
||||
_rowEven.normal.background = MakeTexture(1, 1, ColRowEven);
|
||||
|
||||
_stylesReady = true;
|
||||
}
|
||||
|
||||
private void DrawColoredLabel(string text, Color color, params GUILayoutOption[] options)
|
||||
{
|
||||
var prev = GUI.color;
|
||||
GUI.color = color;
|
||||
GUILayout.Label(text, EditorStyles.miniLabel, options);
|
||||
GUI.color = prev;
|
||||
}
|
||||
|
||||
private static Texture2D MakeTexture(int width, int height, Color color)
|
||||
{
|
||||
var tex = new Texture2D(width, height);
|
||||
tex.SetPixel(0, 0, color);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
Assets/_Game/Scripts/Editor/Addressables/AddressableRules.cs
Normal file
122
Assets/_Game/Scripts/Editor/Addressables/AddressableRules.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Core.Assets;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Addressable 分组与标签的权威规则数据。
|
||||
/// 规范来源:<c>Docs/Standards/AddressablesLabelSpec.md §3</c> 与
|
||||
/// <c>Docs/Standards/AssetFolderSpec.md §8</c>。
|
||||
///
|
||||
/// <see cref="AddressableBatchTool"/> 和 <see cref="AddressableRuleSyncWindow"/> 均引用此处,
|
||||
/// 保证两个工具的分组/标签判断完全一致,修改规则时只需改这一处。
|
||||
/// </summary>
|
||||
public static class AddressableRules
|
||||
{
|
||||
// ── 前缀 → 分组名 ──────────────────────────────────────────────────────
|
||||
// 规则:按 AssetFolderSpec §8.1 Group 划分策略。
|
||||
// 顺序:更长/更具体的前缀必须排在更短/更泛化的前缀之前,否则短前缀会先匹配。
|
||||
// 特殊:Room_/Boss_ 地址的分组名在运行时动态计算,见 GetExpectedGroup()。
|
||||
public static readonly (string Prefix, string Group)[] PrefixGroupMap =
|
||||
{
|
||||
("Scene_", "Scenes"),
|
||||
("PLY_", "Player"),
|
||||
("WPN_", "Player"), // 武器与玩家 Prefab 同组(AssetFolderSpec §8.1)
|
||||
("ENM_", "Enemies"),
|
||||
("PROJ_", "Projectiles"),
|
||||
("VFX_", "VFX_Common"), // 通用特效组(AssetFolderSpec §8.1)
|
||||
("UI_", "UI"),
|
||||
("COL_", "Collectibles"),
|
||||
("CHM_", "Config"), // 护身符 SO 归入 Config 组(AddressablesLabelSpec §3.9)
|
||||
("Config/", "Config"),
|
||||
("AUD_", "Audio_Music"),
|
||||
};
|
||||
|
||||
// ── 精确地址 → 标签(优先级高于前缀规则)────────────────────────────────
|
||||
private static readonly Dictionary<string, string[]> ExactLabelMap =
|
||||
new(StringComparer.Ordinal)
|
||||
{
|
||||
// Scene_MainMenu 是唯一需要 Preload 的场景
|
||||
{ AddressKeys.SceneMainMenu, new[] { AddressKeys.Labels.Preload } },
|
||||
// Persistent 场景无需标签(随引擎启动,不通过 label 批量加载)
|
||||
{ AddressKeys.ScenePersistent, Array.Empty<string>() },
|
||||
// FloatingDamageText 是 Poolable + Preload(UI_ 前缀通常无 label,此处例外)
|
||||
{ AddressKeys.PrefabUIFloatingDmgText, new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload } },
|
||||
// FootstepCatalog 是首帧必须可用的配置
|
||||
{ AddressKeys.DataFootstepCatalog, new[] { AddressKeys.Labels.Config, AddressKeys.Labels.Preload } },
|
||||
};
|
||||
|
||||
// ── 前缀 → 标签列表 ─────────────────────────────────────────────────────
|
||||
// 顺序:更具体的前缀(AUD_BGM_)在更泛化的前缀(AUD_)之前。
|
||||
private static readonly (string Prefix, string[] Labels)[] PrefixLabelMap =
|
||||
{
|
||||
("AUD_BGM_", new[] { AddressKeys.Labels.BGM }),
|
||||
("AUD_SFX_", new[] { AddressKeys.Labels.SFX }),
|
||||
("AUD_", new[] { AddressKeys.Labels.BGM }), // 未细分音频默认归 BGM
|
||||
("Scene_", Array.Empty<string>()), // 除 MainMenu 外场景无 label
|
||||
("PLY_", new[] { AddressKeys.Labels.Preload }),
|
||||
("WPN_", new[] { AddressKeys.Labels.Weapon, AddressKeys.Labels.Preload }),
|
||||
("ENM_", new[] { AddressKeys.Labels.Enemy }),
|
||||
("PROJ_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }),
|
||||
("VFX_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }),
|
||||
("UI_", Array.Empty<string>()), // 除 FloatingDamageText 外 UI 无默认 label
|
||||
("COL_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }),
|
||||
("CHM_", new[] { AddressKeys.Labels.Charms }),
|
||||
("Config/", new[] { AddressKeys.Labels.Config }),
|
||||
};
|
||||
|
||||
// ── 公开 API ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 根据 Addressable 地址字符串返回期望的分组名称。
|
||||
/// <list type="bullet">
|
||||
/// <item><c>Room_Forest_01</c> → <c>Room_Forest</c>(动态计算)</item>
|
||||
/// <item><c>Boss_CaoZhi</c> → <c>Boss_CaoZhi</c>(动态计算)</item>
|
||||
/// <item>无匹配前缀时返回 <c>null</c>(调用方可回退到 Default Group)</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static string GetExpectedGroup(string address)
|
||||
{
|
||||
if (string.IsNullOrEmpty(address)) return null;
|
||||
|
||||
// Room_/Boss_ 的分组名在地址中动态编码
|
||||
if (address.StartsWith("Room_", StringComparison.Ordinal))
|
||||
{
|
||||
var parts = address.Split('_');
|
||||
return parts.Length >= 2 ? $"Room_{parts[1]}" : "Room_Unknown";
|
||||
}
|
||||
if (address.StartsWith("Boss_", StringComparison.Ordinal))
|
||||
{
|
||||
// Boss_CaoZhi → 整个地址即为分组名(与 AssetFolderSpec §8.1 一致)
|
||||
return address;
|
||||
}
|
||||
|
||||
foreach (var (prefix, group) in PrefixGroupMap)
|
||||
{
|
||||
if (address.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return group;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 Addressable 地址字符串返回期望的标签集合。
|
||||
/// 精确地址匹配优先,其次前缀匹配,均无匹配时返回空数组。
|
||||
/// </summary>
|
||||
public static string[] GetExpectedLabels(string address)
|
||||
{
|
||||
if (string.IsNullOrEmpty(address)) return Array.Empty<string>();
|
||||
|
||||
if (ExactLabelMap.TryGetValue(address, out var exact))
|
||||
return exact;
|
||||
|
||||
foreach (var (prefix, labels) in PrefixLabelMap)
|
||||
{
|
||||
if (address.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return labels;
|
||||
}
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.MainMenu;
|
||||
using BaseGames.UI.Menus;
|
||||
using BaseGames.UI.Splash;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
@@ -27,8 +28,8 @@ namespace BaseGames.Editor
|
||||
{
|
||||
// ── 常量 ──────────────────────────────────────────────────────────────
|
||||
private const string EventRoot = "Assets/_Game/Data/Events";
|
||||
private const string PersistentName = "Scene_Persistent";
|
||||
private const string MainMenuName = "Scene_MainMenu";
|
||||
private const string PersistentName = "Persistent";
|
||||
private const string MainMenuName = "MainMenu";
|
||||
|
||||
// 启动流程所需的事件频道资产清单 (subfolder, assetName, SO type)
|
||||
private static readonly (string folder, string name, System.Type type)[] BootChannels =
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dcbbced2e8951c542a2af85944c0c29a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -202,8 +202,6 @@ namespace BaseGames.Editor
|
||||
AssignReference(cameraStateController, "_brain", brain);
|
||||
AssignReference(cameraStateController, "_impulseSource", impulseSource);
|
||||
AssignReference(cameraStateController, "_lookSystem", lookSystem);
|
||||
AssignReference(cameraStateController, "_vcamA", vcamA);
|
||||
AssignReference(cameraStateController, "_vcamB", vcamB);
|
||||
AssignAsset(cameraStateController, "_onPlayerSpawned", report, true, "EVT_PlayerSpawned");
|
||||
AssignAsset(cameraStateController, "_lensConfig", report, false, "CAM_LensConfig", "LensConfig", "CameraLensConfig");
|
||||
|
||||
|
||||
8
Assets/_Game/Scripts/UI/MainMenu.meta
Normal file
8
Assets/_Game/Scripts/UI/MainMenu.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a81c593ef6b215e4c9752363738c131b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
11
Assets/_Game/Scripts/UI/MainMenu/MainMenuController.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/MainMenu/MainMenuController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e1f947f274273f4c9a5a310fc40a625
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Scripts/UI/Splash.meta
Normal file
8
Assets/_Game/Scripts/UI/Splash.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4714b32299804648acbfef71cf9ac81
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI.Splash
|
||||
@@ -35,6 +36,7 @@ namespace BaseGames.UI.Splash
|
||||
[SerializeField] private VoidEventChannelSO _onSplashComplete; // Raise
|
||||
|
||||
private bool _skipRequested;
|
||||
private InputAction _anyButtonAction;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 生命周期 ─────────────────────────────────────────────────────────
|
||||
@@ -45,10 +47,28 @@ namespace BaseGames.UI.Splash
|
||||
SetAlpha(_splashRoot, 1f, blocksRaycasts: true);
|
||||
SetAlpha(_studioLogoGroup, 0f, blocksRaycasts: false);
|
||||
SetAlpha(_gameTitleGroup, 0f, blocksRaycasts: false);
|
||||
|
||||
// 通配符绑定:捕获任意设备的任意按键/手柄键/触屏按压
|
||||
_anyButtonAction = new InputAction(binding: "/*/<button>");
|
||||
_anyButtonAction.performed += _ => _skipRequested = true;
|
||||
}
|
||||
|
||||
private void OnEnable() => _onSplashStartRequest?.Subscribe(OnStartRequested).AddTo(_subs);
|
||||
private void OnDisable() => _subs.Clear();
|
||||
private void OnEnable()
|
||||
{
|
||||
_onSplashStartRequest?.Subscribe(OnStartRequested).AddTo(_subs);
|
||||
_anyButtonAction.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
_anyButtonAction.Disable();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_anyButtonAction.Dispose();
|
||||
}
|
||||
|
||||
// ── 入口 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -94,14 +114,6 @@ namespace BaseGames.UI.Splash
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Unity 输入(任意按键跳过)────────────────────────────────────────
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.anyKeyDown)
|
||||
_skipRequested = true;
|
||||
}
|
||||
|
||||
// ── 内部工具 ─────────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator FadeGroup(CanvasGroup group, float from, float to, float duration)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b14de374a70f8234bb49050dd91c9b6a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user