using System.Collections.Generic; using UnityEngine; using BaseGames.Core; using BaseGames.Core.Events; namespace BaseGames.UI { /// /// 面板 ID 枚举。新增面板时只需在此添加值并在 Inspector 的 _panels 数组中注册, /// 无需修改 UIManager 的其他代码,满足开闭原则。 /// public enum PanelId { Pause, Settings, Map, Shop, CharmPanel, SpellSelect, } [DefaultExecutionOrder(+50)] public class UIManager : MonoBehaviour, IUIManager { // ── 状态驱动根节点(不进入面板栈,仅根据 GameState 显示/隐藏)──────── [Header("状态驱动根节点(非面板栈)")] [SerializeField] private GameObject _hudRoot; [SerializeField] private GameObject _deathScreenRoot; // ── 面板栈注册表 ────────────────────────────────────────────────────── [Header("面板栈注册表(Inspector 配置,可运行时扩展)")] [Tooltip("将 PanelId 与对应的根 GameObject 绑定。" + "新增面板只需在此添加一行,无需修改 UIManager 代码。")] [SerializeField] private PanelRegistration[] _panels; [Header("Event Channels")] [SerializeField] private GameStateEventChannelSO _onGameStateChanged; [SerializeField] private VoidEventChannelSO _onPauseRequested; [SerializeField] private VoidEventChannelSO _onFastTravelOpen; [SerializeField] private StringEventChannelSO _onShopOpen; [SerializeField] private VoidEventChannelSO _onMapOpen; [SerializeField] private VoidEventChannelSO _onCharmPanelOpen; [SerializeField] private VoidEventChannelSO _onSpellSelectOpen; // ── 面板栈结构 ──────────────────────────────────────────────────────── private readonly Stack _panelStack = new(); /// O(1) 成员判断,与 _panelStack 保持同步,替代 Stack.Contains O(n)。 private readonly HashSet _openPanelSet = new(); private readonly Dictionary _panelRegistry = new(); private readonly CompositeDisposable _subs = new(); // ── 序列化辅助结构 ──────────────────────────────────────────────────── [System.Serializable] private struct PanelRegistration { [Tooltip("面板标识符(与代码中的 PanelId 枚举对应)。")] public PanelId id; [Tooltip("该面板的根 GameObject(通常是 Canvas 的直接子节点)。")] public GameObject root; } // ── 生命周期 ────────────────────────────────────────────────────────── private void Awake() { if (_panels != null) foreach (var p in _panels) if (p.root != null) _panelRegistry[p.id] = p.root; } private void OnEnable() { ServiceLocator.Register(this); _onGameStateChanged?.Subscribe(HandleGameStateChanged).AddTo(_subs); _onPauseRequested?.Subscribe(TogglePause).AddTo(_subs); _onFastTravelOpen?.Subscribe(OpenMap).AddTo(_subs); _onShopOpen?.Subscribe(OpenShop).AddTo(_subs); _onMapOpen?.Subscribe(OpenMap).AddTo(_subs); _onCharmPanelOpen?.Subscribe(OpenCharmPanel).AddTo(_subs); _onSpellSelectOpen?.Subscribe(OpenSpellSelect).AddTo(_subs); } private void OnDisable() { ServiceLocator.Unregister(this); _subs.Clear(); } // ── 面板注册(运行时动态扩展入口)──────────────────────────────────── /// /// 运行时注册或覆盖面板绑定(如场景加载后动态添加的面板)。 /// Inspector 中已配置的面板无需调用此方法。 /// public void RegisterPanel(PanelId id, GameObject root) { if (root != null) _panelRegistry[id] = root; } // ── 状态响应 ────────────────────────────────────────────────────────── private void HandleGameStateChanged(GameStateId state) { bool showHud = state == GameStates.Gameplay || state == GameStates.BossFight; if (_hudRoot != null) _hudRoot.SetActive(showHud); if (state == GameStates.Dead) { if (_deathScreenRoot != null) _deathScreenRoot.SetActive(true); } else { if (_deathScreenRoot != null) _deathScreenRoot.SetActive(false); if (state == GameStates.Cutscene) if (_hudRoot != null) _hudRoot.SetActive(false); } } // ── 面板栈 API ──────────────────────────────────────────────────────── /// 通过 ID 打开已注册的面板。 public void OpenPanel(PanelId id) { if (_panelRegistry.TryGetValue(id, out var panel)) OpenPanel(panel); } /// 打开指定 GameObject 面板并压栈;已在栈中则忽略(O(1) 判断)。 public void OpenPanel(GameObject panel) { if (panel == null) return; if (!_openPanelSet.Add(panel)) return; // HashSet.Add 返回 false = 已存在 if (_panelStack.Count > 0) _panelStack.Peek().SetActive(false); panel.SetActive(true); _panelStack.Push(panel); } /// 关闭栈顶面板并恢复上一层(如有);上一层若实现 IFocusable 则自动恢复焦点。 public void CloseTopPanel() { if (_panelStack.Count == 0) return; var top = _panelStack.Pop(); _openPanelSet.Remove(top); top.SetActive(false); if (_panelStack.Count > 0) { var restored = _panelStack.Peek(); restored.SetActive(true); restored.GetComponent()?.OnFocusRestored(); } } // ── 快捷事件回调 ────────────────────────────────────────────────────── private void TogglePause() { if (_panelRegistry.TryGetValue(PanelId.Pause, out var pausePanel) && _panelStack.Count > 0 && _panelStack.Peek() == pausePanel) CloseTopPanel(); else OpenPanel(PanelId.Pause); } private void OpenShop(string _) => OpenPanel(PanelId.Shop); private void OpenMap() => OpenPanel(PanelId.Map); private void OpenCharmPanel() => OpenPanel(PanelId.CharmPanel); private void OpenSpellSelect() => OpenPanel(PanelId.SpellSelect); // ── 编辑器工具 (不入构建) ────────────────────────────────────────────────── /// 验证面板注册表是否完整、无重复、无空引用。 [ContextMenu("验证面板注册表")] private void EditorValidateRegistry() { if (_panels == null || _panels.Length == 0) { Debug.LogWarning("[UIManager] 面板注册表为空!", this); return; } var seen = new System.Collections.Generic.HashSet(); bool ok = true; foreach (var p in _panels) { if (p.root == null) { Debug.LogWarning($"[UIManager] PanelId.{p.id} 的 GameObject 引用为 null!", this); ok = false; } if (!seen.Add(p.id)) { Debug.LogError($"[UIManager] PanelId.{p.id} 重复注册!", this); ok = false; } } if (ok) Debug.Log($"[UIManager] 验证通过 ✔ 已注册 {_panels.Length} 个面板。", this); } #if UNITY_EDITOR /// 仅供 UIManagerEditor 实时可视化面板栈(由栈顶到栈底顺序)。 public GameObject[] EditorGetPanelSnapshot() => _panelStack.ToArray(); #endif [ContextMenu("测试:打开 Pause 面板")] private void EditorOpenPause() => OpenPanel(PanelId.Pause); [ContextMenu("测试:打开 Map 面板")] private void EditorOpenMap() => OpenPanel(PanelId.Map); [ContextMenu("测试:打开 Shop 面板")] private void EditorOpenShop() => OpenPanel(PanelId.Shop); [ContextMenu("测试:关闭栈顶面板")] private void EditorCloseTop() => CloseTopPanel(); } }