Files
ichni_Official/Assets/Scripts/Menu/MessagePage/MessageUIPage.cs
SoulliesOfficial 9fd5f098ab 设置扩展
2026-07-19 16:44:58 -04:00

289 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using Ichni.Menu.UI;
using Ichni.UI;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Ichni.Story.UI
{
/// <summary>
/// 剧情与菜单共用的弹窗页面。
/// MessageBox 与 SelectionBox 共用同一遮罩、容器和显示队列:两者不会重叠,
/// 后续系统只需向本页提交“消息”或“选项”请求,无需再创建平行的 UIPage。
/// </summary>
public class MessageUIPage : UIPageBase
{
public static MessageUIPage instance;
[Header("Prefabs & Containers")]
[Tooltip("默认使用的 MessageBox 预制体(请从 Project 中拖拽,而非场景中的实例)")]
public MessageBox defaultMessageBoxPrefab;
[Tooltip("生成的 MessageBox 挂载在哪?如果不填,默认挂载在自己身上")]
public Transform messageContainer;
[Tooltip("通用多选弹窗预制体。TutorialBlock、奖励选择等系统都通过本页动态生成它。")]
public SelectionBox defaultSelectionBoxPrefab;
private enum PopupType
{
Message,
Selection
}
// 用于统一排队的内部弹窗数据结构。
private struct PendingPopup
{
public PopupType type;
public string title;
public string content;
public MessageBox messagePrefab;
public List<SelectionBoxOption> selectionOptions;
}
private readonly Queue<PendingPopup> _popupQueue = new();
private MessageBox _currentMessageBox;
private SelectionBox _currentSelectionBox;
private bool _isPageActive = false;
protected override void Awake()
{
base.Awake();
if (instance == null)
{
instance = this;
}
else
{
Debug.LogWarning("[StoryMessageBoxUIPage] 场景中存在多个实例,正在销毁多余的实例。");
Destroy(gameObject);
return;
}
if (messageContainer == null)
{
messageContainer = this.transform;
}
}
/// <summary>
/// 将歌曲解锁提示加入弹窗队列。
/// </summary>
public void ShowUnlockMessage(string songUnlockKey)
{
string title = "New Song Unlocked!";
string content = $"You have unlocked the song: {songUnlockKey}!";
EnqueueMessage(title, content, defaultMessageBoxPrefab);
}
/// <summary>
/// 将自定义文本提示加入弹窗队列。
/// </summary>
public void ShowCustomMessage(string title, string content)
{
EnqueueMessage(title, content, defaultMessageBoxPrefab);
}
/// <summary>
/// Inspector 专用的运行时调试入口。
/// 通过正常队列创建一条基础测试文本,因此可以同时验证新 Prefab 的布局、
/// 入退场动画、输入拦截与队列收尾,而不会绕过正式流程或污染剧情数据。
/// </summary>
[Button("调试:创建空 MessageBox", ButtonSizes.Medium)]
private void DebugCreateEmptyMessageBox()
{
if (!Application.isPlaying)
{
Debug.LogWarning("[MessageUIPage] 空 MessageBox 调试按钮只能在 Play Mode 中使用。", this);
return;
}
ShowCustomMessage(
"MessageBox Debug",
"这是一条用于检查基础布局、文本换行与 Rail 动效的测试文本。\n\n" +
"This is a basic MessageBox test message.");
}
/// <summary>
/// Inspector 专用的运行时调试入口。
/// 通过正常队列创建一条基础测试文本,因此可以同时验证新 Prefab 的布局、
/// 入退场动画、输入拦截与队列收尾,而不会绕过正式流程或污染剧情数据。
/// </summary>
[Button("调试:创建空 SelectionBox", ButtonSizes.Medium)]
private void DebugCreateEmptySelectionBox()
{
if (!Application.isPlaying)
{
Debug.LogWarning("[MessageUIPage] 空 SelectionBox 调试按钮只能在 Play Mode 中使用。", this);
return;
}
ShowSelectionBox(
"SelectionBox Debug",
"这是一条用于检查基础布局、文本换行与 Rail 动效的测试文本。\n\n" +
"This is a basic SelectionBox test message.",
new List<SelectionBoxOption>
{
new SelectionBoxOption("Option 1", () => Debug.Log("Option 1 selected")),
new SelectionBoxOption("Option 2", () => Debug.Log("Option 2 selected")),
new SelectionBoxOption("Option 3", () => Debug.Log("Option 3 selected"))
});
}
/// <summary>
/// 将一组运行时选项加入通用弹窗队列。
/// 若 MessageBox 或其它 SelectionBox 正在显示,选项会按提交顺序等待,而不会叠在同一遮罩上。
/// </summary>
public bool ShowSelectionBox(string title, string content, IReadOnlyList<SelectionBoxOption> options)
{
if (defaultSelectionBoxPrefab == null)
{
Debug.LogError("[MessageUIPage] 未配置 defaultSelectionBoxPrefab无法显示选项框。");
return false;
}
if (options == null || options.Count == 0)
{
Debug.LogWarning("[MessageUIPage] 未提供任何 SelectionBoxOption忽略空选项框。");
return false;
}
_popupQueue.Enqueue(new PendingPopup
{
type = PopupType.Selection,
title = title,
content = content,
// 复制调用方传入的列表,防止调用方随后改动集合而改变已排队的玩家选择。
selectionOptions = new List<SelectionBoxOption>(options)
});
BeginQueueIfNeeded();
return true;
}
/// <summary>
/// 将指定消息入队。如果是空闲状态,则唤醒大页面并开始处理队列。
/// </summary>
private void EnqueueMessage(string title, string content, MessageBox prefab)
{
if (prefab == null)
{
Debug.LogError("[StoryMessageBoxUIPage] 没有可用的 MessageBox 预制体!");
return;
}
_popupQueue.Enqueue(new PendingPopup
{
type = PopupType.Message,
title = title,
content = content,
messagePrefab = prefab
});
BeginQueueIfNeeded();
}
/// <summary>
/// 若当前没有弹窗,激活共享遮罩后开始依次显示队列。遮罩从出现瞬间拦截输入,
/// 避免 UIPageBase 默认淡入结束前的短暂底层点击窗口。
/// </summary>
private void BeginQueueIfNeeded()
{
if (_isPageActive) return;
_isPageActive = true;
mainCanvasGroup.gameObject.SetActive(true);
mainCanvasGroup.interactable = true;
mainCanvasGroup.blocksRaycasts = true;
FadeIn(0f, false, ShowNextInQueue);
}
/// <summary>
/// 从队列中取出一个并实例化显示。
/// </summary>
private void ShowNextInQueue()
{
if (_popupQueue.Count == 0)
{
// 队列处理完毕,大页面整体退场。
_currentMessageBox = null;
_currentSelectionBox = null;
_isPageActive = false;
FadeOut(0f);
return;
}
PendingPopup popup = _popupQueue.Dequeue();
if (popup.type == PopupType.Selection)
{
ShowSelectionPopup(popup);
return;
}
ShowMessagePopup(popup);
}
private void ShowMessagePopup(PendingPopup popup)
{
if (popup.messagePrefab == null)
{
Debug.LogError("[MessageUIPage] 队列中的 MessageBox Prefab 已丢失,跳过该消息。");
ShowNextInQueue();
return;
}
MessageBox messageBox = Instantiate(popup.messagePrefab, messageContainer);
_currentMessageBox = messageBox;
// MessageBox 只显示一条消息;队列顺序始终由本页统一管理。
messageBox.Closed += HandleMessageBoxClosed;
messageBox.gameObject.SetActive(true);
messageBox.SetUp(popup.title, popup.content);
}
private void HandleMessageBoxClosed(MessageBox messageBox)
{
messageBox.Closed -= HandleMessageBoxClosed;
Destroy(messageBox.gameObject);
if (_currentMessageBox == messageBox)
_currentMessageBox = null;
ShowNextInQueue();
}
private void ShowSelectionPopup(PendingPopup popup)
{
if (defaultSelectionBoxPrefab == null)
{
Debug.LogError("[MessageUIPage] 队列中的 SelectionBox Prefab 已丢失,跳过该选项框。");
ShowNextInQueue();
return;
}
SelectionBox selectionBox = Instantiate(defaultSelectionBoxPrefab, messageContainer);
_currentSelectionBox = selectionBox;
selectionBox.Closed += HandleSelectionBoxClosed;
if (!selectionBox.SetUp(popup.title, popup.content, popup.selectionOptions))
{
selectionBox.Closed -= HandleSelectionBoxClosed;
Destroy(selectionBox.gameObject);
_currentSelectionBox = null;
ShowNextInQueue();
}
}
private void HandleSelectionBoxClosed(SelectionBox selectionBox)
{
if (selectionBox != _currentSelectionBox) return;
selectionBox.Closed -= HandleSelectionBoxClosed;
Destroy(selectionBox.gameObject);
_currentSelectionBox = null;
ShowNextInQueue();
}
}
}