摄像机区域的架构改动
This commit is contained in:
147
Assets/_Game/Scripts/UI/Menus/SaveSlotController.cs
Normal file
147
Assets/_Game/Scripts/UI/Menus/SaveSlotController.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// 驱动主菜单存档槽选择面板(新游戏 / 继续 / 删除)。
|
||||
/// </summary>
|
||||
public class SaveSlotController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private SaveSlotUI[] _slotUIs; // 存档槽 UI(数量由 Inspector 决定)
|
||||
[SerializeField] private GameSaveManager _saveManager;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private IntEventChannelSO _onSlotConfirmed; // 携带槽索引,供 GameManager 监听
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
var task = RefreshAsync();
|
||||
// 捕获 async Task 异常,避免 async void 吞掉未处理异常
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Debug.LogException(t.Exception?.InnerException ?? t.Exception, this);
|
||||
}, TaskScheduler.FromCurrentSynchronizationContext());
|
||||
}
|
||||
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
if (_saveManager == null) return;
|
||||
for (int i = 0; i < _slotUIs.Length; i++)
|
||||
{
|
||||
if (_slotUIs[i] == null) continue;
|
||||
var summary = await _saveManager.GetSlotSummaryAsync(i);
|
||||
_slotUIs[i].Refresh(summary);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>选中指定槽位(新局或继续)。由 SaveSlotUI 内部按钮调用。</summary>
|
||||
public void OnSlotSelected(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= _slotUIs.Length || _saveManager == null) return;
|
||||
_ = SelectSlotAsync(slotIndex);
|
||||
}
|
||||
|
||||
private async Task SelectSlotAsync(int slotIndex)
|
||||
{
|
||||
if (_saveManager.SlotExists(slotIndex))
|
||||
await _saveManager.LoadAsync(slotIndex);
|
||||
else
|
||||
_saveManager.CreateSlot(slotIndex);
|
||||
|
||||
_onSlotConfirmed?.Raise(slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>删除指定槽位存档并刷新 UI。由 SaveSlotUI 内部按钮调用。</summary>
|
||||
public void OnSlotDeleteRequested(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= _slotUIs.Length || _saveManager == null) return;
|
||||
_ = DeleteAndRefreshAsync(slotIndex);
|
||||
}
|
||||
|
||||
private async Task DeleteAndRefreshAsync(int slotIndex)
|
||||
{
|
||||
await _saveManager.DeleteSlotAsync(slotIndex);
|
||||
await RefreshAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个存档槽卡片组件,负责显示存档摘要或空槽提示。
|
||||
/// </summary>
|
||||
public class SaveSlotUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text _playtimeText;
|
||||
[SerializeField] private TMP_Text _regionText;
|
||||
[SerializeField] private TMP_Text _lastSavedText;
|
||||
[SerializeField] private Image _formIcon;
|
||||
[SerializeField] private GameObject _emptyIndicator; // 空槽时显示的"新游戏"提示
|
||||
[SerializeField] private GameObject _dataIndicator; // 有数据时显示的内容根
|
||||
[SerializeField] private Button _selectButton;
|
||||
[SerializeField] private Button _deleteButton;
|
||||
|
||||
private int _slotIndex;
|
||||
private SaveSlotController _controller;
|
||||
|
||||
/// <summary>由 SaveSlotController 在 Awake 或初始化时调用以完成按钮绑定。</summary>
|
||||
public void Init(int slotIndex, SaveSlotController controller)
|
||||
{
|
||||
_slotIndex = slotIndex;
|
||||
_controller = controller;
|
||||
|
||||
if (_selectButton != null)
|
||||
{
|
||||
_selectButton.onClick.RemoveAllListeners();
|
||||
_selectButton.onClick.AddListener(() => _controller.OnSlotSelected(_slotIndex));
|
||||
}
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.onClick.RemoveAllListeners();
|
||||
_deleteButton.onClick.AddListener(() => _controller.OnSlotDeleteRequested(_slotIndex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>用摘要数据刷新显示;summary 为 null 表示空槽。</summary>
|
||||
public void Refresh(SlotSummary summary)
|
||||
{
|
||||
bool hasData = summary != null;
|
||||
|
||||
if (_emptyIndicator != null) _emptyIndicator.SetActive(!hasData);
|
||||
if (_dataIndicator != null) _dataIndicator.SetActive(hasData);
|
||||
if (_deleteButton != null) _deleteButton.gameObject.SetActive(hasData);
|
||||
|
||||
if (!hasData) return;
|
||||
|
||||
if (_playtimeText != null) _playtimeText.text = FormatPlaytime(summary.Playtime);
|
||||
if (_regionText != null) _regionText.text = summary.SceneName ?? string.Empty;
|
||||
if (_lastSavedText != null) _lastSavedText.text = FormatDateTime(summary.LastSaved);
|
||||
}
|
||||
|
||||
private static string FormatPlaytime(float seconds)
|
||||
{
|
||||
int h = (int)(seconds / 3600);
|
||||
int m = (int)((seconds % 3600) / 60);
|
||||
int s = (int)(seconds % 60);
|
||||
return $"{h:D2}:{m:D2}:{s:D2}";
|
||||
}
|
||||
|
||||
private static string FormatDateTime(string iso8601)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iso8601)) return string.Empty;
|
||||
if (DateTime.TryParse(iso8601,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.RoundtripKind,
|
||||
out DateTime dt))
|
||||
{
|
||||
return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
return iso8601;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user