Files
ichni_Official/Assets/Scripts/Settings/SettingsManager.cs
SoulliesOfficial 9fd5f098ab 设置扩展
2026-07-19 16:44:58 -04:00

334 lines
13 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.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Localization.Settings;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
namespace Ichni
{
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";
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 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;
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();
}
ApplyAudioSettings();
ApplyGraphicSettings();
}
/// <summary>
/// 将音频分类的当前设置写入 Wwise RTPC。
/// </summary>
public void ApplyAudioSettings()
{
AkSoundEngine.SetRTPCValue("MasterVolume", settingsSaveData.masterVolume);
AkSoundEngine.SetRTPCValue("MusicVolume", settingsSaveData.musicVolume);
AkSoundEngine.SetRTPCValue("SoundFXVolume", settingsSaveData.soundEffectVolume);
AkSoundEngine.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>
/// 将当前语言索引应用到既有的 I2 与 Unity Localization 管线。
/// 语言将于 Localization 重构时改为稳定的 Locale ID本阶段只迁移数据承载方式。
/// </summary>
public void ApplyLanguageSettings()
{
I2.Loc.LocalizationManager.CurrentLanguage = MenuManager.instance.languageList[settingsSaveData.languageIndex];
LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.Locales[settingsSaveData.languageIndex];
I2.Loc.LocalizationManager.UpdateSources();
}
/// <summary>
/// 判断本谱面是否启用全屏判定。谱面强制要求的配置优先于玩家开关。
/// </summary>
public bool ShouldUseFullScreenJudge()
{
return settingsSaveData.judgeType || InformationTransistor.instance?.difficulty?.ForceFullScreenJudge == true;
}
}
}