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.Menus;
using BaseGames.UI.Splash;
namespace BaseGames.Editor
{
///
/// 启动流程安装向导(四步检查清单窗口)。
///
/// 功能:
/// Step 1 — 检测并创建启动流程所需的全部事件频道 SO 资产。
/// Step 2 — 检测 Persistent 场景中 BootSequencer / SplashScreenController /
/// LoadingScreenManager 的存在性并提供一键脚手架入口。
/// Step 3 — 检测 Scene_MainMenu 是否存在并提供一键脚手架入口。
/// Step 4 — 全局验证(必要字段绑定状态),输出操作报告。
///
/// 菜单:BaseGames / Tools / Boot Flow Wizard
///
public class BootFlowSetupWizard : EditorWindow
{
// ── 常量 ──────────────────────────────────────────────────────────────
private const string EventRoot = "Assets/_Game/Data/Events";
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 =
{
("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 check)> _checks = new();
// ── 菜单入口 ──────────────────────────────────────────────────────────
[MenuItem("BaseGames/Scene/Setup/Boot Flow Wizard", priority = 10)]
public static void Open()
{
var wnd = GetWindow();
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 fn)[]
{
("BootSequencer 组件存在", () => FindInLoadedScene(PersistentName) != null),
("SplashScreenController 组件存在", () => FindInLoadedScene(PersistentName) != null),
("LoadingScreenManager 组件存在", () => FindInLoadedScene(PersistentName) != null),
("BootSequencer._onSplashStartRequest 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_onSplashStartRequest")),
("BootSequencer._onSplashComplete 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_onSplashComplete")),
("SceneService._onLoadingStarted 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_onLoadingStarted")),
("SceneService._onLoadingComplete 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_onLoadingComplete")),
("SceneLoader._onLoadingProgressUpdated 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_onLoadingProgressUpdated")),
("LoadingScreenManager._loadingRoot 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_loadingRoot")),
("LoadingScreenManager._progressFill 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_progressFill")),
("LoadingScreenManager._tipText 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_tipText")),
("LoadingScreenManager._backgroundArts 非空",
() => IsArrayFieldNonEmpty(FindInLoadedScene(PersistentName), "_backgroundArts")),
("LoadingScreenManager._config 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_config")),
("GameManager._bootSequencer 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_bootSequencer")),
("GameManager._onSceneLoadRequest 已绑定",
() => IsFieldBound(FindInLoadedScene(PersistentName), "_onSceneLoadRequest")),
("GameManager._onSceneLoaded 已绑定",
() => IsFieldBound(FindInLoadedScene(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 fn)[]
{
("DataDrivenMainMenuController 组件存在", () => FindInLoadedScene(MainMenuName) != null),
("SaveSlotController 组件存在", () => FindInLoadedScene(MainMenuName) != null),
("DataDrivenMainMenuController._onSceneLoadRequest 已绑定",
() => IsFieldBound(FindInLoadedScene(MainMenuName), "_onSceneLoadRequest")),
("DataDrivenMainMenuController._onSlotConfirmed 已绑定",
() => IsFieldBound(FindInLoadedScene(MainMenuName), "_onSlotConfirmed")),
("DataDrivenMainMenuController._firstGameSceneKey 非空",
() => IsStringFieldNonEmpty(FindInLoadedScene(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.flexGrow = 1;
_statusBar.style.color = new Color(0.60f, 0.60f, 0.60f);
_statusBar.style.fontSize = 11;
var refreshBtn = new Button(RefreshAllChecks) { text = "↻ 刷新" };
refreshBtn.style.width = 60;
refreshBtn.style.height = 22;
refreshBtn.style.fontSize = 10;
refreshBtn.style.marginRight = 8;
var bar = new VisualElement();
bar.style.flexDirection = FlexDirection.Row;
bar.style.alignItems = Align.Center;
bar.style.borderTopWidth = 1;
bar.style.borderTopColor = new Color(0.0f, 0.0f, 0.0f, 0.20f);
bar.style.paddingTop = 2;
bar.style.paddingBottom = 2;
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();
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(PersistentName, "BootSequencer", report, ref passCount, ref failCount);
CheckComponent(PersistentName, "SplashScreenController", report, ref passCount, ref failCount);
CheckComponent(PersistentName, "LoadingScreenManager", report, ref passCount, ref failCount);
CheckComponent(PersistentName, "GameManager", report, ref passCount, ref failCount);
CheckComponent(PersistentName, "SceneLoader", report, ref passCount, ref failCount);
// Field bindings
CheckField(PersistentName, "_onSplashStartRequest", report, ref passCount, ref failCount);
CheckField(PersistentName, "_onSplashComplete", report, ref passCount, ref failCount);
CheckField(PersistentName, "_bootSequencer", report, ref passCount, ref failCount);
CheckField(PersistentName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
CheckField(PersistentName, "_onSceneLoaded", report, ref passCount, ref failCount);
CheckField(PersistentName, "_onLoadingStarted", report, ref passCount, ref failCount);
CheckField(PersistentName, "_onLoadingComplete", report, ref passCount, ref failCount);
CheckField(PersistentName, "_onLoadingProgressUpdated", report, ref passCount, ref failCount);
// LoadingScreenManager 自身 UI 字段
CheckField(PersistentName, "_loadingRoot", report, ref passCount, ref failCount);
CheckField(PersistentName, "_progressFill", report, ref passCount, ref failCount);
CheckField(PersistentName, "_tipText", report, ref passCount, ref failCount);
if (IsArrayFieldNonEmpty(FindInLoadedScene(PersistentName), "_backgroundArts"))
passCount++;
else { failCount++; report.Add("[未绑定] LoadingScreenManager._backgroundArts 为空(在 Persistent)。"); }
CheckField(PersistentName, "_config", report, ref passCount, ref failCount);
// Main menu scene
CheckComponent(MainMenuName, "DataDrivenMainMenuController", report, ref passCount, ref failCount);
CheckField(MainMenuName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
CheckField(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.5f, 0.5f, 0.5f, 0.05f);
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.60f, 0.60f, 0.60f);
lbl.style.fontSize = 11;
lbl.style.marginBottom = 6;
return lbl;
}
private static (VisualElement row, Label statusLabel) MakeCheckRow(string text, System.Func 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(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(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 IsArrayFieldNonEmpty(Object target, string fieldName)
{
if (target == null) return false;
var so = new SerializedObject(target);
var prop = so.FindProperty(fieldName);
if (prop == null || !prop.isArray) return false;
for (int i = 0; i < prop.arraySize; i++)
if (prop.GetArrayElementAtIndex(i).objectReferenceValue != null)
return true;
return false;
}
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(string sceneName, string label,
List report, ref int pass, ref int fail) where T : Component
{
if (FindInLoadedScene(sceneName) != null) pass++;
else { fail++; report.Add($"[缺失] {sceneName} 中未找到 {label}。请先打开该场景并运行脚手架。"); }
}
private static void CheckField(string sceneName, string field,
List report, ref int pass, ref int fail,
bool requireNonEmpty = false) where T : Component
{
var comp = FindInLoadedScene(sceneName);
bool ok = requireNonEmpty
? IsStringFieldNonEmpty(comp, field)
: IsFieldBound(comp, field);
if (ok) pass++;
else { fail++; report.Add($"[未绑定] {typeof(T).Name}.{field}(在 {sceneName})。"); }
}
}
}