Files
zeling_v2/Assets/_Game/Scripts/Editor/Scene/BootFlowSetupWizard.cs
2026-06-07 11:49:55 +08:00

569 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <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 = "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<bool> check)> _checks = new();
// ── 菜单入口 ──────────────────────────────────────────────────────────
[MenuItem("BaseGames/Scene/Setup/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")),
("SceneService._onLoadingStarted 已绑定",
() => IsFieldBound(FindInLoadedScene<SceneService>(PersistentName), "_onLoadingStarted")),
("SceneService._onLoadingComplete 已绑定",
() => IsFieldBound(FindInLoadedScene<SceneService>(PersistentName), "_onLoadingComplete")),
("SceneLoader._onLoadingProgressUpdated 已绑定",
() => IsFieldBound(FindInLoadedScene<SceneLoader>(PersistentName), "_onLoadingProgressUpdated")),
("LoadingScreenManager._loadingRoot 已绑定",
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_loadingRoot")),
("LoadingScreenManager._progressFill 已绑定",
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_progressFill")),
("LoadingScreenManager._tipText 已绑定",
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_tipText")),
("LoadingScreenManager._backgroundArts 非空",
() => IsArrayFieldNonEmpty(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_backgroundArts")),
("LoadingScreenManager._config 已绑定",
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_config")),
("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)[]
{
("DataDrivenMainMenuController 组件存在", () => FindInLoadedScene<DataDrivenMainMenuController>(MainMenuName) != null),
("SaveSlotController 组件存在", () => FindInLoadedScene<SaveSlotController>(MainMenuName) != null),
("DataDrivenMainMenuController._onSceneLoadRequest 已绑定",
() => IsFieldBound(FindInLoadedScene<DataDrivenMainMenuController>(MainMenuName), "_onSceneLoadRequest")),
("DataDrivenMainMenuController._onSlotConfirmed 已绑定",
() => IsFieldBound(FindInLoadedScene<DataDrivenMainMenuController>(MainMenuName), "_onSlotConfirmed")),
("DataDrivenMainMenuController._firstGameSceneKey 非空",
() => IsStringFieldNonEmpty(FindInLoadedScene<DataDrivenMainMenuController>(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<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<SceneService>(PersistentName, "_onLoadingStarted", report, ref passCount, ref failCount);
CheckField<SceneService>(PersistentName, "_onLoadingComplete", report, ref passCount, ref failCount);
CheckField<SceneLoader>(PersistentName, "_onLoadingProgressUpdated", report, ref passCount, ref failCount);
// LoadingScreenManager 自身 UI 字段
CheckField<LoadingScreenManager>(PersistentName, "_loadingRoot", report, ref passCount, ref failCount);
CheckField<LoadingScreenManager>(PersistentName, "_progressFill", report, ref passCount, ref failCount);
CheckField<LoadingScreenManager>(PersistentName, "_tipText", report, ref passCount, ref failCount);
if (IsArrayFieldNonEmpty(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_backgroundArts"))
passCount++;
else { failCount++; report.Add("[未绑定] LoadingScreenManager._backgroundArts 为空(在 Persistent。"); }
CheckField<LoadingScreenManager>(PersistentName, "_config", report, ref passCount, ref failCount);
// Main menu scene
CheckComponent<DataDrivenMainMenuController>(MainMenuName, "DataDrivenMainMenuController", report, ref passCount, ref failCount);
CheckField<DataDrivenMainMenuController>(MainMenuName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
CheckField<DataDrivenMainMenuController>(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<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);
}
/// <summary>数组字段是否存在且至少有一个非空(对象引用)元素。</summary>
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<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})。"); }
}
}
}