UI相关优化补充
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
@@ -22,17 +24,23 @@ namespace BaseGames.UI
|
||||
[DefaultExecutionOrder(+50)]
|
||||
public class UIManager : MonoBehaviour, IUIManager
|
||||
{
|
||||
// ── 状态驱动根节点(不进入面板栈,仅根据 GameState 显示/隐藏)────────
|
||||
// ── 状态驱动根节点(不进入面板栈,仅根据 GameState 显示/隐藏)─────────
|
||||
[Header("状态驱动根节点(非面板栈)")]
|
||||
[SerializeField] private GameObject _hudRoot;
|
||||
[SerializeField] private GameObject _deathScreenRoot;
|
||||
|
||||
// ── 面板栈注册表 ──────────────────────────────────────────────────────
|
||||
[Header("面板栈注册表(Inspector 配置,可运行时扩展)")]
|
||||
[Tooltip("将 PanelId 与对应的根 GameObject 绑定。" +
|
||||
"新增面板只需在此添加一行,无需修改 UIManager 代码。")]
|
||||
[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;
|
||||
@@ -43,11 +51,11 @@ namespace BaseGames.UI
|
||||
[SerializeField] private VoidEventChannelSO _onSpellSelectOpen;
|
||||
|
||||
// ── 面板栈结构 ────────────────────────────────────────────────────────
|
||||
private readonly Stack<GameObject> _panelStack = new();
|
||||
private readonly Stack<GameObject> _panelStack = new();
|
||||
/// <summary>O(1) 成员判断,与 _panelStack 保持同步,替代 Stack.Contains O(n)。</summary>
|
||||
private readonly HashSet<GameObject> _openPanelSet = new();
|
||||
private readonly HashSet<GameObject> _openPanelSet = new();
|
||||
private readonly Dictionary<PanelId, GameObject> _panelRegistry = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 序列化辅助结构 ────────────────────────────────────────────────────
|
||||
[System.Serializable]
|
||||
@@ -59,13 +67,42 @@ namespace BaseGames.UI
|
||||
public GameObject root;
|
||||
}
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
[System.Serializable]
|
||||
private struct AddressablePanelRegistration
|
||||
{
|
||||
[Tooltip("面板标识符。")]
|
||||
public PanelId id;
|
||||
[Tooltip("Addressable 引用;首次 OpenPanel 时异步加载并实例化。")]
|
||||
public AssetReferenceGameObject reference;
|
||||
[Tooltip("关闭后保留实例(仅隐藏,不释放)。开启时常驻内存,避免反复加载抖动;适合频繁打开的面板。")]
|
||||
public bool keepLoadedAfterClose;
|
||||
}
|
||||
|
||||
/// <summary>记录 Addressable 加载状态,避免重复加载与正确释放。</summary>
|
||||
private class AddressablePanelHandle
|
||||
{
|
||||
public AsyncOperationHandle<GameObject> Handle;
|
||||
public GameObject Instance;
|
||||
public bool IsLoading;
|
||||
public bool KeepLoaded;
|
||||
}
|
||||
private readonly Dictionary<PanelId, AddressablePanelHandle> _addressableHandles = new();
|
||||
private readonly Dictionary<PanelId, AssetReferenceGameObject> _addressableRefs = new();
|
||||
private readonly Dictionary<PanelId, bool> _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()
|
||||
@@ -84,21 +121,36 @@ namespace BaseGames.UI
|
||||
{
|
||||
ServiceLocator.Unregister<IUIManager>(this);
|
||||
_subs.Clear();
|
||||
ReleaseAllAddressablePanels();
|
||||
}
|
||||
|
||||
/// <summary>释放所有由 Addressables 加载的面板(防止场景切换泄漏)。</summary>
|
||||
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())
|
||||
{
|
||||
Addressables.Release(h.Handle);
|
||||
}
|
||||
}
|
||||
_addressableHandles.Clear();
|
||||
}
|
||||
|
||||
// ── 面板注册(运行时动态扩展入口)────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 运行时注册或覆盖面板绑定(如场景加载后动态添加的面板)。
|
||||
/// Inspector 中已配置的面板无需调用此方法。
|
||||
/// </summary>
|
||||
/// <summary>运行时注册或覆盖面板绑定(如场景加载后动态添加的面板)。</summary>
|
||||
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;
|
||||
@@ -118,19 +170,72 @@ namespace BaseGames.UI
|
||||
}
|
||||
|
||||
// ── 面板栈 API ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>通过 ID 打开已注册的面板。</summary>
|
||||
/// <summary>通过 ID 打开面板:优先同步注册表,其次 Addressable 异步加载。</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>异步加载并打开 Addressable 面板。首次触发 InstantiateAsync,后续复用。</summary>
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>打开指定 GameObject 面板并压栈;已在栈中则忽略(O(1) 判断)。</summary>
|
||||
public void OpenPanel(GameObject panel)
|
||||
{
|
||||
if (panel == null) return;
|
||||
if (!_openPanelSet.Add(panel)) return; // HashSet.Add 返回 false = 已存在
|
||||
if (!_openPanelSet.Add(panel)) return;
|
||||
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(false);
|
||||
panel.SetActive(true);
|
||||
_panelStack.Push(panel);
|
||||
@@ -152,7 +257,6 @@ namespace BaseGames.UI
|
||||
}
|
||||
|
||||
// ── 快捷事件回调 ──────────────────────────────────────────────────────
|
||||
|
||||
private void TogglePause()
|
||||
{
|
||||
if (_panelRegistry.TryGetValue(PanelId.Pause, out var pausePanel)
|
||||
@@ -166,35 +270,53 @@ namespace BaseGames.UI
|
||||
private void OpenCharmPanel() => OpenPanel(PanelId.CharmPanel);
|
||||
private void OpenSpellSelect() => OpenPanel(PanelId.SpellSelect);
|
||||
|
||||
// ── 编辑器工具 (不入构建) ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>验证面板注册表是否完整、无重复、无空引用。</summary>
|
||||
// ── 编辑器工具 ────────────────────────────────────────────────────────
|
||||
[ContextMenu("验证面板注册表")]
|
||||
private void EditorValidateRegistry()
|
||||
{
|
||||
if (_panels == null || _panels.Length == 0)
|
||||
if ((_panels == null || _panels.Length == 0) && (_addressablePanels == null || _addressablePanels.Length == 0))
|
||||
{
|
||||
Debug.LogWarning("[UIManager] 面板注册表为空!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
var seen = new System.Collections.Generic.HashSet<PanelId>();
|
||||
var seen = new HashSet<PanelId>();
|
||||
bool ok = true;
|
||||
foreach (var p in _panels)
|
||||
if (_panels != null)
|
||||
{
|
||||
if (p.root == null)
|
||||
foreach (var p in _panels)
|
||||
{
|
||||
Debug.LogWarning($"[UIManager] PanelId.{p.id} 的 GameObject 引用为 null!", this);
|
||||
ok = false;
|
||||
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 (!seen.Add(p.id))
|
||||
}
|
||||
if (_addressablePanels != null)
|
||||
{
|
||||
var seenAddr = new HashSet<PanelId>();
|
||||
foreach (var p in _addressablePanels)
|
||||
{
|
||||
Debug.LogError($"[UIManager] PanelId.{p.id} 重复注册!", this);
|
||||
ok = false;
|
||||
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} 个面板。", this);
|
||||
Debug.Log($"[UIManager] 验证通过 ✔ 同步 {_panels?.Length ?? 0} 个 / Addressable {_addressablePanels?.Length ?? 0} 个。", this);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -203,15 +325,15 @@ namespace BaseGames.UI
|
||||
#endif
|
||||
|
||||
[ContextMenu("测试:打开 Pause 面板")]
|
||||
private void EditorOpenPause() => OpenPanel(PanelId.Pause);
|
||||
private void EditorOpenPause() => OpenPanel(PanelId.Pause);
|
||||
|
||||
[ContextMenu("测试:打开 Map 面板")]
|
||||
private void EditorOpenMap() => OpenPanel(PanelId.Map);
|
||||
private void EditorOpenMap() => OpenPanel(PanelId.Map);
|
||||
|
||||
[ContextMenu("测试:打开 Shop 面板")]
|
||||
private void EditorOpenShop() => OpenPanel(PanelId.Shop);
|
||||
private void EditorOpenShop() => OpenPanel(PanelId.Shop);
|
||||
|
||||
[ContextMenu("测试:关闭栈顶面板")]
|
||||
private void EditorCloseTop() => CloseTopPanel();
|
||||
private void EditorCloseTop() => CloseTopPanel();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user