Files
zeling_v2/Docs/Guides/01_BootFlow_Setup_Guide.md
2026-05-19 23:20:44 +08:00

581 lines
25 KiB
Markdown
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.
# 游戏启动流程开发手册
> 文件位置:`Docs/Guides/01_BootFlow_Setup_Guide.md`
> 版本1.0 · 适用项目zeling_v2
---
## 目录
1. [架构概览](#1-架构概览)
2. [完整启动时序图](#2-完整启动时序图)
3. [关键脚本说明](#3-关键脚本说明)
4. [事件频道速查表](#4-事件频道速查表)
5. [分步配置教程](#5-分步配置教程)
- 5.1 [创建事件频道资产Step 1](#51-创建事件频道资产step-1)
- 5.2 [配置 Persistent 场景Step 2](#52-配置-persistent-场景step-2)
- 5.3 [配置 Splash 演出Step 2 续)](#53-配置-splash-演出step-2-续)
- 5.4 [配置 Loading 画面Step 2 续)](#54-配置-loading-画面step-2-续)
- 5.5 [创建并配置主菜单场景Step 3](#55-创建并配置主菜单场景step-3)
- 5.6 [Build Settings 配置Step 4](#56-build-settings-配置step-4)
6. [使用编辑器向导工具](#6-使用编辑器向导工具)
7. [FSM 状态转换关系](#7-fsm-状态转换关系)
8. [自定义扩展指南](#8-自定义扩展指南)
9. [常见问题排查](#9-常见问题排查)
---
## 1. 架构概览
启动流程由以下四个程序集中的组件协同完成,通过事件频道(`ScriptableObject` 事件总线)彻底解耦:
```
BaseGames.Core BaseGames.UI
───────────────────────────── ─────────────────────────────
GameManager ◄──────────── SplashScreenController
└─ BootSequencer ────────────► (via EVT_SplashStartRequest)
└─ GameStateMachine MainMenuController
LoadingScreenManager
BaseGames.Core.Events (共享)
─────────────────────────────
VoidEventChannelSO FloatEventChannelSO
IntEventChannelSO StringEventChannelSO
SceneLoadRequestEventChannelSO
```
**核心设计原则:**
- `BaseGames.Core` 不依赖 `BaseGames.UI`,所有跨程序集通信通过 SO 事件频道进行。
- 所有场景加载请求都汇聚到 `EVT_SceneLoadRequest`GameManager 和 SceneService 共享同一 SO 实例。
- GameStateMachine 是状态权威,所有 UI 面板的显隐跟随 `EVT_GameStateChanged` 事件。
---
## 2. 完整启动时序图
```
应用启动(首场景加载前)
├── [RuntimeInitializeOnLoadMethod] GameBootstrap
│ └── 以 Additive 模式加载 Scene_Persistent
Scene_Persistent 加载完成
├── GameServiceRegistrar.Awake(-2000)
│ └── 向 ServiceLocator 注册SceneService、DeathRespawnService、
│ EventChannelRegistry、GameSaveManager、…
├── GameManager.Awake(-1000)
│ ├── DontDestroyOnLoad(root)
│ ├── FSM.TransitionTo(Initializing)
│ └── 注册所有 FSM 状态
└── GameManager.Start()
├── Raise(EVT_GameStateChanged, Initializing)
└── StartCoroutine(BootCoroutine)
├── BootSequencer.RunBootSequenceCoroutine()
│ ├── Raise(EVT_SplashStartRequest) ──► SplashScreenController 开始
│ │ 播放演出(工作室 Logo →
│ │ 游戏标题)可任意键跳过
│ │
│ ├── [并行] PreloadCoroutine()
│ │ └── Addressables.DownloadDependenciesAsync("Preload")
│ │ 每帧 Raise(EVT_LoadingProgressUpdated, progress*0.9)
│ │
│ └── WaitUntil(SplashDone && PreloadDone)
│ SplashScreenController 结束 → Raise(EVT_SplashComplete)
└── SceneService.LoadMainMenuCoroutine()
└── SceneLoader.LoadSceneCoroutine(Scene_MainMenu)
└── 加载成功 → Raise(EVT_SceneLoaded, "Scene_MainMenu")
└── GameManager.HandleSceneLoaded
└── FSM: Initializing → MainMenu
Raise(EVT_GameStateChanged, MainMenu)
─────────────────────────────────────────────────────────────────────────────
主菜单激活
├── MainMenuController.OnEnable()
│ └── 订阅 EVT_GameStateChanged / EVT_SlotConfirmed
└── MainMenuController 响应 EVT_GameStateChanged(MainMenu)
└── 播放入场动画(菜单面板下滑)
└── RefreshContinueButton()(检查存档是否存在)
─────────────────────────────────────────────────────────────────────────────
新游戏 / 继续
├── 玩家点击「新游戏」或「继续」
│ └── 显示 SaveSlotPanel选择存档槽 0/1/2
├── SaveSlotController → Raise(EVT_SlotConfirmed, slotIndex)
├── MainMenuController.HandleSlotConfirmed()
│ └── 关闭 SaveSlotPanel
│ Raise(EVT_SceneLoadRequest, {firstGameSceneKey, Scene, ShowLoadingScreen=true})
├── GameManager.HandleSceneLoadRequest()
│ └── FSM: MainMenu → LoadingScene
│ Raise(EVT_GameStateChanged, LoadingScene)
├── SceneLoader.LoadSceneCoroutine()
│ ├── Raise(EVT_LoadingStarted) → LoadingScreenManager 显示进度画面
│ ├── 每帧 Raise(EVT_LoadingProgressUpdated, p*0.9)
│ ├── Raise(EVT_LoadingProgressUpdated, 1.0)
│ └── Raise(EVT_LoadingComplete) → LoadingScreenManager 隐藏
│ Raise(EVT_SceneLoaded, gameSceneName)
└── GameManager.HandleSceneLoaded()
└── FSM: LoadingScene → Gameplay
Raise(EVT_GameStateChanged, Gameplay)
─────────────────────────────────────────────────────────────────────────────
暂停 → 返回主菜单
├── PauseMenuController.GoToMainMenu()
│ └── Raise(EVT_SceneLoadRequest, {Scene_MainMenu, Scene, ShowLoadingScreen=false})
├── GameManager.HandleSceneLoadRequest()
│ └── 目标为 Scene_MainMenu → 跳过 LoadingScene 中间状态
└── SceneLoader 加载完成 → Raise(EVT_SceneLoaded, "Scene_MainMenu")
└── FSM: Paused → MainMenu
```
---
## 3. 关键脚本说明
### `GameManager`Core/GameManager.cs
全局游戏管理器,持有 FSM 并协调所有顶层服务。
| 新增字段 | 用途 |
|---|---|
| `_bootSequencer` | 引用 Persistent 场景中的 BootSequencer |
| `_onSceneLoadRequest` | 监听场景加载请求(与 SceneService 共享同一 SO|
| `_onSceneLoaded` | 监听 SceneLoader 加载完成(携带场景名)|
`Start()` 启动 `BootCoroutine()``HandleSceneLoadRequest()` / `HandleSceneLoaded()` 自动驱动 FSM 转换。
---
### `BootSequencer`Core/BootSequencer.cs
挂载在 Persistent 场景,驱动 Splash 演出与 Addressable 预热并行执行。
| Inspector 字段 | 说明 |
|---|---|
| `_preloadLabel` | Addressable 预热标签(留空则跳过预热)|
| `_onSplashStartRequest` | 赋 `EVT_SplashStartRequest`Raise|
| `_onSplashComplete` | 赋 `EVT_SplashComplete`Listen|
| `_onPreloadProgress` | 赋 `EVT_LoadingProgressUpdated`(可选,供进度条显示)|
**重要:** `_onSplashComplete` 留空时BootSequencer 不等待 Splash直接进入主菜单。适合调试阶段快速跳过 Splash。
---
### `SplashScreenController`UI/Splash/SplashScreenController.cs
挂载在 `Canvas_Splash`(排序层 100播放两段淡入/淡出动画,任意键可跳过。
| Inspector 字段 | 说明 |
|---|---|
| `_studioLogoGroup` | 工作室 Logo 的 CanvasGroup |
| `_gameTitleGroup` | 游戏标题的 CanvasGroup |
| `_onSplashStartRequest` | 赋 `EVT_SplashStartRequest`Listen|
| `_onSplashComplete` | 赋 `EVT_SplashComplete`Raise|
| `_fadeDuration` | 淡入/淡出时长(秒,默认 1.0|
| `_holdDuration` | 每帧持续时长(秒,默认 1.5|
---
### `MainMenuController`UI/MainMenu/MainMenuController.cs
挂载在主菜单场景的 Canvas 上,管理按钮、子面板、入场动画。
| Inspector 字段 | 说明 |
|---|---|
| `_onGameStateChanged` | 赋 `EVT_GameStateChanged`Listen|
| `_onSceneLoadRequest` | 赋 `EVT_SceneLoadRequest`Raise|
| `_onSlotConfirmed` | 赋 `EVT_SlotConfirmed`Listen|
| `_firstGameSceneKey` | 第一个游戏场景的 Addressable Key**必填**|
| `_btnNewGame/Continue/Settings/Credits/Quit` | 各按钮引用 |
| `_menuPanel` | 主按钮区 GameObject用于入场动画|
| `_saveSlotPanel / _settingsPanel / _creditsPanel` | 子面板 |
| `_saveSlotController` | SaveSlotController 引用 |
---
### `SceneLoader`Core/SceneLoader.cs
加载时序改为逐帧轮询,当 `ShowLoadingScreen = true` 时自动发布进度事件。
| 新增字段 | 用途 |
|---|---|
| `_onLoadingStarted` | 赋 `EVT_LoadingStarted`Raise 给 LoadingScreenManager|
| `_onLoadingComplete` | 赋 `EVT_LoadingComplete`Raise 给 LoadingScreenManager|
| `_onLoadingProgressUpdated` | 赋 `EVT_LoadingProgressUpdated`Raise 进度值 0~1|
---
## 4. 事件频道速查表
| SO 资产名 | 类型 | 发布者 | 监听者 | 说明 |
|---|---|---|---|---|
| `EVT_SplashStartRequest` | VoidEventChannelSO | BootSequencer | SplashScreenController | 触发 Splash 演出开始 |
| `EVT_SplashComplete` | VoidEventChannelSO | SplashScreenController | BootSequencer | Splash 结束通知 |
| `EVT_LoadingStarted` | VoidEventChannelSO | SceneLoader | LoadingScreenManager | 加载开始,显示进度画面 |
| `EVT_LoadingComplete` | VoidEventChannelSO | SceneLoader | LoadingScreenManager | 加载结束,隐藏进度画面 |
| `EVT_LoadingProgressUpdated` | FloatEventChannelSO | SceneLoader / BootSequencer | LoadingScreenManager | 进度值 0~1 |
| `EVT_SlotConfirmed` | IntEventChannelSO | SaveSlotController | MainMenuController | 存档槽选择完成(携带槽索引)|
| `EVT_SceneLoadRequest` | SceneLoadRequestEventChannelSO | MainMenuController / PauseMenuController / … | GameManager + SceneService | 场景加载请求(共享同一 SO|
| `EVT_SceneLoaded` | StringEventChannelSO | SceneLoader | GameManager | 加载完成(携带场景名)|
| `EVT_GameStateChanged` | GameStateEventChannelSO | GameManager | MainMenuController / UIManager / … | FSM 状态改变通知 |
> **注意:** `EVT_SceneLoadRequest` 和 `EVT_SceneLoaded` 在 Inspector 中必须使用同一个 SO 实例赋给所有相关组件。不同组件使用不同 SO 实例是最常见的配置错误。
---
## 5. 分步配置教程
### 5.1 创建事件频道资产Step 1
**方法 A — 一键创建(推荐):**
1. 打开菜单 **BaseGames → Tools → Boot Flow Wizard**
2.**Step 1** 区域点击 **「一键创建所有缺失资产」**。
3. 资产将创建在 `Assets/_Game/Data/Events/UI/Splash/``UI/Loading/``UI/MainMenu/``Core/` 目录下。
4. 检查清单全部变绿后进入下一步。
**方法 B — 手动创建:**
在 Project 窗口右键 → Create按以下清单逐一创建
```
Assets/_Game/Data/Events/UI/Splash/
EVT_SplashStartRequest.asset (VoidEventChannelSO)
EVT_SplashComplete.asset (VoidEventChannelSO)
Assets/_Game/Data/Events/UI/Loading/
EVT_LoadingStarted.asset (VoidEventChannelSO)
EVT_LoadingComplete.asset (VoidEventChannelSO)
EVT_LoadingProgressUpdated.asset (FloatEventChannelSO)
Assets/_Game/Data/Events/UI/MainMenu/
EVT_SlotConfirmed.asset (IntEventChannelSO)
```
---
### 5.2 配置 Persistent 场景Step 2
**前置条件:** 已完成 Step 1`Scene_Persistent` 已在 Hierarchy 中打开。
**方法 A — 自动脚手架(推荐):**
1. 在 Hierarchy 中打开 `Scene_Persistent`(双击或在 Build Settings 中打开)。
2. 打开 **BaseGames → Tools → Boot Flow Wizard**,点击 **Step 2** 中的 **「脚手架 Persistent 场景」**。
3. 工具将自动:
-`[Services]` 下创建 `BootSequencer` GameObject 并挂载组件。
-`[UI]` 下创建 `Canvas_Splash`(排序层 100并挂载 `SplashScreenController`
-`[UI]` 下创建 `Canvas_Loading`(排序层 99并挂载 `LoadingScreenManager`
- 自动绑定所有存在的事件频道 SO。
-`GameManager` 绑定 `_bootSequencer``_onSceneLoadRequest``_onSceneLoaded`
-`SceneLoader` 绑定三个加载事件频道。
4. 保存场景Ctrl+S
**方法 B — 手动配置(如需精细控制):**
1.`[Services]` 下新建空 GameObject 命名为 `BootSequencer`,挂载 `BootSequencer` 组件。
2. 在 Inspector 中赋值:
- `_onSplashStartRequest``EVT_SplashStartRequest`
- `_onSplashComplete``EVT_SplashComplete`
- `_onPreloadProgress``EVT_LoadingProgressUpdated`(可选)
- `_preloadLabel` → Addressable 标签名(如 `"Preload"`,留空则跳过)
3.`GameManager` 组件 Inspector 中赋值:
- `_bootSequencer` → 上述 BootSequencer 组件
- `_onSceneLoadRequest``EVT_SceneLoadRequest`
- `_onSceneLoaded``EVT_SceneLoaded`
4.`SceneLoader` 组件 Inspector 中赋值:
- `_onLoadingStarted``EVT_LoadingStarted`
- `_onLoadingComplete``EVT_LoadingComplete`
- `_onLoadingProgressUpdated``EVT_LoadingProgressUpdated`
---
### 5.3 配置 Splash 演出Step 2 续)
1. 找到 `Canvas_Splash` GameObject。
2. 在子节点中创建两个 Image + CanvasGroup 结构:
```
Canvas_Splash (SplashScreenController)
└── StudioLogo <- 工作室 Logo 图片
└── CanvasGroup
└── GameTitle <- 游戏标题图片/文字
└── CanvasGroup
```
3. 将 `StudioLogo` 的 CanvasGroup 赋给 `SplashScreenController._studioLogoGroup`。
4. 将 `GameTitle` 的 CanvasGroup 赋给 `SplashScreenController._gameTitleGroup`。
5. 调整 `_fadeDuration`(淡入/淡出时长)和 `_holdDuration`(停留时长)。
**跳过 Splash调试模式**
- 将 `BootSequencer._onSplashComplete` 留空BootSequencer 不会等待 Splash 完成,直接进入主菜单。
---
### 5.4 配置 Loading 画面Step 2 续)
1. 找到 `Canvas_Loading` GameObject默认已由脚手架创建初始 `SetActive(false)`)。
2. 为 `LoadingScreenManager` 创建所需 UI 子节点:
```
Canvas_Loading (LoadingScreenManager)
└── LoadingPanel (背景遮罩 + 内容)
└── ProgressBar (Slider)
└── TipText (可选:随机提示文字)
```
3. 在 `LoadingScreenManager` Inspector 中赋值:
- `_loadingPanel` → `LoadingPanel` GameObject
- `_progressBar` → `ProgressBar` Slider 组件
- `_onLoadingStarted` → `EVT_LoadingStarted`(已由脚手架绑定)
- `_onLoadingComplete` → `EVT_LoadingComplete`(已由脚手架绑定)
- `_onLoadingProgressUpdated` → `EVT_LoadingProgressUpdated`(已由脚手架绑定)
---
### 5.5 创建并配置主菜单场景Step 3
**创建场景:**
1. 在 Project 窗口右键 → Create → Scene命名为 `Scene_MainMenu`。
2. 将其放置在 `Assets/_Game/Scenes/` 目录下。
3. 双击打开场景。
**自动脚手架:**
1. 打开 **Boot Flow Wizard → Step 3**,点击 **「脚手架 MainMenu 场景」**。
2. 工具将在场景中生成:
```
[MainMenu]
└── Canvas_MainMenu (Canvas, CanvasScaler, GraphicRaycaster, MainMenuController)
└── MenuPanel (VerticalLayoutGroup)
├── Btn_NewGame (Image, Button)
├── Btn_Continue (Image, Button)
├── Btn_Settings (Image, Button)
├── Btn_Credits (Image, Button)
└── Btn_Quit (Image, Button)
├── SaveSlotPanel (SetActive=false, SaveSlotController)
├── SettingsPanel (SetActive=false)
└── CreditsPanel (SetActive=false)
```
3. **必须手动填写** `MainMenuController._firstGameSceneKey`
- 在 Inspector 中输入第一个游戏场景的 Addressable Key字符串
- 例如:`"Scene_Prologue"` 或 `"Scene_Town_01"`。
**SaveSlotPanel 配置:**
1. 打开 `SaveSlotPanel`。
2. 为 `SaveSlotController` 补充三个存档槽按钮引用(`_slot0Btn`、`_slot1Btn`、`_slot2Btn`)。
3. 为每个按钮添加适当的 UI 样式(背景图、存档信息文本等)。
**SettingsPanel / CreditsPanel**
这两个面板为空节点,由各自项目美术/策划填充内容。`MainMenuController` 通过 `_settingsPanel.SetActive(true/false)` 控制其显隐。
---
### 5.6 Build Settings 配置Step 4
1. 打开菜单 **File → Build Settings**(或 Boot Flow Wizard 中点击 **「打开 Build Settings」**)。
2. 将以下场景加入 Scenes in Build顺序重要
| 索引 | 场景 | 说明 |
|---|---|---|
| 0 | `Assets/_Game/Scenes/Scene_Boot.unity` | 启动入口(仅包含 `GameBootstrap`|
| — | `Assets/_Game/Scenes/Scene_Persistent.unity` | DontDestroyOnLoad 场景(不需要显式索引)|
| — | `Assets/_Game/Scenes/Scene_MainMenu.unity` | 主菜单(通过 Addressables 加载)|
3. 确保 `Scene_Boot` 为索引 0Player 设置中的第一场景)。
> **注意:** 其他所有游戏场景(关卡等)应通过 Addressables 打包,**不应**加入 Build Settings 的 Scene 列表,以避免包体膨胀。
---
## 6. 使用编辑器向导工具
### Boot Flow Wizard
菜单:**BaseGames → Tools → Boot Flow Wizard**
窗口提供四个步骤的实时状态检查:
- **✅(绿色)** = 该项已正确配置
- **⬜(灰色)** = 该项尚未完成
| 步骤 | 功能 |
|---|---|
| Step 1 | 检测所有 8 个启动流程事件频道资产,一键创建缺失项 |
| Step 2 | 检测 Persistent 场景中 9 个组件/字段绑定状态,一键脚手架 |
| Step 3 | 检测主菜单场景组件状态,一键脚手架,快速打开 Build Settings |
| Step 4 | 运行全量验证并在 Console 输出带位置信息的报告 |
底部状态栏实时显示 `通过 N / 总计 M 项检查`。
### Create Event Channel Assets
菜单:**BaseGames → Tools → Create Event Channel Assets**
在 `Assets/_Game/Data/Events/` 目录下批量创建**所有**系统所需事件频道(含启动流程部分)。已存在资产自动跳过(幂等操作)。
### Scaffold Persistent Scene / Scaffold Main Menu Scene
菜单:**BaseGames → Tools → Scaffold Persistent Scene**
菜单:**BaseGames → Tools → Scaffold Main Menu Scene**(优先级 202
独立的脚手架工具,适合不打开向导窗口时直接执行。
---
## 7. FSM 状态转换关系
启动流程涉及的 GameStateMachine 转换:
```
Initializing
└──(EVT_SceneLoaded: Scene_MainMenu)──► MainMenu
┌─────────────────────┤
│ │
(新游戏/继续) (其他)
LoadingScene
└──(EVT_SceneLoaded: GameScene)──► Gameplay
┌──────────┤
│ │
(Pause) (PlayerDied)
│ │
Paused Dead
┌──────────────┤
│ │
(Resume) (GoToMainMenu)
│ │
Gameplay MainMenu
GameOver ──(EVT_SceneLoaded: Scene_MainMenu)──► MainMenu
```
**`HandleSceneLoadRequest` 逻辑GameManager**
- `TransitionType.Scene` + 目标 ≠ `Scene_MainMenu` → FSM 当前在 MainMenu/Gameplay/BossFight → 转换到 `LoadingScene`
- `TransitionType.Scene` + 目标 = `Scene_MainMenu` → 不经过 `LoadingScene`,由 `HandleSceneLoaded` 直接转换
- `TransitionType.Room` → 完全忽略,状态保持 Gameplay
---
## 8. 自定义扩展指南
### 添加第三段 Splash如游戏内 IP 授权方 Logo
在 `SplashScreenController` 中 `PlayAsync()` 方法末尾追加:
```csharp
// 第三段IP Logo
if (_ipLogoGroup != null)
{
yield return StartCoroutine(Fade(_ipLogoGroup, 0f, 1f, _fadeDuration));
yield return new WaitForSeconds(_holdDuration);
yield return StartCoroutine(Fade(_ipLogoGroup, 1f, 0f, _fadeDuration));
}
```
同时在 Inspector 中添加 `[SerializeField] private CanvasGroup _ipLogoGroup;` 字段。
---
### 修改 Loading 画面为视频背景
1. 给 `Canvas_Loading` 添加 `RawImage` 组件用于显示视频。
2. 在 `LoadingScreenManager` 的 `Show()` 中启动 `VideoPlayer.Play()``Hide()` 中停止。
3. 使用 `EVT_LoadingStarted` / `EVT_LoadingComplete` 频道触发视频播放/停止。
---
### 新游戏时跳过存档选择(单存档模式)
在 `MainMenuController` 中,将 `OnNewGameClicked` 修改为直接触发槽 0
```csharp
private void OnNewGameClicked()
{
// 单存档:直接使用槽 0
HandleSlotConfirmed(0);
}
```
---
### 添加主菜单背景音乐
在 `MainMenuController.Start()` 或响应 `EVT_GameStateChanged(MainMenu)` 时:
```csharp
_onBGMRequest?.Raise("BGM_MainMenu"); // 赋值 EVT_BGMRequest 频道
```
AudioManager 的 `EVT_BGMRequest` 监听器会自动淡入播放。
---
## 9. 常见问题排查
### ❌ 游戏启动后停在黑屏,不显示 Splash 也不进入主菜单
**原因:** `GameManager._bootSequencer` 未绑定BootCoroutine 无法执行。
**解决:** 打开 Boot Flow Wizard → Step 2检查 `GameManager._bootSequencer 已绑定` 是否为绿色;否则重新执行脚手架。
---
### ❌ Splash 演出播放完毕后一直黑屏(不进入主菜单)
**原因 1** `GameManager._onSceneLoaded` 未绑定或绑定了错误的 SO 实例,`HandleSceneLoaded` 从未被调用。
**解决:** 确认 `GameManager._onSceneLoaded` 与 `SceneLoader._onSceneLoaded` 绑定同一个 `EVT_SceneLoaded.asset`。
**原因 2** `ISceneService` 未注册(`GameServiceRegistrar` 未正确引用 `SceneService`)。
**解决:** Console 中搜索 `[GameManager] ISceneService 未注册`,检查 Persistent 场景中的 `GameServiceRegistrar._sceneService` 引用。
---
### ❌ 点击「新游戏」后进入黑屏,加载进度条不出现
**原因 1** `LoadingScreenManager._onLoadingStarted` 未绑定。
**原因 2** `LoadingScreenManager._loadingPanel` 为空(未赋值 UI 根节点)。
**解决:** 检查 Boot Flow Wizard Step 2 中 `SceneLoader._onLoadingStarted 已绑定` 状态。
---
### ❌ `MainMenuController._firstGameSceneKey` 填写后仍报 Addressable 加载失败
**原因:** Key 填写有误,或对应场景未在 Addressables 中标记。
**解决:**
1. 打开 **Window → Asset Management → Addressables → Groups**。
2. 找到目标场景资产,确认其 Address 与 `_firstGameSceneKey` 完全一致(大小写敏感)。
3. 确保该场景已勾选 Include in Build。
---
### ❌ 返回主菜单时 FSM 报 Invalid transition 警告
**原因:** 通常是从 `Dead` 或 `GameOver` 状态直接触发 `HandleSceneLoaded(MainMenu)`,而这两个状态的 `ValidNextStates` 不包含 `MainMenu`。
**解决:** 确保 `DeathRespawnService.StartGameOverCoroutine()` 在加载主菜单场景**之前**已调用 `GameManager.RequestTransition(GameOver)`。参见 `BuiltinGameStates.cs` 中 `GameOverState.ValidNextStates`。
---
### ❌ 编辑器中运行正常,打包后 Splash 不显示
**原因:** `Canvas_Splash` 上的 Image Sprite 未加入 Addressable Build 或 Sprite Atlas。
**解决:** 将 Splash 使用的所有 Texture/Sprite 加入 Addressable Group标签 `"Preload"` 以便启动时预热),或直接内嵌进默认 Resources。
---
*文档最后更新2026-05-19*