UI系统
This commit is contained in:
182
Assets/_Game/Scripts/UI/Navigation/UINavigator.cs
Normal file
182
Assets/_Game/Scripts/UI/Navigation/UINavigator.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.SceneManagement;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IUINavigator"/> 的实现:统一 UI 导航栈。常驻 Persistent 场景,
|
||||
/// 经 ServiceLocator 暴露,主菜单与游戏内共用同一套栈语义。
|
||||
///
|
||||
/// <para>压栈:记录压栈前焦点;据新面板 <see cref="PushMode"/> 处理下方面板
|
||||
/// (Replace→停用;Modal→保留可见但 <see cref="UIPanelBase.SetInteractableLayer"/>(false)
|
||||
/// 使其退出导航图);激活新面板并延后一帧聚焦其首项。</para>
|
||||
/// <para>出栈:关闭栈顶,按其压栈时的 mode 还原下方面板,恢复焦点到压栈前的选中项。</para>
|
||||
/// <para>取消:本类是 EVT_UICancelPressed 的唯一消费者,仅在栈顶 <see cref="UIPanelBase.CanCancel"/>
|
||||
/// 时出栈一层(逐层回退)。</para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(+40)] // 早于 UIManager(+50),确保委托方解析得到本服务
|
||||
public class UINavigator : MonoBehaviour, IUINavigator
|
||||
{
|
||||
[Tooltip("UI 取消操作(ESC / 手柄 B·Circle)。本导航器为唯一订阅者,按下时关闭栈顶一层。对应 EVT_UICancelPressed。")]
|
||||
[SerializeField] private VoidEventChannelSO _onUICancelPressed;
|
||||
|
||||
private readonly Stack<UIStackEntry> _stack = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private Coroutine _focusRoutine;
|
||||
|
||||
public UIPanelBase Top => _stack.Count > 0 ? _stack.Peek().Panel : null;
|
||||
public int Depth => _stack.Count;
|
||||
|
||||
public event Action StackChanged;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
private void OnEnable()
|
||||
{
|
||||
ServiceLocator.Register<IUINavigator>(this);
|
||||
_onUICancelPressed?.Subscribe(HandleCancel).AddTo(_subs);
|
||||
SceneManager.sceneUnloaded += OnSceneUnloaded;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
SceneManager.sceneUnloaded -= OnSceneUnloaded;
|
||||
_subs.Clear();
|
||||
ServiceLocator.Unregister<IUINavigator>(this);
|
||||
}
|
||||
|
||||
// ── 压栈 ──────────────────────────────────────────────────────────────
|
||||
public void Push(UIPanelBase panel, PushMode? mode = null)
|
||||
{
|
||||
if (panel == null) return;
|
||||
PurgeDead();
|
||||
|
||||
PushMode m = mode ?? panel.DefaultMode;
|
||||
|
||||
// 处理下方面板:Replace 停用、Modal 屏蔽交互(退出导航图,杜绝上下键穿透)。
|
||||
if (_stack.Count > 0)
|
||||
{
|
||||
var below = _stack.Peek().Panel;
|
||||
if (below != null)
|
||||
{
|
||||
below.OnFocusLost();
|
||||
if (m == PushMode.Replace) below.gameObject.SetActive(false);
|
||||
else below.SetInteractableLayer(false);
|
||||
}
|
||||
}
|
||||
|
||||
var entry = new UIStackEntry
|
||||
{
|
||||
Panel = panel,
|
||||
Mode = m,
|
||||
FocusToRestore = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null,
|
||||
OwningScene = panel.gameObject.scene,
|
||||
};
|
||||
_stack.Push(entry);
|
||||
|
||||
panel.gameObject.SetActive(true);
|
||||
panel.SetInteractableLayer(true);
|
||||
FocusNextFrame(panel.FirstSelectableGO);
|
||||
|
||||
StackChanged?.Invoke();
|
||||
}
|
||||
|
||||
// ── 出栈 ──────────────────────────────────────────────────────────────
|
||||
public void Pop()
|
||||
{
|
||||
PurgeDead();
|
||||
if (_stack.Count == 0) return;
|
||||
|
||||
var top = _stack.Pop();
|
||||
if (top.Panel != null) top.Panel.gameObject.SetActive(false);
|
||||
|
||||
UIPanelBase below = _stack.Count > 0 ? _stack.Peek().Panel : null;
|
||||
if (below != null)
|
||||
{
|
||||
// 按 top 压栈时的 mode 还原下方面板。
|
||||
if (top.Mode == PushMode.Replace) below.gameObject.SetActive(true);
|
||||
else below.SetInteractableLayer(true);
|
||||
below.OnFocusGained();
|
||||
}
|
||||
|
||||
// 恢复焦点到压栈前的选中项(失效则回落到下层首项)。
|
||||
GameObject restore = top.FocusToRestore != null && top.FocusToRestore.activeInHierarchy
|
||||
? top.FocusToRestore
|
||||
: below != null ? below.FirstSelectableGO : null;
|
||||
FocusNextFrame(restore);
|
||||
|
||||
StackChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void PopToRoot()
|
||||
{
|
||||
while (_stack.Count > 0) Pop();
|
||||
}
|
||||
|
||||
// ── 取消(ESC / 手柄 B)──────────────────────────────────────────────
|
||||
private void HandleCancel()
|
||||
{
|
||||
PurgeDead();
|
||||
if (_stack.Count == 0) return; // 栈空(如主菜单根):无操作
|
||||
if (Top != null && !Top.CanCancel) return;
|
||||
Pop(); // 仅关栈顶一层
|
||||
}
|
||||
|
||||
// ── 结果面板 ──────────────────────────────────────────────────────────
|
||||
public Task<T> PushForResultAsync<T>(UIResultPanel<T> panel, CancellationToken ct = default)
|
||||
{
|
||||
if (panel == null) return Task.FromResult<T>(default);
|
||||
Task<T> task = panel.BeginResult(ct);
|
||||
Push(panel, PushMode.Modal); // 结果对话框天然模态:下层保留可见但屏蔽交互
|
||||
return task;
|
||||
}
|
||||
|
||||
// ── 场景卸载清理 ──────────────────────────────────────────────────────
|
||||
private void OnSceneUnloaded(Scene s)
|
||||
{
|
||||
if (_stack.Count == 0) return;
|
||||
|
||||
// Stack 无法删中间项:过滤后按原序重建(保留非本场景且未销毁的层)。
|
||||
var kept = new List<UIStackEntry>();
|
||||
foreach (var e in _stack) // 枚举顺序:栈顶→栈底
|
||||
if (e.Panel != null && e.OwningScene != s) kept.Add(e);
|
||||
|
||||
if (kept.Count == _stack.Count) return; // 无变化
|
||||
|
||||
_stack.Clear();
|
||||
for (int i = kept.Count - 1; i >= 0; i--) _stack.Push(kept[i]);
|
||||
StackChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PurgeDead()
|
||||
{
|
||||
bool changed = false;
|
||||
while (_stack.Count > 0 && _stack.Peek().Panel == null) { _stack.Pop(); changed = true; }
|
||||
if (changed) StackChanged?.Invoke();
|
||||
}
|
||||
|
||||
// ── 焦点(延后一帧,避开 OnEnable / UISelectionRestorer 同帧竞争)────────
|
||||
private void FocusNextFrame(GameObject target)
|
||||
{
|
||||
if (_focusRoutine != null) StopCoroutine(_focusRoutine);
|
||||
if (!isActiveAndEnabled) return;
|
||||
_focusRoutine = StartCoroutine(FocusRoutine(target));
|
||||
}
|
||||
|
||||
private IEnumerator FocusRoutine(GameObject target)
|
||||
{
|
||||
yield return null;
|
||||
_focusRoutine = null;
|
||||
if (target == null || !target.activeInHierarchy) yield break;
|
||||
if (EventSystem.current != null)
|
||||
EventSystem.current.SetSelectedGameObject(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user