地图系统
This commit is contained in:
520
Docs/Guides/05_UISystem_Architecture_Guide.md
Normal file
520
Docs/Guides/05_UISystem_Architecture_Guide.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# UI 系统架构手册
|
||||
|
||||
> 文件位置:`Docs/Guides/05_UISystem_Architecture_Guide.md`
|
||||
> 版本:1.0 · 适用项目:zeling_v2
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构概览](#1-架构概览)
|
||||
2. [UIManager 面板栈](#2-uimanager-面板栈)
|
||||
3. [GameState 驱动的 UI 显隐逻辑](#3-gamestate-驱动的-ui-显隐逻辑)
|
||||
4. [主菜单流程(MainMenuController)](#4-主菜单流程mainmenucontroller)
|
||||
5. [存档槽选择面板(SaveSlotController)](#5-存档槽选择面板saveslotcontroller)
|
||||
6. [Loading Screen 体验设计](#6-loading-screen-体验设计)
|
||||
7. [游戏内覆盖层 UI(Pause / Death / HUD)](#7-游戏内覆盖层-uipause--death--hud)
|
||||
8. [输入焦点管理(IFocusable)](#8-输入焦点管理ifocusable)
|
||||
9. [Inspector 配置指南](#9-inspector-配置指南)
|
||||
10. [扩展指南](#10-扩展指南)
|
||||
11. [商业对标评估](#11-商业对标评估)
|
||||
12. [常见问题排查](#12-常见问题排查)
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构概览
|
||||
|
||||
UI 系统采用**面板栈(Panel Stack)+ 游戏状态驱动**的混合模式,将 UI 生命周期与游戏逻辑彻底解耦:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ GameManager(FSM) │
|
||||
│ └─ Raise EVT_GameStateChanged(GameStateId) │
|
||||
└────────────────────────┬───────────────────────────────────────────┘
|
||||
│ (SO 事件频道,跨程序集)
|
||||
┌────────────────────────▼───────────────────────────────────────────┐
|
||||
│ UIManager(IUIManager) │
|
||||
│ ├─ 面板栈(Stack<GameObject>) │
|
||||
│ │ ├─ OpenPanel(PanelId / GameObject) │
|
||||
│ │ └─ CloseTopPanel() │
|
||||
│ ├─ 状态驱动的根节点(HUDRoot / DeathScreenRoot) │
|
||||
│ └─ 订阅各 Open/Close 事件频道(Pause / Map / Shop / Inventory …)│
|
||||
└────────────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
静态面板(预设) Addressable 面板(按需加载) 状态根节点
|
||||
PauseMenuRoot InventoryPanel(异步) HUDRoot
|
||||
SettingsRoot MapPanel(异步) DeathScreenRoot
|
||||
ShopRoot …
|
||||
```
|
||||
|
||||
**层级与渲染顺序(Sort Order):**
|
||||
|
||||
| 层级 | Sort Order | 说明 |
|
||||
|---|---|---|
|
||||
| HUD Canvas | 10 | 永远在游戏世界之上 |
|
||||
| 菜单 / 面板 Canvas | 20 | 暂停、商店、符文等 |
|
||||
| Loading Screen Canvas | 99 | 覆盖所有游戏 UI |
|
||||
| Splash Screen Canvas | 100 | 仅在启动时显示 |
|
||||
| Scene Fade Overlay Canvas | 110 | 最顶层,场景切换遮罩 |
|
||||
|
||||
---
|
||||
|
||||
## 2. UIManager 面板栈
|
||||
|
||||
### 核心数据结构
|
||||
|
||||
```csharp
|
||||
private Stack<GameObject> _panelStack; // 当前打开的面板顺序
|
||||
private HashSet<GameObject> _openPanelSet; // O(1) 成员检查
|
||||
private Dictionary<PanelId, GameObject> _panelRegistry; // 静态面板注册表
|
||||
private Dictionary<PanelId, AddressablePanelHandle> _addressableHandles; // 异步面板缓存
|
||||
```
|
||||
|
||||
### 面板操作 API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `OpenPanel(PanelId id)` | 查注册表 → 若为 Addressable 则异步加载 → 激活 → 压栈 |
|
||||
| `OpenPanel(GameObject panel)` | 直接引用压栈(幂等:已在栈中则不重复压)|
|
||||
| `CloseTopPanel()` | 弹栈 → 隐藏 → 恢复下层 `IFocusable.OnFocusRestored()` |
|
||||
| `CloseAllPanels()` | 清空栈,隐藏所有面板 |
|
||||
| `RegisterPanel(PanelId, GameObject)` | 运行时动态注册(供场景级 UI 使用)|
|
||||
|
||||
### 面板栈行为规范
|
||||
|
||||
```
|
||||
初始状态(Gameplay):栈空,HUDRoot 可见
|
||||
|
||||
玩家按暂停:
|
||||
OpenPanel(PanelId.Pause)
|
||||
Stack → [PauseMenuRoot]
|
||||
|
||||
玩家在暂停菜单打开设置:
|
||||
OpenPanel(PanelId.Settings)
|
||||
Stack → [PauseMenuRoot, SettingsRoot]
|
||||
|
||||
玩家按取消(Cancel 输入):
|
||||
CloseTopPanel() → 隐藏 SettingsRoot,恢复 PauseMenuRoot 焦点
|
||||
Stack → [PauseMenuRoot]
|
||||
|
||||
玩家按恢复:
|
||||
CloseTopPanel() → 隐藏 PauseMenuRoot
|
||||
Stack → [](空)
|
||||
GameManager 响应 EVT_ResumeRequested → FSM: Paused → Gameplay
|
||||
```
|
||||
|
||||
> **原则:** 面板栈只管 UI 层级,不管游戏状态。游戏状态转换始终由 `GameManager` 通过 FSM 负责。
|
||||
> `UIManager.OpenPanel(Pause)` 和 `GameManager.TransitionTo(Paused)` 是两个独立的调用,
|
||||
> 它们分别由 `EVT_PauseRequested` 触发(UIManager 订阅开面板,GameManager 订阅切状态)。
|
||||
|
||||
---
|
||||
|
||||
## 3. GameState 驱动的 UI 显隐逻辑
|
||||
|
||||
`UIManager` 订阅 `EVT_GameStateChanged`,根据新状态决定 HUD 和 DeathScreen 的可见性:
|
||||
|
||||
```
|
||||
EVT_GameStateChanged(GameStateId state)
|
||||
│
|
||||
├─ Gameplay / BossFight → HUDRoot.SetActive(true) DeathScreenRoot.SetActive(false)
|
||||
├─ Dead → HUDRoot.SetActive(false) DeathScreenRoot.SetActive(true)
|
||||
├─ Paused → 不改变(保持 HUD 可见,仅叠加暂停面板)
|
||||
├─ MainMenu → HUDRoot.SetActive(false) DeathScreenRoot.SetActive(false)
|
||||
│ CloseAllPanels()
|
||||
├─ LoadingScene → HUDRoot.SetActive(false) DeathScreenRoot.SetActive(false)
|
||||
└─ Cutscene → HUDRoot.SetActive(false)(不显示 UI)
|
||||
```
|
||||
|
||||
**UIManager 不处理哪些事情:**
|
||||
- 不决定是否进入 Paused 状态(那是 GameManager 的职责)
|
||||
- 不监听场景加载事件(那是 SceneService 的职责)
|
||||
- 不直接修改 GameStateMachine(只通过 Raise 事件频道间接驱动)
|
||||
|
||||
---
|
||||
|
||||
## 4. 主菜单流程(MainMenuController)
|
||||
|
||||
`MainMenuController` 挂载在 `Scene_MainMenu` 的 Canvas 上,随场景一起加载/卸载。
|
||||
|
||||
### 完整状态机(MainMenuController 内部)
|
||||
|
||||
```
|
||||
[初始化]
|
||||
│
|
||||
└─ EVT_GameStateChanged(MainMenu) 到达
|
||||
│
|
||||
├─ 播放按钮组入场动画(_mainButtonsRect 下移 → 回弹)
|
||||
└─ RefreshContinueButton()
|
||||
└─ ISaveService.HasAnySave() → 「继续」按钮是否可点击
|
||||
|
||||
─────────────────────────────
|
||||
|
||||
按钮点击逻辑:
|
||||
|
||||
Btn_NewGame → _saveSlotController.SetMode(NewGame)
|
||||
_saveSlotPanel.SetActive(true)
|
||||
|
||||
Btn_Continue → _saveSlotController.SetMode(Continue)
|
||||
_saveSlotPanel.SetActive(true)
|
||||
|
||||
Btn_Settings → _settingsPanel.SetActive(true)
|
||||
|
||||
Btn_Credits → _creditsPanel.SetActive(true)
|
||||
|
||||
Btn_Quit → Application.Quit()
|
||||
|
||||
─────────────────────────────
|
||||
|
||||
EVT_SlotConfirmed(slotIndex) 到达(由 SaveSlotController 发布)
|
||||
│
|
||||
├─ _saveSlotPanel.SetActive(false)
|
||||
├─ _currentSlot = slotIndex
|
||||
└─ Raise EVT_SceneLoadRequest({ Key: _firstGameSceneKey, Type: Scene, ShowLoading: true })
|
||||
└─ 游戏场景加载流程启动(参见 01_BootFlow_Setup_Guide.md 第 2 节)
|
||||
```
|
||||
|
||||
### Inspector 字段速查(MainMenuController)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `_firstGameSceneKey` | `string` | 第一个游戏场景的 Addressable Key(**必填**)|
|
||||
| `_onGameStateChanged` | GameStateEventChannelSO | 监听 FSM 状态变化 |
|
||||
| `_onSceneLoadRequest` | SceneLoadRequestEventChannelSO | 发布场景加载请求 |
|
||||
| `_onSlotConfirmed` | IntEventChannelSO | 监听存档槽确认 |
|
||||
| `_mainButtonsGroup` | `CanvasGroup` | 入场动画目标 |
|
||||
| `_mainButtonsRect` | `RectTransform` | 入场位移动画目标 |
|
||||
| `_saveSlotPanel` | `GameObject` | 存档槽选择面板根节点 |
|
||||
| `_settingsPanel` | `GameObject` | 设置面板根节点 |
|
||||
| `_creditsPanel` | `GameObject` | 制作人员表面板根节点 |
|
||||
| `_saveSlotController` | `SaveSlotController` | 存档槽逻辑控制器 |
|
||||
| `_btnNewGame/Continue/Settings/Credits/Quit` | `Button` | 各主按钮引用 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 存档槽选择面板(SaveSlotController)
|
||||
|
||||
### 模式与交互逻辑
|
||||
|
||||
```
|
||||
SaveSlotController.SetMode(mode)
|
||||
│
|
||||
├─ mode = Continue
|
||||
│ ├─ 有存档的槽:可点击 → LoadAsync(slot) → Raise EVT_SlotConfirmed(slot)
|
||||
│ └─ 空槽:灰显不可交互
|
||||
│
|
||||
└─ mode = NewGame
|
||||
├─ 空槽点击 ────────────────────────────────────► NewGameModeController.Show()
|
||||
│ 玩家选择普通 / 钢铁之魂
|
||||
│ → ISaveService.CreateSlot(slot, steelSoul)
|
||||
│ → Raise EVT_SlotConfirmed(slot)
|
||||
│
|
||||
└─ 有存档的槽点击 → ConfirmDialogController.Show("是否覆盖?")
|
||||
│
|
||||
├─ 确认 ────────────────────► NewGameModeController.Show()(同上)
|
||||
└─ 取消 ────────────────────► 关闭 ConfirmDialog,回到槽列表
|
||||
```
|
||||
|
||||
### SaveSlotUI 卡片渲染
|
||||
|
||||
每张存档卡调用 `ISaveService.GetSlotSummaryAsync(i)` 刷新,渲染规则:
|
||||
|
||||
| 状态 | 显示内容 |
|
||||
|---|---|
|
||||
| `HasData = false` | 「空存档」占位,Continue 模式下灰显 |
|
||||
| `HasData = true, IsSteelSoul = false` | 时长 + 位置 + 零珠 + HP |
|
||||
| `HasData = true, IsSteelSoul = true` | 同上,叠加钢铁之魂特殊徽章与边框 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Loading Screen 体验设计
|
||||
|
||||
### 视觉层次结构(LoadingScreenManager)
|
||||
|
||||
```
|
||||
Canvas_Loading (Sort Order 99)
|
||||
└── LoadingRoot(默认 SetActive=false)
|
||||
├── BackgroundImage[] ← 随机显示一张艺术图
|
||||
├── ProgressBarFill ← Image.Filled, FillMethod=Horizontal
|
||||
└── TipText ← TextMeshProUGUI,显示随机本地化提示
|
||||
```
|
||||
|
||||
### 体验节奏控制
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `_minDisplayTime` | `0.5s` | 防止快速设备上的 Loading Screen 一闪而过(视觉抖动)|
|
||||
| 进度更新频率 | 每帧( `Update` 中 Raise)| 进度条连续平滑 |
|
||||
| 进度范围 | 0 → 0.9(Addressables 下载)→ 1.0(加载完成)| 避免进度条长时间卡在 100% |
|
||||
|
||||
### 进度数据流
|
||||
|
||||
```
|
||||
SceneLoader(每帧轮询 AsyncOperationHandle)
|
||||
└─ Raise EVT_LoadingProgressUpdated(handle.PercentComplete * 0.9f)
|
||||
└─ LoadingScreenManager.SetProgress(value)
|
||||
└─ _progressFill.fillAmount = value
|
||||
|
||||
加载完成:
|
||||
SceneLoader.Raise(EVT_LoadingProgressUpdated, 1.0f)
|
||||
SceneLoader.Raise(EVT_LoadingComplete)
|
||||
└─ LoadingScreenManager.StartCoroutine(HideAfterMinTime())
|
||||
└─ yield WaitForSeconds(remainingMinTime)
|
||||
└─ _loadingRoot.SetActive(false)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 游戏内覆盖层 UI(Pause / Death / HUD)
|
||||
|
||||
### 暂停菜单(PauseMenuController)
|
||||
|
||||
挂载在 `UIRoot/PauseMenuRoot`,随 Persistent 场景常驻。
|
||||
|
||||
| 按钮 | 行为 | 事件 |
|
||||
|---|---|---|
|
||||
| 恢复 | Raise EVT_ResumeRequested → GameManager FSM 切回 Gameplay | VoidEventChannelSO |
|
||||
| 设置 | UIManager.OpenPanel(PanelId.Settings)(面板栈叠加)| — |
|
||||
| 返回主菜单 | Raise EVT_SceneLoadRequest({ Scene_MainMenu, Scene, NoLoading })| SceneLoadRequestEventChannelSO |
|
||||
| 退出游戏 | Application.Quit() | — |
|
||||
|
||||
### 死亡画面(DeathScreenController)
|
||||
|
||||
```
|
||||
EVT_GameStateChanged(Dead) 到达
|
||||
└─ UIManager 激活 DeathScreenRoot
|
||||
└─ DeathScreenController.OnEnable()
|
||||
└─ StartCoroutine(ShowAfterDelay(_showDelay=1.5s))
|
||||
└─ 1.5 秒后:显示复活按钮 + 设置焦点
|
||||
|
||||
玩家点击复活按钮:
|
||||
DeathScreenController.Raise(EVT_DeathScreenConfirmed)
|
||||
└─ DeathRespawnService.StartRespawnCoroutine()
|
||||
└─ 从最近检查点复活
|
||||
```
|
||||
|
||||
**`_showDelay` 设计意图:** 给死亡动画(击退、溶解特效)留出播放窗口,避免死亡画面在视觉上覆盖动画高光时刻。
|
||||
|
||||
### HUD(HUDController)
|
||||
|
||||
HUD 挂载在 `UIRoot/HUD Canvas`,始终在最低优先级的 UI 层(Sort Order 10)。
|
||||
|
||||
GameState → Gameplay / BossFight 时显示,其余状态隐藏。
|
||||
具体字段配置参见 `Docs/Design/UI_HUD_Spec.md`(美术规范文档)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 输入焦点管理(IFocusable)
|
||||
|
||||
面板实现 `IFocusable` 接口后,`UIManager.CloseTopPanel()` 会在弹出顶层面板后自动调用下层面板的 `OnFocusRestored()`,将 EventSystem 焦点恢复到该面板的默认第一个可交互元素。
|
||||
|
||||
```csharp
|
||||
public interface IFocusable
|
||||
{
|
||||
void OnFocusRestored(); // 设置 EventSystem.SetSelectedGameObject(defaultButton)
|
||||
}
|
||||
```
|
||||
|
||||
**接入示例(PauseMenuController):**
|
||||
|
||||
```csharp
|
||||
public class PauseMenuController : MonoBehaviour, IFocusable
|
||||
{
|
||||
[SerializeField] private Button _firstButton; // Inspector 中指定第一个按钮
|
||||
|
||||
public void OnFocusRestored()
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(_firstButton.gameObject);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**为什么需要 IFocusable:**
|
||||
|
||||
| 场景 | 不处理焦点 | 处理焦点 |
|
||||
|---|---|---|
|
||||
| 从设置面板退回暂停面板 | 无法通过手柄操作,焦点丢失 | 焦点自动回到暂停菜单的第一个按钮 |
|
||||
| 死亡画面出现 | 需要手动移动手柄选中复活按钮 | 自动聚焦,可直接按确认键复活 |
|
||||
| PC 键盘操作 | Tab 键循环焦点失效 | 始终从正确的第一个元素开始循环 |
|
||||
|
||||
---
|
||||
|
||||
## 9. Inspector 配置指南
|
||||
|
||||
### UIManager(`UIRoot` GameObject,Persistent 场景)
|
||||
|
||||
| Inspector 字段 | 赋值 | 说明 |
|
||||
|---|---|---|
|
||||
| `_hudRoot` | `UIRoot/HUD Canvas` | HUD 根节点 |
|
||||
| `_deathScreenRoot` | `UIRoot/DeathScreen Canvas` | 死亡画面根节点 |
|
||||
| `_panels` | 数组(4项)| 静态面板注册:Pause / Settings / Map / Shop |
|
||||
| `_onGameStateChanged` | `EVT_GameStateChanged` | 订阅状态变化 |
|
||||
| `_onPauseRequested` | `EVT_PauseRequested` | 订阅暂停请求 |
|
||||
| `_onMapOpen` | `EVT_MapOpen`(可选)| 订阅地图打开请求 |
|
||||
| `_onShopOpen` | `EVT_ShopOpen`(可选)| 订阅商店打开请求 |
|
||||
|
||||
**`_panels` 数组元素格式:**
|
||||
|
||||
| PanelId | Root GameObject |
|
||||
|---|---|
|
||||
| `Pause` | `UIRoot/PauseMenuRoot` |
|
||||
| `Settings` | `UIRoot/SettingsRoot` |
|
||||
| `Map` | `UIRoot/MapRoot` |
|
||||
| `Shop` | `UIRoot/ShopRoot` |
|
||||
|
||||
### 事件频道速查(UI 相关)
|
||||
|
||||
| SO 名称 | 类型 | 发布者 | 订阅者 |
|
||||
|---|---|---|---|
|
||||
| `EVT_GameStateChanged` | GameStateEventChannelSO | GameManager | UIManager / MainMenuController |
|
||||
| `EVT_PauseRequested` | VoidEventChannelSO | InputReader | GameManager / UIManager |
|
||||
| `EVT_ResumeRequested` | VoidEventChannelSO | PauseMenuController | GameManager |
|
||||
| `EVT_DeathScreenConfirmed` | VoidEventChannelSO | DeathScreenController | DeathRespawnService |
|
||||
| `EVT_SlotConfirmed` | IntEventChannelSO | SaveSlotController | MainMenuController |
|
||||
| `EVT_MapOpen`(可选) | VoidEventChannelSO | InputReader / HUD 按钮 | UIManager |
|
||||
| `EVT_ShopOpen`(可选) | VoidEventChannelSO | NPC 交互 | UIManager |
|
||||
|
||||
---
|
||||
|
||||
## 10. 扩展指南
|
||||
|
||||
### 添加新的游戏内面板(如技能树)
|
||||
|
||||
1. 在 `Assets/_Game/Scenes/Persistent.unity` 的 `UIRoot` 下新建 `SkillTreeRoot`,初始 `SetActive(false)`。
|
||||
2. 在 `PanelId` 枚举中添加 `SkillTree`。
|
||||
3. 在 `UIManager._panels` 数组中注册:`PanelId.SkillTree → SkillTreeRoot`。
|
||||
4. 创建 `EVT_SkillTreeOpen`(VoidEventChannelSO)并在 `UIManager.OnEnable` 中订阅 `_onSkillTreeOpen → () => OpenPanel(PanelId.SkillTree)`。
|
||||
5. 在 `InputReader` 中添加 `SkillTree` 输入动作,触发时 Raise `EVT_SkillTreeOpen`。
|
||||
|
||||
### 使用 Addressable 按需加载大型面板
|
||||
|
||||
适合一次性使用的大型面板(如藏品图鉴、成就列表),避免 Persistent 场景启动内存占用过高:
|
||||
|
||||
1. 将面板 Prefab 标记为 Addressable,设置 Address 为 `"UI_Journal"`。
|
||||
2. 在 `UIManager._addressablePanels` 中注册:`PanelId.Journal → Address: "UI_Journal"`。
|
||||
3. 调用 `UIManager.OpenPanel(PanelId.Journal)`——内部自动执行 `Addressables.LoadAssetAsync → Instantiate → 压栈`。
|
||||
4. 已加载的 Handle 会被缓存(`_addressableHandles`),第二次打开无需重新下载。
|
||||
|
||||
### 添加过场动画期间的 UI 屏蔽
|
||||
|
||||
在 `UIManager` 的 `EVT_GameStateChanged(Cutscene)` 分支:
|
||||
|
||||
```csharp
|
||||
case GameStateId.Cutscene:
|
||||
_hudRoot.SetActive(false);
|
||||
CloseAllPanels();
|
||||
_inputBlocker.SetActive(true); // 全屏透明 Raycast Target,屏蔽所有点击输入
|
||||
break;
|
||||
```
|
||||
|
||||
### 实现「按任意键继续」的过渡画面
|
||||
|
||||
在 `UIManager` 中监听 `EVT_GameStateChanged(LoadingScene)`:
|
||||
|
||||
```csharp
|
||||
case GameStateId.LoadingScene:
|
||||
// 等待输入后才真正触发场景加载(视觉节奏控制)
|
||||
StartCoroutine(WaitForAnyKeyThenLoad());
|
||||
break;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 商业对标评估
|
||||
|
||||
以下评估以"丝之歌"级别商业 2D 动作游戏(Steam/主机发行标准)为基准,逐项审核当前 UI 系统的完备性与玩家体验成熟度。
|
||||
|
||||
### ✅ 已达到商业标准
|
||||
|
||||
| 维度 | 当前实现 | 评价 |
|
||||
|---|---|---|
|
||||
| **面板栈管理** | Stack<GameObject> + O(1) 成员检查 | 支持任意深度的嵌套面板,避免重复打开 |
|
||||
| **状态驱动 UI** | EVT_GameStateChanged 统一驱动 HUD/Death 可见性 | UI 层零侵入游戏逻辑 |
|
||||
| **组件-事件解耦** | UIManager/MainMenuController 不持有 GameManager 直接引用 | Core 程序集不依赖 UI 程序集(反向依赖零) |
|
||||
| **Loading 防闪烁** | `_minDisplayTime = 0.5s` | 消除快速设备上的 Loading Screen 闪烁问题 |
|
||||
| **加载进度可视化** | 进度 0→1 连续更新,0.9 上限留出 100% 假象 | 与主流 AAA 游戏相同的用户感知优化 |
|
||||
| **死亡节奏控制** | `_showDelay = 1.5s` 延迟显示复活按钮 | 保护死亡动画的视觉高光,符合《空洞骑士》级别体验 |
|
||||
| **Addressable 面板** | 异步加载 + Handle 缓存 | 大型面板不占用启动内存 |
|
||||
|
||||
### ⚠️ 建议改进项
|
||||
|
||||
| 维度 | 当前状态 | 建议 |
|
||||
|---|---|---|
|
||||
| **手柄导航完整性** | IFocusable 接口已定义,但未见所有面板都实现 | 对 Pause / Death / Settings / SaveSlot 的每个面板补全 `OnFocusRestored`,主机认证前必须测试全程无鼠标操作 |
|
||||
| **输入方案自适应 UI** | 未见键盘 vs 手柄按钮图标自动切换 | 建议监听 `InputSystem.onActionChange` 事件,切换 Sprite Atlas(键盘: `[Space]` → 手柄: `[A]`)|
|
||||
| **主菜单背景** | 按钮组有入场动画,但无背景动效 | 商业发布版本通常有视差滚动背景或粒子动效,配合 BGM 形成视听配合 |
|
||||
| **存档槽截图预览** | 当前槽卡片无截图 | 补全后与 `04_SaveSystem_DataPersistence_Guide.md` 第 9 节「存档截图预览」联动 |
|
||||
| **过渡动画统一管理** | 各面板自行处理 SetActive,无统一的打开/关闭动画 | 建议在 UIManager.OpenPanel / CloseTopPanel 中统一调用 DOTween / LeanTween 淡入淡出,保持全局一致的 UI 节奏感 |
|
||||
| **Loading 提示文字本地化** | `_tipMessages` 为 string[] 存储 Key,需手动填写 | 建议改为 `LocalizedStringTable` 引用,确保多语言运行时不遗漏 |
|
||||
|
||||
### ❌ 商业发行前必须补全
|
||||
|
||||
| 维度 | 当前状态 | 影响 |
|
||||
|---|---|---|
|
||||
| **主机 UI 合规(TRC/TCR)** | 未见针对 PS5/Xbox Series 的按钮提示图标覆盖 | Sony TRC 要求所有交互提示图标与手柄型号实时匹配(DualSense vs DualShock 4) |
|
||||
| **无障碍选项** | 未见字体大小调整、色盲模式、振动开关等 | Steam 无障碍标签与主机认证的基础要求;建议至少提供字体缩放和高对比模式 |
|
||||
| **ESC / 返回键全局处理** | 未见统一的 Cancel 输入处理器 | 当前各面板自行响应 Cancel,可能出现 ESC 无法关闭某个面板的问题;建议在 UIManager 层统一处理 Cancel → CloseTopPanel |
|
||||
|
||||
---
|
||||
|
||||
## 12. 常见问题排查
|
||||
|
||||
### ❌ 暂停后按 ESC/Cancel 无法关闭设置面板,只能关闭暂停菜单
|
||||
|
||||
**原因:** 设置面板没有订阅 `Cancel` 输入动作,`UIManager` 也没有全局 Cancel 处理。
|
||||
**解决:** 在 `UIManager.Update()` 中添加全局 Cancel 输入监听:
|
||||
```csharp
|
||||
private void Update()
|
||||
{
|
||||
if (_inputReader.IsCancelPressed && _panelStack.Count > 0)
|
||||
CloseTopPanel();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ 从主菜单返回游戏时 HUD 不显示
|
||||
|
||||
**原因:** `EVT_GameStateChanged(Gameplay)` 触发时,`UIManager` 的 `OnEnable` 尚未执行(场景还在加载)。
|
||||
**解决:** `UIManager.OnEnable` 必须在 Persistent 场景中运行(它始终存在),不应随主菜单场景卸载。检查 `UIManager` 是否错误地挂载在主菜单场景的 Canvas 上而非 Persistent 场景的 `UIRoot` 上。
|
||||
|
||||
---
|
||||
|
||||
### ❌ 手柄操作时焦点在面板打开后丢失(无高亮按钮)
|
||||
|
||||
**原因:** 面板没有实现 `IFocusable`,或实现了但 `_firstButton` 为空引用。
|
||||
**解决:**
|
||||
1. 确认面板组件实现了 `IFocusable`。
|
||||
2. 在 Inspector 中为 `_firstButton` 赋值(面板打开时应选中的默认按钮)。
|
||||
3. 确保面板在 `SetActive(true)` 后的下一帧 `OnEnable` 调用 `EventSystem.SetSelectedGameObject`(使用 `StartCoroutine` 延迟一帧执行,避免 EventSystem 还未刷新)。
|
||||
|
||||
---
|
||||
|
||||
### ❌ Addressable 面板第一次打开慢,之后快
|
||||
|
||||
**行为正常。** 第一次调用 `Addressables.LoadAssetAsync` 会进行网络/磁盘下载,之后 Handle 被缓存。
|
||||
如果希望消除首次延迟,可在游戏启动的 `PreloadCoroutine` 中预热特定面板:
|
||||
```csharp
|
||||
await Addressables.DownloadDependenciesAsync("UI_Journal");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ 主菜单场景卸载后 MainMenuController 事件订阅没有清理,导致空引用报错
|
||||
|
||||
**原因:** `OnEnable` 中订阅了 SO 事件频道,但 `OnDisable` 中没有取消订阅,场景卸载后 SO 仍持有对已销毁对象的回调引用。
|
||||
**解决:** 确保 `MainMenuController.OnDisable` 中对所有 `_onXxx?.Unsubscribe(HandleXxx)` 显式取消订阅,或使用 `EventSubscription` Dispose 模式(框架已提供):
|
||||
```csharp
|
||||
private EventSubscription _stateSubscription;
|
||||
|
||||
private void OnEnable()
|
||||
=> _stateSubscription = _onGameStateChanged.Subscribe(HandleGameStateChanged);
|
||||
|
||||
private void OnDisable()
|
||||
=> _stateSubscription?.Dispose();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*文档最后更新:2026-06-04*
|
||||
Reference in New Issue
Block a user