2026-07-07 01:28:27 -04:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Ichni.Menu.UI;
|
|
|
|
|
|
using Ichni.UI;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
using Sirenix.OdinInspector;
|
2026-07-07 01:28:27 -04:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Story.UI
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2026-07-19 16:44:58 -04:00
|
|
|
|
/// 剧情与菜单共用的弹窗页面。
|
|
|
|
|
|
/// MessageBox 与 SelectionBox 共用同一遮罩、容器和显示队列:两者不会重叠,
|
|
|
|
|
|
/// 后续系统只需向本页提交“消息”或“选项”请求,无需再创建平行的 UIPage。
|
2026-07-07 01:28:27 -04:00
|
|
|
|
/// </summary>
|
2026-07-18 16:51:18 -04:00
|
|
|
|
public class MessageUIPage : UIPageBase
|
2026-07-07 01:28:27 -04:00
|
|
|
|
{
|
2026-07-18 16:51:18 -04:00
|
|
|
|
public static MessageUIPage instance;
|
2026-07-07 01:28:27 -04:00
|
|
|
|
|
|
|
|
|
|
[Header("Prefabs & Containers")]
|
|
|
|
|
|
[Tooltip("默认使用的 MessageBox 预制体(请从 Project 中拖拽,而非场景中的实例)")]
|
|
|
|
|
|
public MessageBox defaultMessageBoxPrefab;
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("生成的 MessageBox 挂载在哪?如果不填,默认挂载在自己身上")]
|
|
|
|
|
|
public Transform messageContainer;
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
[Tooltip("通用多选弹窗预制体。TutorialBlock、奖励选择等系统都通过本页动态生成它。")]
|
|
|
|
|
|
public SelectionBox defaultSelectionBoxPrefab;
|
|
|
|
|
|
|
|
|
|
|
|
private enum PopupType
|
2026-07-07 01:28:27 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
Message,
|
|
|
|
|
|
Selection
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用于统一排队的内部弹窗数据结构。
|
|
|
|
|
|
private struct PendingPopup
|
|
|
|
|
|
{
|
|
|
|
|
|
public PopupType type;
|
2026-07-07 01:28:27 -04:00
|
|
|
|
public string title;
|
|
|
|
|
|
public string content;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
public MessageBox messagePrefab;
|
|
|
|
|
|
public List<SelectionBoxOption> selectionOptions;
|
2026-07-07 01:28:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
private readonly Queue<PendingPopup> _popupQueue = new();
|
|
|
|
|
|
private MessageBox _currentMessageBox;
|
|
|
|
|
|
private SelectionBox _currentSelectionBox;
|
2026-07-07 01:28:27 -04:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 01:28:27 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 将指定消息入队。如果是空闲状态,则唤醒大页面并开始处理队列。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void EnqueueMessage(string title, string content, MessageBox prefab)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (prefab == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("[StoryMessageBoxUIPage] 没有可用的 MessageBox 预制体!");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
_popupQueue.Enqueue(new PendingPopup
|
2026-07-07 01:28:27 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
type = PopupType.Message,
|
2026-07-07 01:28:27 -04:00
|
|
|
|
title = title,
|
|
|
|
|
|
content = content,
|
2026-07-19 16:44:58 -04:00
|
|
|
|
messagePrefab = prefab
|
2026-07-07 01:28:27 -04:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
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);
|
2026-07-07 01:28:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从队列中取出一个并实例化显示。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void ShowNextInQueue()
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
if (_popupQueue.Count == 0)
|
2026-07-07 01:28:27 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
// 队列处理完毕,大页面整体退场。
|
|
|
|
|
|
_currentMessageBox = null;
|
|
|
|
|
|
_currentSelectionBox = null;
|
2026-07-07 01:28:27 -04:00
|
|
|
|
_isPageActive = false;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
FadeOut(0f);
|
2026-07-07 01:28:27 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
PendingPopup popup = _popupQueue.Dequeue();
|
|
|
|
|
|
if (popup.type == PopupType.Selection)
|
|
|
|
|
|
{
|
|
|
|
|
|
ShowSelectionPopup(popup);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-07 01:28:27 -04:00
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
ShowMessagePopup(popup);
|
|
|
|
|
|
}
|
2026-07-07 01:28:27 -04:00
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
private void ShowMessagePopup(PendingPopup popup)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (popup.messagePrefab == null)
|
2026-07-07 01:28:27 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
Debug.LogError("[MessageUIPage] 队列中的 MessageBox Prefab 已丢失,跳过该消息。");
|
2026-07-07 01:28:27 -04:00
|
|
|
|
ShowNextInQueue();
|
2026-07-19 16:44:58 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-07 01:28:27 -04:00
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
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();
|
2026-07-07 01:28:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|