Files
zeling_v2/Docs/Architecture/10_UIModule.md
2026-05-08 11:04:00 +08:00

25 KiB
Raw Permalink Blame History

10 · UI 模块

命名空间 BaseGames.UI
程序集 BaseGames.UI
路径 Assets/Scripts/UI/
依赖 BaseGames.Core.EventsTextMeshPro


目录

  1. Canvas 架构Persistent 场景内)
  2. UIManager
  3. HUDController
  4. BossHPBar
  5. PauseMenuController
  6. DeathScreenController
  7. SettingsPanelController
  8. SaveSlotController
  9. SaveIndicator
  10. LoadingScreenManager
  11. IBossHPProvider 接口
  12. LoadingOverlay
  13. DialogueBoxHUD Overlay
  14. FloatingDamageText伤害数字
  15. ToastNotification通知弹窗
  16. InputDeviceIconSwitcher
  17. PanelStack控制器导航
  18. 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 BoolEventChannelSOtrue 触发淡入,false 触发淡出),告知玩家正在保存中。


2. UIManager

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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;  // 01

    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 接口

// 路径: Assets/Scripts/UI/HUD/IBossHPProvider.cs
// 解耦接口:让 BossHPBar 不直接依赖 BossBaseUI → Combat 逆向耐合)
// BossBase 在运行时实现此接口BossOrchestrator 配置到 BossHPBar._provider 中
public interface IBossHPProvider
{
    string  BossId         { get; }      // Boss 前缀 ID
    string  BossNameKey    { get; }      // 本地化 Key
    float   HPRatio        { get; }      // 01 实时 HP 比例
    int     TotalPhases    { get; }      // Boss 总阶段数为阶段标记数
    float[] PhaseThresholds { get; }     // 各阶段切换 HP 阈值
}

8. LoadingOverlay

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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

// 路径: 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

// 已集成在 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 DeathScreenControllerGameManager
EVT_DeathScreenConfirmed VoidEventChannelSO DeathScreenControllerRespawn 按钮) GameManager(启动 RespawnCoroutine
EVT_ShowInteractPrompt StringEventChannelSO InteractableDetector HUDController
EVT_HideInteractPrompt VoidEventChannelSO InteractableDetector HUDController
EVT_BossFightToggled BoolEventChannelSOtrue=开始false=结束) BossOrchestrator BossHPBarAudioManager(切 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 StringEventChannelSOabilityId PlayerStats.UnlockAbility ToastManagerHUDController
EVT_PlayerFormChanged IntEventChannelSO FormController HUDControllerSkillHUD
EVT_InputDeviceChanged BoolEventChannelSO 输入系统(设备切换回调) InputDeviceIconSwitcher
EVT_LoadingStarted VoidEventChannelSO SceneLoader LoadingScreenManager
EVT_LoadingComplete VoidEventChannelSO SceneLoader LoadingScreenManager
EVT_LoadingProgressUpdated FloatEventChannelSO SceneLoader LoadingScreenManager
EVT_SaveIndicatorVisible BoolEventChannelSOtrue=显示false=隐藏) SaveManager.SaveAsync() SaveIndicator