阶段性完成
This commit is contained in:
@@ -1,11 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Ichni.Localization;
|
||||
using Ichni.Menu.UI;
|
||||
using Ichni.UI;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Tables;
|
||||
|
||||
namespace Ichni.Story.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 运行时弹窗的一段本地化文本请求。
|
||||
/// <para>请求进入队列时不会立即翻译;<see cref="MessageUIPage"/> 即将显示该弹窗时才读取当前 Locale,
|
||||
/// 因而不会因异步加载导致 FIFO 顺序错乱,也不会把语言逻辑散落到各业务调用方。</para>
|
||||
/// <para>动态参数统一使用 Smart String 的位置参数,例如 <c>{0}</c>、<c>{1}</c>。</para>
|
||||
/// </summary>
|
||||
public readonly struct LocalizedPopupText
|
||||
{
|
||||
public readonly TableReference tableReference;
|
||||
public readonly string entryKey;
|
||||
public readonly object[] arguments;
|
||||
|
||||
public LocalizedPopupText(TableReference tableReference, string entryKey, params object[] arguments)
|
||||
{
|
||||
this.tableReference = tableReference;
|
||||
this.entryKey = entryKey;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
/// <summary>异步解析当前 Locale 的文本;缺失时显示 Key,便于开发阶段定位错误配置。</summary>
|
||||
public Task<string> ResolveAsync() =>
|
||||
LocalizationTextService.ResolveAsync(tableReference, entryKey, entryKey, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SelectionBox 的本地化按钮定义。每个按钮可以独立指定 String Table,
|
||||
/// 因此业务专用操作与 UI 通用操作(如“取消”)可复用各自的词条。
|
||||
/// </summary>
|
||||
public readonly struct LocalizedSelectionBoxOption
|
||||
{
|
||||
public readonly LocalizedPopupText label;
|
||||
public readonly UnityAction action;
|
||||
|
||||
public LocalizedSelectionBoxOption(LocalizedPopupText label, UnityAction action)
|
||||
{
|
||||
this.label = label;
|
||||
this.action = action;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 剧情与菜单共用的弹窗页面。
|
||||
/// MessageBox 与 SelectionBox 共用同一遮罩、容器和显示队列:两者不会重叠,
|
||||
@@ -39,6 +84,10 @@ namespace Ichni.Story.UI
|
||||
public string content;
|
||||
public MessageBox messagePrefab;
|
||||
public List<SelectionBoxOption> selectionOptions;
|
||||
public bool useLocalizedText;
|
||||
public LocalizedPopupText localizedTitle;
|
||||
public LocalizedPopupText localizedContent;
|
||||
public List<LocalizedSelectionBoxOption> localizedSelectionOptions;
|
||||
}
|
||||
|
||||
private readonly Queue<PendingPopup> _popupQueue = new();
|
||||
@@ -69,12 +118,13 @@ namespace Ichni.Story.UI
|
||||
|
||||
/// <summary>
|
||||
/// 将歌曲解锁提示加入弹窗队列。
|
||||
/// <param name="songDisplayName">玩家可见的歌曲名称;调用方不得传入内部 Unlock Key。</param>
|
||||
/// </summary>
|
||||
public void ShowUnlockMessage(string songUnlockKey)
|
||||
public void ShowUnlockMessage(string songDisplayName)
|
||||
{
|
||||
string title = "New Song Unlocked!";
|
||||
string content = $"You have unlocked the song: {songUnlockKey}!";
|
||||
EnqueueMessage(title, content, defaultMessageBoxPrefab);
|
||||
ShowLocalizedMessage(
|
||||
new LocalizedPopupText("Message", "system_unlock_song_title"),
|
||||
new LocalizedPopupText("Message", "system_unlock_song_content", songDisplayName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,6 +135,32 @@ namespace Ichni.Story.UI
|
||||
EnqueueMessage(title, content, defaultMessageBoxPrefab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将本地化 MessageBox 加入队列。文本会在该弹窗真正显示前才按当前 Locale 解析;
|
||||
/// 对于含动态参数的词条,请使用 <see cref="LocalizedPopupText"/> 的位置参数构造函数。
|
||||
/// </summary>
|
||||
public void ShowLocalizedMessage(LocalizedPopupText title, LocalizedPopupText content,
|
||||
MessageBox prefab = null)
|
||||
{
|
||||
MessageBox targetPrefab = prefab ?? defaultMessageBoxPrefab;
|
||||
if (targetPrefab == null)
|
||||
{
|
||||
Debug.LogError("[MessageUIPage] 没有可用的 MessageBox Prefab,无法显示本地化消息。");
|
||||
return;
|
||||
}
|
||||
|
||||
_popupQueue.Enqueue(new PendingPopup
|
||||
{
|
||||
type = PopupType.Message,
|
||||
messagePrefab = targetPrefab,
|
||||
useLocalizedText = true,
|
||||
localizedTitle = title,
|
||||
localizedContent = content
|
||||
});
|
||||
|
||||
BeginQueueIfNeeded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspector 专用的运行时调试入口。
|
||||
/// 通过正常队列创建一条基础测试文本,因此可以同时验证新 Prefab 的布局、
|
||||
@@ -162,6 +238,40 @@ namespace Ichni.Story.UI
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将本地化 SelectionBox 加入队列。每个选项的标签单独保存本地化引用,
|
||||
/// 因此“进入教程”“跳过教程”等业务按钮不会被误替换为通用的“确定/取消”。
|
||||
/// </summary>
|
||||
public bool ShowLocalizedSelectionBox(
|
||||
LocalizedPopupText title,
|
||||
LocalizedPopupText content,
|
||||
IReadOnlyList<LocalizedSelectionBoxOption> 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,
|
||||
useLocalizedText = true,
|
||||
localizedTitle = title,
|
||||
localizedContent = content,
|
||||
localizedSelectionOptions = new List<LocalizedSelectionBoxOption>(options)
|
||||
});
|
||||
|
||||
BeginQueueIfNeeded();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将指定消息入队。如果是空闲状态,则唤醒大页面并开始处理队列。
|
||||
/// </summary>
|
||||
@@ -202,7 +312,7 @@ namespace Ichni.Story.UI
|
||||
/// <summary>
|
||||
/// 从队列中取出一个并实例化显示。
|
||||
/// </summary>
|
||||
private void ShowNextInQueue()
|
||||
private async void ShowNextInQueue()
|
||||
{
|
||||
if (_popupQueue.Count == 0)
|
||||
{
|
||||
@@ -215,6 +325,11 @@ namespace Ichni.Story.UI
|
||||
}
|
||||
|
||||
PendingPopup popup = _popupQueue.Dequeue();
|
||||
if (popup.useLocalizedText)
|
||||
{
|
||||
popup = await ResolveLocalizedPopupAsync(popup);
|
||||
}
|
||||
|
||||
if (popup.type == PopupType.Selection)
|
||||
{
|
||||
ShowSelectionPopup(popup);
|
||||
@@ -224,6 +339,30 @@ namespace Ichni.Story.UI
|
||||
ShowMessagePopup(popup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 Popup 自己的请求依次解析标题、正文与按钮。该方法只由队列消费端调用,
|
||||
/// 所以即使 String Table 仍在异步加载,也不会打乱此前入队的弹窗顺序。
|
||||
/// </summary>
|
||||
private static async Task<PendingPopup> ResolveLocalizedPopupAsync(PendingPopup popup)
|
||||
{
|
||||
popup.title = await popup.localizedTitle.ResolveAsync();
|
||||
popup.content = await popup.localizedContent.ResolveAsync();
|
||||
|
||||
if (popup.type != PopupType.Selection || popup.localizedSelectionOptions == null)
|
||||
{
|
||||
return popup;
|
||||
}
|
||||
|
||||
popup.selectionOptions = new List<SelectionBoxOption>(popup.localizedSelectionOptions.Count);
|
||||
foreach (LocalizedSelectionBoxOption localizedOption in popup.localizedSelectionOptions)
|
||||
{
|
||||
string label = await localizedOption.label.ResolveAsync();
|
||||
popup.selectionOptions.Add(new SelectionBoxOption(label, localizedOption.action));
|
||||
}
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
private void ShowMessagePopup(PendingPopup popup)
|
||||
{
|
||||
if (popup.messagePrefab == null)
|
||||
|
||||
Reference in New Issue
Block a user