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})。"); } } } }