Files
ichni_Official/Assets/Scripts/Settings/SettingsManager.cs
SoulliesOfficial fe00ecfcc7 微调
2026-07-24 03:43:11 -04:00

497 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
namespace Ichni
{
[DefaultExecutionOrder(-10000)]
public class SettingsManager : SerializedMonoBehaviour
{
private const string SettingsSaveFileName = "SettingsSave.json";
private const string SettingsSaveKey = "SettingsSave";
private const string PerformantQualityName = "Performant";
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")]
public SettingsUIPage settingsUIPage;
[FormerlySerializedAs("gameSettings")]
public SettingsSaveData settingsSaveData;
// 记录相机在玩家关闭后处理前的原始状态。重新开启时必须恢复原始值,不能无条件将所有相机改为开启。
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;
}
else
{
Destroy(gameObject);
}
}
private void OnDestroy()
{
if (instance != this)
{
return;
}
SceneManager.sceneLoaded -= OnSceneLoaded;
RenderPipelineManager.beginCameraRendering -= ApplyPostProcessingSettingBeforeCameraRender;
if (applyLanguageSettingsCoroutine != null)
{
StopCoroutine(applyLanguageSettingsCoroutine);
applyLanguageSettingsCoroutine = null;
}
isLanguageRoutineRunning = false;
instance = null;
}
private void Start()
{
LoadGameSettings();
}
[Button]
public void SaveGameSettings()
{
string savePath = Application.persistentDataPath + "/GameData/" + SettingsSaveFileName;
ES3.Save(SettingsSaveKey, settingsSaveData, savePath);
}
[Button]
public void LoadGameSettings()
{
string savePath = Application.persistentDataPath + "/GameData/" + SettingsSaveFileName;
if (ES3.FileExists(savePath))
{
// 保留原有 ES3 Key确保本次数据模型重命名不会让已有本地设置失效。
settingsSaveData = ES3.Load<SettingsSaveData>(SettingsSaveKey, savePath);
}
else
{
settingsSaveData = CreateDefaultSettingsSaveData();
}
Debug.Log($"[SettingsManager] 已加载存档:{savePath},当前语言索引={settingsSaveData.languageIndex}");
ApplyAudioSettings();
ApplyGraphicSettings();
ApplyLanguageSettings();
}
/// <summary>
/// 将音频分类的当前设置写入 Wwise RTPC。
/// </summary>
public void ApplyAudioSettings()
{
AkUnitySoundEngine.SetRTPCValue("MasterVolume", settingsSaveData.masterVolume);
AkUnitySoundEngine.SetRTPCValue("MusicVolume", settingsSaveData.musicVolume);
AkUnitySoundEngine.SetRTPCValue("SoundFXVolume", settingsSaveData.soundEffectVolume);
AkUnitySoundEngine.SetRTPCValue("UIVolume", settingsSaveData.uiVolume);
ApplyInGameAudioEffectsSettings();
}
/// <summary>
/// 应用游戏内音乐特效开关。关闭时立即清除歌曲正在使用的高通与低通滤波,
/// 防止设置变更后残留上一帧的 RTPC 值。
/// </summary>
public void ApplyInGameAudioEffectsSettings()
{
if (settingsSaveData.enableInGameAudioEffects)
{
return;
}
GameManager.Instance?.songPlayer?.ResetInGameAudioEffects();
}
/// <summary>
/// 将现有图形设置应用到运行时。更细的 Quality、VSync 与平台策略会在 Graphics 分类重构时接入。
/// </summary>
public void ApplyGraphicSettings()
{
int qualityLevelIndex = GetQualityLevelIndex(settingsSaveData.graphicsQualityPreset);
if (QualitySettings.GetQualityLevel() != qualityLevelIndex)
{
QualitySettings.SetQualityLevel(qualityLevelIndex, true);
}
Application.targetFrameRate = settingsSaveData.targetFrame;
UniversalRenderPipelineAsset currentUrpAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
currentUrpAsset.renderScale = 0.5f + 0.1f * settingsSaveData.renderScaleLevel;
ApplyDisplaySettings();
ApplyPostProcessingSettings();
}
/// <summary>
/// 应用后处理总开关与 Bloom 档位。
/// 关闭总开关时,已存在的 URP 相机会直接关闭 <c>renderPostProcessing</c>,并关闭 GameScene 的 Global Volume。
/// 这样既不会执行原生后处理,也不会执行 ScriptablePostProcessor 的自定义 Render Pass。
/// </summary>
public void ApplyPostProcessingSettings()
{
bool enablePostProcessing = settingsSaveData.enablePostProcessing;
UniversalAdditionalCameraData[] cameraDataList =
FindObjectsByType<UniversalAdditionalCameraData>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (UniversalAdditionalCameraData cameraData in cameraDataList)
{
int instanceId = cameraData.GetInstanceID();
if (!postProcessingCameraStates.ContainsKey(instanceId))
{
postProcessingCameraStates.Add(instanceId, cameraData.renderPostProcessing);
}
cameraData.renderPostProcessing = enablePostProcessing && postProcessingCameraStates[instanceId];
}
if (PostProcessingManager.Instance != null)
{
PostProcessingManager.Instance.ApplySettings(enablePostProcessing, settingsSaveData.bloomQuality);
}
}
/// <summary>
/// 处理场景切换后新加载的相机与 Global Volume。SettingsManager 会跨场景保留,
/// 因此不能只在首次读取存档时应用一次图形设置。
/// </summary>
private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
if (settingsSaveData != null)
{
ApplyPostProcessingSettings();
}
}
/// <summary>
/// 兜底处理运行时晚于场景加载创建的相机(例如 Theme 或演出对象中的相机)。
/// 仅在玩家关闭后处理时执行,保证该相机在首次渲染前也不会运行后处理。
/// </summary>
private void ApplyPostProcessingSettingBeforeCameraRender(ScriptableRenderContext context, Camera camera)
{
if (settingsSaveData == null || settingsSaveData.enablePostProcessing ||
!camera.TryGetComponent(out UniversalAdditionalCameraData cameraData))
{
return;
}
int instanceId = cameraData.GetInstanceID();
if (!postProcessingCameraStates.ContainsKey(instanceId))
{
postProcessingCameraStates.Add(instanceId, cameraData.renderPostProcessing);
}
cameraData.renderPostProcessing = false;
}
/// <summary>
/// 将 VSync、窗口模式和分辨率立即应用到 Standalone / Unity Editor。
/// 移动端由系统管理显示模式与分辨率,因此不执行这些设置。
/// </summary>
public void ApplyDisplaySettings()
{
#if UNITY_STANDALONE || UNITY_EDITOR
QualitySettings.vSyncCount = settingsSaveData.vSyncEnabled ? 1 : 0;
if (settingsSaveData.displayResolutionWidth <= 0 || settingsSaveData.displayResolutionHeight <= 0)
{
settingsSaveData.displayResolutionWidth = Screen.width;
settingsSaveData.displayResolutionHeight = Screen.height;
}
Screen.SetResolution(settingsSaveData.displayResolutionWidth, settingsSaveData.displayResolutionHeight,
GetFullScreenMode(settingsSaveData.displayMode));
#endif
}
/// <summary>
/// 首次创建设置存档时,继承 Project Settings 为当前平台指定的默认 Quality Level。
/// 之后玩家的选择由 SettingsSaveData 持久化,不会在下次启动时被平台默认值覆盖。
/// </summary>
private SettingsSaveData CreateDefaultSettingsSaveData()
{
SettingsSaveData defaultSettings = new SettingsSaveData();
defaultSettings.graphicsQualityPreset = GetGraphicsQualityPreset(QualitySettings.GetQualityLevel());
#if UNITY_STANDALONE || UNITY_EDITOR
defaultSettings.displayMode = GetGraphicsDisplayMode(Screen.fullScreenMode);
defaultSettings.displayResolutionWidth = Screen.width;
defaultSettings.displayResolutionHeight = Screen.height;
#endif
return defaultSettings;
}
private FullScreenMode GetFullScreenMode(GraphicsDisplayMode displayMode)
{
return displayMode switch
{
GraphicsDisplayMode.Fullscreen => FullScreenMode.ExclusiveFullScreen,
GraphicsDisplayMode.Windowed => FullScreenMode.Windowed,
_ => FullScreenMode.FullScreenWindow
};
}
private GraphicsDisplayMode GetGraphicsDisplayMode(FullScreenMode fullScreenMode)
{
return fullScreenMode switch
{
FullScreenMode.ExclusiveFullScreen => GraphicsDisplayMode.Fullscreen,
FullScreenMode.Windowed => GraphicsDisplayMode.Windowed,
_ => GraphicsDisplayMode.BorderlessFullscreen
};
}
/// <summary>
/// 将存档枚举映射到 Project Settings 中同名的 Quality Level。
/// 若配置缺失,回退到 Balanced这是移动端的首发默认档位。
/// </summary>
private int GetQualityLevelIndex(GraphicsQualityPreset qualityPreset)
{
string qualityName = GetQualityName(qualityPreset);
string[] qualityNames = QualitySettings.names;
for (int index = 0; index < qualityNames.Length; index++)
{
if (qualityNames[index] == qualityName)
{
return index;
}
}
Debug.LogWarning($"[SettingsManager] 未找到 Quality Level '{qualityName}',回退至 Balanced。");
return FindQualityLevelOrFallback(BalancedQualityName);
}
/// <summary>
/// 将当前 Quality Level 索引转换为可保存的枚举。未知的自定义档位统一按 Balanced 保存。
/// </summary>
private GraphicsQualityPreset GetGraphicsQualityPreset(int qualityLevelIndex)
{
string[] qualityNames = QualitySettings.names;
if (qualityLevelIndex < 0 || qualityLevelIndex >= qualityNames.Length)
{
return GraphicsQualityPreset.Balanced;
}
return qualityNames[qualityLevelIndex] switch
{
PerformantQualityName => GraphicsQualityPreset.Performant,
HighFidelityQualityName => GraphicsQualityPreset.HighFidelity,
_ => GraphicsQualityPreset.Balanced
};
}
private int FindQualityLevelOrFallback(string qualityName)
{
string[] qualityNames = QualitySettings.names;
for (int index = 0; index < qualityNames.Length; index++)
{
if (qualityNames[index] == qualityName)
{
return index;
}
}
return Mathf.Clamp(QualitySettings.GetQualityLevel(), 0, qualityNames.Length - 1);
}
private string GetQualityName(GraphicsQualityPreset qualityPreset)
{
return qualityPreset switch
{
GraphicsQualityPreset.Performant => PerformantQualityName,
GraphicsQualityPreset.HighFidelity => HighFidelityQualityName,
_ => BalancedQualityName
};
}
/// <summary>
/// 将当前语言索引应用到 Unity Localization 管线。
/// 本方法只等待 Unity 官方 InitializationOperation并在其完成后应用玩家最新一次语言选择。
/// </summary>
public void ApplyLanguageSettings()
{
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>
/// 判断本谱面是否启用全屏判定。谱面强制要求的配置优先于玩家开关。
/// </summary>
public bool ShouldUseFullScreenJudge()
{
return settingsSaveData.judgeType || InformationTransistor.instance?.difficulty?.ForceFullScreenJudge == true;
}
}
}