完整启动流程

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
+580
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` 为索引 0(Player 设置中的第一场景)。
> **注意:** 其他所有游戏场景(关卡等)应通过 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*
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,956 +0,0 @@
# PathBerserker2d 技术评估与使用手册
> **版本**: PathBerserker2d (Unity Asset Store)
> **Unity 要求**: 2020.3+
> **依赖**: `com.unity.modules.physics2d`
> **许可**: Unity Asset Store EULA
> **包路径**: `Assets/PathBerserker2d/`
> **官方文档**: https://oribow.github.io/PathBerserker2dDemo/Documentation/
---
## 目录
1. [概述与设计哲学](#1-概述与设计哲学)
2. [架构总览](#2-架构总览)
3. [核心组件](#3-核心组件)
4. [NavAgent 详解](#4-navagent-详解)
5. [NavSurface 详解](#5-navsurface-详解)
6. [NavLink 与 NavLinkCluster](#6-navlink-与-navlinkcluster)
7. [辅助组件](#7-辅助组件)
8. [全局系统 — PBWorld 与 Settings](#8-全局系统--pbworld-与-settings)
9. [路径与寻路请求](#9-路径与寻路请求)
10. [预置行为脚本](#10-预置行为脚本)
11. [TransformBasedMovement 移动系统](#11-transformbasedmovement-移动系统)
12. [Demo 场景与使用模式](#12-demo-场景与使用模式)
13. [Corgi 引擎集成](#13-corgi-引擎集成)
14. [全局设置参考](#14-全局设置参考)
15. [与 BaseGames 架构集成方案](#15-与-basegames-架构集成方案)
16. [性能分析与优化建议](#16-性能分析与优化建议)
17. [优缺点总结](#17-优缺点总结)
18. [总结与建议](#18-总结与建议)
---
## 1. 概述与设计哲学
### 1.1 什么是 PathBerserker2d
PathBerserker2d 是一款专为 **2D 游戏** 设计的导航/寻路插件。与 Unity 内置的 NavMesh(面向 3D)不同,它基于 **线段(Segment** 而非面片构建导航图,天然适合 2D 横版游戏的地形表示。
核心理念:
> **从 2D Collider 自动烘焙导航线段,通过多线程 A\* 异步寻路,以事件驱动的方式委托移动实现。**
### 1.2 核心设计原则
| 原则 | 说明 |
|------|------|
| **线段导航** | 导航数据为沿 Collider2D 表面的线段,适合平台跳跃和横版场景 |
| **自动烘焙** | 从子对象的 Collider2D 自动提取导航线段,无需手动标记 |
| **异步多线程** | 寻路计算在后台线程完成(默认4线程),不阻塞主线程 |
| **事件驱动** | NavAgent 通过事件通知移动组件,移动实现完全解耦 |
| **动态世界** | 支持运行时烘焙、移动平台、动态链接启停 |
| **链接系统** | 跳跃、下落、攀爬、电梯、传送等复杂行为通过 NavLink 建模 |
### 1.3 适用场景
- 2D 横版平台跳跃(Metroidvania / Platformer
- 2D 俯视角 RPG360° 模式)
- 需要 AI 自动寻路的 2D 游戏
- 含电梯、梯子、传送门、移动平台等复杂地形的关卡
---
## 2. 架构总览
### 2.1 系统架构图
```
NavSurface (烘焙 Collider2D → 线段数据)
↓ OnEnable 时添加
PBWorld.NavGraph (全局导航图,B2DynamicTree 空间索引)
↑ NavLink / NavLinkCluster 添加连接
↑ NavAreaMarker 标记区域 NavTag
↑ NavSegmentSubstractor 裁剪线段
NavAgent.PathTo() → PBWorld.PathTo(PathRequest)
↓ 入队
PathfinderThread (多线程 A*) → PathRequest.Fulfill(Path)
↓ 主线程轮询
NavAgent.HandlePathRequest() → 开始 FollowPath
↓ 事件驱动
TransformBasedMovement / 自定义移动 (实际移动)
↓ 完成后回调
NavAgent.CompleteSegmentTraversal() / CompleteLinkTraversal()
```
### 2.2 关键设计模式
| 模式 | 说明 |
|------|------|
| **异步多线程寻路** | PathRequest 入队 → PathfinderThread 后台 A\* → 主线程轮询结果 |
| **事件驱动移动** | NavAgent 触发事件,外部组件响应事件实现移动逻辑 |
| **动态世界** | NavSurface 可运行时烘焙/加载/卸载,链接可动态添加/移除/切换可穿越性 |
| **移动平台支持** | 所有坐标相对 NavSurface 本地坐标存储,Transform 变化时自动跟随 |
| **自动重新寻路** | `autoRepathIntervall` 控制周期性重算路径,应对世界变化 |
| **WebGL 兼容** | 检测平台后使用协程替代线程,确保单线程环境可用 |
### 2.3 目录结构
```
Assets/PathBerserker2d/
├── Scripts/PathBerserker2d/ # 核心源码
│ ├── NavAgent/ # NavAgent.cs, TransformBasedMovement.cs
│ │ └── NavAgentUsers/ # 预置行为脚本 (8个)
│ ├── NavSurface/ # NavSurface.cs, 碰撞体过滤, 线段创建
│ │ ├── NavSegments/ # 线段数据结构
│ │ └── Creation/ # 烘焙管线
│ ├── NavObjects/ # NavLink, NavLinkCluster, NavAreaMarker 等
│ ├── NavGraph/ # 导航图, 图节点, 空间索引
│ ├── Pathfinder/ # A* 寻路, 路径请求, 多线程
│ ├── PBWorld.cs # 全局单例管理器
│ ├── PathBerserker2dSettings.cs # 全局设置
│ └── IVelocityProvider.cs # 速度接口
├── Demo/ # 演示场景与脚本
│ ├── Scenes/ # 15+ 场景
│ └── Scripts/ # 演示脚本
├── Corgi/ # Corgi Engine 集成层
├── Resources/ # 全局设置资源文件
└── Documentation/ # 官方文档 (zip)
```
---
## 3. 核心组件
### 3.1 组件总览
| 组件 | 菜单路径 | 职责 |
|------|---------|------|
| **NavSurface** | `PathBerserker2d/Nav Surface` | 从 Collider2D 烘焙导航线段 |
| **NavAgent** | `PathBerserker2d/Nav Agent` | 寻路请求与路径跟随 |
| **NavLink** | `PathBerserker2d/Nav Link` | 两点间导航链接(跳跃/下落/攀爬等) |
| **NavLinkCluster** | `PathBerserker2d/Nav Link Cluster` | 多点互联链接集群(电梯/梯子) |
| **NavAreaMarker** | `PathBerserker2d/Nav Area Marker` | 区域 NavTag 标记 |
| **NavSegmentSubstractor** | — | 在烘焙时裁剪指定区域的线段 |
| **DynamicObstacle** | — | 标记 GameObject 在烘焙时被忽略 |
| **TransformBasedMovement** | — | 基于 Transform 的默认移动实现 |
### 3.2 组件依赖关系
```
NavAgent ←需要→ TransformBasedMovement (或自定义移动组件)
NavAgent ←依赖→ PBWorld (全局单例,自动创建)
NavSurface ←依赖→ 子对象的 Collider2D
NavLink / NavLinkCluster ←映射→ NavSurface 上的线段
NavAreaMarker ←需要→ RectTransform
NavSegmentSubstractor ←需要→ RectTransform
```
---
## 4. NavAgent 详解
### 4.1 状态机
```
NavAgent 状态:
┌─────────┐
│ Idle │ ←── 初始 / Stop() / ForceStop() / 到达目标
└────┬─────┘
│ PathTo()
┌─────────────────────────────┐
│ FollowPath │
│ ┌─────────────────────┐ │
│ │ OnSegment │ ←─ 在线段上移动
│ ├─────────────────────┤ │
│ │ WaitForLinkOnSegment│ ←─ 等待链接可用
│ ├─────────────────────┤ │
│ │ OnLink │ ←─ 正在穿越链接
│ └─────────────────────┘ │
└─────────────────────────────┘
```
### 4.2 公共方法
| 方法 | 返回值 | 说明 |
|------|--------|------|
| `PathTo(Vector2 goal)` | `bool` | 寻路到目标位置 |
| `PathTo(params Vector2[] goals)` | `bool` | 寻路到多个目标中最近的 |
| `UpdatePath()` | `void` | 触发重新寻路(不改变目标) |
| `SetRandomDestination()` | `bool` | 设置随机目标 |
| `Stop()` | `void` | 优雅停止(完成当前链接后停止) |
| `ForceStop()` | `void` | 立即强制停止 |
| `CompleteSegmentTraversal()` | `void` | 通知已完成线段移动(由移动组件调用) |
| `CompleteLinkTraversal()` | `void` | 通知已完成链接穿越(由移动组件调用) |
| `WarpToNearestSegment()` | `void` | 传送到最近线段 |
| `CanTraverseLink(int linkType)` | `bool` | 检查是否可穿越指定类型链接 |
| `CanReach(Vector2 goal)` | `bool` | 同步检查目标是否可达 |
| `CreatePathRequest()` | `PathRequest` | 创建自定义寻路请求 |
### 4.3 公共属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `CurrentStatus` | `NavAgentStatus` | 当前状态枚举 |
| `IsIdle` | `bool` | 是否空闲 |
| `IsFollowingAPath` | `bool` | 是否正在跟随路径 |
| `IsOnLink` | `bool` | 是否正在链接上 |
| `IsMovingOnSegment` | `bool` | 是否在线段上移动 |
| `HasValidPosition` | `bool` | 当前位置是否有效(在线段上) |
| `Height` | `float` | 代理高度 |
| `MaxSlopeAngle` | `float` | 最大坡度角 |
| `CurrentSegmentNormal` | `Vector2` | 当前线段法线 |
| `PathGoal` | `Vector2` | 当前路径目标 |
| `Position` | `Vector2` | 代理位置 |
| `CurrentNavTagVector` | `int` | 当前位置的 NavTag |
| `TimeOnLink` | `float` | 在当前链接上经过的时间 |
### 4.4 事件
| 事件 | 参数 | 触发时机 |
|------|------|---------|
| `OnStartLinkTraversal` | `NavAgent, PathSegment` | 开始穿越链接 |
| `OnLinkTraversal` | `NavAgent, PathSegment, float t` | 链接穿越每帧回调(t: 0→1) |
| `OnStartSegmentTraversal` | `NavAgent, PathSegment` | 开始线段移动 |
| `OnSegmentTraversal` | `NavAgent, PathSegment, float t` | 线段移动每帧回调 |
| `OnFailedToFindPath` | — | 寻路失败 |
| `OnStop` | — | 停止移动 |
| `OnReachedGoal` | — | 到达目标 |
| `OnStartFollowingNewPath` | — | 开始跟随新路径 |
### 4.5 Inspector 配置
| 字段 | 默认值 | 说明 |
|------|--------|------|
| `height` | 1 | 代理高度(影响净空检查) |
| `maxSlopeAngle` | 60 | 最大可行走坡度角 |
| `autoRepathIntervall` | 1 | 自动重新寻路间隔(秒) |
| `maximumDistanceToPathStart` | ∞ | 到路径起点的最大距离 |
| `linkTraversalCostMultipliers[]` | — | 各链接类型代价乘数 |
| `navTagTraversalCostMultipliers[]` | — | 各 NavTag 代价乘数 |
| `allowCloseEnoughPath` | `false` | 允许"近似到达"路径 |
| `enableDebugMessages` | `false` | 启用调试日志 |
---
## 5. NavSurface 详解
### 5.1 核心概念
NavSurface 是导航数据的来源。它扫描子对象上的 **Collider2D**,沿碰撞体表面生成导航线段,并在烘焙完成后将数据添加到全局 NavGraph。
关键特性:
- **自动烘焙**:编辑器中或运行时均可烘焙
- **移动平台**:烘焙数据以本地坐标存储,NavSurface Transform 移动时路径跟随
- **动态加载**`OnEnable` 添加到 NavGraph`OnDisable` 移除
### 5.2 公共属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `WorldBounds` | `Rect` | 世界空间包围盒 |
| `MaxClearance` | `float` | 最大净空检查高度 |
| `MinClearance` | `float` | 最小净空(低于此值的线段被移除) |
| `CellSize` | `float` | 净空检查精度 |
| `ColliderMask` | `LayerMask` | 碰撞体过滤层 |
| `TotalLineLength` | `float` | 所有线段总长度 |
| `MaxSlopeAngle` | `float` | 最大坡度角 |
| `LocalToWorldMatrix` | `Matrix4x4` | 本地到世界矩阵 |
### 5.3 公共方法
| 方法 | 说明 |
|------|------|
| `IEnumerator Bake()` | 运行时烘焙(协程) |
| `Vector2 LocalToWorld(Vector2 pos)` | 本地坐标转世界坐标 |
| `Vector2 WorldToLocal(Vector2 pos)` | 世界坐标转本地坐标 |
### 5.4 事件
| 事件 | 说明 |
|------|------|
| `OnBakingCompleted` | 烘焙完成 |
| `OnReadyToPathfind` | 已添加到 NavGraph,可以开始寻路 |
| `OnRemovedFromPathfinding` | 已从 NavGraph 移除 |
### 5.5 Inspector 烘焙配置
| 字段 | 默认值 | 说明 |
|------|--------|------|
| `maxClearance` | 1.8 | 障碍检查最大高度 |
| `minClearance` | 0.1 | 净空低于此值的线段被移除 |
| `cellSize` | 0.1 | 障碍检查精度(越小越精确但越慢) |
| `includedColliders` | ~0 | 参与烘焙的层 |
| `onlyStaticColliders` | false | 仅包含静态碰撞体 |
| `maxSlopeAngle` | 180 | 移除超过此角度的线段 |
| `smallestDistanceYouCareAbout` | 0.1 | RDP 线简化容差 |
| `minSegmentLength` | 0.1 | 最小线段长度 |
### 5.6 使用示例
```
场景层级结构:
NavSurface (GameObject)
├── Ground (BoxCollider2D)
├── Platform_1 (BoxCollider2D)
├── Platform_2 (PolygonCollider2D)
└── Wall (BoxCollider2D)
```
> **注意**: NavSurface 只扫描 **子对象** 上的 Collider2D,自身上的碰撞体不会被烘焙。
---
## 6. NavLink 与 NavLinkCluster
### 6.1 NavLink — 单个导航链接
NavLink 连接两个点,代表跳跃、下落、攀爬等需要特殊处理的路径段。
#### 可视化类型
| 类型 | 说明 |
|------|------|
| `Linear` | 直线 |
| `QuadradticBezier` | 二次贝塞尔曲线 |
| `Projectile` | 抛物线 |
| `Teleport` | 传送(瞬移) |
| `None` | 无可视化 |
| `TransformBasedMovement` | 使用 TransformBasedMovement 的默认处理 |
#### 公共属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `GoalWorldPosition` | `Vector2` | 目标世界位置(可读写) |
| `StartWorldPosition` | `Vector2` | 起点世界位置(可读写) |
| `IsBidirectional` | `bool` | 是否双向 |
| `IsAddedToWorld` | `bool` | 是否已添加到 NavGraph |
#### 公共方法
| 方法 | 说明 |
|------|------|
| `UpdateMapping()` | 位置改变后更新链接映射 |
| `SetStartToGoalLinkTraversable(bool)` | 设置正向可穿越性 |
| `SetGoalToStartLinkTraversable(bool)` | 设置反向可穿越性 |
#### 共有基类属性 (BaseNavLink)
| 属性 | 说明 |
|------|------|
| `LinkType` | 链接类型索引(corner=0, jump=1, fall=2, teleport=3, climb=4, elevator=5 |
| `Clearance` | 允许通过的最大代理高度 |
| `AvgWaitTime` | 平均等待时间(影响寻路代价) |
| `CostOverride` | 代价覆盖(≤0 使用距离计算) |
| `NavTag` | NavTag 标记 |
| `MaxTraversableDistance` | 最大可穿越距离 |
| `autoMap` | 自动映射到 NavGraph |
### 6.2 NavLinkCluster — 链接集群
NavLinkCluster 适用于多点互联场景,如 **电梯**(多层站点)、**梯子**(多个停靠点)。
内部为每对点自动生成一个链接。
#### LinkPoint 结构
```csharp
struct LinkPoint {
Vector2 point;
PointTraversalType traversalType; // Exit | Entry | Both
}
```
#### 公共方法
| 方法 | 说明 |
|------|------|
| `UpdateMapping()` | 更新所有链接映射 |
| `SetLinksTraversable(Func<Vector2, Vector2, bool>)` | 根据起终点函数设置各链接可穿越性 |
#### 使用场景
```csharp
// 电梯到达某层时,只允许穿越与该层相关的链接
navLinkCluster.SetLinksTraversable((start, goal) =>
Mathf.Abs(start.y - currentFloor.position.y) < 0.1f ||
Mathf.Abs(goal.y - currentFloor.position.y) < 0.1f);
// 离开时禁用所有链接
navLinkCluster.SetLinksTraversable((start, goal) => false);
```
---
## 7. 辅助组件
### 7.1 NavAreaMarker — 区域标记
**要求**: `RectTransform`
将矩形区域内的所有导航线段标记为指定 NavTag。常用于标记水域、危险区域、门区域等。
| 属性/方法 | 说明 |
|-----------|------|
| `NavTag` | NavTag 索引 |
| `MarkerColor` | 标记颜色 |
| `updateAfterTimeOfNoMovement` | 停止移动后更新延迟 |
| `updateAfterTime` | 最大更新间隔 |
| `UpdateMappings()` | 手动更新映射 |
**门控模式**: 通过启用/禁用 NavAreaMarker 来控制门的通行:
```csharp
// 开门 → 禁用标记 → 代理可通过
navAreaMarker.enabled = false;
// 关门 → 启用标记 → 区域被标记为不可通行 NavTag
navAreaMarker.enabled = true;
```
### 7.2 NavSegmentSubstractor — 线段裁剪器
**要求**: `RectTransform`
在烘焙时从指定矩形区域移除线段。支持角度过滤:
| 字段 | 说明 |
|------|------|
| `fromAngle` | 最小角度过滤 |
| `toAngle` | 最大角度过滤 |
### 7.3 DynamicObstacle — 动态障碍物
空标记组件。附加到 GameObject 上,使其在 NavSurface 烘焙时被忽略。
### 7.4 IVelocityProvider — 速度接口
```csharp
interface IVelocityProvider {
Vector2 WorldVelocity { get; }
}
```
用于 `Follower` 组件的目标预测功能。在跟随目标上实现此接口即可。
---
## 8. 全局系统 — PBWorld 与 Settings
### 8.1 PBWorld — 全局单例
自动实例化,`DontDestroyOnLoad`。管理全局 NavGraph 和寻路调度。
#### 静态 API
```csharp
// === 点映射 ===
// 映射世界点到最近导航位置
static bool TryMapPoint(Vector2 position, out NavSegmentPositionPointer pointer)
// 自定义搜索半径
static bool TryMapPoint(Vector2 position, float searchRadius, out NavSegmentPositionPointer pointer)
// 确保映射位置对代理可通行
static bool TryMapPoint(Vector2 position, float searchRadius, NavAgent agent, out NavSegmentPositionPointer pointer)
// === 随机 ===
static Vector2 GetRandomPointOnGraph() // 返回图上随机点
// === 寻路 ===
static void PathTo(PathRequest pathRequest) // 入队异步寻路请求
// === 空间查询 ===
static List<NavSubsegmentPointer> BoxCast(Rect rect, float rotation, float fromAngle, float toAngle)
// === 图变化监听 ===
static INavGraphChangeSource NavGraphChangeSource // 订阅 OnGraphChange 事件
```
#### NavGraphChange 事件枚举
| 值 | 说明 |
|----|------|
| `NavSurfaceAdded` | NavSurface 添加 |
| `NavSurfaceRemoved` | NavSurface 移除 |
| `NavLinkAdded` | NavLink 添加 |
| `NavLinkRemoved` | NavLink 移除 |
| `SegmentModifierAdded` | 线段修改器添加 |
| `SegmentModifierRemoved` | 线段修改器移除 |
| `NavLinkMoved` | NavLink 位置变更 |
### 8.2 NavSegmentPositionPointer
导航位置指针结构体,用于表示一个在导航图上的有效位置:
| 属性 | 类型 | 说明 |
|------|------|------|
| `Position` | `Vector2` | 世界位置 |
| `Normal` | `Vector2` | 法线 |
| `IsValid()` | `bool` | 是否有效 |
| `IsInvalid()` | `bool` | 是否无效 |
---
## 9. 路径与寻路请求
### 9.1 PathRequest — 寻路请求
| 枚举 | 值 |
|------|-----|
| **RequestState** | `Draft`, `Pending`, `Finished`, `Failed` |
| **RequestFailReason** | `CouldntMapStart`, `CouldntMapGoal`, `MappedStartChanged`, `AllMappedGoalsChanged`, `NoPathFromStartToGoal`, `WorldWasDestroyed`, `ToFarFromStart` |
| 属性 | 类型 | 说明 |
|------|------|------|
| `Status` | `RequestState` | 请求状态 |
| `FailReason` | `RequestFailReason` | 失败原因 |
| `Path` | `Path` | 计算出的路径 |
| `start` | `NavSegmentPositionPointer` | 起点 |
| `goals` | `IList<NavSegmentPositionPointer>` | 目标列表 |
| `client` | `NavAgent` | 请求代理 |
### 9.2 Path — 路径
| 属性/方法 | 说明 |
|-----------|------|
| `Current` | 当前路径片段 (`PathSegment`) |
| `NextSegment` | 下一个片段 |
| `HasNext` | 是否有下一个 |
| `Goal` | 目标位置 |
| `Start` | 起点位置 |
| `totalCosts` | 总代价 |
| `MoveNext()` | 前进到下一片段 |
| `AllPathPoints()` | 获取所有路径点列表 |
| `RemainingPathPoints()` | 获取剩余路径点列表 |
### 9.3 PathSegment — 路径片段
| 属性/方法 | 说明 |
|-----------|------|
| `Next` | 下一个片段 |
| `LinkStart` | 链接起点(世界坐标,随 NavSurface 移动) |
| `LinkEnd` | 链接终点 |
| `Normal` | 线段法线 |
| `Tangent` | 线段切线 |
| `Point` | 线段上的参考点 |
| `link` | 关联的链接实例 |
| `owner` | 所属 NavSurface |
| `GetTagVector(float t)` | 获取位置的 NavTag |
| `DistanceAlongSegment(Vector2 pos)` | 投影距离 |
---
## 10. 预置行为脚本
PathBerserker2d 提供 8 个即用的 NavAgent 行为脚本:
| 组件 | 说明 | 典型用途 |
|------|------|---------|
| **MouseWalker** | 鼠标点击处寻路 | 调试、点击移动原型 |
| **PatrolWalker** | 按顺序巡逻路线,循环 | 巡逻敌人 |
| **RandomWalker** | 持续随机行走 | NPC 闲逛 |
| **MultiGoalWalker** | 寻路到多个目标中最近的一个 | 收集物搜索 |
| **Follower** | 跟随目标 Transform,支持目标预测 | 追踪型敌人 |
| **KeepGrounded** | 通过 Raycast 检测移动平台并 parent | 移动平台上的代理 |
| **AdjustRotation** | 旋转代理匹配线段法线方向 | 沿曲面行走的方向适配 |
| **FootStepSounds** | 根据 NavTag 播放不同脚步声 | 环境音效 |
### 使用方式
直接将这些脚本添加到带有 NavAgent 的 GameObject 上即可。它们自动获取同 GameObject 上的 NavAgent 引用。
---
## 11. TransformBasedMovement 移动系统
### 11.1 概述
`TransformBasedMovement` 是 PathBerserker2d 提供的默认移动实现。它订阅 NavAgent 的事件,在回调中直接修改 Transform 位置,并在完成后调用 `CompleteSegmentTraversal()` / `CompleteLinkTraversal()` 通知 NavAgent。
### 11.2 FeatureFlags
```csharp
[Flags] enum FeatureFlags {
SegmentMovement = 1, // 线段移动
JumpLinks = 2, // 跳跃链接
CornerLinks = 4, // 拐角链接
FallLinks = 8, // 下落链接
TeleportLinks = 16, // 传送链接
ClimbLinks = 32, // 攀爬链接
ElevatorLinks = 64, // 电梯链接
OtherLinks = 128, // 其他链接
}
```
### 11.3 配置属性
| 字段 | 默认值 | 说明 |
|------|--------|------|
| `movementSpeed` | 5 | 线段移动速度 (unit/s) |
| `cornerSpeed` | 100 | 拐角链接速度 (degrees/s) |
| `jumpSpeed` | 5 | 跳跃速度 (unit/s) |
| `fallSpeed` | 5 | 下落速度 (unit/s) |
| `climbSpeed` | 5 | 攀爬速度 (unit/s) |
| `enableAgentRotation` | true | 是否旋转代理朝向移动方向 |
| `enabledFeatures` | 全部启用 | 控制由本组件处理的功能 |
### 11.4 自定义移动组件
如果 `TransformBasedMovement` 不满足需求(例如需要物理驱动移动),可以创建自定义移动组件:
```csharp
public class CustomMovement : MonoBehaviour
{
NavAgent navAgent;
void Awake() => navAgent = GetComponent<NavAgent>();
void OnEnable()
{
navAgent.OnStartSegmentTraversal += HandleSegmentStart;
navAgent.OnSegmentTraversal += HandleSegmentMove;
navAgent.OnStartLinkTraversal += HandleLinkStart;
navAgent.OnLinkTraversal += HandleLinkMove;
}
void HandleSegmentMove(NavAgent agent, PathSegment seg, float t)
{
// 自定义线段移动逻辑
// ...
// 完成后:
agent.CompleteSegmentTraversal();
}
void HandleLinkMove(NavAgent agent, PathSegment seg, float t)
{
// 自定义链接穿越逻辑
// ...
// 完成后:
agent.CompleteLinkTraversal();
}
}
```
---
## 12. Demo 场景与使用模式
### 12.1 Demo 场景一览
| 场景 | 展示内容 |
|------|---------|
| `breakable_wall` | 可破坏墙壁 + 寻路 |
| `door` | NavAreaMarker 门控 |
| `elevator` | NavLinkCluster 电梯系统 |
| `follow_target` | Follower 跟随行为 |
| `ladder` | 攀爬链接 |
| `moving_platform` | 移动平台导航 |
| `multi_surface` | 多 NavSurface 场景 |
| `procedural_endless_jumper` | 运行时烘焙 |
| `runtime_bake` | 运行时动态烘焙 |
| `simple_patrol` | 基础巡逻 |
| `teleport` | 传送链接 |
| `threesixtydegree` | 360° 全向导航 |
| `trafficlight` | 交通灯 + 等待逻辑 |
| `tricky_jump` | 复杂跳跃场景 |
### 12.2 常用使用模式
#### 模式 1:最简单的寻路
```csharp
// GoalWalker 模式:检查距离 + IsIdle → PathTo
void Update()
{
if (Vector2.Distance(goal.position, navAgent.transform.position) > 0.5f
&& (navAgent.IsIdle || goal.hasChanged))
{
goal.hasChanged = false;
navAgent.PathTo(goal.position);
}
}
```
#### 模式 2:电梯 — 动态链接切换
```csharp
// 电梯到达某层 → 启用相关链接
navLinkCluster.SetLinksTraversable((start, goal) =>
Mathf.Abs(start.y - levels[nextLevel].position.y) < 0.1f ||
Mathf.Abs(goal.y - levels[nextLevel].position.y) < 0.1f);
// 电梯离开 → 禁用所有链接
navLinkCluster.SetLinksTraversable((start, goal) => false);
```
#### 模式 3:门 — NavAreaMarker 控制
```csharp
// 开门:禁用 NavAreaMarker → 区域不再有阻挡标记
navAreaMarker.enabled = false;
// 关门:启用 NavAreaMarker → 门区域被标记为不可通行 NavTag
navAreaMarker.enabled = true;
```
#### 模式 4:移动平台
```csharp
// MovingPlatform 是 NavSurface 的子对象
// 只需移动 NavSurface 的 Transform,导航数据自动跟随
transform.Translate(velocity * Time.deltaTime);
```
#### 模式 5:运行时烘焙
```csharp
// 在运行时触发烘焙
StartCoroutine(navSurface.Bake());
// 监听烘焙完成
navSurface.OnBakingCompleted += () => Debug.Log("Bake done!");
navSurface.OnReadyToPathfind += () => Debug.Log("Ready for pathfinding!");
```
---
## 13. Corgi 引擎集成
PathBerserker2d 附带 **CorgiBasedMovement** 组件,替代 `TransformBasedMovement` 以对接 Corgi Engine 的角色控制系统。
### 13.1 核心映射
| 导航行为 | Corgi 映射 |
|---------|-----------|
| 线段移动 | `CharacterHorizontalMovement.SetHorizontalMove()` |
| 跳跃链接 | `CharacterJump.JumpStart()` |
| 攀爬链接 | `CharacterLadder`(反射调用) |
| 传送链接 | 直接设置 Position |
| 拐角链接 | 直接 `CompleteLinkTraversal()` |
| 卡住检测 | `fallbackTeleportDelay` 秒后传送 |
### 13.2 Corgi AI Actions
| 脚本 | 说明 |
|------|------|
| `AIActionPBMoveTowardsTarget` | 移向目标 |
| `AIActionPBMoveTowardsClosestTarget` | 移向最近目标 |
| `AIActionPBMoveTowardsRandomPathableTarget` | 移向可达随机目标 |
| `AIActionPBPatrol` | 巡逻 |
| `AIDecisionPBHasReachedGoal` | 判断是否到达 |
| `AIDecisionPBPathfindingFailed` | 判断寻路失败 |
> **注意**: 本项目使用自研架构而非 Corgi Engine,此集成仅作参考。
---
## 14. 全局设置参考
**位置**: `Edit > Project Settings > PathBerserker2d`
**资源文件**: `Assets/PathBerserker2d/Resources/PathBerserker2dSettings`
### 14.1 设置项
| 属性 | 类型 | 当前值 | 说明 |
|------|------|--------|------|
| `PointMappingDistance` | float | 0.2 | 点映射最大距离(性能关键,应尽量小) |
| `PathfinderThreadCount` | int | 4 | 寻路线程数(WebGL 强制 = 1) |
| `InitiateUpdateInterval` | float | 0.1s | NavGraph 更新间隔 |
| `DrawGraphWhilePlaying` | bool | — | 运行时绘制导航图 |
| `NavSurfaceLineWidth` | float | — | 线宽 |
| `UsePolygonCollider2dPathsForBaking` | bool | — | 多边形碰撞体烘焙方式 |
### 14.2 内置链接类型
| 索引 | 名称 | 说明 |
|------|------|------|
| 0 | `corner` | 拐角(自动生成) |
| 1 | `jump` | 跳跃 |
| 2 | `fall` | 下落 |
| 3 | `teleport` | 传送 |
| 4 | `climb` | 攀爬 |
| 5 | `elevator` | 电梯 |
### 14.3 内置 NavTag
| 名称 | 用途 |
|------|------|
| `default` | 默认 |
| `water` | 水域 |
| `lava` | 岩浆 |
| `grass` | 草地 |
| `concrete` | 混凝土 |
| `dirt` | 泥土 |
---
## 15. 与 BaseGames 架构集成方案
### 15.1 防腐层原则
根据 BaseGames 架构总纲(`00_Architecture_Overview.md`)第 3 条设计理念:
> **防腐层隔离**: 业务代码永远不直接调用 PathBerserker2d 等第三方 API。所有第三方交互通过 `Adapters/` 层代理。
因此 PathBerserker2d 的所有功能必须通过 **适配器层** 暴露给业务系统。
### 15.2 适配器设计
```
Layer 3 — Adapters/
└── NavAgentAdapter.cs # PathBerserker2d NavAgent 的防腐包装
业务层调用链:
EnemyAI (Layer 5) → NavAgentAdapter (Layer 3) → NavAgent (第三方)
```
#### NavAgentAdapter 接口建议
```csharp
namespace BaseGames.Adapters
{
/// <summary>
/// PathBerserker2d NavAgent 的防腐层适配器。
/// 业务代码通过此接口进行寻路,不直接引用 PathBerserker2d API。
/// </summary>
public interface INavigation
{
bool PathTo(Vector2 goal);
bool PathTo(params Vector2[] goals);
void Stop();
void ForceStop();
bool IsIdle { get; }
bool IsFollowingPath { get; }
bool CanReach(Vector2 goal);
Vector2 CurrentPosition { get; }
event Action OnReachedGoal;
event Action OnPathFailed;
event Action OnStopped;
}
}
```
### 15.3 与其他系统的集成点
| BaseGames 系统 | 集成方式 |
|----------------|---------|
| **敌人 AI (20_Enemy_AI)** | 行为树通过 `INavigation` 接口调用寻路 |
| **环境系统 (11_Environment)** | MovingPlatform 使用 NavSurface 子对象模式 |
| **环境系统 (11_Environment)** | BreakableWall 销毁后触发 NavSurface 重新烘焙 |
| **环境系统 (11_Environment)** | TeleportGate 对应 NavLinkteleport 类型) |
| **物理动画 (08_Physics)** | 自定义移动组件桥接 `IPhysicsBody` 与 NavAgent 事件 |
| **标签系统 (02_GameTag)** | NavTag 可映射为 GameTagSO 用于环境效果触发 |
### 15.4 自定义移动组件集成
由于 BaseGames 使用自研物理系统 (`IPhysicsBody` / `RaycastBody2D`),不应使用默认的 `TransformBasedMovement`。需要创建桥接移动组件:
```csharp
namespace BaseGames.Adapters
{
public class PhysicsBasedNavMovement : MonoBehaviour
{
// 订阅 NavAgent 事件
// 将移动指令转发给 IPhysicsBody
// 通过 IPhysicsBody 驱动实际移动
// 移动完成后调用 CompleteSegmentTraversal() / CompleteLinkTraversal()
}
}
```
---
## 16. 性能分析与优化建议
### 16.1 性能特性
| 方面 | 评估 |
|------|------|
| **寻路** | 多线程 A\*,不阻塞主线程。线程数可配置(默认 4) |
| **烘焙** | 协程方式,可分帧执行。复杂场景烘焙较重 |
| **内存** | 线段数据以本地坐标存储,紧凑 |
| **空间索引** | B2DynamicTreeBox2D 动态树),查询高效 |
| **WebGL** | 自动降级为单线程协程模式 |
### 16.2 优化建议
| 建议 | 说明 |
|------|------|
| **减小 `PointMappingDistance`** | 当前 0.2,越小映射查询越快 |
| **调整 `autoRepathIntervall`** | 不要过于频繁,1-2 秒通常足够 |
| **合理设置 `cellSize`** | 烘焙精度越高越慢,0.1 适合大多数情况 |
| **分区管理 NavSurface** | 按房间/区域拆分 NavSurface,按需启用/禁用 |
| **限制同时寻路数** | 大量代理时排队处理,避免请求洪峰 |
| **善用 `onlyStaticColliders`** | 排除动态碰撞体可加速烘焙 |
---
## 17. 优缺点总结
### 17.1 优势
| 优势 | 说明 |
|------|------|
| **原生 2D 支持** | 基于线段而非面片,天然适合 2D 横版游戏 |
| **完整源码** | 所有代码可见,便于调试和定制 |
| **链接系统丰富** | 内置 6 种链接类型,覆盖跳跃/下落/攀爬/电梯/传送/拐角 |
| **动态世界** | 运行时烘焙、移动平台、动态链接切换 |
| **多线程寻路** | 不阻塞主线程,支持多代理并发 |
| **事件驱动架构** | 移动实现完全解耦,易于替换为自定义移动系统 |
| **预置行为丰富** | 8 个即用行为脚本覆盖常见场景 |
| **移动平台支持** | 本地坐标体系自动跟随 NavSurface Transform 变化 |
### 17.2 不足
| 不足 | 说明 |
|------|------|
| **文档较简陋** | 官方文档以 Demo 为主,缺少系统性 API 文档 |
| **无官方维护迹象** | Asset Store 版本更新不频繁 |
| **烘焙仅支持 Collider2D** | 不支持 Tilemap 直接烘焙(需转换为 Collider) |
| **链接需手动放置** | 跳跃/攀爬等链接需要手动在编辑器中放置 |
| **360° 模式限制** | 虽支持 360° 但主要为横版优化 |
| **缺少动态避障** | 无 RVO / ORCA 等动态避障实现 |
| **NavTag 数量有限** | 内置 6 个 NavTag,扩展需修改设置 |
### 17.3 技术评分
| 维度 | 评分 (1-10) | 说明 |
|------|-------------|------|
| 2D 适配度 | **9** | 专为 2D 设计,线段导航非常贴合横版场景 |
| API 设计 | **8** | 事件驱动架构清晰,易于扩展 |
| 文档质量 | **5** | Demo 丰富但系统文档不足 |
| 性能 | **8** | 多线程寻路 + B2DynamicTree 空间索引 |
| 易用性 | **7** | 基础使用简单,高级功能学习曲线稍陡 |
| 可维护性 | **7** | 完整源码可见,但注释较少 |
| 扩展性 | **8** | 事件驱动 + 接口隔离,易接入自定义系统 |
| **综合** | **7.4** | 适合 2D Metroidvania 项目的可靠寻路方案 |
---
## 18. 总结与建议
### 18.1 总体评价
PathBerserker2d 是目前 Unity 生态中少有的 **专为 2D 横版设计** 的寻路方案。其基于线段的导航模型天然适配平台跳跃类游戏,多线程异步寻路保证了性能,事件驱动架构提供了良好的扩展性。
对于 BaseGames 这样的 2D Metroidvania 项目,它提供了:
- **完整的 2D 导航能力**:地面行走、跳跃、下落、攀爬、电梯、传送
- **移动平台支持**:天然适配关卡中的动态地形
- **与自定义物理系统的解耦**:通过事件驱动架构可桥接 `IPhysicsBody`
### 18.2 集成建议
1. **严格通过适配器层访问** — 遵循 BaseGames 防腐层原则,创建 `NavAgentAdapter` / `INavigation` 接口
2. **替换 TransformBasedMovement** — 创建 `PhysicsBasedNavMovement` 桥接 `IPhysicsBody`
3. **按房间分区 NavSurface** — 配合关卡加载系统,按需启用/禁用导航区域
4. **NavTag 映射 GameTagSO** — 统一标签体系,复用效果系统触发逻辑
5. **行为树集成** — 通过 `INavigation` 接口在行为树节点中调用寻路
### 18.3 注意事项
- 可破坏墙壁销毁后需触发 NavSurface 重新烘焙
- 电梯/升降台需使用 NavLinkCluster 配合动态可穿越性控制
- 大量 AI 代理场景需排队管理寻路请求避免性能瓶颈
- WebGL 构建时自动降级为单线程,需额外测试性能