存档重构,游戏内容解锁机制;教程完善(未完成)
This commit is contained in:
114
Assets/Scripts/UI/Elements/SelectionBox.cs
Normal file
114
Assets/Scripts/UI/Elements/SelectionBox.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Ichni.Menu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 可复用的多选弹窗元素。调用方提供标题、内容与任意数量的 <see cref="SelectionBoxOption"/>,
|
||||
/// 本类动态实例化对应按钮,并确保一次显示最多只会执行一个选项回调。
|
||||
/// </summary>
|
||||
public class SelectionBox : MonoBehaviour
|
||||
{
|
||||
[Header("Selection Box")]
|
||||
public CanvasGroup canvasGroup;
|
||||
public TMP_Text titleText;
|
||||
public TMP_Text contentText;
|
||||
public RectTransform optionContainer;
|
||||
public SelectionBoxButton optionButtonPrefab;
|
||||
|
||||
private readonly List<SelectionBoxButton> _generatedButtons = new();
|
||||
private bool _isResolved;
|
||||
|
||||
/// <summary>
|
||||
/// 选项被选择并且自身淡出结束后触发。页面容器使用它销毁本次生成的 SelectionBox。
|
||||
/// </summary>
|
||||
public event Action<SelectionBox> Closed;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
canvasGroup ??= GetComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据运行时选项初始化弹窗。至少需要一个选项;失败时返回 false,且不会显示半配置的弹窗。
|
||||
/// </summary>
|
||||
public bool SetUp(string title, string content, IReadOnlyList<SelectionBoxOption> options)
|
||||
{
|
||||
if (canvasGroup == null || optionContainer == null || optionButtonPrefab == null ||
|
||||
options == null || options.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[SelectionBox] 缺少 CanvasGroup、选项容器、选项按钮预制体,或未提供任何选项。");
|
||||
return false;
|
||||
}
|
||||
|
||||
ClearGeneratedButtons();
|
||||
_isResolved = false;
|
||||
|
||||
if (titleText != null) titleText.text = title ?? string.Empty;
|
||||
if (contentText != null) contentText.text = content ?? string.Empty;
|
||||
|
||||
foreach (SelectionBoxOption option in options)
|
||||
{
|
||||
SelectionBoxButton button = Instantiate(optionButtonPrefab, optionContainer);
|
||||
_generatedButtons.Add(button);
|
||||
button.SetUp(option.label, () => Select(option.action));
|
||||
}
|
||||
|
||||
gameObject.SetActive(true);
|
||||
canvasGroup.alpha = 0f;
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
canvasGroup.DOFade(1f, 0.2f).Play();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Select(UnityAction action)
|
||||
{
|
||||
if (_isResolved) return;
|
||||
_isResolved = true;
|
||||
|
||||
// 先禁用所有输入,再执行回调。回调可以触发存档、场景切换或打开下一层 UI,
|
||||
// 因而不能让同一帧内的其它按钮再次触发。
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
foreach (SelectionBoxButton button in _generatedButtons)
|
||||
{
|
||||
if (button?.button != null)
|
||||
button.button.interactable = false;
|
||||
}
|
||||
|
||||
action?.Invoke();
|
||||
canvasGroup.DOFade(0f, 0.2f).OnComplete(() => Closed?.Invoke(this)).Play();
|
||||
}
|
||||
|
||||
private void ClearGeneratedButtons()
|
||||
{
|
||||
foreach (SelectionBoxButton button in _generatedButtons)
|
||||
{
|
||||
if (button != null)
|
||||
Destroy(button.gameObject);
|
||||
}
|
||||
|
||||
_generatedButtons.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SelectionBox 的单个运行时选项。它不序列化到场景或存档;显示时由调用方临时创建。
|
||||
/// </summary>
|
||||
public readonly struct SelectionBoxOption
|
||||
{
|
||||
public readonly string label;
|
||||
public readonly UnityAction action;
|
||||
|
||||
public SelectionBoxOption(string label, UnityAction action)
|
||||
{
|
||||
this.label = label;
|
||||
this.action = action;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/Elements/SelectionBox.cs.meta
Normal file
2
Assets/Scripts/UI/Elements/SelectionBox.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ac6b73d890a4a7aaed8e2fa64001764
|
||||
40
Assets/Scripts/UI/Elements/SelectionBoxButton.cs
Normal file
40
Assets/Scripts/UI/Elements/SelectionBoxButton.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Menu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// SelectionBox 动态生成的单个选项按钮。
|
||||
/// 预制体只需提供 Button 和 TMP 文本;运行时由 <see cref="SelectionBox"/> 写入显示文字与点击回调。
|
||||
/// </summary>
|
||||
public class SelectionBoxButton : MonoBehaviour
|
||||
{
|
||||
public Button button;
|
||||
public TMP_Text labelText;
|
||||
|
||||
/// <summary>
|
||||
/// 重新绑定按钮文字与本次 SelectionBox 的选择回调。
|
||||
/// 每次生成时都会清空旧监听,防止对象复用或预制体事件造成重复触发。
|
||||
/// </summary>
|
||||
public void SetUp(string label, UnityAction onSelected)
|
||||
{
|
||||
button ??= GetComponent<Button>();
|
||||
labelText ??= GetComponentInChildren<TMP_Text>(true);
|
||||
|
||||
if (labelText != null)
|
||||
labelText.text = label ?? string.Empty;
|
||||
|
||||
if (button == null)
|
||||
{
|
||||
Debug.LogWarning("[SelectionBoxButton] 选项预制体缺少 Button 组件。");
|
||||
return;
|
||||
}
|
||||
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => onSelected?.Invoke());
|
||||
button.interactable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/Elements/SelectionBoxButton.cs.meta
Normal file
2
Assets/Scripts/UI/Elements/SelectionBoxButton.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2c45ae5390d4a4aa16e62b5704dbf0a
|
||||
73
Assets/Scripts/UI/Elements/SelectionBoxUIPage.cs
Normal file
73
Assets/Scripts/UI/Elements/SelectionBoxUIPage.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using Ichni.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Menu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用 SelectionBox 的页面宿主,职责等同于 MessageBox 的页面容器:
|
||||
/// 显示时动态生成一个 SelectionBox,选择完成后统一淡出并销毁本次实例。
|
||||
/// </summary>
|
||||
public class SelectionBoxUIPage : UIPageBase
|
||||
{
|
||||
[Header("Selection Box Page")]
|
||||
public SelectionBox selectionBoxPrefab;
|
||||
public RectTransform selectionBoxContainer;
|
||||
|
||||
private SelectionBox _currentSelectionBox;
|
||||
private bool _isShowing;
|
||||
|
||||
/// <summary>当前是否已有一个 SelectionBox 等待玩家选择。</summary>
|
||||
public bool IsShowing => _isShowing;
|
||||
|
||||
/// <summary>
|
||||
/// 生成并显示一组运行时选项。页面显示期间拒绝重复请求,避免两个弹窗争夺输入。
|
||||
/// </summary>
|
||||
public bool Show(string title, string content, IReadOnlyList<SelectionBoxOption> options)
|
||||
{
|
||||
if (_isShowing)
|
||||
{
|
||||
Debug.LogWarning("[SelectionBoxUIPage] 已有 SelectionBox 显示中,忽略重复请求。");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mainCanvasGroup == null || selectionBoxPrefab == null || selectionBoxContainer == null)
|
||||
{
|
||||
Debug.LogWarning("[SelectionBoxUIPage] 缺少 CanvasGroup、SelectionBox Prefab 或容器,无法显示选项框。");
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentSelectionBox = Instantiate(selectionBoxPrefab, selectionBoxContainer);
|
||||
_currentSelectionBox.Closed += HandleSelectionBoxClosed;
|
||||
if (!_currentSelectionBox.SetUp(title, content, options))
|
||||
{
|
||||
Destroy(_currentSelectionBox.gameObject);
|
||||
_currentSelectionBox = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_isShowing = true;
|
||||
|
||||
// UIPageBase 默认淡入完成后才开启射线;选择框从出现瞬间就必须阻断底层输入。
|
||||
mainCanvasGroup.gameObject.SetActive(true);
|
||||
mainCanvasGroup.interactable = true;
|
||||
mainCanvasGroup.blocksRaycasts = true;
|
||||
FadeIn();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleSelectionBoxClosed(SelectionBox selectionBox)
|
||||
{
|
||||
if (selectionBox != _currentSelectionBox) return;
|
||||
|
||||
FadeOut(0.2f, false, () =>
|
||||
{
|
||||
if (_currentSelectionBox != null)
|
||||
Destroy(_currentSelectionBox.gameObject);
|
||||
|
||||
_currentSelectionBox = null;
|
||||
_isShowing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/Elements/SelectionBoxUIPage.cs.meta
Normal file
2
Assets/Scripts/UI/Elements/SelectionBoxUIPage.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8b607f8c5654e1eb3bf1f7ca33fd3a4
|
||||
Reference in New Issue
Block a user