2025-08-11 14:04:06 -04:00
|
|
|
|
using Ichni.Menu;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
using System.Collections.Generic;
|
2025-06-06 10:14:55 -04:00
|
|
|
|
using Sirenix.OdinInspector;
|
|
|
|
|
|
using UnityEngine;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
using UnityEngine.Localization.Settings;
|
|
|
|
|
|
using UnityEngine.Rendering;
|
|
|
|
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
|
|
using UnityEngine.SceneManagement;
|
2025-06-06 10:14:55 -04:00
|
|
|
|
using UnityEngine.Serialization;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni
|
|
|
|
|
|
{
|
|
|
|
|
|
public class SettingsManager : SerializedMonoBehaviour
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
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";
|
|
|
|
|
|
|
2025-06-06 10:14:55 -04:00
|
|
|
|
public static SettingsManager instance;
|
|
|
|
|
|
|
2025-08-11 14:04:06 -04:00
|
|
|
|
[FormerlySerializedAs("settingsPage")]
|
|
|
|
|
|
public SettingsUIPage settingsUIPage;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
[FormerlySerializedAs("gameSettings")]
|
|
|
|
|
|
public SettingsSaveData settingsSaveData;
|
|
|
|
|
|
|
|
|
|
|
|
// 记录相机在玩家关闭后处理前的原始状态。重新开启时必须恢复原始值,不能无条件将所有相机改为开启。
|
|
|
|
|
|
private readonly Dictionary<int, bool> postProcessingCameraStates = new Dictionary<int, bool>();
|
2025-06-06 10:14:55 -04:00
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
2025-08-11 14:04:06 -04:00
|
|
|
|
if (instance == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
instance = this;
|
|
|
|
|
|
DontDestroyOnLoad(gameObject);
|
2026-07-19 16:44:58 -04:00
|
|
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
|
|
|
|
|
RenderPipelineManager.beginCameraRendering += ApplyPostProcessingSettingBeforeCameraRender;
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Destroy(gameObject);
|
|
|
|
|
|
}
|
2025-06-06 10:14:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
private void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (instance != this)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
|
|
|
|
RenderPipelineManager.beginCameraRendering -= ApplyPostProcessingSettingBeforeCameraRender;
|
|
|
|
|
|
instance = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 10:14:55 -04:00
|
|
|
|
private void Start()
|
|
|
|
|
|
{
|
|
|
|
|
|
LoadGameSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Button]
|
|
|
|
|
|
public void SaveGameSettings()
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
string savePath = Application.persistentDataPath + "/GameData/" + SettingsSaveFileName;
|
|
|
|
|
|
ES3.Save(SettingsSaveKey, settingsSaveData, savePath);
|
2025-06-06 10:14:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Button]
|
|
|
|
|
|
public void LoadGameSettings()
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
string savePath = Application.persistentDataPath + "/GameData/" + SettingsSaveFileName;
|
2025-06-06 10:14:55 -04:00
|
|
|
|
if (ES3.FileExists(savePath))
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
// 保留原有 ES3 Key,确保本次数据模型重命名不会让已有本地设置失效。
|
|
|
|
|
|
settingsSaveData = ES3.Load<SettingsSaveData>(SettingsSaveKey, savePath);
|
2025-06-06 10:14:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
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);
|
2025-06-06 10:14:55 -04:00
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
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;
|
2025-06-06 10:14:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
}
|