UI系统
This commit is contained in:
@@ -3,13 +3,10 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.World.Map;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.MainMenu;
|
||||
|
||||
namespace BaseGames.UI.Menus
|
||||
@@ -26,19 +23,21 @@ namespace BaseGames.UI.Menus
|
||||
/// <summary>
|
||||
/// 驱动主菜单存档槽选择面板(新游戏 / 继续 / 删除)。
|
||||
///
|
||||
/// <para>本面板经 <see cref="IUINavigator"/> 压栈(由主菜单控制器 Push)。覆盖确认 / 模式选择
|
||||
/// 这两个子对话框走导航器结果面板(<see cref="ConfirmDialogController.ShowAsync"/> /
|
||||
/// <see cref="NewGameModeController.ShowAsync"/>)——线性 <c>await</c>,无回调嵌套,ESC 逐层回退。</para>
|
||||
///
|
||||
/// 前端选档流程:
|
||||
/// · 新游戏开档前先选模式(普通 / 钢铁之魂),占用槽位需覆盖确认。
|
||||
/// · 删除强制走通用确认对话框(无静默删除旁路)。
|
||||
/// · 卡片展示游玩时长 / 区域 / 货币 / 生命 / 钢魂徽章。
|
||||
///
|
||||
/// 模式由 MainMenuController 在打开面板前通过 <see cref="SetMode"/> 指定。
|
||||
/// ConfirmDialog 与 NewGameMode 面板经 Inspector 注入(同处 MainMenu 场景)。
|
||||
/// 模式由主菜单控制器在压栈前经 <see cref="SetMode"/> 指定。
|
||||
/// </summary>
|
||||
public class SaveSlotController : MonoBehaviour, IFocusable
|
||||
public class SaveSlotController : UIPanelBase
|
||||
{
|
||||
[SerializeField] private SaveSlotUI[] _slotUIs;
|
||||
|
||||
[Header("子面板(Inspector 注入,同处 MainMenu 场景)")]
|
||||
[Header("子对话框(Inspector 注入,同处 MainMenu 场景,走导航器结果面板)")]
|
||||
[Tooltip("通用确认对话框,用于覆盖 / 删除确认。为空时:覆盖退化为直接建档,删除被忽略(绝不静默删除)。")]
|
||||
[SerializeField] private ConfirmDialogController _confirmDialog;
|
||||
[Tooltip("新游戏模式选择面板。为空时新游戏退化为普通模式。")]
|
||||
@@ -48,7 +47,7 @@ namespace BaseGames.UI.Menus
|
||||
[SerializeField] private IntEventChannelSO _onSlotConfirmed;
|
||||
|
||||
[Header("焦点")]
|
||||
[Tooltip("面板恢复为栈顶时自动聚焦的默认按钮,通常为第一个存档槽的选择按钮。")]
|
||||
[Tooltip("面板默认聚焦的按钮,通常为第一个存档槽的选择按钮。")]
|
||||
[SerializeField] private Button _defaultFocusButton;
|
||||
|
||||
private SaveSlotPanelMode _mode = SaveSlotPanelMode.NewGame;
|
||||
@@ -60,40 +59,24 @@ namespace BaseGames.UI.Menus
|
||||
if (_slotUIs[i] != null) _slotUIs[i].Init(i, this);
|
||||
}
|
||||
|
||||
/// <summary>由 MainMenuController 在 SetActive(true) 之前调用,决定本次打开语境。</summary>
|
||||
/// <summary>由主菜单控制器在压栈前调用,决定本次打开语境。</summary>
|
||||
public void SetMode(SaveSlotPanelMode mode) => _mode = mode;
|
||||
|
||||
// ── IFocusable ────────────────────────────────────────────────────────
|
||||
public void OnFocusRestored() => StartCoroutine(RestoreFocusNextFrame());
|
||||
/// <summary>默认 / 恢复焦点回到首个存档槽按钮。</summary>
|
||||
protected override GameObject ResolveFirstSelected()
|
||||
=> _defaultFocusButton != null ? _defaultFocusButton.gameObject : null;
|
||||
|
||||
private System.Collections.IEnumerator RestoreFocusNextFrame()
|
||||
{
|
||||
yield return null;
|
||||
if (UnityEngine.EventSystems.EventSystem.current != null && _defaultFocusButton != null)
|
||||
UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(_defaultFocusButton.gameObject);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
// ── 生命周期(UIPanelBase 驱动)──────────────────────────────────────
|
||||
protected override void OnPanelOpen()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
var ct = _cts.Token;
|
||||
var task = RefreshAsync(ct);
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted && !(t.Exception?.InnerException is OperationCanceledException))
|
||||
Debug.LogException(t.Exception?.InnerException ?? t.Exception, this);
|
||||
}, TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
// 面板打开时设置初始焦点(键盘 / 手柄导航入口)
|
||||
StartCoroutine(RestoreFocusNextFrame());
|
||||
RunGuarded(RefreshAsync(_cts.Token));
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
protected override void OnPanelClose()
|
||||
{
|
||||
// 关闭子对话框,避免下次打开残留
|
||||
_confirmDialog?.Close();
|
||||
_modeSelect?.Close();
|
||||
|
||||
// 注意:不再级联 Close 子对话框(旧实现于此强关 confirm/mode,是"ESC 一次关多层"的根因)。
|
||||
// 子对话框生命周期完全交给导航器;若本面板随场景卸载,其 await 由结果面板兜底收口。
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
@@ -119,47 +102,42 @@ namespace BaseGames.UI.Menus
|
||||
}
|
||||
|
||||
// ── 选槽 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>选中指定槽位。行为取决于当前模式与槽位是否有档。由 SaveSlotUI 内部按钮调用。</summary>
|
||||
public void OnSlotSelected(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return;
|
||||
RunGuarded(HandleSlotSelectedAsync(slotIndex));
|
||||
}
|
||||
|
||||
private async Task HandleSlotSelectedAsync(int slotIndex)
|
||||
{
|
||||
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc == null) return;
|
||||
|
||||
bool hasData = svc.HasSave(slotIndex);
|
||||
var ct = _cts?.Token ?? CancellationToken.None;
|
||||
|
||||
if (_mode == SaveSlotPanelMode.Continue)
|
||||
{
|
||||
if (!hasData) return; // 继续模式:空槽不可选
|
||||
_ = ContinueSlotAsync(slotIndex);
|
||||
if (!svc.HasSave(slotIndex)) return; // 继续模式:空槽不可选
|
||||
await ContinueSlotAsync(slotIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 新游戏模式 ──
|
||||
if (hasData)
|
||||
if (svc.HasSave(slotIndex) && _confirmDialog != null)
|
||||
{
|
||||
// 占用槽位:先覆盖确认
|
||||
if (_confirmDialog != null)
|
||||
_confirmDialog.Show("CONFIRM_OVERWRITE_TITLE", "CONFIRM_OVERWRITE_BODY",
|
||||
onConfirm: () => BeginNewGameFlow(slotIndex));
|
||||
else
|
||||
BeginNewGameFlow(slotIndex); // 无对话框时退化为直接建档
|
||||
bool ok = await _confirmDialog.ShowAsync("CONFIRM_OVERWRITE_TITLE", "CONFIRM_OVERWRITE_BODY", ct);
|
||||
if (!ok) return; // 取消覆盖
|
||||
}
|
||||
else
|
||||
{
|
||||
BeginNewGameFlow(slotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>开新档流程:选模式 → 建档。</summary>
|
||||
private void BeginNewGameFlow(int slotIndex)
|
||||
{
|
||||
DifficultyLevel level = DifficultyLevel.Normal;
|
||||
if (_modeSelect != null)
|
||||
_modeSelect.Show(level => _ = StartNewGameAsync(slotIndex, level));
|
||||
else
|
||||
_ = StartNewGameAsync(slotIndex, DifficultyLevel.Normal); // 无模式面板时退化为普通
|
||||
{
|
||||
DifficultyLevel? chosen = await _modeSelect.ShowAsync(ct);
|
||||
if (chosen == null) return; // 取消模式选择
|
||||
level = chosen.Value;
|
||||
}
|
||||
|
||||
await StartNewGameAsync(slotIndex, level);
|
||||
}
|
||||
|
||||
private async Task StartNewGameAsync(int slotIndex, DifficultyLevel level)
|
||||
@@ -190,7 +168,6 @@ namespace BaseGames.UI.Menus
|
||||
}
|
||||
|
||||
// ── 删除(强制确认)────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>请求删除指定槽位。强制经通用确认对话框;无对话框时忽略,绝不静默删除。</summary>
|
||||
public void OnSlotDeleteRequested(int slotIndex)
|
||||
{
|
||||
@@ -201,9 +178,15 @@ namespace BaseGames.UI.Menus
|
||||
Debug.LogWarning("[SaveSlotController] 未配置 ConfirmDialog,删除请求被忽略(防止静默删除)。");
|
||||
return;
|
||||
}
|
||||
RunGuarded(DeleteFlowAsync(slotIndex));
|
||||
}
|
||||
|
||||
_confirmDialog.Show("CONFIRM_DELETE_TITLE", "CONFIRM_DELETE_BODY",
|
||||
onConfirm: () => _ = DeleteAndRefreshAsync(slotIndex));
|
||||
private async Task DeleteFlowAsync(int slotIndex)
|
||||
{
|
||||
var ct = _cts?.Token ?? CancellationToken.None;
|
||||
bool ok = await _confirmDialog.ShowAsync("CONFIRM_DELETE_TITLE", "CONFIRM_DELETE_BODY", ct);
|
||||
if (!ok) return;
|
||||
await DeleteAndRefreshAsync(slotIndex);
|
||||
}
|
||||
|
||||
private async Task DeleteAndRefreshAsync(int slotIndex)
|
||||
@@ -213,5 +196,16 @@ namespace BaseGames.UI.Menus
|
||||
await svc.DeleteSlotAsync(slotIndex);
|
||||
await RefreshAsync(_cts?.Token ?? CancellationToken.None);
|
||||
}
|
||||
|
||||
// ── 工具 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>fire-and-forget Task 的统一异常护栏(吞掉取消,记录其余)。</summary>
|
||||
private void RunGuarded(Task task)
|
||||
{
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted && !(t.Exception?.InnerException is OperationCanceledException))
|
||||
Debug.LogException(t.Exception?.InnerException ?? t.Exception, this);
|
||||
}, TaskScheduler.FromCurrentSynchronizationContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user