完整启动流程

This commit is contained in:
2026-05-19 23:20:44 +08:00
parent d0a1112737
commit 5fd981f5b9
22 changed files with 1938 additions and 14 deletions

View File

@@ -0,0 +1,580 @@
# 游戏启动流程开发手册
> 文件位置:`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*

View File

@@ -58,7 +58,13 @@
|---|---|---|
| `TriggerZone` | 存档点(`CheckpointMarker`)、存档台(`SavePoint`)、传送站(`TeleportStation`)、房间过渡(`RoomTransition` / `DoorTransition`)、相机区域(`CameraArea` | 纯触发碰撞体isTrigger不参与物理阻挡统一使用此 Layer 方便在碰撞矩阵中集中管理 |
### 2.5 特殊用途
### 2.5 环境危险
| Layer 名称 | 挂载对象 | 用途说明 |
|---|---|---|
| `HazardHitBox` | 对双方均造成伤害的环境危险落石、熔岩、毒区、AOE 爆炸范围等) | 同时与 `PlayerHurtBox``EnemyHurtBox` 碰撞;区别于 `EnemyHitBox`(只对玩家),此 Layer 用于阵营中立的环境伤害源 |
### 2.6 特殊用途
| Layer 名称 | 挂载对象 | 用途说明 |
|---|---|---|
@@ -85,6 +91,10 @@
| `EnemyProjectile` | `Ground` | ✅ | 敌人投射物命中地形 |
| `PlayerHitBox` | `PlayerHurtBox` | ❌ | 玩家不自伤 |
| `PlayerProjectile` | `EnemyProjectile` | ❌ | 子弹不互相碰撞Clash 系统单独处理) |
| `HazardHitBox` | `PlayerHurtBox` | ✅ | 环境危险伤害玩家 |
| `HazardHitBox` | `EnemyHurtBox` | ✅ | 环境危险伤害敌人(中立伤害) |
| `HazardHitBox` | `PlayerHitBox` | ❌ | 环境不触发拼刀 |
| `HazardHitBox` | `EnemyHitBox` | ❌ | 环境不触发拼刀 |
> 未在上表中列出的 Layer 对默认继承 Unity 全局设置(默认全部碰撞)。
@@ -122,7 +132,83 @@ void Awake()
---
## 5. 检查与修复工具
## 6. 复杂场景处理模式
### 6.1 弹反投射物EnemyProjectile → 伤害敌人)
**问题**`EnemyProjectile ↔ EnemyHurtBox = 不碰撞`,弹反后的投射物无法对敌人造成伤害。
**正确方案:运行时 Layer 切换**(不新增 Layer
弹反成功时,将投射物的 Layer 从 `EnemyProjectile` 切换为 `PlayerProjectile`,同时反转飞行方向。`Projectile` 已有 `SetFactionLayer(int ownerLayer)` 方法,弹反逻辑只需调用它:
```csharp
// HurtBox.ReceiveDamage() 弹反成功后步骤2
if (_parrySystem.ConsumeParry())
{
// 若攻击来源是投射物,翻转其阵营 Layer
if (info.SourceProjectile != null)
{
info.SourceProjectile.ReflectAsPlayerProjectile();
// ReflectAsPlayerProjectile() 内部:
// gameObject.layer = LayerMask.NameToLayer("PlayerProjectile");
// rb.velocity = -rb.velocity; // 反向
}
return;
}
```
> **注意**:当前 `DamageInfo` 尚未携带 `SourceProjectile` 字段,实现此功能需要:
> 1. 在 `DamageInfo` 中添加 `Projectile SourceProjectile` 字段
> 2. `HitBox.OnTriggerEnter2D` 通过 `_attackerTransform.GetComponent<Projectile>()` 填入
> 3. `Projectile` 实现 `ReflectAsPlayerProjectile()` 方法
**为什么不新建 `ParriedProjectile` 层?**
`PlayerProjectile` 的语义本就是"对敌人有效、对玩家无效"的攻击来源,弹反后的投射物完全符合此语义,无需新层。
---
### 6.2 环境伤害(同时对玩家与敌人有效)
`LethalTrap`(陷阱)当前使用 `EnemyHitBox` 层,只对玩家生效,是**有意为之**的设计(跑酷陷阱不伤敌人)。
对于确实需要**同时伤害双方**的环境机关落石、熔岩、AOE 区域等),使用 `HazardHitBox` 层并配合 `HazardHitBoxTrigger` 组件(待实现),收到碰撞时向 `PlayerHurtBox``EnemyHurtBox` 均发送伤害:
```
HazardHitBox ↔ PlayerHurtBox → 碰撞
HazardHitBox ↔ EnemyHurtBox → 碰撞
```
---
### 6.3 敌人之间互伤(友伤/AOE
`EnemyHitBox ↔ EnemyHurtBox = 碰撞` 已在碰撞矩阵中开启。
自伤防护由 `HitBox.OnTriggerEnter2D` 的根节点比较负责:
```csharp
if (other.transform.root == _attackerTransform.root) return; // 排除自身
```
这意味着:
- 同一 GameObject 树的 EnemyHitBox 不会命中自身的 EnemyHurtBox ✅
- 敌人 A 的 HitBox 可以命中敌人 B 的 HurtBox ✅(天然支持友伤)
- Boss 的 AOE 技能可以对场景中所有其他敌人造成伤害 ✅
---
### 6.4 不需要细分的场景
以下场景**不需要新增 Layer**,通过 `DamageInfo` 字段在逻辑层区分即可:
| 场景 | 处理方式 |
|---|---|
| 区分攻击来源(玩家技能 vs 玩家普攻) | `DamageInfo.SkillId` / `DamageInfo.SourceId` |
| 不同类型伤害(物理 vs 魔法) | `DamageInfo.Type``DamageType` 枚举) |
| Boss 阶段专属伤害规则 | `DamageInfo.Tags``DamageTags` 标记位) |
| 不可弹反的攻击 | `DamageInfo.Flags` 不含 `CanBeParried` |
| 穿透无敌帧的伤害 | `DamageInfo.Flags``IgnoreIFrame` |
项目提供了 **Physics2DLayerReport** 编辑器工具,位于菜单: