完整启动流程

This commit is contained in:
2026-05-19 23:20:44 +08:00
parent d0a1112737
commit 5fd981f5b9
22 changed files with 1938 additions and 14 deletions

View File

@@ -69,6 +69,18 @@ namespace BaseGames.Editor
CreateAsset<VoidEventChannelSO> ("UI", "EVT_MapOpen");
CreateAsset<ColorblindModeEventChannelSO> ("UI", "EVT_ColorblindMode");
// ── 启动流程 / Splash ─────────────────────────────────────────────
CreateAsset<VoidEventChannelSO> ("UI/Splash", "EVT_SplashStartRequest");
CreateAsset<VoidEventChannelSO> ("UI/Splash", "EVT_SplashComplete");
// ── 启动流程 / Loading 画面 ───────────────────────────────────────
CreateAsset<VoidEventChannelSO> ("UI/Loading", "EVT_LoadingStarted");
CreateAsset<VoidEventChannelSO> ("UI/Loading", "EVT_LoadingComplete");
CreateAsset<FloatEventChannelSO> ("UI/Loading", "EVT_LoadingProgressUpdated");
// ── 启动流程 / 主菜单 ─────────────────────────────────────────────
CreateAsset<IntEventChannelSO> ("UI/MainMenu", "EVT_SlotConfirmed");
// ── World ─────────────────────────────────────────────────────────
CreateAsset<StringEventChannelSO> ("World", "EVT_SavePointActivated");
CreateAsset<VoidEventChannelSO> ("World", "EVT_CheckpointReached");

View File

