阶段性完成
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using I2.Loc;
|
||||
using UnityEngine;
|
||||
using Ichni.Localization;
|
||||
using Ichni.UI;
|
||||
using Michsky.MUIP;
|
||||
using Sirenix.OdinInspector;
|
||||
@@ -23,6 +23,10 @@ namespace Ichni.RhythmGame
|
||||
public Image characterImage;
|
||||
public TMP_Text characterNameText;
|
||||
public TMP_Text sentenceText;
|
||||
[Tooltip("角色名的 Unity Localization 文本。应配置 UI/ui_summary_soullies_name。")]
|
||||
public LocalizedTMPText characterNameLocalizedText;
|
||||
[Tooltip("角色短句的 Unity Localization 文本。应配置 UI/ui_summary_soullies_sentence_0。")]
|
||||
public LocalizedTMPText sentenceLocalizedText;
|
||||
|
||||
[Title("Difficulty")]
|
||||
public TMP_Text difficultyText;
|
||||
@@ -61,8 +65,12 @@ namespace Ichni.RhythmGame
|
||||
|
||||
illustrationImage.sprite = info.song.illustration;
|
||||
|
||||
characterNameText.GetComponent<Localize>().SetTerm("Characters/Soullies");
|
||||
sentenceText.GetComponent<Localize>().SetTerm("Sentences/Soullies_Summary_Sentence_0");
|
||||
// Summary 页面没有再依赖 I2 Term。迁移期间允许旧场景先不绑定新字段:
|
||||
// 此时不会中断结算页面;完成 Inspector 配置后,两个 LocalizedTMPText 会自动跟随 Locale 切换。
|
||||
characterNameLocalizedText ??= characterNameText.GetComponent<LocalizedTMPText>();
|
||||
sentenceLocalizedText ??= sentenceText.GetComponent<LocalizedTMPText>();
|
||||
characterNameLocalizedText?.RefreshText();
|
||||
sentenceLocalizedText?.RefreshText();
|
||||
|
||||
difficultyText.text = info.difficulty.GetDifficultyName();
|
||||
levelText.text = info.difficulty.difficultyValue.ToString();
|
||||
@@ -89,4 +97,4 @@ namespace Ichni.RhythmGame
|
||||
backButton.onClick.AddListener(GameManager.ReturnToMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
208
Assets/Scripts/Localization/LocalizedTMPText.cs
Normal file
208
Assets/Scripts/Localization/LocalizedTMPText.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.Localization;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.Localization.Tables;
|
||||
#endif
|
||||
|
||||
namespace Ichni.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// 将静态 <see cref="TMP_Text"/> 与 Unity Localization 的
|
||||
/// <see cref="LocalizedString"/> 绑定。
|
||||
/// <para>
|
||||
/// 组件启用时订阅 <see cref="LocalizedString.StringChanged"/>。因此切换 Locale,
|
||||
/// 或在 Play Mode 中修改 Inspector 里的 String Reference 后,TMP 文本会自动更新。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Inspector 自带的 String Reference 可预览各 Locale 的文本。Edit Mode 中变更引用时,
|
||||
/// 本组件只将项目默认 Locale 的文本同步到 TMP_Text,便于直接预览;不保存回退文本,
|
||||
/// 也不提供额外的编辑器自检功能。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(TMP_Text))]
|
||||
[AddComponentMenu("Localization/Localized TMP Text")]
|
||||
public sealed class LocalizedTMPText : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("要更新的 TMP 文本。留空时会自动使用当前 GameObject 上的 TMP_Text。")]
|
||||
private TMP_Text targetText;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("在 Inspector 中选择 String Table Collection 和对应的 Entry。")]
|
||||
private LocalizedString stringReference = new LocalizedString();
|
||||
|
||||
private bool _isListening;
|
||||
|
||||
/// <summary>当前绑定的 TMP 文本。</summary>
|
||||
public TMP_Text TargetText => targetText;
|
||||
|
||||
/// <summary>当前使用的 Unity Localization 字符串引用。</summary>
|
||||
public LocalizedString StringReference => stringReference;
|
||||
|
||||
/// <summary>
|
||||
/// 在运行时替换当前文本使用的 String Reference。
|
||||
/// <para>
|
||||
/// 该接口主要供动态生成的 UI 使用,例如 Dropdown 的选项 Item。替换前会先解除旧引用的
|
||||
/// <see cref="LocalizedString.StringChanged"/> 订阅,再为新引用建立订阅,因此不会残留
|
||||
/// 旧语言表的回调,也不会重复监听。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void SetStringReference(LocalizedString reference)
|
||||
{
|
||||
StopListening();
|
||||
stringReference = reference ?? new LocalizedString();
|
||||
EnsureTargetText();
|
||||
|
||||
if (isActiveAndEnabled)
|
||||
{
|
||||
StartListening();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
ApplyEditorPreviewText();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
EnsureTargetText();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
EnsureTargetText();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
// Play Mode 中通过 Inspector 更换 Table 或 Entry 后立即刷新。
|
||||
if (_isListening)
|
||||
{
|
||||
stringReference?.RefreshString();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
ApplyEditorPreviewText();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EnsureTargetText();
|
||||
StartListening();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
StopListening();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopListening();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主动请求重新解析当前 String Reference。
|
||||
/// 通常仅在运行时修改 Smart String 参数后调用;普通语言切换会自动刷新。
|
||||
/// </summary>
|
||||
public void RefreshText()
|
||||
{
|
||||
if (_isListening)
|
||||
{
|
||||
stringReference.RefreshString();
|
||||
}
|
||||
}
|
||||
|
||||
private void StopListening()
|
||||
{
|
||||
if (!_isListening || stringReference == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stringReference.StringChanged -= ApplyText;
|
||||
_isListening = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前 String Reference 建立语言变化监听。
|
||||
/// Unity Localization 会在首次订阅时请求当前 Locale 的文本;这里不使用
|
||||
/// <c>WaitForCompletion</c>,以避免阻塞主线程或干扰 Addressables 队列。
|
||||
/// </summary>
|
||||
private void StartListening()
|
||||
{
|
||||
if (_isListening || stringReference == null || stringReference.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stringReference.StringChanged += ApplyText;
|
||||
_isListening = true;
|
||||
}
|
||||
|
||||
private void EnsureTargetText()
|
||||
{
|
||||
if (targetText == null)
|
||||
{
|
||||
targetText = GetComponent<TMP_Text>();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyText(string localizedText)
|
||||
{
|
||||
EnsureTargetText();
|
||||
if (targetText != null)
|
||||
{
|
||||
targetText.text = localizedText ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// 在编辑器中将项目默认 Locale 的已导入文本同步到 TMP_Text。
|
||||
/// 这里直接读取 String Table 资产,不调用异步 Localization API,因而不会加载 Addressables。
|
||||
/// </summary>
|
||||
private void ApplyEditorPreviewText()
|
||||
{
|
||||
if (targetText == null || stringReference == null || stringReference.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Locale projectLocale = LocalizationSettings.ProjectLocale;
|
||||
StringTableCollection tableCollection =
|
||||
LocalizationEditorSettings.GetStringTableCollection(stringReference.TableReference);
|
||||
StringTable table = projectLocale == null
|
||||
? null
|
||||
: tableCollection?.GetTable(projectLocale.Identifier) as StringTable;
|
||||
StringTableEntry entry = table?.GetEntryFromReference(stringReference.TableEntryReference);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string previewText = entry.LocalizedValue ?? string.Empty;
|
||||
if (targetText.text == previewText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
targetText.text = previewText;
|
||||
EditorUtility.SetDirty(targetText);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Localization/LocalizedTMPText.cs.meta
Normal file
2
Assets/Scripts/Localization/LocalizedTMPText.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 209017a926f587a4394f52dc1e88a61e
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
@@ -147,9 +148,9 @@ namespace Ichni.Story
|
||||
|
||||
[FoldoutGroup("$EditorHeader/Text Content")]
|
||||
[ShowIf("blockType", StoryBlockType.Text)]
|
||||
[LabelText("Title Key")]
|
||||
[Tooltip("该 TextBlock 标题使用的 Unity Localization Entry Key;留空时视觉层可使用 Block ID 作为开发阶段回退。")]
|
||||
public string titleKey;
|
||||
[LabelText("Title")]
|
||||
[Tooltip("该 TextBlock 的 Unity Localization String Reference。请在此直接选择 String Table 与 Entry;留空时视觉层使用 Block ID 作为开发阶段回退。")]
|
||||
public LocalizedString title = new LocalizedString();
|
||||
|
||||
/// <summary>
|
||||
/// TextBlock 的视觉和叙事层级。Important 用于主线转折与后续 checkpoint;
|
||||
@@ -229,7 +230,9 @@ namespace Ichni.Story
|
||||
{
|
||||
string title = blockType switch
|
||||
{
|
||||
StoryBlockType.Text => titleKey,
|
||||
StoryBlockType.Text => this.title != null && !this.title.IsEmpty
|
||||
? this.title.TableEntryReference.ToString()
|
||||
: null,
|
||||
StoryBlockType.Song => songName,
|
||||
StoryBlockType.Tutorial => tutorialName,
|
||||
_ => null
|
||||
|
||||
@@ -3,7 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Localization;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
@@ -146,10 +148,7 @@ namespace Ichni.Story
|
||||
issues.Add($"[Error] Timeline Marker '{marker.markerId}' 的 Anchor Block '{marker.anchorBlockId}' 不是 TextBlock。");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(marker.labelKey))
|
||||
{
|
||||
issues.Add($"[Warning] Timeline Marker '{marker.markerId}' 未配置 Label Key。");
|
||||
}
|
||||
ValidateLocalizedString(marker.label, $"Timeline Marker '{marker.markerId}' Label", issues);
|
||||
}
|
||||
|
||||
ValidateTimelineMarkerAnchors(reachableBlockIds, issues);
|
||||
@@ -203,8 +202,10 @@ namespace Ichni.Story
|
||||
|
||||
switch (block.blockType)
|
||||
{
|
||||
case StoryBlockType.Text when string.IsNullOrWhiteSpace(block.yarnNodeName):
|
||||
issues.Add($"[Warning] TextBlock '{block.blockId}' 未配置 Yarn Node。");
|
||||
case StoryBlockType.Text:
|
||||
if (string.IsNullOrWhiteSpace(block.yarnNodeName))
|
||||
issues.Add($"[Warning] TextBlock '{block.blockId}' 未配置 Yarn Node。");
|
||||
ValidateLocalizedString(block.title, $"TextBlock '{block.blockId}' Title", issues);
|
||||
break;
|
||||
case StoryBlockType.Song when string.IsNullOrWhiteSpace(block.songName):
|
||||
issues.Add($"[Warning] SongBlock '{block.blockId}' 未配置 Song ID。");
|
||||
@@ -295,6 +296,36 @@ namespace Ichni.Story
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 LocalizedString 是否已选择有效的 String Table 与 Entry。
|
||||
/// 只读取 Unity Localization 的 Shared Table Data,不加载 Addressables,也不修改资产;
|
||||
/// 因此可在 StoryData Inspector 中尽早发现 CSV 尚未导入或引用错误。
|
||||
/// </summary>
|
||||
private static void ValidateLocalizedString(
|
||||
LocalizedString reference,
|
||||
string owner,
|
||||
List<string> issues)
|
||||
{
|
||||
if (reference == null || reference.IsEmpty)
|
||||
{
|
||||
issues.Add($"[Warning] {owner} 未配置 LocalizedString。");
|
||||
return;
|
||||
}
|
||||
|
||||
StringTableCollection tableCollection =
|
||||
LocalizationEditorSettings.GetStringTableCollection(reference.TableReference);
|
||||
if (tableCollection?.SharedData == null)
|
||||
{
|
||||
issues.Add($"[Error] {owner} 引用的 String Table '{reference.TableReference}' 不存在或无法读取 Shared Table Data。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tableCollection.SharedData.GetEntryFromReference(reference.TableEntryReference) == null)
|
||||
{
|
||||
issues.Add($"[Error] {owner} 引用的 Entry '{reference.TableEntryReference}' 不存在于 String Table '{tableCollection.TableCollectionName}'。");
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<string> ValidateStoryGraph(HashSet<string> blockIds, List<string> issues)
|
||||
{
|
||||
Dictionary<string, List<string>> adjacency = new Dictionary<string, List<string>>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
@@ -8,8 +9,8 @@ namespace Ichni.Story
|
||||
/// 一个章节 Timeline 标记的静态定义。
|
||||
/// <para>它不保存独立的横坐标:运行时始终跟随锚定 Block 的 <c>gridColumn</c>,
|
||||
/// 使时间轴与剧情树在横向拖动时保持对齐。</para>
|
||||
/// <para>显示文本和 ID 使用 Unity Localization 的 Entry Key;本阶段不绑定具体 String Table,
|
||||
/// 以便后续统一迁移本地化系统时集中配置。</para>
|
||||
/// <para>显示文本直接使用 <see cref="LocalizedString"/>,因此可在 Inspector 中选择
|
||||
/// String Table 与 Entry,并预览各语言的值。</para>
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[InlineProperty]
|
||||
@@ -37,13 +38,13 @@ namespace Ichni.Story
|
||||
public string anchorBlockId;
|
||||
|
||||
/// <summary>
|
||||
/// Timeline 上显示的本地化文本 Key。通常用于时间描述,也可用于特殊叙事文本。
|
||||
/// Timeline 上显示的本地化文本。通常用于时间描述,也可用于特殊叙事文本。
|
||||
/// </summary>
|
||||
[HorizontalGroup("Identity")]
|
||||
[LabelText("Label Key")]
|
||||
[LabelWidth(54)]
|
||||
[Tooltip("Timeline 上显示的 Unity Localization Entry Key,通常用于时间或特殊叙事文本。")]
|
||||
public string labelKey;
|
||||
[LabelText("Label")]
|
||||
[LabelWidth(40)]
|
||||
[Tooltip("Timeline 上显示的 Unity Localization String Reference。请在此直接选择对应 String Table 与 Entry。")]
|
||||
public LocalizedString label = new LocalizedString();
|
||||
|
||||
/// <summary>
|
||||
/// 是否参与章节主线进度百分比计算。支线或纯装饰性时间标记应关闭此项。
|
||||
|
||||
@@ -3,6 +3,8 @@ using Ichni.Menu.UI;
|
||||
using Ichni.Story.UI;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Tables;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
@@ -13,6 +15,9 @@ namespace Ichni.Story
|
||||
/// </summary>
|
||||
public static class TutorialFlowController
|
||||
{
|
||||
/// <summary>教程确认弹窗统一使用 Message String Table,避免业务代码内嵌可见文案。</summary>
|
||||
private static readonly TableReference MessageTable = "Message";
|
||||
|
||||
/// <summary>
|
||||
/// 由 <see cref="UI.TutorialBlockView"/> 点击时调用。配置无效或 MessageUIPage 未配置时只报错,不修改剧情进度。
|
||||
/// </summary>
|
||||
@@ -32,14 +37,18 @@ namespace Ichni.Story
|
||||
}
|
||||
|
||||
// TutorialFlowController 只描述“有哪些选择、各自触发什么效果”;
|
||||
// 具体按钮由通用 SelectionBox 在运行时生成,后续其它系统可复用同一 UI。
|
||||
messageUIPage.ShowSelectionBox(
|
||||
tutorialBlock.tutorialName,
|
||||
string.Empty,
|
||||
new List<SelectionBoxOption>
|
||||
// 具体按钮由通用 SelectionBox 在运行时生成,且按钮文案与标题均由 Message 表解析。
|
||||
messageUIPage.ShowLocalizedSelectionBox(
|
||||
new LocalizedPopupText(MessageTable, "system_tutorial_entry_title"),
|
||||
new LocalizedPopupText(MessageTable, "system_tutorial_entry_content"),
|
||||
new List<LocalizedSelectionBoxOption>
|
||||
{
|
||||
new("游玩教程", () => PlayTutorial(tutorialBlock, chapter, song, difficulty)),
|
||||
new("跳过教程", () => SkipTutorial(tutorialBlock))
|
||||
new(
|
||||
new LocalizedPopupText(MessageTable, "system_tutorial_enter"),
|
||||
() => PlayTutorial(tutorialBlock, chapter, song, difficulty)),
|
||||
new(
|
||||
new LocalizedPopupText(MessageTable, "system_tutorial_skip"),
|
||||
() => SkipTutorial(tutorialBlock))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Localization;
|
||||
using Ichni.Menu.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.Localization.Tables;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Story.UI
|
||||
@@ -39,15 +36,13 @@ namespace Ichni.Story.UI
|
||||
[Tooltip("自动在左右各扩展一个刻度周期,保证刻度条循环平移时不会从 Timeline 边缘露出空白。")]
|
||||
public bool expandTickRepeatRect = true;
|
||||
|
||||
[Header("Localization")]
|
||||
[Tooltip("StoryTimeline 的 Unity Localization String Table。Marker.labelKey 在该 Table 中查找。")]
|
||||
public TableReference localizationTable;
|
||||
|
||||
private sealed class RuntimeMarker
|
||||
{
|
||||
public StoryTimelineMarkerDefinition definition;
|
||||
public StoryBlockView anchorView;
|
||||
public StoryTimelineMarkerView view;
|
||||
public string localizedLabel;
|
||||
public LocalizedString.ChangeHandler localizedLabelChanged;
|
||||
}
|
||||
|
||||
private readonly List<RuntimeMarker> _markers = new List<RuntimeMarker>();
|
||||
@@ -69,7 +64,6 @@ namespace Ichni.Story.UI
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
LocalizationSettings.SelectedLocaleChanged += OnSelectedLocaleChanged;
|
||||
SubscribeToStoryScrollRect();
|
||||
|
||||
// StoryPage 从对话界面返回后,Canvas 与 StoryTree 会在同一帧重新启用。
|
||||
@@ -79,7 +73,6 @@ namespace Ichni.Story.UI
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
LocalizationSettings.SelectedLocaleChanged -= OnSelectedLocaleChanged;
|
||||
UnsubscribeFromStoryScrollRect();
|
||||
}
|
||||
|
||||
@@ -124,6 +117,7 @@ namespace Ichni.Story.UI
|
||||
|
||||
foreach (RuntimeMarker marker in _markers)
|
||||
{
|
||||
UnbindLocalizedLabel(marker);
|
||||
if (marker.view != null)
|
||||
{
|
||||
Destroy(marker.view.gameObject);
|
||||
@@ -356,16 +350,19 @@ namespace Ichni.Story.UI
|
||||
|
||||
StoryTimelineMarkerView view = Instantiate(markerPrefab, markerContainer);
|
||||
view.name = $"StoryTimelineMarker_{marker.markerId}";
|
||||
view.SetLabel(GetLocalizedLabel(marker.labelKey));
|
||||
|
||||
RuntimeMarker runtimeMarker = new RuntimeMarker
|
||||
{
|
||||
definition = marker,
|
||||
anchorView = anchorView,
|
||||
view = view
|
||||
view = view,
|
||||
localizedLabel = marker.markerId
|
||||
};
|
||||
view.BindClick(() => RequestRollback(runtimeMarker));
|
||||
_markers.Add(runtimeMarker);
|
||||
// LocalizedString 首次读取是异步的;先显示稳定 Marker ID,避免 Prefab 默认文字短暂闪现。
|
||||
view.SetLabel(runtimeMarker.localizedLabel);
|
||||
BindLocalizedLabel(runtimeMarker);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -388,48 +385,52 @@ namespace Ichni.Story.UI
|
||||
return;
|
||||
}
|
||||
|
||||
string markerLabel = GetLocalizedLabel(marker.definition.labelKey);
|
||||
messageUIPage.ShowSelectionBox(
|
||||
"重开本章节",
|
||||
$"将重置“{markerLabel}”所在列及其之后的剧情选择与进度。歌曲成绩、解锁内容、设置和其它章节不会受到影响。",
|
||||
new List<SelectionBoxOption>
|
||||
string markerLabel = marker.localizedLabel;
|
||||
messageUIPage.ShowLocalizedSelectionBox(
|
||||
new LocalizedPopupText("Message", "system_rollback_chapter_title"),
|
||||
new LocalizedPopupText("Message", "system_rollback_chapter_content", markerLabel),
|
||||
new List<LocalizedSelectionBoxOption>
|
||||
{
|
||||
new SelectionBoxOption("重开此时间点", () => _treeController.TryRollbackToMarker(marker.definition.markerId)),
|
||||
new SelectionBoxOption("取消", () => { })
|
||||
new LocalizedSelectionBoxOption(
|
||||
new LocalizedPopupText("Message", "system_rollback_confirm"),
|
||||
() => _treeController.TryRollbackToMarker(marker.definition.markerId)),
|
||||
new LocalizedSelectionBoxOption(
|
||||
new LocalizedPopupText("UI", "ui_common_cancel"),
|
||||
() => { })
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 StoryTimeline 指定的 Unity Localization String Table 解析 Label Key。
|
||||
/// String Table 或条目尚未配置时回退显示 Key,确保开发阶段仍能从 UI 上定位缺失翻译。
|
||||
/// 订阅 Marker 自身的 LocalizedString。LocalizedString 会负责首次异步读取与 Locale
|
||||
/// 切换后的自动刷新,Timeline 不再维护独立的 Table / Key 解析逻辑。
|
||||
/// </summary>
|
||||
private string GetLocalizedLabel(string labelKey)
|
||||
private static void BindLocalizedLabel(RuntimeMarker marker)
|
||||
{
|
||||
if (string.IsNullOrEmpty(labelKey))
|
||||
if (marker?.definition?.label == null || marker.definition.label.IsEmpty)
|
||||
{
|
||||
return string.Empty;
|
||||
marker?.view?.SetLabel(marker?.localizedLabel ?? string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
return LocalizationTextService.Resolve(localizationTable, labelKey, labelKey);
|
||||
marker.localizedLabelChanged = localizedText =>
|
||||
{
|
||||
marker.localizedLabel = string.IsNullOrEmpty(localizedText)
|
||||
? marker.definition.markerId
|
||||
: localizedText;
|
||||
|
||||
if (marker.view != null)
|
||||
marker.view.SetLabel(marker.localizedLabel);
|
||||
};
|
||||
marker.definition.label.StringChanged += marker.localizedLabelChanged;
|
||||
}
|
||||
|
||||
private async void OnSelectedLocaleChanged(Locale _)
|
||||
private static void UnbindLocalizedLabel(RuntimeMarker marker)
|
||||
{
|
||||
foreach (RuntimeMarker marker in _markers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(marker.definition.labelKey)) continue;
|
||||
if (marker?.definition?.label == null || marker.localizedLabelChanged == null)
|
||||
return;
|
||||
|
||||
string localizedLabel = await LocalizationTextService.ResolveAsync(
|
||||
localizationTable,
|
||||
marker.definition.labelKey,
|
||||
marker.definition.labelKey);
|
||||
|
||||
// 异步等待结束后,防范对象在此期间被销毁。
|
||||
if (marker.view != null)
|
||||
{
|
||||
marker.view.SetLabel(localizedLabel);
|
||||
}
|
||||
}
|
||||
marker.definition.label.StringChanged -= marker.localizedLabelChanged;
|
||||
marker.localizedLabelChanged = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using Ichni.Story.Dialogue;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Story.UI
|
||||
@@ -24,6 +25,21 @@ namespace Ichni.Story.UI
|
||||
// 当前已实例化的状态背景对象;状态切换时销毁并替换
|
||||
private GameObject _currentVisual;
|
||||
|
||||
// 直接持有 StoryData 的 LocalizedString,并订阅其 StringChanged 回调。
|
||||
// Unity Localization 会在首次订阅和语言切换时自行异步刷新文本。
|
||||
private LocalizedString _titleReference;
|
||||
private bool _isTitleLocalizationSubscribed;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
StartTitleLocalization();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
StopTitleLocalization();
|
||||
}
|
||||
|
||||
public override void Initialize(StoryBlockDefinition def, Vector2 position, StoryBlockState blockState)
|
||||
{
|
||||
// 注意:base.Initialize 末尾会调用 ApplyState(state),届时已生成对应背景
|
||||
@@ -34,9 +50,43 @@ namespace Ichni.Story.UI
|
||||
if (storyIdText != null)
|
||||
storyIdText.text = def.blockId;
|
||||
|
||||
// 当前仍直接显示 titleKey;正式内容接入时需要由 Unity Localization 解析玩家可见标题。
|
||||
StopTitleLocalization();
|
||||
_titleReference = def.title;
|
||||
StartTitleLocalization();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅当前 TextBlock 的 LocalizedString。无需手写 Table / Key 读取或监听全局 Locale,
|
||||
/// LocalizedString 会遵循 Unity 官方生命周期异步加载,并在语言变化后触发 StringChanged。
|
||||
/// </summary>
|
||||
private void StartTitleLocalization()
|
||||
{
|
||||
if (_isTitleLocalizationSubscribed || titleText == null)
|
||||
return;
|
||||
|
||||
if (_titleReference == null || _titleReference.IsEmpty)
|
||||
{
|
||||
titleText.text = blockId;
|
||||
return;
|
||||
}
|
||||
|
||||
_titleReference.StringChanged += ApplyLocalizedTitle;
|
||||
_isTitleLocalizationSubscribed = true;
|
||||
}
|
||||
|
||||
private void StopTitleLocalization()
|
||||
{
|
||||
if (!_isTitleLocalizationSubscribed || _titleReference == null)
|
||||
return;
|
||||
|
||||
_titleReference.StringChanged -= ApplyLocalizedTitle;
|
||||
_isTitleLocalizationSubscribed = false;
|
||||
}
|
||||
|
||||
private void ApplyLocalizedTitle(string localizedTitle)
|
||||
{
|
||||
if (titleText != null)
|
||||
titleText.text = def.titleKey;
|
||||
titleText.text = string.IsNullOrEmpty(localizedTitle) ? blockId : localizedTitle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Ichni.Story.Dialogue;
|
||||
using Ichni.Story.UI;
|
||||
using Ichni.Localization;
|
||||
using Ichni.Menu;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
@@ -14,14 +15,17 @@ namespace Ichni.Story.YarnFunctions
|
||||
// ── Yarn Commands ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 兼容既有 Yarn 脚本:授予歌曲相关的解锁 Key,并在当前对话结束后弹出提示。
|
||||
/// 格式: <<unlock_song unlock_key>>
|
||||
/// 授予歌曲相关的解锁 Key,并在当前对话结束后弹出提示。
|
||||
/// 格式: <<unlock_song unlock_key "song_id">>
|
||||
/// <para>Key 必须使用小写字母、数字和下划线,例如 <c>story_ch0_prologue_completed</c>。</para>
|
||||
/// <para><c>song_id</c> 必须是 <see cref="SongItemData.songName"/> 的稳定标识,而不是显示名称或本地化文本。
|
||||
/// 该参数用于解析玩家可见的歌曲名称,绝不能将技术 Unlock Key 显示给玩家。</para>
|
||||
/// 新内容若不需要歌曲提示,优先使用 <see cref="GrantUnlock"/>。
|
||||
/// </summary>
|
||||
/// <param name="songUnlockKey">要授予的稳定解锁 Key,而不是歌曲显示名称。</param>
|
||||
/// <param name="songId">对应 <see cref="SongItemData.songName"/> 的稳定歌曲 ID。</param>
|
||||
[YarnCommand("unlock_song")]
|
||||
public static void UnlockSong(string songUnlockKey)
|
||||
public static void UnlockSong(string songUnlockKey, string songId)
|
||||
{
|
||||
ExecuteAfterDialogueCommit(() =>
|
||||
{
|
||||
@@ -31,7 +35,7 @@ namespace Ichni.Story.YarnFunctions
|
||||
}
|
||||
|
||||
if (MessageUIPage.instance != null)
|
||||
MessageUIPage.instance.ShowUnlockMessage(songUnlockKey);
|
||||
MessageUIPage.instance.ShowUnlockMessage(ResolveSongDisplayName(songId));
|
||||
else
|
||||
Debug.LogError("[StoryTreeCommands] 弹窗失败:场景中未找到 StoryMessageBoxUIPage 实例!");
|
||||
|
||||
@@ -57,7 +61,7 @@ namespace Ichni.Story.YarnFunctions
|
||||
|
||||
/// <summary>
|
||||
/// 在对话结束后,弹出一个自定义 MessageBox 提示。
|
||||
/// 会尝试使用 Unity Localization 进行翻译,如果找不到对应条目则回退显示原文本(Key)。
|
||||
/// 文本 Key 会交给 MessageUIPage 的 FIFO 队列,并在该弹窗真正显示前才按当前 Locale 解析。
|
||||
/// 格式: <<show_message titleKey contentKey [tableName]>>
|
||||
/// </summary>
|
||||
/// <param name="titleKey">标题文本或本地化键</param>
|
||||
@@ -66,13 +70,13 @@ namespace Ichni.Story.YarnFunctions
|
||||
[YarnCommand("show_message")]
|
||||
public static void ShowMessage(string titleKey, string contentKey, string tableName = "Message")
|
||||
{
|
||||
string translatedTitle = GetTranslatedText(tableName, titleKey);
|
||||
string translatedContent = GetTranslatedText(tableName, contentKey);
|
||||
Debug.Log($"[StoryTreeCommands] 准备在对话结束后显示自定义弹窗:标题='{translatedTitle}',内容='{translatedContent}'");
|
||||
Debug.Log($"[StoryTreeCommands] 准备在对话结束后显示本地化弹窗:Table='{tableName}',TitleKey='{titleKey}',ContentKey='{contentKey}'。");
|
||||
ExecuteAfterDialogueCommit(() =>
|
||||
{
|
||||
if (MessageUIPage.instance != null)
|
||||
MessageUIPage.instance.ShowCustomMessage(translatedTitle, translatedContent);
|
||||
MessageUIPage.instance.ShowLocalizedMessage(
|
||||
new LocalizedPopupText(tableName, titleKey),
|
||||
new LocalizedPopupText(tableName, contentKey));
|
||||
else
|
||||
Debug.LogError("[StoryTreeCommands] 无法显示自定义弹窗:未找到 MessageUIPage 实例。");
|
||||
});
|
||||
@@ -128,9 +132,36 @@ namespace Ichni.Story.YarnFunctions
|
||||
|
||||
// ── 内部辅助 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static string GetTranslatedText(string tableName, string key)
|
||||
/// <summary>
|
||||
/// 以稳定 Song ID 查找当前项目中配置的歌曲显示名。当前阶段使用 <see cref="SongItemData.displaySongName"/>;
|
||||
/// 将来歌曲标题接入 Chapter Content 本地化后,只需替换本函数内部的返回逻辑,Yarn 与弹窗 API 无需修改。
|
||||
/// </summary>
|
||||
private static string ResolveSongDisplayName(string songId)
|
||||
{
|
||||
return LocalizationTextService.Resolve(tableName, key, key);
|
||||
List<ChapterSelectionUnit> chapters = ChapterSelectionManager.instance?.chapters;
|
||||
if (chapters != null)
|
||||
{
|
||||
foreach (ChapterSelectionUnit chapter in chapters)
|
||||
{
|
||||
if (chapter?.songs == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (SongItemData song in chapter.songs)
|
||||
{
|
||||
if (song != null && song.songName == songId)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(song.displaySongName)
|
||||
? song.songName
|
||||
: song.displaySongName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[StoryTreeCommands] 无法为歌曲解锁提示解析 Song ID '{songId}',将显示该 ID。", ChapterSelectionManager.instance);
|
||||
return songId ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
//using I2.Loc;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 历史兼容扩展。
|
||||
/// <para>
|
||||
/// 项目不再通过字符串扩展读取 I2 Localization;运行时动态本地化请使用
|
||||
/// <c>Ichni.Localization.LocalizationTextService</c>,静态 TMP 文本请使用
|
||||
/// <c>Ichni.Localization.LocalizedTMPText</c>。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class StringExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 保持旧调用点的二进制/源码兼容。当前不会执行任何本地化解析。
|
||||
/// </summary>
|
||||
public static string Localize(this string original)
|
||||
{
|
||||
/*if (LocalizationManager.TryGetTranslation(original, out string translated))
|
||||
{
|
||||
return translated;
|
||||
}*/
|
||||
|
||||
return original;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd4a697d946242343a065437c29f0cf2
|
||||
guid: fd4a697d946242343a065437c29f0cf2
|
||||
|
||||
@@ -1,48 +1,298 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Localization;
|
||||
using Michsky.MUIP;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
|
||||
namespace Ichni.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings 页面使用的下拉控件。
|
||||
/// <para>
|
||||
/// 本地化枚举项由 <see cref="SetUpLocalized(int,string,string[])"/> 在代码中传入 String Table 名称与
|
||||
/// Entry Key。控件会创建对应的 <see cref="LocalizedString"/>,并在 Michsky 生成 Item 后为每个
|
||||
/// 可见文字绑定 <see cref="LocalizedTMPText"/>;切换语言时不重建列表。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 分辨率、语言自称等运行时文本则使用 <see cref="SetUpRuntime"/>,不会附加本地化组件。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class Dropdown : SettingsUIElementBase
|
||||
{
|
||||
public List<string> options;
|
||||
// 仅保存本次初始化创建的运行时引用,不在 Inspector 中维护本地化选项列表。
|
||||
private readonly List<LocalizedString> _localizedOptions = new List<LocalizedString>();
|
||||
|
||||
// 保留原字段以兼容已有的外部读取。静态本地化选项使用稳定的占位名称;实际显示内容由
|
||||
// 动态生成 Item 上的 LocalizedTMPText 管理。
|
||||
[HideInInspector] public List<string> options = new List<string>();
|
||||
public CustomDropdown dropdown;
|
||||
public int selectedIndex;
|
||||
public string selectedOption;
|
||||
|
||||
public void SetUp(int initialValue, List<string> options, string title = "", bool preservePrefabTitle = false)
|
||||
|
||||
private bool _usesLocalizedOptions;
|
||||
private bool _isValueChangeListenerRegistered;
|
||||
private Coroutine _bindGeneratedItemsCoroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 使用代码提供的 String Table 与 Entry Key 初始化静态枚举选项。
|
||||
/// <paramref name="entryKeys"/> 的顺序即设置枚举的索引顺序,必须保持稳定。
|
||||
/// 标题和说明仍由其 TMP 对象上的 <see cref="LocalizedTMPText"/> 独立管理。
|
||||
/// </summary>
|
||||
public void SetUpLocalized(int initialValue, string tableCollection, params string[] entryKeys)
|
||||
{
|
||||
base.SetUp(title, preservePrefabTitle: preservePrefabTitle);
|
||||
|
||||
this.options = options;
|
||||
this.dropdown.items = new List<CustomDropdown.Item>();
|
||||
foreach (string option in options)
|
||||
base.SetUp();
|
||||
|
||||
ClearLocalizedBindings();
|
||||
ClearLocalizedOptionDefinitions();
|
||||
_usesLocalizedOptions = true;
|
||||
|
||||
foreach (string entryKey in entryKeys)
|
||||
{
|
||||
dropdown.items.Add(new CustomDropdown.Item { itemName = option });
|
||||
_localizedOptions.Add(new LocalizedString(tableCollection, entryKey));
|
||||
}
|
||||
|
||||
dropdown.onValueChanged.AddListener(value =>
|
||||
{
|
||||
SetValue(value);
|
||||
updateValueAction?.Invoke();
|
||||
});
|
||||
|
||||
|
||||
BuildLocalizedOptionPlaceholders();
|
||||
EnsureValueChangeListener();
|
||||
SetValue(initialValue);
|
||||
ApplyOptionsToDropdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用运行时生成的纯文本选项初始化下拉框。
|
||||
/// 仅用于分辨率、语言自称等不应随当前 Locale 再次翻译的内容。
|
||||
/// </summary>
|
||||
public void SetUpRuntime(int initialValue, IReadOnlyList<string> runtimeOptions)
|
||||
{
|
||||
base.SetUp();
|
||||
|
||||
ClearLocalizedBindings();
|
||||
ClearLocalizedOptionDefinitions();
|
||||
_usesLocalizedOptions = false;
|
||||
options = runtimeOptions == null ? new List<string>() : new List<string>(runtimeOptions);
|
||||
EnsureValueChangeListener();
|
||||
SetValue(initialValue);
|
||||
ApplyOptionsToDropdown();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_usesLocalizedOptions)
|
||||
{
|
||||
QueueGeneratedItemBinding();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
StopGeneratedItemBinding();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopGeneratedItemBinding();
|
||||
ClearLocalizedOptionDefinitions();
|
||||
}
|
||||
|
||||
public int GetOptionIndex(string option)
|
||||
{
|
||||
return options.IndexOf(option);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 只更新当前选择状态,不调用 <see cref="updateValueAction"/>。
|
||||
/// 因此语言切换和 Dropdown 列表重建都不会重新应用玩家的设置。
|
||||
/// </summary>
|
||||
public void SetValue(int index)
|
||||
{
|
||||
selectedIndex = index;
|
||||
if (options == null || options.Count == 0)
|
||||
{
|
||||
selectedIndex = 0;
|
||||
selectedOption = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIndex = Mathf.Clamp(index, 0, options.Count - 1);
|
||||
selectedOption = options[selectedIndex];
|
||||
dropdown.selectedItemIndex = selectedIndex;
|
||||
dropdown.UpdateItemLayout();
|
||||
|
||||
if (_usesLocalizedOptions)
|
||||
{
|
||||
BindSelectedLocalizedText();
|
||||
}
|
||||
else if (dropdown.selectedText != null)
|
||||
{
|
||||
dropdown.selectedText.text = selectedOption;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureValueChangeListener()
|
||||
{
|
||||
if (_isValueChangeListenerRegistered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.onValueChanged.AddListener(OnDropdownValueChanged);
|
||||
_isValueChangeListenerRegistered = true;
|
||||
}
|
||||
|
||||
private void OnDropdownValueChanged(int index)
|
||||
{
|
||||
SetValue(index);
|
||||
updateValueAction?.Invoke();
|
||||
}
|
||||
|
||||
private void BuildLocalizedOptionPlaceholders()
|
||||
{
|
||||
options = new List<string>(_localizedOptions.Count);
|
||||
for (int index = 0; index < _localizedOptions.Count; index++)
|
||||
{
|
||||
// Michsky 需要 itemName 创建 Item;真正可见的内容会在创建完成后被 LocalizedTMPText 覆盖。
|
||||
options.Add($"LocalizedOption_{index}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyOptionsToDropdown()
|
||||
{
|
||||
if (options == null || options.Count == 0)
|
||||
{
|
||||
dropdown.items = new List<CustomDropdown.Item>();
|
||||
return;
|
||||
}
|
||||
|
||||
SetValue(selectedIndex);
|
||||
dropdown.items = new List<CustomDropdown.Item>(options.Count);
|
||||
foreach (string option in options)
|
||||
{
|
||||
dropdown.items.Add(new CustomDropdown.Item { itemName = option });
|
||||
}
|
||||
|
||||
if (dropdown.gameObject.activeInHierarchy)
|
||||
{
|
||||
if (dropdown.isOn)
|
||||
{
|
||||
dropdown.Animate();
|
||||
}
|
||||
|
||||
dropdown.SetupDropdown();
|
||||
}
|
||||
else
|
||||
{
|
||||
dropdown.UpdateItemLayout();
|
||||
}
|
||||
|
||||
if (_usesLocalizedOptions)
|
||||
{
|
||||
QueueGeneratedItemBinding();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待一帧,确保 Michsky 已销毁旧 Item 并完成当前批次的实例化,再为每个 Item 绑定引用。
|
||||
/// 这样不会保留已销毁 Item 的语言监听,也避免把引用绑定到上一批列表对象。
|
||||
/// </summary>
|
||||
private void QueueGeneratedItemBinding()
|
||||
{
|
||||
if (!isActiveAndEnabled || _bindGeneratedItemsCoroutine != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bindGeneratedItemsCoroutine = StartCoroutine(BindGeneratedItemsNextFrame());
|
||||
}
|
||||
|
||||
private IEnumerator BindGeneratedItemsNextFrame()
|
||||
{
|
||||
yield return null;
|
||||
_bindGeneratedItemsCoroutine = null;
|
||||
BindSelectedLocalizedText();
|
||||
|
||||
if (dropdown.itemParent == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
int itemCount = Mathf.Min(dropdown.itemParent.childCount, _localizedOptions.Count);
|
||||
for (int index = 0; index < itemCount; index++)
|
||||
{
|
||||
TMP_Text itemText = dropdown.itemParent.GetChild(index).GetComponentInChildren<TMP_Text>(true);
|
||||
BindLocalizedText(itemText, _localizedOptions[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindSelectedLocalizedText()
|
||||
{
|
||||
if (!_usesLocalizedOptions || _localizedOptions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int optionIndex = Mathf.Clamp(selectedIndex, 0, _localizedOptions.Count - 1);
|
||||
BindLocalizedText(dropdown.selectedText, _localizedOptions[optionIndex]);
|
||||
}
|
||||
|
||||
private static void BindLocalizedText(TMP_Text text, LocalizedString reference)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LocalizedTMPText localizedText = text.GetComponent<LocalizedTMPText>();
|
||||
if (localizedText == null)
|
||||
{
|
||||
localizedText = text.gameObject.AddComponent<LocalizedTMPText>();
|
||||
}
|
||||
|
||||
localizedText.SetStringReference(reference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将本 Dropdown 先前动态附加的本地化引用清空。
|
||||
/// 同一控件改用运行时文本列表时必须执行此步骤,否则旧 Locale 回调会覆盖新的原始文本。
|
||||
/// </summary>
|
||||
private void ClearLocalizedBindings()
|
||||
{
|
||||
ClearLocalizedBinding(dropdown.selectedText);
|
||||
|
||||
if (dropdown.itemParent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Transform item in dropdown.itemParent)
|
||||
{
|
||||
ClearLocalizedBinding(item.GetComponentInChildren<TMP_Text>(true));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearLocalizedBinding(TMP_Text text)
|
||||
{
|
||||
LocalizedTMPText localizedText = text == null ? null : text.GetComponent<LocalizedTMPText>();
|
||||
if (localizedText != null)
|
||||
{
|
||||
localizedText.SetStringReference(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearLocalizedOptionDefinitions()
|
||||
{
|
||||
// LocalizedString 的订阅由每个 LocalizedTMPText 持有;ClearLocalizedBindings 已经解除它们。
|
||||
// 当前 Unity Localization 版本没有公开的 LocalizedString.Dispose,因此清空运行时列表即可。
|
||||
_localizedOptions.Clear();
|
||||
}
|
||||
|
||||
private void StopGeneratedItemBinding()
|
||||
{
|
||||
if (_bindGeneratedItemsCoroutine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(_bindGeneratedItemsCoroutine);
|
||||
_bindGeneratedItemsCoroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,22 @@ namespace Ichni.UI
|
||||
public TMP_Text keyText;
|
||||
public GameObject waitingForInputCover;
|
||||
|
||||
public void SetUp(string title, string subtitle, string actionName, int bindingIndex)
|
||||
private bool _isClickListenerRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化重绑定行为。标题和说明由 Prefab 上的 LocalizedTMPText 配置;
|
||||
/// <paramref name="actionName"/> 仅用于定位 Input System Action,不是本地化 Key。
|
||||
/// </summary>
|
||||
public void SetUp(string actionName, int bindingIndex)
|
||||
{
|
||||
base.SetUp(title, subtitle);
|
||||
base.SetUp();
|
||||
if (_isClickListenerRegistered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
button.onClick.AddListener(() => StartRebinding(actionName, bindingIndex));
|
||||
_isClickListenerRegistered = true;
|
||||
}
|
||||
|
||||
// 开始重绑定流程
|
||||
@@ -82,4 +94,4 @@ namespace Ichni.UI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using I2.Loc;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@@ -10,9 +7,6 @@ namespace Ichni.UI
|
||||
{
|
||||
public abstract class SettingsUIElementBase : MonoBehaviour
|
||||
{
|
||||
public string title;
|
||||
public string subTitle;
|
||||
|
||||
public Image titleBackground;
|
||||
public TMP_Text titleText;
|
||||
public TMP_Text subTitleText;
|
||||
@@ -21,52 +15,22 @@ namespace Ichni.UI
|
||||
|
||||
/// <summary>
|
||||
/// 初始化设置控件的标题区域。
|
||||
/// <paramref name="preservePrefabTitle"/> 用于已经由 Unity Localization 直接驱动标题的控件:
|
||||
/// 运行时代码不会覆盖或隐藏 Prefab 中的本地化标题。
|
||||
/// <para>
|
||||
/// 标题与说明不再由此基础类写入 I2 Key。请在对应 <see cref="TMP_Text"/> 上配置
|
||||
/// <c>LocalizedTMPText</c>,使静态文案与控件逻辑完全解耦。
|
||||
/// </para>
|
||||
/// <paramref name="preservePrefabTitle"/> 为 true 时保留 Prefab 的标题区域;这是设置页面的默认行为。
|
||||
/// </summary>
|
||||
public virtual void SetUp(string title = "", string subTitle = "", bool preservePrefabTitle = false)
|
||||
public virtual void SetUp(bool preservePrefabTitle = true)
|
||||
{
|
||||
if (preservePrefabTitle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (title != "")
|
||||
{
|
||||
SetTitle(title, subTitle);
|
||||
}
|
||||
else
|
||||
{
|
||||
titleBackground.gameObject.SetActive(false);
|
||||
titleText.gameObject.SetActive(false);
|
||||
subTitleText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTitle(string title, string subTitle = "")
|
||||
{
|
||||
this.title = title;
|
||||
this.subTitle = subTitle;
|
||||
titleText.gameObject.SetActive(true);
|
||||
var titleLoc = titleText.GetComponent<Localize>();
|
||||
if (titleLoc != null)
|
||||
titleLoc.SetTerm(title);
|
||||
else
|
||||
titleText.text = title;
|
||||
|
||||
if (subTitle != "")
|
||||
{
|
||||
subTitleText.gameObject.SetActive(true);
|
||||
var subLoc = subTitleText.GetComponent<Localize>();
|
||||
if (subLoc != null)
|
||||
subLoc.SetTerm(subTitle);
|
||||
else
|
||||
subTitleText.text = subTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
subTitleText.gameObject.SetActive(false);
|
||||
}
|
||||
titleBackground.gameObject.SetActive(false);
|
||||
titleText.gameObject.SetActive(false);
|
||||
subTitleText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,27 @@ namespace Ichni.UI
|
||||
{
|
||||
public bool value;
|
||||
public SwitchManager switchManager;
|
||||
private bool _isValueChangeListenerRegistered;
|
||||
|
||||
public void SetUp(bool initialValue, string title = "", bool preservePrefabTitle = false)
|
||||
public void SetUp(bool initialValue, bool preservePrefabTitle = true)
|
||||
{
|
||||
base.SetUp(title, preservePrefabTitle: preservePrefabTitle);
|
||||
base.SetUp(preservePrefabTitle);
|
||||
|
||||
switchManager.onValueChanged.AddListener(isOn =>
|
||||
if (!_isValueChangeListenerRegistered)
|
||||
{
|
||||
value = isOn;
|
||||
updateValueAction?.Invoke();
|
||||
});
|
||||
switchManager.onValueChanged.AddListener(OnSwitchValueChanged);
|
||||
_isValueChangeListenerRegistered = true;
|
||||
}
|
||||
|
||||
SetValue(initialValue);
|
||||
}
|
||||
|
||||
private void OnSwitchValueChanged(bool isOn)
|
||||
{
|
||||
value = isOn;
|
||||
updateValueAction?.Invoke();
|
||||
}
|
||||
|
||||
public void SetValue(bool value)
|
||||
{
|
||||
this.value = value;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using I2.Loc;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
@@ -10,25 +6,28 @@ namespace Ichni.UI
|
||||
public class TextButton : SettingsUIElementBase
|
||||
{
|
||||
public Button button;
|
||||
public TMP_Text buttonText;
|
||||
public void SetUp(string title, string subTitle, string textContent)
|
||||
private bool _isClickListenerRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化按钮逻辑。标题、说明及按钮文字均由 Prefab 上的 LocalizedTMPText 配置,
|
||||
/// 不再把字符串当作 I2 的 Term 传入。
|
||||
/// </summary>
|
||||
public void SetUp()
|
||||
{
|
||||
base.SetUp(title, subTitle);
|
||||
|
||||
var localize = buttonText.GetComponent<Localize>();
|
||||
if (localize != null)
|
||||
base.SetUp();
|
||||
|
||||
if (_isClickListenerRegistered)
|
||||
{
|
||||
localize.SetTerm(textContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText.text = textContent;
|
||||
return;
|
||||
}
|
||||
|
||||
button.onClick.AddListener(() =>
|
||||
{
|
||||
updateValueAction?.Invoke();
|
||||
});
|
||||
button.onClick.AddListener(InvokeUpdateValueAction);
|
||||
_isClickListenerRegistered = true;
|
||||
}
|
||||
|
||||
private void InvokeUpdateValueAction()
|
||||
{
|
||||
updateValueAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using I2.Loc;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@@ -27,13 +26,13 @@ namespace Ichni.UI
|
||||
public string suffix;
|
||||
public bool showPositiveMark;
|
||||
|
||||
public void SetUp(int initialValue, int step, string title = "")
|
||||
public void SetUp(int initialValue, int step)
|
||||
{
|
||||
intStep = step;
|
||||
intMin = int.MinValue;
|
||||
intMax = int.MaxValue;
|
||||
|
||||
base.SetUp(title);
|
||||
base.SetUp();
|
||||
|
||||
increaseButton.SetUpButton(IncreaseValue, IncreaseValue);
|
||||
decreaseButton.SetUpButton(DecreaseValue, DecreaseValue);
|
||||
@@ -94,7 +93,7 @@ namespace Ichni.UI
|
||||
{
|
||||
|
||||
valueText.text = prefix + (showPositiveMark && intValue > 0 ? "+" : "") + intValue.ToString() + suffix;
|
||||
var target = new Vector2((float)(intValue - intMin) / (intMax - intMin) * 660, 20);
|
||||
var target = new Vector2((float)(intValue - intMin) / (intMax - intMin) * 560, 20);
|
||||
barImage.rectTransform.sizeDelta = target;
|
||||
updateValueAction?.Invoke();
|
||||
}
|
||||
@@ -106,7 +105,7 @@ namespace Ichni.UI
|
||||
if (LastValue == intValue) return;
|
||||
tween?.Complete();
|
||||
valueText.text = prefix + (showPositiveMark && intValue > 0 ? "+" : "") + intValue.ToString() + suffix;
|
||||
var target = new Vector2((float)(intValue - intMin) / (intMax - intMin) * 660, 20);
|
||||
var target = new Vector2((float)(intValue - intMin) / (intMax - intMin) * 560, 20);
|
||||
tween = barImage.rectTransform.DOSizeDelta(target, 0.2f).SetEase(Ease.OutExpo).Play();
|
||||
LastValue = intValue;
|
||||
updateValueAction?.Invoke();
|
||||
@@ -117,4 +116,4 @@ namespace Ichni.UI
|
||||
return intValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Ichni.Menu
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
mainVolumeModifier.SetUp(gameSettings.masterVolume, 10, "Menu UI/Settings_Master_Volume");
|
||||
mainVolumeModifier.SetUp(gameSettings.masterVolume, 10);
|
||||
mainVolumeModifier.SetMinMax(0, 100);
|
||||
mainVolumeModifier.updateValueAction = () =>
|
||||
{
|
||||
@@ -23,7 +23,7 @@ namespace Ichni.Menu
|
||||
SettingsManager.instance.ApplyAudioSettings();
|
||||
};
|
||||
|
||||
musicVolumeModifier.SetUp(gameSettings.musicVolume, 10, "Menu UI/Settings_Music_Volume");
|
||||
musicVolumeModifier.SetUp(gameSettings.musicVolume, 10);
|
||||
musicVolumeModifier.SetMinMax(0, 100);
|
||||
musicVolumeModifier.updateValueAction = () =>
|
||||
{
|
||||
@@ -31,7 +31,7 @@ namespace Ichni.Menu
|
||||
SettingsManager.instance.ApplyAudioSettings();
|
||||
};
|
||||
|
||||
sfxVolumeModifier.SetUp(gameSettings.soundEffectVolume, 10, "Menu UI/Settings_SFX_Volume");
|
||||
sfxVolumeModifier.SetUp(gameSettings.soundEffectVolume, 10);
|
||||
sfxVolumeModifier.SetMinMax(0, 100);
|
||||
sfxVolumeModifier.updateValueAction = () =>
|
||||
{
|
||||
@@ -39,7 +39,7 @@ namespace Ichni.Menu
|
||||
SettingsManager.instance.ApplyAudioSettings();
|
||||
};
|
||||
|
||||
uiVolumeModifier.SetUp(gameSettings.uiVolume, 10, "Menu UI/Settings_UI_Volume");
|
||||
uiVolumeModifier.SetUp(gameSettings.uiVolume, 10);
|
||||
uiVolumeModifier.SetMinMax(0, 100);
|
||||
uiVolumeModifier.updateValueAction = () =>
|
||||
{
|
||||
@@ -47,8 +47,7 @@ namespace Ichni.Menu
|
||||
SettingsManager.instance.ApplyAudioSettings();
|
||||
};
|
||||
|
||||
inGameAudioEffectsSwitch.SetUp(gameSettings.enableInGameAudioEffects,
|
||||
"Menu UI/Settings_InGame_Audio_Effects");
|
||||
inGameAudioEffectsSwitch.SetUp(gameSettings.enableInGameAudioEffects);
|
||||
inGameAudioEffectsSwitch.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.enableInGameAudioEffects = inGameAudioEffectsSwitch.GetValue();
|
||||
|
||||
@@ -13,19 +13,19 @@ namespace Ichni.Menu
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
debugModeSwitch.SetUp(gameSettings.debugMode, "Menu UI/Settings_Debug_Mode");
|
||||
debugModeSwitch.SetUp(gameSettings.debugMode);
|
||||
debugModeSwitch.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.debugMode = debugModeSwitch.GetValue();
|
||||
};
|
||||
|
||||
judgeTypeSwitch.SetUp(gameSettings.judgeType, "Menu UI/Settings_Judge_Type");
|
||||
judgeTypeSwitch.SetUp(gameSettings.judgeType);
|
||||
judgeTypeSwitch.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.judgeType = judgeTypeSwitch.GetValue();
|
||||
};
|
||||
|
||||
autoPlaySwitch.SetUp(gameSettings.autoPlay, "Menu UI/Settings_Auto_Play");
|
||||
autoPlaySwitch.SetUp(gameSettings.autoPlay);
|
||||
autoPlaySwitch.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.autoPlay = autoPlaySwitch.GetValue();
|
||||
@@ -39,4 +39,4 @@ namespace Ichni.Menu
|
||||
autoPlaySwitch.SetValue(gameSettings.autoPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Ichni.UI
|
||||
public Dropdown languageDropdown;
|
||||
public override void Initialize()
|
||||
{
|
||||
offsetEditorButton.SetUp("Menu UI/Settings_Offset_Editor", "", "Menu UI/Settings_Enter");
|
||||
offsetEditorButton.SetUp();
|
||||
offsetEditorButton.updateValueAction = () =>
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
@@ -19,7 +19,10 @@ namespace Ichni.UI
|
||||
MenuManager.instance.settingsUIPage.settingsWindowController.buttonsContainer.gameObject.SetActive(false);
|
||||
};
|
||||
|
||||
languageDropdown.SetUp(SettingsManager.instance.GetLanguageDropdownIndex(), MenuManager.instance.displayLanguageList, "Menu UI/Settings_Language");
|
||||
// 语言列表采用各语言的自称(例如日本語、한국어),不应随当前 Locale 再次翻译。
|
||||
languageDropdown.SetUpRuntime(
|
||||
SettingsManager.instance.GetLanguageDropdownIndex(),
|
||||
MenuManager.instance.displayLanguageList);
|
||||
languageDropdown.updateValueAction = () =>
|
||||
{
|
||||
SettingsManager.instance.SetLanguageByDropdownIndex(languageDropdown.selectedIndex);
|
||||
|
||||
@@ -8,32 +8,34 @@ namespace Ichni.Menu
|
||||
{
|
||||
public class GraphicsSettingsWindow : SettingsWindow
|
||||
{
|
||||
private static readonly List<string> QualityPresetOptions = new List<string>
|
||||
private const string UiTable = "UI";
|
||||
|
||||
private static readonly string[] QualityPresetKeys =
|
||||
{
|
||||
"Performant",
|
||||
"Balanced",
|
||||
"High Fidelity"
|
||||
"ui_settings_quality_performance",
|
||||
"ui_settings_quality_balanced",
|
||||
"ui_settings_quality_high_fidelity"
|
||||
};
|
||||
|
||||
private static readonly List<string> DisplayModeOptions = new List<string>
|
||||
private static readonly string[] BloomQualityKeys =
|
||||
{
|
||||
"Fullscreen",
|
||||
"Borderless Fullscreen",
|
||||
"Windowed"
|
||||
"ui_settings_bloom_off",
|
||||
"ui_settings_bloom_performance",
|
||||
"ui_settings_bloom_standard"
|
||||
};
|
||||
|
||||
private static readonly List<string> BloomQualityOptions = new List<string>
|
||||
private static readonly string[] VfxQualityKeys =
|
||||
{
|
||||
"Off",
|
||||
"Performance",
|
||||
"Standard"
|
||||
"ui_settings_vfx_low",
|
||||
"ui_settings_vfx_medium",
|
||||
"ui_settings_vfx_high"
|
||||
};
|
||||
|
||||
private static readonly List<string> VfxQualityOptions = new List<string>
|
||||
private static readonly string[] DisplayModeKeys =
|
||||
{
|
||||
"Low",
|
||||
"Medium",
|
||||
"High"
|
||||
"ui_settings_display_fullscreen",
|
||||
"ui_settings_display_borderless_fullscreen",
|
||||
"ui_settings_display_windowed"
|
||||
};
|
||||
|
||||
private readonly List<Vector2Int> _availableDisplayResolutions = new List<Vector2Int>();
|
||||
@@ -52,15 +54,14 @@ namespace Ichni.Menu
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
qualityPresetDropdown.SetUp((int)gameSettings.graphicsQualityPreset, QualityPresetOptions,
|
||||
"Menu UI/Settings_Quality_Preset");
|
||||
qualityPresetDropdown.SetUpLocalized((int)gameSettings.graphicsQualityPreset, UiTable, QualityPresetKeys);
|
||||
qualityPresetDropdown.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.graphicsQualityPreset = (GraphicsQualityPreset)qualityPresetDropdown.selectedIndex;
|
||||
SettingsManager.instance.ApplyGraphicSettings();
|
||||
};
|
||||
|
||||
renderScaleModifier.SetUp(gameSettings.renderScaleLevel, 1, "Menu UI/Settings_Render_Scale");
|
||||
renderScaleModifier.SetUp(gameSettings.renderScaleLevel, 1);
|
||||
renderScaleModifier.SetMinMax(0, 5);
|
||||
renderScaleModifier.updateValueAction = () =>
|
||||
{
|
||||
@@ -68,7 +69,7 @@ namespace Ichni.Menu
|
||||
SettingsManager.instance.ApplyGraphicSettings();
|
||||
};
|
||||
|
||||
targetFrameModifier.SetUp(gameSettings.targetFrame, 30, "Menu UI/Settings_Target_Frame");
|
||||
targetFrameModifier.SetUp(gameSettings.targetFrame, 30);
|
||||
targetFrameModifier.SetMinMax(30, 120);
|
||||
targetFrameModifier.updateValueAction = () =>
|
||||
{
|
||||
@@ -119,21 +120,21 @@ namespace Ichni.Menu
|
||||
return;
|
||||
}
|
||||
|
||||
postProcessingSwitch.SetUp(gameSettings.enablePostProcessing, preservePrefabTitle: true);
|
||||
postProcessingSwitch.SetUp(gameSettings.enablePostProcessing);
|
||||
postProcessingSwitch.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.enablePostProcessing = postProcessingSwitch.GetValue();
|
||||
SettingsManager.instance.ApplyPostProcessingSettings();
|
||||
};
|
||||
|
||||
bloomQualityDropdown.SetUp((int)gameSettings.bloomQuality, BloomQualityOptions, preservePrefabTitle: true);
|
||||
bloomQualityDropdown.SetUpLocalized((int)gameSettings.bloomQuality, UiTable, BloomQualityKeys);
|
||||
bloomQualityDropdown.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.bloomQuality = (BloomQuality)bloomQualityDropdown.selectedIndex;
|
||||
SettingsManager.instance.ApplyPostProcessingSettings();
|
||||
};
|
||||
|
||||
vfxQualityDropdown.SetUp((int)gameSettings.vfxQuality, VfxQualityOptions, preservePrefabTitle: true);
|
||||
vfxQualityDropdown.SetUpLocalized((int)gameSettings.vfxQuality, UiTable, VfxQualityKeys);
|
||||
vfxQualityDropdown.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.vfxQuality = (VfxQuality)vfxQualityDropdown.selectedIndex;
|
||||
@@ -145,23 +146,21 @@ namespace Ichni.Menu
|
||||
{
|
||||
BuildDisplayResolutionOptions();
|
||||
|
||||
vSyncSwitch.SetUp(gameSettings.vSyncEnabled, "Menu UI/Settings_VSync");
|
||||
vSyncSwitch.SetUp(gameSettings.vSyncEnabled);
|
||||
vSyncSwitch.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.vSyncEnabled = vSyncSwitch.GetValue();
|
||||
SettingsManager.instance.ApplyDisplaySettings();
|
||||
};
|
||||
|
||||
displayModeDropdown.SetUp((int)gameSettings.displayMode, DisplayModeOptions,
|
||||
"Menu UI/Settings_Display_Mode");
|
||||
displayModeDropdown.SetUpLocalized((int)gameSettings.displayMode, UiTable, DisplayModeKeys);
|
||||
displayModeDropdown.updateValueAction = () =>
|
||||
{
|
||||
gameSettings.displayMode = (GraphicsDisplayMode)displayModeDropdown.selectedIndex;
|
||||
SettingsManager.instance.ApplyDisplaySettings();
|
||||
};
|
||||
|
||||
displayResolutionDropdown.SetUp(GetDisplayResolutionIndex(), _displayResolutionOptions,
|
||||
"Menu UI/Settings_Display_Resolution");
|
||||
displayResolutionDropdown.SetUpRuntime(GetDisplayResolutionIndex(), _displayResolutionOptions);
|
||||
displayResolutionDropdown.updateValueAction = () =>
|
||||
{
|
||||
Vector2Int resolution = _availableDisplayResolutions[displayResolutionDropdown.selectedIndex];
|
||||
|
||||
@@ -25,10 +25,10 @@ namespace Ichni.Menu
|
||||
foreach (RebindActionData data in rebindActions["Keyboard"])
|
||||
{
|
||||
// 在C# 8.0+ 中,可以使用静态匿名函数来避免闭包分配
|
||||
data.rebindButton.SetUp(data.title, data.subtitle, data.actionName, data.bindingIndex);
|
||||
data.rebindButton.SetUp(data.actionName, data.bindingIndex);
|
||||
}
|
||||
|
||||
resetButton.SetUp("Menu UI/Settings_ResetRebinding", "", "Menu UI/Settings_Confirm");
|
||||
resetButton.SetUp();
|
||||
resetButton.updateValueAction = ResetAllBindings;
|
||||
}
|
||||
|
||||
@@ -114,9 +114,6 @@ namespace Ichni.Menu
|
||||
[System.Serializable]
|
||||
public class RebindActionData
|
||||
{
|
||||
public string title;
|
||||
[FormerlySerializedAs("description")] public string subtitle;
|
||||
|
||||
[Tooltip("必须与Input Actions Asset中的Action名称完全匹配")]
|
||||
public string actionName;
|
||||
|
||||
@@ -132,4 +129,4 @@ namespace Ichni.Menu
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user