完整启动流程
This commit is contained in:
530
Assets/_Game/Scripts/Editor/Scene/BootFlowSetupWizard.cs
Normal file
530
Assets/_Game/Scripts/Editor/Scene/BootFlowSetupWizard.cs
Normal 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})。"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 绑定 _progressBar(Slider)和 _loadingPanel(GameObject)。");
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user