微调
This commit is contained in:
158
Assets/Scripts/Settings/LocalizationBootstrap.cs
Normal file
158
Assets/Scripts/Settings/LocalizationBootstrap.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Ichni.Diagnostics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
|
||||
namespace Ichni
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity Localization 的统一启动入口。
|
||||
/// <para>
|
||||
/// 本类只包装 Unity 官方提供的 <see cref="LocalizationSettings.InitializationOperation"/>,
|
||||
/// 不自行初始化 Addressables、不查询 Locale Label,也不复制 Localization Package 的内部加载流程。
|
||||
/// 因此 Settings、Yarn 与其它本地化消费者始终等待同一个由 Unity 管理的初始化操作。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class LocalizationBootstrap
|
||||
{
|
||||
public enum InitializationState
|
||||
{
|
||||
NotStarted,
|
||||
Initializing,
|
||||
Ready,
|
||||
Failed
|
||||
}
|
||||
|
||||
private static AsyncOperationHandle<LocalizationSettings> _initializationOperation;
|
||||
|
||||
public static InitializationState State { get; private set; } = InitializationState.NotStarted;
|
||||
public static bool IsReady => State == InitializationState.Ready;
|
||||
public static bool HasFailed => State == InitializationState.Failed;
|
||||
public static string FailureReason { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unity Localization 官方初始化 Handle。
|
||||
/// 获取该属性会以幂等方式启动初始化,但不会同步阻塞主线程。
|
||||
/// </summary>
|
||||
public static AsyncOperationHandle<LocalizationSettings> InitializationOperation
|
||||
{
|
||||
get
|
||||
{
|
||||
BeginInitialization();
|
||||
return _initializationOperation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在关闭 Domain Reload 的 Editor 配置下,清理上一次 Play Mode 留下的项目侧状态。
|
||||
/// Localization Package 会自行重建其内部 Handle,本类不持有也不释放 Package 的资源。
|
||||
/// </summary>
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetState()
|
||||
{
|
||||
_initializationOperation = default;
|
||||
State = InitializationState.NotStarted;
|
||||
FailureReason = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 Unity 官方流程获取唯一的 Localization 初始化操作。
|
||||
/// 可以由多个系统重复调用;已经启动后不会创建第二条加载链。
|
||||
/// </summary>
|
||||
public static void BeginInitialization()
|
||||
{
|
||||
if (State != InitializationState.NotStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 不设置 InitializeSynchronously,也不直接访问 SelectedLocale。
|
||||
// InitializationOperation 会异步完成 Locale 选择及所有配置为 Preload 的数据库初始化。
|
||||
_initializationOperation = LocalizationSettings.InitializationOperation;
|
||||
State = InitializationState.Initializing;
|
||||
Log("开始等待 Unity Localization 官方 InitializationOperation。");
|
||||
RefreshState();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Fail("无法创建 Unity Localization 初始化操作。", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新初始化结果。供 YarnTask 等不能直接 yield AsyncOperationHandle 的异步流程调用。
|
||||
/// </summary>
|
||||
public static void RefreshState()
|
||||
{
|
||||
if (State != InitializationState.Initializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_initializationOperation.IsValid())
|
||||
{
|
||||
Fail("Unity Localization 返回了无效的 InitializationOperation Handle。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_initializationOperation.IsDone)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_initializationOperation.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Fail("Unity Localization 初始化失败。", _initializationOperation.OperationException);
|
||||
return;
|
||||
}
|
||||
|
||||
LocaleReady();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 供 MonoBehaviour 协程等待 Unity Localization 初始化。
|
||||
/// 此处直接 yield 官方 Handle,不使用轮询超时或自建 Addressables 状态机。
|
||||
/// </summary>
|
||||
public static IEnumerator WaitForInitialization()
|
||||
{
|
||||
BeginInitialization();
|
||||
|
||||
if (State == InitializationState.Initializing)
|
||||
{
|
||||
yield return _initializationOperation;
|
||||
RefreshState();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LocaleReady()
|
||||
{
|
||||
State = InitializationState.Ready;
|
||||
FailureReason = null;
|
||||
|
||||
// SelectedLocale 只有在完整 InitializationOperation 成功后才允许同步读取。
|
||||
string localeCode = LocalizationSettings.SelectedLocale != null
|
||||
? LocalizationSettings.SelectedLocale.Identifier.Code
|
||||
: "<null>";
|
||||
Log($"Unity Localization 初始化完成。SelectedLocale={localeCode}");
|
||||
}
|
||||
|
||||
private static void Fail(string message, Exception exception = null)
|
||||
{
|
||||
FailureReason = exception == null ? message : $"{message} Exception={exception}";
|
||||
State = InitializationState.Failed;
|
||||
|
||||
Debug.LogError($"[LocalizationBootstrap] {FailureReason}");
|
||||
BuildHangTracer.Log("LOCALIZATION_BOOTSTRAP", $"[Failed] {FailureReason}");
|
||||
}
|
||||
|
||||
private static void Log(string message)
|
||||
{
|
||||
Debug.Log($"[LocalizationBootstrap] {message}");
|
||||
BuildHangTracer.Log("LOCALIZATION_BOOTSTRAP", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Settings/LocalizationBootstrap.cs.meta
Normal file
11
Assets/Scripts/Settings/LocalizationBootstrap.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a3bb067e72f4b1ca56834f12479b841
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,7 +1,10 @@
|
||||
using Ichni.Menu;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Diagnostics;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
@@ -10,6 +13,7 @@ using UnityEngine.Serialization;
|
||||
|
||||
namespace Ichni
|
||||
{
|
||||
[DefaultExecutionOrder(-10000)]
|
||||
public class SettingsManager : SerializedMonoBehaviour
|
||||
{
|
||||
private const string SettingsSaveFileName = "SettingsSave.json";
|
||||
@@ -19,6 +23,21 @@ namespace Ichni
|
||||
private const string BalancedQualityName = "Balanced";
|
||||
private const string HighFidelityQualityName = "High Fidelity";
|
||||
|
||||
/// <summary>
|
||||
/// 与设置界面 Dropdown 顺序严格一致的 Locale Code。
|
||||
/// 不使用 AvailableLocales.Locales 的返回顺序,因为 Addressables Catalog 的条目顺序不属于稳定存档契约。
|
||||
/// </summary>
|
||||
private static readonly string[] SupportedLocaleCodes =
|
||||
{
|
||||
"zh-CN",
|
||||
"en",
|
||||
"zh-TW",
|
||||
"ja",
|
||||
"ko",
|
||||
"vi-VN",
|
||||
"th"
|
||||
};
|
||||
|
||||
public static SettingsManager instance;
|
||||
|
||||
[FormerlySerializedAs("settingsPage")]
|
||||
@@ -30,11 +49,17 @@ namespace Ichni
|
||||
// 记录相机在玩家关闭后处理前的原始状态。重新开启时必须恢复原始值,不能无条件将所有相机改为开启。
|
||||
private readonly Dictionary<int, bool> postProcessingCameraStates = new Dictionary<int, bool>();
|
||||
|
||||
// 语言切换请求统一由一个协程串行处理。协程运行期间的新请求只覆盖目标索引。
|
||||
private Coroutine applyLanguageSettingsCoroutine;
|
||||
private bool isLanguageRoutineRunning;
|
||||
private int pendingLanguageIndex = -1;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = this;
|
||||
|
||||
DontDestroyOnLoad(gameObject);
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
RenderPipelineManager.beginCameraRendering += ApplyPostProcessingSettingBeforeCameraRender;
|
||||
@@ -54,6 +79,14 @@ namespace Ichni
|
||||
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
RenderPipelineManager.beginCameraRendering -= ApplyPostProcessingSettingBeforeCameraRender;
|
||||
|
||||
if (applyLanguageSettingsCoroutine != null)
|
||||
{
|
||||
StopCoroutine(applyLanguageSettingsCoroutine);
|
||||
applyLanguageSettingsCoroutine = null;
|
||||
}
|
||||
|
||||
isLanguageRoutineRunning = false;
|
||||
instance = null;
|
||||
}
|
||||
|
||||
@@ -82,9 +115,10 @@ namespace Ichni
|
||||
{
|
||||
settingsSaveData = CreateDefaultSettingsSaveData();
|
||||
}
|
||||
|
||||
Debug.Log($"[SettingsManager] 已加载存档:{savePath},当前语言索引={settingsSaveData.languageIndex}");
|
||||
ApplyAudioSettings();
|
||||
ApplyGraphicSettings();
|
||||
ApplyLanguageSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,10 +126,10 @@ namespace Ichni
|
||||
/// </summary>
|
||||
public void ApplyAudioSettings()
|
||||
{
|
||||
AkSoundEngine.SetRTPCValue("MasterVolume", settingsSaveData.masterVolume);
|
||||
AkSoundEngine.SetRTPCValue("MusicVolume", settingsSaveData.musicVolume);
|
||||
AkSoundEngine.SetRTPCValue("SoundFXVolume", settingsSaveData.soundEffectVolume);
|
||||
AkSoundEngine.SetRTPCValue("UIVolume", settingsSaveData.uiVolume);
|
||||
AkUnitySoundEngine.SetRTPCValue("MasterVolume", settingsSaveData.masterVolume);
|
||||
AkUnitySoundEngine.SetRTPCValue("MusicVolume", settingsSaveData.musicVolume);
|
||||
AkUnitySoundEngine.SetRTPCValue("SoundFXVolume", settingsSaveData.soundEffectVolume);
|
||||
AkUnitySoundEngine.SetRTPCValue("UIVolume", settingsSaveData.uiVolume);
|
||||
ApplyInGameAudioEffectsSettings();
|
||||
}
|
||||
|
||||
@@ -312,14 +346,143 @@ namespace Ichni
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前语言索引应用到既有的 I2 与 Unity Localization 管线。
|
||||
/// 语言将于 Localization 重构时改为稳定的 Locale ID;本阶段只迁移数据承载方式。
|
||||
/// 将当前语言索引应用到 Unity Localization 管线。
|
||||
/// 本方法只等待 Unity 官方 InitializationOperation,并在其完成后应用玩家最新一次语言选择。
|
||||
/// </summary>
|
||||
public void ApplyLanguageSettings()
|
||||
{
|
||||
I2.Loc.LocalizationManager.CurrentLanguage = MenuManager.instance.languageList[settingsSaveData.languageIndex];
|
||||
LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.Locales[settingsSaveData.languageIndex];
|
||||
I2.Loc.LocalizationManager.UpdateSources();
|
||||
if (settingsSaveData == null)
|
||||
{
|
||||
BuildHangTracer.Log("SETTINGS_MGR", "[ApplyLanguageSettings] settingsSaveData 为空,取消语言切换。");
|
||||
return;
|
||||
}
|
||||
|
||||
pendingLanguageIndex = settingsSaveData.languageIndex;
|
||||
|
||||
if (isLanguageRoutineRunning)
|
||||
{
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_MGR",
|
||||
$"[ApplyLanguageSettings] 已有语言切换流程运行,更新待处理索引为 {pendingLanguageIndex}。");
|
||||
return;
|
||||
}
|
||||
|
||||
isLanguageRoutineRunning = true;
|
||||
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_MGR",
|
||||
$"[ApplyLanguageSettings] 启动语言切换流程,目标索引={pendingLanguageIndex}。");
|
||||
|
||||
//LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale(SupportedLocaleCodes[pendingLanguageIndex]);
|
||||
|
||||
applyLanguageSettingsCoroutine = StartCoroutine(ApplyLanguageSettingsRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator ApplyLanguageSettingsRoutine()
|
||||
{
|
||||
// try/finally 确保协程正常结束、被 StopCoroutine 或发生异常时,运行状态都会被清除。
|
||||
try
|
||||
{
|
||||
// 避开 Dropdown.onValueChanged 当前同步调用栈。
|
||||
yield return null;
|
||||
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] 等待 Unity Localization InitializationOperation。" +
|
||||
$"CurrentState={LocalizationBootstrap.State}");
|
||||
|
||||
yield return LocalizationBootstrap.WaitForInitialization();
|
||||
if (!LocalizationBootstrap.IsReady)
|
||||
{
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] Unity Localization 初始化失败,取消语言切换。" +
|
||||
$"Reason={LocalizationBootstrap.FailureReason}");
|
||||
pendingLanguageIndex = -1;
|
||||
yield break;
|
||||
}
|
||||
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] Unity Localization 已就绪。启动 Locale=" +
|
||||
$"{LocalizationSettings.SelectedLocale?.Identifier.Code ?? "<null>"}");
|
||||
|
||||
// 处理流程运行期间收到的最新请求。
|
||||
while (pendingLanguageIndex >= 0)
|
||||
{
|
||||
int requestedLanguageIndex = pendingLanguageIndex;
|
||||
pendingLanguageIndex = -1;
|
||||
|
||||
if (requestedLanguageIndex < 0 ||
|
||||
requestedLanguageIndex >= SupportedLocaleCodes.Length)
|
||||
{
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] 语言索引无效。Index={requestedLanguageIndex}," +
|
||||
$"SupportedCount={SupportedLocaleCodes.Length}。回退到简体中文。");
|
||||
|
||||
// 旧版本存档或损坏存档可能留下越界索引。将它修正为稳定的默认值,
|
||||
// 避免首次启动时保持一个不受 SettingsSave 控制的 Locale。
|
||||
requestedLanguageIndex = 0;
|
||||
settingsSaveData.languageIndex = 0;
|
||||
}
|
||||
|
||||
string targetLocaleCode = SupportedLocaleCodes[requestedLanguageIndex];
|
||||
Locale targetLocale = LocalizationSettings.AvailableLocales.GetLocale(targetLocaleCode);
|
||||
Locale currentLocale = LocalizationSettings.SelectedLocale;
|
||||
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] 准备切换 Locale:" +
|
||||
$"{currentLocale?.Identifier.Code ?? "<null>"} -> " +
|
||||
$"{targetLocale?.Identifier.Code ?? "<null>"}");
|
||||
|
||||
if (targetLocale == null)
|
||||
{
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] Localization Settings 中不存在 Locale '{targetLocaleCode}',取消本次切换。");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentLocale == targetLocale)
|
||||
{
|
||||
BuildHangTracer.Log("SETTINGS_COROUTINE", "[Coroutine] 目标 Locale 与当前 Locale 相同,跳过切换。");
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalizationSettings.SelectedLocale = targetLocale;
|
||||
|
||||
// setter 会同步更新 SelectedLocale,并通知 LocalizedString / LocalizedAsset 按需刷新。
|
||||
// 初始化已在本协程开头完成,因此此处不会触发同步 Locale 加载。
|
||||
yield return null;
|
||||
|
||||
Locale appliedLocale = LocalizationSettings.SelectedLocale;
|
||||
if (appliedLocale == targetLocale)
|
||||
{
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"<<< [CRITICAL] SelectedLocale 已切换:{targetLocale.Identifier.Code}");
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] SelectedLocale 校验失败。Expected={targetLocale.Identifier.Code}," +
|
||||
$"Actual={appliedLocale?.Identifier.Code ?? "<null>"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
applyLanguageSettingsCoroutine = null;
|
||||
isLanguageRoutineRunning = false;
|
||||
|
||||
BuildHangTracer.Log(
|
||||
"SETTINGS_COROUTINE",
|
||||
$"[Coroutine] 语言切换流程结束。PendingIndex={pendingLanguageIndex}");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user