@@ -0,0 +1,530 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.UI;
using BaseGames.UI.MainMenu;
using BaseGames.UI.Splash;
namespace BaseGames.Editor
{
/// <summary>
/// 启动流程安装向导(四步检查清单窗口)。
///
/// 功能:
/// Step 1 — 检测并创建启动流程所需的全部事件频道 SO 资产。
/// Step 2 — 检测 Persistent 场景中 BootSequencer / SplashScreenController /
/// LoadingScreenManager 的存在性并提供一键脚手架入口。
/// Step 3 — 检测 Scene_MainMenu 是否存在并提供一键脚手架入口。
/// Step 4 — 全局验证(必要字段绑定状态),输出操作报告。
///
/// 菜单BaseGames / Tools / Boot Flow Wizard
/// </summary>
public class BootFlowSetupWizard : EditorWindow
{
// ── 常量 ──────────────────────────────────────────────────────────────
private const string EventRoot = "Assets/_Game/Data/Events";
private const string PersistentName = "Scene_Persistent";
private const string MainMenuName = "Scene_MainMenu";
// 启动流程所需的事件频道资产清单 (subfolder, assetName, SO type)
private static readonly (string folder, string name, System.Type type)[] BootChannels =
{
("UI/Splash", "EVT_SplashStartRequest", typeof(VoidEventChannelSO)),
("UI/Splash", "EVT_SplashComplete", typeof(VoidEventChannelSO)),
("UI/Loading", "EVT_LoadingStarted", typeof(VoidEventChannelSO)),
("UI/Loading", "EVT_LoadingComplete", typeof(VoidEventChannelSO)),
("UI/Loading", "EVT_LoadingProgressUpdated",typeof(FloatEventChannelSO)),
("UI/MainMenu", "EVT_SlotConfirmed", typeof(IntEventChannelSO)),
("Core", "EVT_SceneLoadRequest", typeof(SceneLoadRequestEventChannelSO)),
("Core", "EVT_SceneLoaded", typeof(StringEventChannelSO)),
};
// ── UI 引用 ───────────────────────────────────────────────────────────
private ScrollView _scroll;
private Label _statusBar;
private readonly List<(Label label, System.Func<bool> check)> _checks = new();
// ── 菜单入口 ──────────────────────────────────────────────────────────
[MenuItem("BaseGames/Tools/Boot Flow Wizard", priority = 10)]
public static void Open()
{
var wnd = GetWindow<BootFlowSetupWizard>();
wnd.titleContent = new GUIContent("Boot Flow Wizard",
EditorGUIUtility.IconContent("d_PlayButton").image);
wnd.minSize = new Vector2(480, 620);
}
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
rootVisualElement.style.flexDirection = FlexDirection.Column;
BuildHeader();
_scroll = new ScrollView(ScrollViewMode.Vertical);
_scroll.style.flexGrow = 1;
rootVisualElement.Add(_scroll);
BuildStep1();
BuildStep2();
BuildStep3();
BuildStep4();
BuildStatusBar();
RefreshAllChecks();
}
private void OnFocus() => RefreshAllChecks();
// ── UI 构建 ───────────────────────────────────────────────────────────
private void BuildHeader()
{
var header = new VisualElement();
StyleRow(header, new Color(0.17f, 0.17f, 0.17f));
header.style.paddingLeft = 12;
header.style.paddingTop = 10;
header.style.paddingBottom = 10;
var title = new Label("🚀 Boot Flow Setup Wizard");
title.style.fontSize = 15;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
title.style.color = new Color(0.85f, 0.92f, 1f);
header.Add(title);
var sub = new Label("完成以下四步即可激活完整游戏启动流程。");
sub.style.color = new Color(0.55f, 0.55f, 0.55f);
sub.style.fontSize = 11;
header.Add(sub);
rootVisualElement.Add(header);
}
private void BuildStep1()
{
var section = BuildSection("Step 1 — 创建事件频道资产", new Color(0.22f, 0.35f, 0.22f));
section.Add(MakeDescription("以下 SO 资产在 Assets/_Game/Data/Events/ 下创建,是启动流程各组件通信的基础。"));
foreach (var (folder, name, _) in BootChannels)
{
string path = $"{EventRoot}/{folder}/{name}.asset";
string display = $"{name} ({folder})";
var (row, statusLbl) = MakeCheckRow(display, () => AssetDatabase.LoadMainAssetAtPath(path) != null);
_checks.Add((statusLbl, () => AssetDatabase.LoadMainAssetAtPath(path) != null));
section.Add(row);
}
var btn = new Button(OnCreateEventChannels) { text = "一键创建所有缺失资产" };
StylePrimaryButton(btn);
section.Add(btn);
}
private void BuildStep2()
{
var section = BuildSection("Step 2 — 配置 Persistent 场景", new Color(0.22f, 0.27f, 0.38f));
section.Add(MakeDescription(
$"在打开 {PersistentName} 的情况下,点击下方按钮执行脚手架(幂等,已有组件不会重建)。"));
var checks = new (string label, System.Func<bool> fn)[]
{
("BootSequencer 组件存在", () => FindInLoadedScene<BootSequencer>(PersistentName) != null),
("SplashScreenController 组件存在", () => FindInLoadedScene<SplashScreenController>(PersistentName) != null),
("LoadingScreenManager 组件存在", () => FindInLoadedScene<LoadingScreenManager>(PersistentName) != null),
("BootSequencer._onSplashStartRequest 已绑定",
() => IsFieldBound(FindInLoadedScene<BootSequencer>(PersistentName), "_onSplashStartRequest")),
("BootSequencer._onSplashComplete 已绑定",
() => IsFieldBound(FindInLoadedScene<BootSequencer>(PersistentName), "_onSplashComplete")),
("SceneLoader._onLoadingStarted 已绑定",
() => IsFieldBound(FindInLoadedScene<SceneLoader>(PersistentName), "_onLoadingStarted")),
("GameManager._bootSequencer 已绑定",
() => IsFieldBound(FindInLoadedScene<GameManager>(PersistentName), "_bootSequencer")),
("GameManager._onSceneLoadRequest 已绑定",
() => IsFieldBound(FindInLoadedScene<GameManager>(PersistentName), "_onSceneLoadRequest")),
("GameManager._onSceneLoaded 已绑定",
() => IsFieldBound(FindInLoadedScene<GameManager>(PersistentName), "_onSceneLoaded")),
};
foreach (var (lbl, fn) in checks)
{
var (row, statusLbl) = MakeCheckRow(lbl, fn);
_checks.Add((statusLbl, fn));
section.Add(row);
}
var btn = new Button(OnScaffoldPersistent) { text = "脚手架 Persistent 场景(需先打开该场景)" };
StylePrimaryButton(btn);
section.Add(btn);
}
private void BuildStep3()
{
var section = BuildSection("Step 3 — 搭建 Scene_MainMenu", new Color(0.32f, 0.25f, 0.18f));
section.Add(MakeDescription(
$"创建新场景命名为 {MainMenuName},打开后点击下方按钮生成主菜单层级。"));
var checks = new (string label, System.Func<bool> fn)[]
{
("MainMenuController 组件存在", () => FindInLoadedScene<MainMenuController>(MainMenuName) != null),
("SaveSlotController 组件存在", () => FindInLoadedScene<SaveSlotController>(MainMenuName) != null),
("MainMenuController._onSceneLoadRequest 已绑定",
() => IsFieldBound(FindInLoadedScene<MainMenuController>(MainMenuName), "_onSceneLoadRequest")),
("MainMenuController._onSlotConfirmed 已绑定",
() => IsFieldBound(FindInLoadedScene<MainMenuController>(MainMenuName), "_onSlotConfirmed")),
("MainMenuController._firstGameSceneKey 非空",
() => IsStringFieldNonEmpty(FindInLoadedScene<MainMenuController>(MainMenuName), "_firstGameSceneKey")),
("Scene_MainMenu 已加入 Build Settings",
() => IsInBuildSettings(MainMenuName)),
};
foreach (var (lbl, fn) in checks)
{
var (row, statusLbl) = MakeCheckRow(lbl, fn);
_checks.Add((statusLbl, fn));
section.Add(row);
}
var row2 = new VisualElement();
row2.style.flexDirection = FlexDirection.Row;
row2.style.marginTop = 6;
var scaffoldBtn = new Button(OnScaffoldMainMenu) { text = "脚手架 MainMenu 场景" };
StylePrimaryButton(scaffoldBtn);
scaffoldBtn.style.flexGrow = 1;
scaffoldBtn.style.marginRight = 4;
row2.Add(scaffoldBtn);
var buildSettingsBtn = new Button(OpenBuildSettings) { text = "打开 Build Settings" };
StyleSecondaryButton(buildSettingsBtn);
buildSettingsBtn.style.width = 140;
row2.Add(buildSettingsBtn);
section.Add(row2);
}
private void BuildStep4()
{
var section = BuildSection("Step 4 — 全局验证", new Color(0.28f, 0.20f, 0.32f));
section.Add(MakeDescription("点击「运行全量检查」获取带详情的报告,帮助排查剩余遗漏项。"));
var btn = new Button(OnRunFullValidation) { text = "运行全量检查并输出报告" };
StylePrimaryButton(btn);
section.Add(btn);
}
private void BuildStatusBar()
{
_statusBar = new Label("— 点击「刷新」或切换焦点以更新状态 —");
_statusBar.style.paddingLeft = 8;
_statusBar.style.paddingTop = 4;
_statusBar.style.paddingBottom = 4;
_statusBar.style.borderTopWidth = 1;
_statusBar.style.borderTopColor = new Color(0.15f, 0.15f, 0.15f);
_statusBar.style.color = new Color(0.55f, 0.55f, 0.55f);
_statusBar.style.fontSize = 11;
var refreshBtn = new Button(RefreshAllChecks) { text = "↻ 刷新" };
refreshBtn.style.position = Position.Absolute;
refreshBtn.style.right = 8;
refreshBtn.style.top = 2;
refreshBtn.style.width = 60;
refreshBtn.style.height = 18;
refreshBtn.style.fontSize = 10;
var bar = new VisualElement();
bar.style.flexDirection = FlexDirection.Row;
bar.style.alignItems = Align.Center;
bar.Add(_statusBar);
bar.Add(refreshBtn);
rootVisualElement.Add(bar);
}
// ── 操作回调 ──────────────────────────────────────────────────────────
private void OnCreateEventChannels()
{
CreateEventChannelAssets.CreateAll();
AssetDatabase.Refresh();
RefreshAllChecks();
}
private void OnScaffoldPersistent()
{
SceneScaffoldTools.ScaffoldPersistentScene();
RefreshAllChecks();
}
private void OnScaffoldMainMenu()
{
SceneScaffoldTools.ScaffoldMainMenuScene();
RefreshAllChecks();
}
private static void OpenBuildSettings()
=> EditorWindow.GetWindow(System.Type.GetType(
"UnityEditor.BuildPlayerWindow,UnityEditor"));
private void OnRunFullValidation()
{
var report = new List<string>();
int passCount = 0;
int failCount = 0;
// Event channels
foreach (var (folder, name, _) in BootChannels)
{
string path = $"{EventRoot}/{folder}/{name}.asset";
bool ok = AssetDatabase.LoadMainAssetAtPath(path) != null;
if (ok) passCount++;
else { failCount++; report.Add($"[缺失] 事件频道资产:{path}"); }
}
// Components in Persistent
CheckComponent<BootSequencer>(PersistentName, "BootSequencer", report, ref passCount, ref failCount);
CheckComponent<SplashScreenController>(PersistentName, "SplashScreenController", report, ref passCount, ref failCount);
CheckComponent<LoadingScreenManager>(PersistentName, "LoadingScreenManager", report, ref passCount, ref failCount);
CheckComponent<GameManager>(PersistentName, "GameManager", report, ref passCount, ref failCount);
CheckComponent<SceneLoader>(PersistentName, "SceneLoader", report, ref passCount, ref failCount);
// Field bindings
CheckField<BootSequencer>(PersistentName, "_onSplashStartRequest", report, ref passCount, ref failCount);
CheckField<BootSequencer>(PersistentName, "_onSplashComplete", report, ref passCount, ref failCount);
CheckField<GameManager>(PersistentName, "_bootSequencer", report, ref passCount, ref failCount);
CheckField<GameManager>(PersistentName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
CheckField<GameManager>(PersistentName, "_onSceneLoaded", report, ref passCount, ref failCount);
CheckField<SceneLoader>(PersistentName, "_onLoadingStarted", report, ref passCount, ref failCount);
CheckField<SceneLoader>(PersistentName, "_onLoadingComplete", report, ref passCount, ref failCount);
CheckField<SceneLoader>(PersistentName, "_onLoadingProgressUpdated", report, ref passCount, ref failCount);
// Main menu scene
CheckComponent<MainMenuController>(MainMenuName, "MainMenuController", report, ref passCount, ref failCount);
CheckField<MainMenuController>(MainMenuName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
CheckField<MainMenuController>(MainMenuName, "_firstGameSceneKey", report, ref passCount, ref failCount, requireNonEmpty: true);
if (!IsInBuildSettings(MainMenuName))
{
failCount++;
report.Add($"[缺失] {MainMenuName} 未加入 Build Settings。");
}
else passCount++;
// Output
string summary = $"验证完成:{passCount} 项通过 / {failCount} 项需处理。";
if (failCount == 0)
Debug.Log($"[BootFlowWizard] ✅ {summary}");
else
{
foreach (string msg in report)
Debug.LogWarning($"[BootFlowWizard] {msg}");
Debug.LogWarning($"[BootFlowWizard] ⚠ {summary}");
}
_statusBar.text = summary;
_statusBar.style.color = failCount == 0
? new Color(0.4f, 0.9f, 0.4f)
: new Color(1f, 0.75f, 0.3f);
RefreshAllChecks();
}
// ── UI 辅助 ───────────────────────────────────────────────────────────
private VisualElement BuildSection(string title, Color headerColor)
{
var section = new VisualElement();
section.style.marginTop = 8;
section.style.marginLeft = 8;
section.style.marginRight = 8;
section.style.marginBottom = 0;
section.style.borderTopWidth = 1;
section.style.borderBottomWidth = 1;
section.style.borderLeftWidth = 1;
section.style.borderRightWidth = 1;
section.style.borderTopColor = new Color(0.25f, 0.25f, 0.25f);
section.style.borderBottomColor = new Color(0.25f, 0.25f, 0.25f);
section.style.borderLeftColor = new Color(0.25f, 0.25f, 0.25f);
section.style.borderRightColor = new Color(0.25f, 0.25f, 0.25f);
section.style.borderTopLeftRadius = 4;
section.style.borderTopRightRadius = 4;
section.style.borderBottomLeftRadius = 4;
section.style.borderBottomRightRadius = 4;
section.style.overflow = Overflow.Hidden;
var header = new Label(title);
header.style.backgroundColor = headerColor;
header.style.paddingLeft = 10;
header.style.paddingTop = 6;
header.style.paddingBottom = 6;
header.style.unityFontStyleAndWeight = FontStyle.Bold;
header.style.fontSize = 12;
header.style.color = new Color(0.92f, 0.92f, 0.92f);
section.Add(header);
var body = new VisualElement();
body.style.paddingLeft = 10;
body.style.paddingRight = 10;
body.style.paddingTop = 8;
body.style.paddingBottom = 10;
body.style.backgroundColor = new Color(0.18f, 0.18f, 0.18f);
body.name = "body";
section.Add(body);
_scroll.Add(section);
// Return body so callers add children to it
return body;
}
private static Label MakeDescription(string text)
{
var lbl = new Label(text);
lbl.style.whiteSpace = WhiteSpace.Normal;
lbl.style.color = new Color(0.55f, 0.55f, 0.55f);
lbl.style.fontSize = 11;
lbl.style.marginBottom = 6;
return lbl;
}
private static (VisualElement row, Label statusLabel) MakeCheckRow(string text, System.Func<bool> check)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.marginBottom = 2;
bool ok = check();
var icon = new Label(ok ? "✅" : "⬜");
icon.style.width = 22;
icon.style.fontSize = 12;
var lbl = new Label(text);
lbl.style.flexGrow = 1;
lbl.style.fontSize = 11;
lbl.style.color = ok ? new Color(0.75f, 0.95f, 0.75f) : new Color(0.80f, 0.80f, 0.80f);
row.Add(icon);
row.Add(lbl);
return (row, icon);
}
private static void StylePrimaryButton(Button btn)
{
btn.style.backgroundColor = new Color(0.22f, 0.44f, 0.22f);
btn.style.color = Color.white;
btn.style.marginTop = 6;
btn.style.height = 26;
btn.style.borderTopLeftRadius = 3;
btn.style.borderTopRightRadius = 3;
btn.style.borderBottomLeftRadius = 3;
btn.style.borderBottomRightRadius = 3;
}
private static void StyleSecondaryButton(Button btn)
{
btn.style.backgroundColor = new Color(0.25f, 0.25f, 0.32f);
btn.style.color = new Color(0.85f, 0.85f, 0.95f);
btn.style.marginTop = 6;
btn.style.height = 26;
btn.style.borderTopLeftRadius = 3;
btn.style.borderTopRightRadius = 3;
btn.style.borderBottomLeftRadius = 3;
btn.style.borderBottomRightRadius = 3;
}
private static void StyleRow(VisualElement el, Color bg)
{
el.style.backgroundColor = bg;
}
private void RefreshAllChecks()
{
int done = 0;
foreach (var (lbl, fn) in _checks)
{
bool ok = fn();
lbl.text = ok ? "✅" : "⬜";
if (ok) done++;
}
if (_statusBar != null)
{
_statusBar.text = $"通过 {done} / {_checks.Count} 项检查";
_statusBar.style.color = done == _checks.Count
? new Color(0.4f, 0.9f, 0.4f)
: new Color(0.75f, 0.75f, 0.75f);
}
}
// ── 检查辅助 ──────────────────────────────────────────────────────────
private static T FindInLoadedScene<T>(string sceneName) where T : Component
{
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
{
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i);
if (!scene.isLoaded) continue;
if (!string.IsNullOrEmpty(sceneName) &&
!Path.GetFileNameWithoutExtension(scene.path).Equals(
sceneName, System.StringComparison.OrdinalIgnoreCase))
continue;
foreach (GameObject go in scene.GetRootGameObjects())
{
var comp = go.GetComponentInChildren<T>(true);
if (comp != null) return comp;
}
}
return null;
}
private static bool IsFieldBound(Object target, string fieldName)
{
if (target == null) return false;
var so = new SerializedObject(target);
var prop = so.FindProperty(fieldName);
if (prop == null) return false;
return prop.propertyType == SerializedPropertyType.ObjectReference
? prop.objectReferenceValue != null
: !string.IsNullOrEmpty(prop.stringValue);
}
private static bool IsStringFieldNonEmpty(Object target, string fieldName)
{
if (target == null) return false;
var so = new SerializedObject(target);
var prop = so.FindProperty(fieldName);
return prop != null && !string.IsNullOrEmpty(prop.stringValue);
}
private static bool IsInBuildSettings(string sceneName)
{
foreach (var scene in EditorBuildSettings.scenes)
{
if (Path.GetFileNameWithoutExtension(scene.path)
.Equals(sceneName, System.StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private static void CheckComponent<T>(string sceneName, string label,
List<string> report, ref int pass, ref int fail) where T : Component
{
if (FindInLoadedScene<T>(sceneName) != null) pass++;
else { fail++; report.Add($"[缺失] {sceneName} 中未找到 {label}。请先打开该场景并运行脚手架。"); }
}
private static void CheckField<T>(string sceneName, string field,
List<string> report, ref int pass, ref int fail,
bool requireNonEmpty = false) where T : Component
{
var comp = FindInLoadedScene<T>(sceneName);
bool ok = requireNonEmpty
? IsStringFieldNonEmpty(comp, field)
: IsFieldBound(comp, field);
if (ok) pass++;
else { fail++; report.Add($"[未绑定] {typeof(T).Name}.{field}(在 {sceneName})。"); }
}
}
}

View File

@@ -8,7 +8,9 @@ using BaseGames.Core.Pool;
using BaseGames.Input;
using BaseGames.UI;
using BaseGames.UI.HUD;
using BaseGames.UI.MainMenu;
using BaseGames.UI.Menus;
using BaseGames.UI.Splash;
using BaseGames.World;
using PathBerserker2d;
using Unity.Cinemachine;
@@ -133,6 +135,38 @@ namespace BaseGames.Editor
GetOrAddComponent<Image>(respawnButtonGo);
Button respawnButton = GetOrAddComponent<Button>(respawnButtonGo);
// ── BootSequencer启动流程──────────────────────────────────────
GameObject bootSequencerGo = GetOrCreateChild(services, "BootSequencer").gameObject;
BootSequencer bootSequencer = GetOrAddComponent<BootSequencer>(bootSequencerGo);
AssignAsset(bootSequencer, "_onSplashStartRequest", report, false, "EVT_SplashStartRequest");
AssignAsset(bootSequencer, "_onSplashComplete", report, false, "EVT_SplashComplete");
AssignAsset(bootSequencer, "_onPreloadProgress", report, false, "EVT_LoadingProgressUpdated");
AssignReference(gameManager, "_bootSequencer", bootSequencer);
AssignAsset(gameManager, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
AssignAsset(gameManager, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
AssignAsset(sceneLoader, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
AssignAsset(sceneLoader, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
AssignAsset(sceneLoader, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
// ── Canvas_Splash启动演出──────────────────────────────────────
GameObject splashCanvasGo = GetOrCreateCanvas(ui.transform, "Canvas_Splash", 100);
SplashScreenController splashCtrl = GetOrAddComponent<SplashScreenController>(splashCanvasGo);
AssignAsset(splashCtrl, "_onSplashStartRequest", report, false, "EVT_SplashStartRequest");
AssignAsset(splashCtrl, "_onSplashComplete", report, false, "EVT_SplashComplete");
// ── LoadingScreenManager加载遮罩──────────────────────────────
GameObject loadingCanvasGo = GetOrCreateCanvas(ui.transform, "Canvas_Loading", 99);
LoadingScreenManager loadingMgr = GetOrAddComponent<LoadingScreenManager>(loadingCanvasGo);
AssignAsset(loadingMgr, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
AssignAsset(loadingMgr, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
AssignAsset(loadingMgr, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
loadingCanvasGo.SetActive(false);
report.Add("Canvas_Splash请将工作室 Logo CanvasGroup 赋给 _studioLogoGroup游戏标题 CanvasGroup 赋给 _gameTitleGroup。");
report.Add("Canvas_Loading请为 LoadingScreenManager 绑定 _progressBarSlider和 _loadingPanelGameObject。");
EnsureAudioSources(audioManagerGo, audioManager, report);
AssignReference(registrar, "_deathRespawnService", deathRespawnService);
@@ -141,8 +175,6 @@ namespace BaseGames.Editor
AssignReference(registrar, "_saveManager", gameSaveManager);
AssignReference(gameManager, "_settingsManager", settingsManager);
AssignReference(gameManager, "_deathRespawnService", deathRespawnService);
AssignReference(gameManager, "_sceneService", sceneService);
AssignAsset(gameManager, "_onPlayerDied", report, true, "EVT_PlayerDied");
AssignAsset(gameManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
AssignAsset(gameManager, "_onResumeRequested", report, false, "EVT_ResumeRequested", "EVT_PauseResumed");
@@ -196,6 +228,76 @@ namespace BaseGames.Editor
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
}
// ─────────────────────────────────────────────────────────────────────
// Scaffold Main Menu Scene
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// 在当前活动场景中生成完整的主菜单场景层级结构:
/// [MainMenu] → [Canvas_MainMenu] → 主按钮组 / SaveSlotPanel / SettingsPanel / CreditsPanel
/// 自动绑定所有已存在的相关事件频道 SO 资产。
/// </summary>
[MenuItem("BaseGames/Tools/Scaffold Main Menu Scene", priority = 202)]
public static void ScaffoldMainMenuScene()
{
var report = new List<string>();
EnsureEventChannelAssets(report);
// ── [MainMenu] 根节点 ─────────────────────────────────────────────
GameObject root = GetOrCreateRoot("[MainMenu]");
// ── Canvas_MainMenu排序层 10显示在 HUD 之上)────────────────
GameObject canvasGo = GetOrCreateCanvas(root.transform, "Canvas_MainMenu", 10);
// ── 主菜单控制器 ──────────────────────────────────────────────────
MainMenuController menuCtrl = GetOrAddComponent<MainMenuController>(canvasGo);
AssignAsset(menuCtrl, "_onGameStateChanged", report, false, "EVT_GameStateChanged", "EVT_GameState");
AssignAsset(menuCtrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
AssignAsset(menuCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
// ── 主按钮区域 ────────────────────────────────────────────────────
GameObject menuPanelGo = GetOrCreateChild(canvasGo.transform, "MenuPanel").gameObject;
GetOrAddComponent<VerticalLayoutGroup>(menuPanelGo);
GameObject btnNewGameGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_NewGame", "新游戏");
GameObject btnContinueGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Continue", "继续");
GameObject btnSettingsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Settings", "设置");
GameObject btnCreditsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Credits", "制作团队");
GameObject btnQuitGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Quit", "退出");
AssignReference(menuCtrl, "_btnNewGame", btnNewGameGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnContinue", btnContinueGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnSettings", btnSettingsGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnCredits", btnCreditsGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnQuit", btnQuitGo.GetComponent<Button>());
AssignReference(menuCtrl, "_menuPanel", menuPanelGo);
// ── SaveSlotPanel ─────────────────────────────────────────────────
GameObject saveSlotPanelGo = GetOrCreateChild(canvasGo.transform, "SaveSlotPanel").gameObject;
saveSlotPanelGo.SetActive(false);
SaveSlotController saveSlotCtrl = GetOrAddComponent<SaveSlotController>(saveSlotPanelGo);
AssignAsset(saveSlotCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
AssignReference(menuCtrl, "_saveSlotPanel", saveSlotPanelGo);
AssignReference(menuCtrl, "_saveSlotController", saveSlotCtrl);
// ── SettingsPanel ─────────────────────────────────────────────────
GameObject settingsPanelGo = GetOrCreateChild(canvasGo.transform, "SettingsPanel").gameObject;
settingsPanelGo.SetActive(false);
AssignReference(menuCtrl, "_settingsPanel", settingsPanelGo);
// ── CreditsPanel ──────────────────────────────────────────────────
GameObject creditsPanelGo = GetOrCreateChild(canvasGo.transform, "CreditsPanel").gameObject;
creditsPanelGo.SetActive(false);
AssignReference(menuCtrl, "_creditsPanel", creditsPanelGo);
report.Add("设置 MainMenuController._firstGameSceneKey 为第一个游戏场景的 Addressable Key字符串。");
report.Add("SaveSlotPanel 需要补充 3 个存档槽 Button 引用_slot0Btn / _slot1Btn / _slot2Btn。");
report.Add("建议为 MenuPanel 添加 RectTransform 入场动画所需的锚点配置,参考 MainMenuController._menuPanel 的偏移量。");
MarkDirtyAndLog("Main Menu 场景脚手架", root, report);
}
// ─────────────────────────────────────────────────────────────────────
// Scaffold Game Room
// ─────────────────────────────────────────────────────────────────────
@@ -403,6 +505,16 @@ namespace BaseGames.Editor
return canvasGo;
}
/// <summary>在指定父节点下创建一个带 Button 的菜单按钮子节点(幂等)。文本由美术后续补充。</summary>
private static GameObject GetOrCreateButtonChild(Transform parent, string name, string label)
{
GameObject go = GetOrCreateChild(parent, name).gameObject;
GetOrAddComponent<Image>(go);
GetOrAddComponent<Button>(go);
_ = label; // 占位:文本内容由美术在 Prefab/Scene 中设置
return go;
}
private static void AssignReference(Object target, string propertyName, Object value)
{
AssignReference(target, propertyName, value, null);

View File

@@ -24,6 +24,10 @@ namespace BaseGames.Editor
/// · EnemyProjectile ↔ Ground → 应碰撞(敌人投射物命中地形)
/// · PlayerHitBox ↔ PlayerHurtBox → 应忽略(玩家不自伤)
/// · PlayerProjectile ↔ EnemyProjectile → 应忽略子弹不互相碰撞Clash 系统单独处理)
/// · HazardHitBox ↔ PlayerHurtBox → 应碰撞(环境危险伤害玩家)
/// · HazardHitBox ↔ EnemyHurtBox → 应碰撞(环境危险伤害敌人,阵营中立)
/// · HazardHitBox ↔ PlayerHitBox → 应忽略(环境不触发拼刀)
/// · HazardHitBox ↔ EnemyHitBox → 应忽略(环境不触发拼刀)
/// </summary>
public static class Physics2DLayerReport
{
@@ -43,6 +47,10 @@ namespace BaseGames.Editor
new("EnemyProjectile", "Ground", true, "敌人投射物命中地形"),
new("PlayerHitBox", "PlayerHurtBox", false, "玩家不自伤"),
new("PlayerProjectile", "EnemyProjectile", false, "子弹不互相碰撞Clash 系统单独处理)"),
new("HazardHitBox", "PlayerHurtBox", true, "环境危险伤害玩家"),
new("HazardHitBox", "EnemyHurtBox", true, "环境危险伤害敌人(阵营中立)"),
new("HazardHitBox", "PlayerHitBox", false, "环境不触发拼刀"),
new("HazardHitBox", "EnemyHitBox", false, "环境不触发拼刀"),
};
// ─────────────────────────────────────────────────────────────────────