设置扩展

This commit is contained in:
SoulliesOfficial
2026-07-19 16:44:58 -04:00
parent dda354ebb9
commit 9fd5f098ab
110 changed files with 8386 additions and 2311 deletions

View File

@@ -3,7 +3,6 @@ using System.Collections;
using System.Collections.Generic;
using AK.Wwise;
using Ichni.Menu;
using Ichni.Menu.UI;
using Ichni.Story;
using Ichni.Story.UI;
using Ichni.UI;
@@ -24,7 +23,6 @@ namespace Ichni
public LoginPage loginPage;
public ChapterSelectionUIPage chapterSelectionUIPage;
public StoryUIPage storyUIPage;
public SelectionBoxUIPage selectionBoxUIPage;
// dialogUIPage 已随旧对话系统移除;阶段 3 将接入新的 VN 对话页面引用
public SongSelectionUIPage songSelectionUIPage;
public TransitionUIPage transitionUIPage;
@@ -88,7 +86,7 @@ namespace Ichni
// 返回目标是一次性运行态信息。无论是否能恢复页面,都不能让它在下次重进菜单时重复生效。
information?.ClearMenuReturnDestination();
Application.targetFrameRate = SettingsManager.instance.gameSettings.targetFrame;
Application.targetFrameRate = SettingsManager.instance.settingsSaveData.targetFrame;
asyncOperation = SceneManager.LoadSceneAsync("GameScene");
asyncOperation.allowSceneActivation = false;
}

View File

@@ -1,13 +1,15 @@
using System.Collections.Generic;
using Ichni.Menu.UI;
using Ichni.UI;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Ichni.Story.UI
{
/// <summary>
/// 用于在剧情系统中管理并顺序展示 MessageBox 的独立 UI 页面。
/// 支持动态实例化给定的 MessageBox Prefab并在所有消息展示完毕后进行大页面的 FadeOut。
/// 剧情与菜单共用的弹窗页面。
/// MessageBox 与 SelectionBox 共用同一遮罩、容器和显示队列:两者不会重叠,
/// 后续系统只需向本页提交“消息”或“选项”请求,无需再创建平行的 UIPage。
/// </summary>
public class MessageUIPage : UIPageBase
{
@@ -20,16 +22,28 @@ namespace Ichni.Story.UI
[Tooltip("生成的 MessageBox 挂载在哪?如果不填,默认挂载在自己身上")]
public Transform messageContainer;
// 用于排队的内部消息数据结构
private struct PendingMessage
[Tooltip("通用多选弹窗预制体。TutorialBlock、奖励选择等系统都通过本页动态生成它。")]
public SelectionBox defaultSelectionBoxPrefab;
private enum PopupType
{
public string title;
public string content;
public MessageBox customPrefab; // 支持未来传入不同种类的 Prefab
Message,
Selection
}
private Queue<PendingMessage> _messageQueue = new Queue<PendingMessage>();
private MessageBox _currentActiveBox;
// 用于统一排队的内部弹窗数据结构。
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()
@@ -71,6 +85,83 @@ namespace Ichni.Story.UI
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>
@@ -82,22 +173,30 @@ namespace Ichni.Story.UI
return;
}
_messageQueue.Enqueue(new PendingMessage
_popupQueue.Enqueue(new PendingPopup
{
type = PopupType.Message,
title = title,
content = content,
customPrefab = prefab
messagePrefab = prefab
});
// 如果当前页面没有在显示中,则开始整体流程
if (!_isPageActive)
{
_isPageActive = true;
// 唤醒大页面(执行 UIPageBase 的渐入并拦截底层射线)
this.FadeIn(0.5f, false, ShowNextInQueue);
}
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>
@@ -105,40 +204,85 @@ namespace Ichni.Story.UI
/// </summary>
private void ShowNextInQueue()
{
if (_messageQueue.Count == 0)
if (_popupQueue.Count == 0)
{
// 队列处理完毕,大页面整体退场
_currentActiveBox = null;
// 队列处理完毕,大页面整体退场
_currentMessageBox = null;
_currentSelectionBox = null;
_isPageActive = false;
this.FadeOut();
FadeOut(0f);
return;
}
PendingMessage msg = _messageQueue.Dequeue();
// 实例化预制体
_currentActiveBox = Instantiate(msg.customPrefab, messageContainer);
// 订阅“当这个消息框彻底结束关闭时”的事件
_currentActiveBox.onAllMessagesClosed.AddListener(() =>
PendingPopup popup = _popupQueue.Dequeue();
if (popup.type == PopupType.Selection)
{
// 销毁旧实例
if (_currentActiveBox != null)
{
Destroy(_currentActiveBox.gameObject);
}
// 检查并显示下一个
ShowNextInQueue();
});
ShowSelectionPopup(popup);
return;
}
// 配置并启动 MessageBox
_currentActiveBox.Clear();
_currentActiveBox.AddInfo(msg.title, msg.content, null);
// MessageBox.SetUp() 内部会激活自己并执行自己的 FadeIn
_currentActiveBox.gameObject.SetActive(true);
_currentActiveBox.SetUp();
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();
}
}
}