using System.Collections.Generic; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using BaseGames.Core; using BaseGames.Core.Assets; using BaseGames.Core.Events; namespace BaseGames.UI { /// /// 面板 ID 枚举。新增面板时只需在此添加值并在 Inspector 的 _panels 数组中注册, /// 无需修改 UIManager 的其他代码,满足开闭原则。 /// public enum PanelId { Pause, Settings, Map, Shop, CharmPanel, SpellSelect, Inventory, } [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("Addressable 面板(按需异步加载,首次打开时实例化)")] [Tooltip("配置 PanelId → AssetReferenceGameObject;同 _panels 重复定义时,本表优先生效。" + "未在此处也未在 _panels 中注册的 PanelId 调用 OpenPanel 将记录警告。")] [SerializeField] private AddressablePanelRegistration[] _addressablePanels; [Tooltip("Addressable 面板实例化后挂载的父节点;为空时挂在 UIManager.transform。")] [SerializeField] private Transform _addressablePanelParent; [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; [Tooltip("打开统一背包菜单(InventoryHub)。对应 EVT_InventoryOpen。")] [SerializeField] private VoidEventChannelSO _onInventoryOpen; // ── 面板栈:委托给统一的 UINavigator(不再自管栈)───────────────────── private IUINavigator _navigator; 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; } [System.Serializable] private struct AddressablePanelRegistration { [Tooltip("面板标识符。")] public PanelId id; [Tooltip("Addressable 引用;首次 OpenPanel 时异步加载并实例化。")] public AssetReferenceGameObject reference; [Tooltip("关闭后保留实例(仅隐藏,不释放)。开启时常驻内存,避免反复加载抖动;适合频繁打开的面板。")] public bool keepLoadedAfterClose; } /// 记录 Addressable 加载状态,避免重复加载与正确释放。 private class AddressablePanelHandle { public AsyncOperationHandle Handle; public GameObject Instance; public bool IsLoading; public bool KeepLoaded; } private readonly Dictionary _addressableHandles = new(); private readonly Dictionary _addressableRefs = new(); private readonly Dictionary _addressableKeep = new(); // ── 生命周期 ────────────────────────────────────────────────────────── private void Awake() { if (_panels != null) foreach (var p in _panels) if (p.root != null) _panelRegistry[p.id] = p.root; if (_addressablePanels != null) foreach (var p in _addressablePanels) { if (p.reference != null) _addressableRefs[p.id] = p.reference; _addressableKeep[p.id] = p.keepLoadedAfterClose; } } private void OnEnable() { ServiceLocator.Register(this); _navigator = ServiceLocator.GetOrDefault(); _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); _onInventoryOpen?.Subscribe(OpenInventory).AddTo(_subs); // 取消键(ESC / 手柄 B)由 UINavigator 统一消费,UIManager 不再订阅。 } private void OnDisable() { ServiceLocator.Unregister(this); _subs.Clear(); ReleaseAllAddressablePanels(); } /// 释放所有由 Addressables 加载的面板(防止场景切换泄漏)。 private void ReleaseAllAddressablePanels() { foreach (var kv in _addressableHandles) { var h = kv.Value; if (h == null) continue; if (h.Instance != null && _addressableRefs.TryGetValue(kv.Key, out var aref)) { aref.ReleaseInstance(h.Instance); } else if (h.Handle.IsValid()) { AssetLoader.Release(h.Handle); } } _addressableHandles.Clear(); } // ── 面板注册(运行时动态扩展入口)──────────────────────────────────── /// 运行时注册或覆盖面板绑定(如场景加载后动态添加的面板)。 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 打开面板:优先同步注册表,其次 Addressable 异步加载。 public void OpenPanel(PanelId id) { if (_panelRegistry.TryGetValue(id, out var panel)) { OpenPanel(panel); return; } if (_addressableRefs.ContainsKey(id)) { OpenPanelAsync(id); return; } Debug.LogWarning($"[UIManager] PanelId.{id} 未注册(Inspector 中既无 _panels 也无 _addressablePanels)。", this); } /// 异步加载并打开 Addressable 面板。首次触发 InstantiateAsync,后续复用。 private void OpenPanelAsync(PanelId id) { if (!_addressableRefs.TryGetValue(id, out var aref) || aref == null) { Debug.LogWarning($"[UIManager] Addressable 面板 {id} 未配置 AssetReference。", this); return; } if (!_addressableHandles.TryGetValue(id, out var entry)) { _addressableKeep.TryGetValue(id, out var keep); entry = new AddressablePanelHandle { IsLoading = true, KeepLoaded = keep }; _addressableHandles[id] = entry; } // 已加载完成:直接复用实例。 if (entry.Instance != null) { _panelRegistry[id] = entry.Instance; OpenPanel(entry.Instance); return; } // 加载中:忽略重复请求。 if (entry.IsLoading && entry.Handle.IsValid()) return; entry.IsLoading = true; var parent = _addressablePanelParent != null ? _addressablePanelParent : transform; entry.Handle = aref.InstantiateAsync(parent, instantiateInWorldSpace: false); entry.Handle.Completed += op => { entry.IsLoading = false; if (op.Status != AsyncOperationStatus.Succeeded || op.Result == null) { Debug.LogError($"[UIManager] Addressable 面板 {id} 加载失败: {op.OperationException}", this); return; } entry.Instance = op.Result; entry.Instance.SetActive(false); // 与 PanelRegistration 同语义:默认隐藏 _panelRegistry[id] = entry.Instance; OpenPanel(entry.Instance); }; } /// 打开指定 GameObject 面板:经统一 压栈。 public void OpenPanel(GameObject panel) { if (panel == null) return; var uiPanel = EnsurePanel(panel); if (uiPanel != null) Navigator?.Push(uiPanel); } /// 关闭栈顶面板并恢复上一层(委托给导航器)。 public void CloseTopPanel() => Navigator?.Pop(); /// /// 适配器:保证面板根挂有 (导航器压栈对象)。 /// 既有面板若未挂则运行时补 + CanvasGroup, /// 使所有游戏内面板无需逐个改脚手架即可纳入统一导航栈。 /// private static UIPanelBase EnsurePanel(GameObject go) { var p = go.GetComponent(); if (p == null) { if (go.GetComponent() == null) go.AddComponent(); p = go.AddComponent(); } return p; } // ── 快捷事件回调 ────────────────────────────────────────────────────── private void TogglePause() { if (_panelRegistry.TryGetValue(PanelId.Pause, out var pausePanel) && Navigator?.Top != null && Navigator.Top.gameObject == pausePanel) Navigator.Pop(); else OpenPanel(PanelId.Pause); } private IUINavigator Navigator => _navigator ??= ServiceLocator.GetOrDefault(); 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); private void OpenInventory() => OpenPanel(PanelId.Inventory); // ── 编辑器工具 ──────────────────────────────────────────────────────── [ContextMenu("验证面板注册表")] private void EditorValidateRegistry() { if ((_panels == null || _panels.Length == 0) && (_addressablePanels == null || _addressablePanels.Length == 0)) { Debug.LogWarning("[UIManager] 面板注册表为空!", this); return; } var seen = new HashSet(); bool ok = true; if (_panels != null) { 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} 在 _panels 中重复!", this); ok = false; } } } if (_addressablePanels != null) { var seenAddr = new HashSet(); foreach (var p in _addressablePanels) { if (p.reference == null) { Debug.LogWarning($"[UIManager] Addressable PanelId.{p.id} 的 reference 为 null!", this); ok = false; } if (!seenAddr.Add(p.id)) { Debug.LogError($"[UIManager] PanelId.{p.id} 在 _addressablePanels 中重复!", this); ok = false; } } } if (ok) Debug.Log($"[UIManager] 验证通过 ✔ 同步 {_panels?.Length ?? 0} 个 / Addressable {_addressablePanels?.Length ?? 0} 个。", this); } #if UNITY_EDITOR /// 仅供 UIManagerEditor 实时可视化栈顶面板(栈本体已迁移至 UINavigator)。 public GameObject[] EditorGetPanelSnapshot() => Navigator?.Top != null ? new[] { Navigator.Top.gameObject } : System.Array.Empty(); #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(); } }