695 lines
25 KiB
Markdown
695 lines
25 KiB
Markdown
# 10 · UI 模块
|
||
|
||
> **命名空间** `BaseGames.UI`
|
||
> **程序集** `BaseGames.UI`
|
||
> **路径** `Assets/Scripts/UI/`
|
||
> **依赖** `BaseGames.Core.Events`、`TextMeshPro`
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [Canvas 架构(Persistent 场景内)](#1-canvas-架构)
|
||
2. [UIManager](#2-uimanager)
|
||
3. [HUDController](#3-hudcontroller)
|
||
4. [BossHPBar](#4-bosshpbar)
|
||
5. [PauseMenuController](#5-pausemenucontroller)
|
||
6. [DeathScreenController](#6-deathscreencontroller)
|
||
7. [SettingsPanelController](#7-settingspanelcontroller)
|
||
8. [SaveSlotController](#75-saveslotcontroller)
|
||
9. [SaveIndicator](#76-saveindicator)
|
||
10. [LoadingScreenManager](#77-loadingscreenmanager)
|
||
11. [IBossHPProvider 接口](#78-ibosshpprovider-接口)
|
||
12. [LoadingOverlay](#8-loadingoverlay)
|
||
13. [DialogueBox(HUD Overlay)](#9-dialoguebox)
|
||
10. [FloatingDamageText(伤害数字)](#10-floatingdamagetext)
|
||
11. [ToastNotification(通知弹窗)](#11-toastnotification)
|
||
12. [InputDeviceIconSwitcher](#12-inputdeviceiconswitcher)
|
||
13. [PanelStack(控制器导航)](#13-panelstack)
|
||
14. [UI 事件频道清单](#14-ui-事件频道清单)
|
||
|
||
---
|
||
|
||
## 1. Canvas 架构
|
||
|
||
所有 Canvas 挂在 **Persistent 场景**内,全程常驻:
|
||
|
||
```
|
||
[UIRoot]
|
||
├── Canvas_HUD Sorting Order: 10 (Screen Space - Overlay)
|
||
│ ├── HPContainer ← HP 格子列表(水平 HorizontalLayoutGroup)
|
||
│ ├── SoulGauge ← 灵力弧形进度条(Image.fillAmount)
|
||
│ ├── SpiritGauge ← 魄元进度条
|
||
│ ├── GeoCounter ← TMP 数字 + 图标
|
||
│ ├── SpringCharges ← 灵泉次数图标列
|
||
│ ├── FormIndicator ← 当前形态图标(3 种形态)
|
||
│ ├── ToolSlotHUD ← 工具槽图标 + 次数
|
||
│ ├── AbilityHints ← 已解锁技能图标
|
||
│ ├── BossHPBar ← 默认隐藏
|
||
│ └── InteractPrompt ← 交互提示文字(默认隐藏)
|
||
│
|
||
├── Canvas_Menu Sorting Order: 20 (Screen Space - Overlay)
|
||
│ ├── MainMenuPanel ← 主菜单(游戏启动时显示)
|
||
│ ├── SaveSlotPanel ← 存档槽选择(新游戏/继续,主菜单子面板)
|
||
│ ├── PauseMenuPanel ← 暂停菜单(默认隐藏)
|
||
│ ├── DeathScreenPanel ← 死亡画面(默认隐藏)
|
||
│ ├── SettingsPanel ← 设置菜单(默认隐藏)
|
||
│ ├── MapPanel ← 地图(默认隐藏)
|
||
│ └── ShopPanel ← 商店(默认隐藏)
|
||
│
|
||
└── Canvas_Overlay Sorting Order: 30 (Screen Space - Overlay)
|
||
├── LoadingOverlay ← 全屏黑幕(过场遮罩)
|
||
├── DialogueBox ← 对话框(底部)
|
||
└── ToastContainer ← 通知弹窗容器(右上)
|
||
```
|
||
|
||
**SaveSlotPanel** 展示 3 个存档槽卡片,每张卡片显示角色形态图标、区域名称、游玩时长、存档时间、完成度百分比;由 `SaveSlotController` 驱动(`SaveManager.GetSlotSummaryAsync(slotIndex)` 提供数据)。
|
||
|
||
**SaveIndicator**:右下角小图标(软盘 + 旋转动画),在自动存档流程中显示(订阅 `EVT_SaveIndicatorVisible` BoolEventChannelSO,`true` 触发淡入,`false` 触发淡出),告知玩家正在保存中。
|
||
|
||
---
|
||
|
||
## 2. UIManager
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/UIManager.cs
|
||
[DefaultExecutionOrder(+50)]
|
||
public class UIManager : MonoBehaviour
|
||
{
|
||
[Header("Canvas Roots")]
|
||
[SerializeField] private GameObject _hudRoot;
|
||
[SerializeField] private GameObject _pauseMenuRoot;
|
||
[SerializeField] private GameObject _deathScreenRoot;
|
||
[SerializeField] private GameObject _settingsRoot;
|
||
[SerializeField] private GameObject _mapRoot;
|
||
[SerializeField] private GameObject _shopRoot;
|
||
|
||
[Header("Event Channels - Subscribe")]
|
||
[SerializeField] private GameStateEventChannelSO _onGameStateChanged;
|
||
[SerializeField] private VoidEventChannelSO _onPauseRequested;
|
||
[SerializeField] private VoidEventChannelSO _onFastTravelOpen;
|
||
[SerializeField] private StringEventChannelSO _onShopOpen;
|
||
[SerializeField] private VoidEventChannelSO _onMapOpen;
|
||
|
||
private Stack<GameObject> _panelStack = new();
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onGameStateChanged.OnEventRaised += HandleGameStateChanged;
|
||
_onPauseRequested.OnEventRaised += TogglePause;
|
||
_onFastTravelOpen.OnEventRaised += OpenMap;
|
||
_onShopOpen.OnEventRaised += OpenShop;
|
||
_onMapOpen.OnEventRaised += OpenMap;
|
||
}
|
||
private void OnDisable()
|
||
{
|
||
_onGameStateChanged.OnEventRaised -= HandleGameStateChanged;
|
||
_onPauseRequested.OnEventRaised -= TogglePause;
|
||
_onFastTravelOpen.OnEventRaised -= OpenMap;
|
||
_onShopOpen.OnEventRaised -= OpenShop;
|
||
_onMapOpen.OnEventRaised -= OpenMap;
|
||
}
|
||
|
||
private void HandleGameStateChanged(GameStateId state)
|
||
{
|
||
// HUD 在 Gameplay 和 BossFight 状态下均显示
|
||
bool showHud = state == GameStates.Gameplay || state == GameStates.BossFight;
|
||
_hudRoot.SetActive(showHud);
|
||
|
||
// ⚠️ GameStateId 为 struct,不可用 switch;用 if/else 比较
|
||
if (state == GameStates.Dead)
|
||
_deathScreenRoot.SetActive(true);
|
||
else if (state == GameStates.Cutscene)
|
||
_hudRoot.SetActive(false); // 过场动画隐藏 HUD
|
||
}
|
||
|
||
public void OpenPanel(GameObject panel)
|
||
{
|
||
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(false);
|
||
panel.SetActive(true);
|
||
_panelStack.Push(panel);
|
||
}
|
||
|
||
public void CloseTopPanel()
|
||
{
|
||
if (_panelStack.Count == 0) return;
|
||
_panelStack.Pop().SetActive(false);
|
||
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(true);
|
||
}
|
||
|
||
private void TogglePause() => OpenPanel(_pauseMenuRoot);
|
||
private void OpenShop(string npcId) => OpenPanel(_shopRoot);
|
||
private void OpenMap() => OpenPanel(_mapRoot);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. HUDController
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/HUD/HUDController.cs
|
||
public class HUDController : MonoBehaviour
|
||
{
|
||
[Header("HP")]
|
||
[SerializeField] private Transform _hpContainer;
|
||
[SerializeField] private GameObject _hpCellPrefab; // 单格 HP 图标
|
||
|
||
[Header("Gauges")]
|
||
[SerializeField] private Image _soulGaugeFill;
|
||
[SerializeField] private Image _spiritGaugeFill;
|
||
[SerializeField] private TMP_Text _geoText;
|
||
|
||
[Header("Spring Charges")]
|
||
[SerializeField] private Transform _springContainer;
|
||
[SerializeField] private GameObject _springIconPrefab;
|
||
|
||
[Header("Form")]
|
||
[SerializeField] private Image[] _formIcons; // 3 forms
|
||
|
||
[Header("Interact Prompt")]
|
||
[SerializeField] private TMP_Text _interactText;
|
||
[SerializeField] private GameObject _interactPromptRoot;
|
||
|
||
[Header("Event Channels - Subscribe")]
|
||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||
[SerializeField] private IntEventChannelSO _onMaxHPChanged;
|
||
[SerializeField] private IntEventChannelSO _onSoulPowerChanged;
|
||
[SerializeField] private IntEventChannelSO _onSpiritPowerChanged;
|
||
[SerializeField] private IntEventChannelSO _onGeoChanged;
|
||
[SerializeField] private IntEventChannelSO _onSpringChargesChanged;
|
||
[SerializeField] private IntEventChannelSO _onFormChanged;
|
||
[SerializeField] private StringEventChannelSO _onShowInteractPrompt;
|
||
[SerializeField] private VoidEventChannelSO _onHideInteractPrompt;
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onHPChanged.OnEventRaised += UpdateHP;
|
||
_onMaxHPChanged.OnEventRaised += RebuildHPCells;
|
||
_onSoulPowerChanged.OnEventRaised += val => _soulGaugeFill.fillAmount = val / 100f;
|
||
_onSpiritPowerChanged.OnEventRaised += val => _spiritGaugeFill.fillAmount = val / 100f;
|
||
_onGeoChanged.OnEventRaised += val => _geoText.text = val.ToString();
|
||
_onSpringChargesChanged.OnEventRaised += RebuildSpringIcons;
|
||
_onFormChanged.OnEventRaised += UpdateFormIcon;
|
||
_onShowInteractPrompt.OnEventRaised += ShowInteractPrompt;
|
||
_onHideInteractPrompt.OnEventRaised += HideInteractPrompt;
|
||
}
|
||
private void OnDisable() { /* 对称 -= */ }
|
||
|
||
private void UpdateHP(int current); // 更新 HP 格子激活/灰化状态
|
||
private void RebuildHPCells(int max); // 重建 HP 格子列表(MaxHP 改变时)
|
||
private void RebuildSpringIcons(int charges);
|
||
private void UpdateFormIcon(int formIndex);
|
||
private void ShowInteractPrompt(string text);
|
||
private void HideInteractPrompt();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. BossHPBar
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/HUD/BossHPBar.cs
|
||
public class BossHPBar : MonoBehaviour
|
||
{
|
||
[SerializeField] private TMP_Text _bossNameText;
|
||
[SerializeField] private Image _hpFill;
|
||
[SerializeField] private Transform _phaseMarkersRoot;
|
||
[SerializeField] private GameObject _phaseMarkerPrefab;
|
||
|
||
[Header("Event Channels")]
|
||
[SerializeField] private BoolEventChannelSO _onBossFightToggled; // true=开始,false=结束
|
||
[SerializeField] private IntEventChannelSO _onBossHPChanged;
|
||
[SerializeField] private StringEventChannelSO _onBossNameSet;
|
||
[SerializeField] private IntEventChannelSO _onBossHPMaxSet;
|
||
|
||
private int _maxHP;
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onBossFightToggled.OnEventRaised += OnBossFightToggled;
|
||
_onBossHPChanged.OnEventRaised += hp => _hpFill.fillAmount = (float)hp / _maxHP;
|
||
_onBossNameSet.OnEventRaised += name => _bossNameText.text = name;
|
||
_onBossHPMaxSet.OnEventRaised += max => _maxHP = max;
|
||
}
|
||
private void OnDisable()
|
||
{
|
||
_onBossFightToggled.OnEventRaised -= OnBossFightToggled;
|
||
_onBossHPChanged.OnEventRaised -= hp => _hpFill.fillAmount = (float)hp / _maxHP;
|
||
_onBossNameSet.OnEventRaised -= name => _bossNameText.text = name;
|
||
_onBossHPMaxSet.OnEventRaised -= max => _maxHP = max;
|
||
}
|
||
|
||
private void OnBossFightToggled(bool started)
|
||
{
|
||
if (started) StartCoroutine(SlideIn());
|
||
else StartCoroutine(SlideOut());
|
||
}
|
||
|
||
private IEnumerator SlideIn(); // 动画:Boss 血条从屏幕底部滑入
|
||
private IEnumerator SlideOut(); // 动画:Boss 血条滑出并隐藏
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. PauseMenuController
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/Menus/PauseMenuController.cs
|
||
public class PauseMenuController : MonoBehaviour
|
||
{
|
||
[SerializeField] private UIManager _uiManager;
|
||
[SerializeField] private Button _btnResume;
|
||
[SerializeField] private Button _btnSettings;
|
||
[SerializeField] private Button _btnMainMenu;
|
||
[SerializeField] private Button _btnQuit;
|
||
|
||
[Header("Event Channels")]
|
||
[SerializeField] private GameStateEventChannelSO _onGameStateChanged;
|
||
[SerializeField] private VoidEventChannelSO _onResumeRequested;
|
||
|
||
private void Awake()
|
||
{
|
||
_btnResume.onClick.AddListener(Resume);
|
||
_btnSettings.onClick.AddListener(() => _uiManager.OpenPanel(_settingsRoot));
|
||
_btnMainMenu.onClick.AddListener(GoToMainMenu);
|
||
_btnQuit.onClick.AddListener(Application.Quit);
|
||
}
|
||
|
||
private void Resume()
|
||
{
|
||
_onResumeRequested.Raise();
|
||
_uiManager.CloseTopPanel();
|
||
}
|
||
|
||
private void GoToMainMenu();
|
||
// 广播 EVT_SceneLoadRequest(目标 = MainMenuScene)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. DeathScreenController
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/Menus/DeathScreenController.cs
|
||
public class DeathScreenController : MonoBehaviour
|
||
{
|
||
[SerializeField] private TMP_Text _deathMessage;
|
||
[SerializeField] private Button _btnRespawn;
|
||
|
||
[Header("Event Channels")]
|
||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||
[SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed; // Raise → GameManager.RespawnCoroutine
|
||
|
||
private void OnEnable() => _onPlayerDied.OnEventRaised += OnPlayerDied;
|
||
private void OnDisable() => _onPlayerDied.OnEventRaised -= OnPlayerDied;
|
||
|
||
// ⚠️ EVT_PlayerDied 发出后需等待 1.5s 死亡动画,否则死亡画面会提前弹出
|
||
private void OnPlayerDied() => StartCoroutine(ShowAfterDelay(1.5f));
|
||
|
||
private IEnumerator ShowAfterDelay(float delay)
|
||
{
|
||
yield return new WaitForSeconds(delay);
|
||
Show();
|
||
}
|
||
|
||
private void Show()
|
||
{
|
||
gameObject.SetActive(true);
|
||
_btnRespawn.onClick.RemoveAllListeners();
|
||
_btnRespawn.onClick.AddListener(Confirm);
|
||
}
|
||
|
||
private void Confirm()
|
||
{
|
||
gameObject.SetActive(false);
|
||
_onDeathScreenConfirmed.Raise(); // GameManager 监听后执行 RespawnCoroutine
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. SettingsPanelController
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/Menus/SettingsPanelController.cs
|
||
// 驱动 SettingsManager 的全部 Set* 方法
|
||
public class SettingsPanelController : MonoBehaviour
|
||
{
|
||
[SerializeField] private SettingsManager _settings;
|
||
|
||
[Header("Audio")]
|
||
[SerializeField] private Slider _masterVolume;
|
||
[SerializeField] private Slider _bgmVolume;
|
||
[SerializeField] private Slider _sfxVolume;
|
||
[SerializeField] private Slider _ambientVolume;
|
||
|
||
[Header("Video")]
|
||
[SerializeField] private Toggle _vSyncToggle;
|
||
[SerializeField] private TMP_Dropdown _fpsDropdown;
|
||
[SerializeField] private TMP_Dropdown _resolutionDropdown;
|
||
|
||
private void Start()
|
||
{
|
||
// 从 SettingsManager 读取当前值并填充控件
|
||
// 绑定 onChange 事件 → 调用对应 _settings.Set*()
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7.5 SaveSlotController
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/Menus/SaveSlotController.cs
|
||
// 驱动主菜单存档槽选择(新游戏 / 继续)
|
||
public class SaveSlotController : MonoBehaviour
|
||
{
|
||
[SerializeField] private SaveSlotUI[] _slotUIs; // 3 个存档槽 UI
|
||
[SerializeField] private SaveManager _saveManager;
|
||
|
||
public async UniTask RefreshAsync()
|
||
{
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var summary = await _saveManager.GetSlotSummaryAsync(i);
|
||
_slotUIs[i].Refresh(summary); // null = 空槽(显示“新局”)
|
||
}
|
||
}
|
||
|
||
public void OnSlotSelected(int slotIndex);
|
||
// 新局:_saveManager.CreateSlot(slotIndex) → 启动游戏
|
||
// 继续:_saveManager.LoadAsync(slotIndex) → 载入存档
|
||
}
|
||
|
||
// 单个存档槽卡片组件
|
||
public class SaveSlotUI : MonoBehaviour
|
||
{
|
||
[SerializeField] private TMP_Text _playtimeText;
|
||
[SerializeField] private TMP_Text _regionText;
|
||
[SerializeField] private TMP_Text _percentText;
|
||
[SerializeField] private Image _formIcon;
|
||
[SerializeField] private TMP_Text _lastSavedText;
|
||
[SerializeField] private GameObject _emptyIndicator; // 空槽提示
|
||
|
||
public void Refresh(SlotSummary summary);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7.6 SaveIndicator
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/SaveIndicator.cs
|
||
// 右下角存档进行中提示字
|
||
[RequireComponent(typeof(CanvasGroup))]
|
||
public class SaveIndicator : MonoBehaviour
|
||
{
|
||
[SerializeField] private CanvasGroup _cg;
|
||
[SerializeField] private float _fadeDuration = 0.2f;
|
||
|
||
[Header("Event Channels")]
|
||
// ⚠️ 统一使用单一 BoolEventChannelSO(对齐 02_EventSystem §4 EVT_SaveIndicatorVisible 和 12_SaveModule §4/§6)
|
||
[SerializeField] private BoolEventChannelSO _onSaveIndicatorVisible; // → EVT_SaveIndicatorVisible
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onSaveIndicatorVisible.OnEventRaised += visible => FadeTo(visible ? 1f : 0f);
|
||
}
|
||
private void OnDisable()
|
||
{
|
||
_onSaveIndicatorVisible.OnEventRaised -= visible => FadeTo(visible ? 1f : 0f);
|
||
}
|
||
|
||
private void FadeTo(float target) => StartCoroutine(FadeCoroutine(target));
|
||
private IEnumerator FadeCoroutine(float target)
|
||
{
|
||
float start = _cg.alpha;
|
||
float t = 0;
|
||
while (t < _fadeDuration)
|
||
{
|
||
_cg.alpha = Mathf.Lerp(start, target, t / _fadeDuration);
|
||
t += Time.unscaledDeltaTime;
|
||
yield return null;
|
||
}
|
||
_cg.alpha = target;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7.7 LoadingScreenManager
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/LoadingScreenManager.cs
|
||
// 全屏加载面:进度条 + 提示文字 + 随机背景图
|
||
public class LoadingScreenManager : MonoBehaviour
|
||
{
|
||
[SerializeField] private GameObject _loadingRoot;
|
||
[SerializeField] private Image _progressFill; // 进度条 fillAmount
|
||
[SerializeField] private TMP_Text _tipText; // 载入提示
|
||
[SerializeField] private Image[] _backgroundArts; // 随机切换的背景图
|
||
[SerializeField] private string[] _tipKeys; // 本地化 Key 数组
|
||
[SerializeField] private float _minDisplayTime = 0.5f; // 载入面最少展示时长
|
||
|
||
[Header("Event Channels")]
|
||
[SerializeField] private VoidEventChannelSO _onLoadingStarted;
|
||
[SerializeField] private VoidEventChannelSO _onLoadingComplete;
|
||
[SerializeField] private FloatEventChannelSO _onLoadingProgressUpdated; // 0–1
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onLoadingStarted.OnEventRaised += Show;
|
||
_onLoadingComplete.OnEventRaised += Hide;
|
||
_onLoadingProgressUpdated.OnEventRaised += SetProgress;
|
||
}
|
||
private void OnDisable()
|
||
{
|
||
_onLoadingStarted.OnEventRaised -= Show;
|
||
_onLoadingComplete.OnEventRaised -= Hide;
|
||
_onLoadingProgressUpdated.OnEventRaised -= SetProgress;
|
||
}
|
||
|
||
private void Show()
|
||
{
|
||
_loadingRoot.SetActive(true);
|
||
_progressFill.fillAmount = 0f;
|
||
// 随机选取背景图和提示文字
|
||
foreach (var bg in _backgroundArts) bg.enabled = false;
|
||
_backgroundArts[Random.Range(0, _backgroundArts.Length)].enabled = true;
|
||
_tipText.text = LocalizationManager.Get(_tipKeys[Random.Range(0, _tipKeys.Length)]);
|
||
}
|
||
|
||
private void Hide() => StartCoroutine(HideAfterMinTime());
|
||
private IEnumerator HideAfterMinTime()
|
||
{
|
||
// 确保载入面至少展示 _minDisplayTime 秒
|
||
yield return new WaitForSecondsRealtime(_minDisplayTime);
|
||
_loadingRoot.SetActive(false);
|
||
}
|
||
|
||
private void SetProgress(float value) => _progressFill.fillAmount = value;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7.8 IBossHPProvider 接口
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/HUD/IBossHPProvider.cs
|
||
// 解耦接口:让 BossHPBar 不直接依赖 BossBase(UI → Combat 逆向耐合)
|
||
// BossBase 在运行时实现此接口,BossOrchestrator 配置到 BossHPBar._provider 中
|
||
public interface IBossHPProvider
|
||
{
|
||
string BossId { get; } // Boss 前缀 ID
|
||
string BossNameKey { get; } // 本地化 Key
|
||
float HPRatio { get; } // 0–1 实时 HP 比例
|
||
int TotalPhases { get; } // Boss 总阶段数为阶段标记数
|
||
float[] PhaseThresholds { get; } // 各阶段切换 HP 阈值
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. LoadingOverlay
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/LoadingOverlay.cs
|
||
// 由 SceneLoader 直接调用(或通过事件),控制全屏黑幕渐入渐出
|
||
public class LoadingOverlay : MonoBehaviour
|
||
{
|
||
[SerializeField] private CanvasGroup _canvasGroup;
|
||
[SerializeField] private float _fadeDuration = 0.3f;
|
||
|
||
[Header("Event Channels")]
|
||
[SerializeField] private BoolEventChannelSO _onLoadingOverlayRequested;
|
||
|
||
private void OnEnable() => _onLoadingOverlayRequested.OnEventRaised += SetVisible;
|
||
private void OnDisable() => _onLoadingOverlayRequested.OnEventRaised -= SetVisible;
|
||
|
||
private void SetVisible(bool visible) => StartCoroutine(FadeCoroutine(visible ? 1f : 0f));
|
||
|
||
private IEnumerator FadeCoroutine(float target)
|
||
{
|
||
float start = _canvasGroup.alpha;
|
||
float t = 0;
|
||
while (t < _fadeDuration)
|
||
{
|
||
_canvasGroup.alpha = Mathf.Lerp(start, target, t / _fadeDuration);
|
||
t += Time.unscaledDeltaTime;
|
||
yield return null;
|
||
}
|
||
_canvasGroup.alpha = target;
|
||
_canvasGroup.blocksRaycasts = target > 0.5f;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. DialogueBox
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/DialogueBox.cs
|
||
// 挂在 Canvas_Overlay 下;由 DialogueManager 控制(见 14_NarrativeModule.md)
|
||
public class DialogueBox : MonoBehaviour
|
||
{
|
||
[SerializeField] private TMP_Text _speakerNameText;
|
||
[SerializeField] private TMP_Text _dialogueText;
|
||
[SerializeField] private GameObject _continuePrompt;
|
||
|
||
// DialogueManager 直接调用(不通过事件频道,避免帧延迟)
|
||
public void Show(string speakerName, string text, bool showContinue);
|
||
public void Hide();
|
||
|
||
// 文字逐字打印协程
|
||
public IEnumerator TypeText(string text, float charDelay = 0.03f);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. FloatingDamageText
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/FloatingDamageText.cs
|
||
// 从对象池取出,显示伤害数字,向上飘动后归还
|
||
public class FloatingDamageText : MonoBehaviour
|
||
{
|
||
[SerializeField] private TMP_Text _text;
|
||
[SerializeField] private float _floatDistance = 1.5f;
|
||
[SerializeField] private float _duration = 0.8f;
|
||
|
||
private string _poolKey = AddressKeys.UI_FloatingDmgText;
|
||
|
||
public void Show(Vector2 worldPosition, int damage, DamageType type);
|
||
// 1. 设置世界坐标(Camera.main.WorldToScreenPoint → RectTransform)
|
||
// 2. 颜色:Normal=白, Fire=橙, Poison=绿, True=黄
|
||
// 3. 协程向上漂移 + alpha 淡出
|
||
// 4. 归还对象池
|
||
|
||
// 由 EVT_DamageDealt 频道触发(HUDController 订阅后调用)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 11. ToastNotification
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/ToastNotification.cs
|
||
// 右上角通知弹窗(能力解锁、成就、提示)
|
||
public class ToastNotification : MonoBehaviour
|
||
{
|
||
[SerializeField] private TMP_Text _titleText;
|
||
[SerializeField] private TMP_Text _bodyText;
|
||
[SerializeField] private Image _icon;
|
||
[SerializeField] private float _displayDuration = 3f;
|
||
|
||
public void Show(string title, string body, Sprite icon = null);
|
||
private IEnumerator AutoHide();
|
||
}
|
||
|
||
// ToastManager:管理通知队列(一次只显示一条)
|
||
public class ToastManager : MonoBehaviour
|
||
{
|
||
[SerializeField] private ToastNotification _toast;
|
||
private Queue<(string title, string body, Sprite icon)> _queue = new();
|
||
|
||
[Header("Event Channels")]
|
||
[SerializeField] private StringEventChannelSO _onAchievementUnlocked; // 广播成就名
|
||
// ... 其他通知来源
|
||
|
||
public void Enqueue(string title, string body, Sprite icon = null);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 12. InputDeviceIconSwitcher
|
||
|
||
```csharp
|
||
// 路径: Assets/Scripts/UI/InputDeviceIconSwitcher.cs
|
||
// 检测输入设备切换(KB/手柄),自动替换 UI 按键图标
|
||
public class InputDeviceIconSwitcher : MonoBehaviour
|
||
{
|
||
[SerializeField] private InputDeviceIconSetSO _kbIconSet;
|
||
[SerializeField] private InputDeviceIconSetSO _padIconSet;
|
||
|
||
[Header("Event Channel")]
|
||
[SerializeField] private BoolEventChannelSO _onDeviceChanged; // true=手柄
|
||
|
||
private void OnEnable() => _onDeviceChanged.OnEventRaised += SwitchIconSet;
|
||
private void OnDisable() => _onDeviceChanged.OnEventRaised -= SwitchIconSet;
|
||
|
||
private void SwitchIconSet(bool isGamepad);
|
||
// 广播给所有 InputIconImage 组件(自行从 IconSet 查找对应 Sprite)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 13. PanelStack
|
||
|
||
```csharp
|
||
// 已集成在 UIManager 内部(OpenPanel / CloseTopPanel)
|
||
// 控制器导航规则:
|
||
// - 每次 OpenPanel 时设置 EventSystem.SetSelectedGameObject(panel.defaultButton)
|
||
// - Escape / 手柄 B 键 → 触发 CloseTopPanel
|
||
// - Stack 为空时 → 若在 Gameplay 状态则无操作,若在 MainMenu 则弹出退出确认
|
||
```
|
||
|
||
---
|
||
|
||
## 14. UI 事件频道清单
|
||
|
||
| 资产名 | 类型 | Raise 方 | Subscribe 方 |
|
||
|--------|------|---------|-------------|
|
||
| `EVT_GameStateChanged` | `GameStateEventChannelSO` | `GameManager` | `UIManager` |
|
||
| `EVT_PlayerDied` | `VoidEventChannelSO` | `PlayerStats` | `DeathScreenController`、`GameManager` |
|
||
| `EVT_DeathScreenConfirmed` | `VoidEventChannelSO` | `DeathScreenController`(Respawn 按钮) | `GameManager`(启动 RespawnCoroutine) |
|
||
| `EVT_ShowInteractPrompt` | `StringEventChannelSO` | `InteractableDetector` | `HUDController` |
|
||
| `EVT_HideInteractPrompt` | `VoidEventChannelSO` | `InteractableDetector` | `HUDController` |
|
||
| `EVT_BossFightToggled` | `BoolEventChannelSO`(true=开始,false=结束) | `BossOrchestrator` | `BossHPBar`、`AudioManager`(切 Boss BGM) |
|
||
| `EVT_BossHPChanged` | `IntEventChannelSO` | `BossBase` | `BossHPBar` |
|
||
| `EVT_BossNameSet` | `StringEventChannelSO` | `BossOrchestrator` | `BossHPBar` |
|
||
| `EVT_BossHPMaxSet` | `IntEventChannelSO` | `BossBase` | `BossHPBar` |
|
||
| `EVT_LoadingOverlay` | `BoolEventChannelSO` | `SceneLoader` | `LoadingOverlay` |
|
||
| `EVT_DamageDealt` | `DamageInfoEventChannelSO` | `HurtBox` | `HUDController`(生成伤害数字)、`AchievementManager` |
|
||
| `EVT_AchievementUnlocked` | `StringEventChannelSO` | `AchievementManager` | `ToastManager` |
|
||
| `EVT_AbilityUnlocked` | `StringEventChannelSO`(abilityId) | `PlayerStats.UnlockAbility` | `ToastManager`、`HUDController` |
|
||
| `EVT_PlayerFormChanged` | `IntEventChannelSO` | `FormController` | `HUDController`、`SkillHUD` |
|
||
| `EVT_InputDeviceChanged` | `BoolEventChannelSO` | 输入系统(设备切换回调) | `InputDeviceIconSwitcher` |
|
||
| `EVT_LoadingStarted` | `VoidEventChannelSO` | `SceneLoader` | `LoadingScreenManager` |
|
||
| `EVT_LoadingComplete` | `VoidEventChannelSO` | `SceneLoader` | `LoadingScreenManager` |
|
||
| `EVT_LoadingProgressUpdated` | `FloatEventChannelSO` | `SceneLoader` | `LoadingScreenManager` |
|
||
| `EVT_SaveIndicatorVisible` | `BoolEventChannelSO`(true=显示,false=隐藏) | `SaveManager.SaveAsync()` | `SaveIndicator` |
|