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();
}
}