334 lines
13 KiB
C#
334 lines
13 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|