refactor(setting): 重构设置系统以支持数据和应用器分离

- 将SettingsModel内部存储分离为_dataSettings和_applicators两个字典
- 添加IDataSettings接口用于标识纯数据设置
- 修改Get方法为GetData以明确区分数据获取
- 添加RegisterApplicator和GetApplicator方法管理可应用设置
- 更新TryGet方法支持从数据和应用器中查找设置
- 扩展SettingsPersistence支持批量保存和加载所有设置数据
- 将AudioBusMap重命名为AudioBusMapSettings并实现ISettingsData接口
- 修改Godot音频和图形设置适配新的接口变更
- [skip ci]
This commit is contained in:
GeWuYou 2026-01-16 23:44:28 +08:00
parent 516a9e2281
commit 442e8e7088
11 changed files with 229 additions and 123 deletions

View File

@ -3,7 +3,7 @@
/// <summary>
/// 音频设置类,用于管理游戏中的音频配置
/// </summary>
public class AudioSettings : ISettingsSection
public class AudioSettings : ISettingsData
{
/// <summary>
/// 获取或设置主音量,控制所有音频的总体音量

View File

@ -3,7 +3,7 @@ namespace GFramework.Game.Abstractions.setting;
/// <summary>
/// 图形设置类,用于管理游戏的图形相关配置
/// </summary>
public class GraphicsSettings : ISettingsSection
public class GraphicsSettings : ISettingsData
{
/// <summary>
/// 获取或设置是否启用全屏模式

View File

@ -0,0 +1,6 @@
namespace GFramework.Game.Abstractions.setting;
/// <summary>
/// 设置数据接口 - 纯数据,可自动创建
/// </summary>
public interface ISettingsData : ISettingsSection;

View File

@ -10,11 +10,11 @@ namespace GFramework.Game.Abstractions.setting;
public interface ISettingsModel : IModel
{
/// <summary>
/// 获取指定类型的设置节实例
/// 获取或创建数据设置(自动创建)
/// </summary>
/// <typeparam name="T">设置节的类型必须是class、实现ISettingsSection接口且具有无参构造函数</typeparam>
/// <returns>指定类型的设置实例</returns>
T Get<T>() where T : class, ISettingsSection, new();
/// <typeparam name="T">设置数据的类型必须继承自class、ISettingsData且具有无参构造函数</typeparam>
/// <returns>指定类型的设置数据实例</returns>
T GetData<T>() where T : class, ISettingsData, new();
/// <summary>
/// 尝试获取指定类型的设置节实例
@ -24,6 +24,13 @@ public interface ISettingsModel : IModel
/// <returns>如果找到指定类型的设置节则返回true否则返回false</returns>
bool TryGet(Type type, out ISettingsSection section);
/// <summary>
/// 获取已注册的可应用设置
/// </summary>
/// <typeparam name="T">可应用设置的类型必须继承自class和IApplyAbleSettings</typeparam>
/// <returns>指定类型的可应用设置实例如果不存在则返回null</returns>
T? GetApplicator<T>() where T : class, IApplyAbleSettings;
/// <summary>
/// 获取所有设置节的集合
/// </summary>
@ -31,8 +38,9 @@ public interface ISettingsModel : IModel
IEnumerable<ISettingsSection> All();
/// <summary>
/// 注册一个可应用的设置对象
/// 注册可应用设置(必须手动注册)
/// </summary>
/// <param name="applyAble">要注册的可应用设置对象</param>
void Register(IApplyAbleSettings applyAble);
/// <typeparam name="T">可应用设置的类型必须继承自class和IApplyAbleSettings</typeparam>
/// <param name="applicator">要注册的可应用设置实例</param>
void RegisterApplicator<T>(T applicator) where T : class, IApplyAbleSettings;
}

View File

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GFramework.Game.Abstractions.setting;
@ -9,31 +11,32 @@ namespace GFramework.Game.Abstractions.setting;
public interface ISettingsPersistence
{
/// <summary>
/// 异步加载指定类型的设置
/// 异步加载指定类型的设置数据
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口并具有无参构造函数</typeparam>
/// <returns>返回加载的设置节实例</returns>
Task<T> LoadAsync<T>() where T : class, ISettingsSection, new();
Task<T> LoadAsync<T>() where T : class, ISettingsData, new();
/// <summary>
/// 异步保存指定的设置
/// 异步保存指定的设置数据
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口</typeparam>
/// <param name="section">要保存的设置节实例</param>
/// <returns>异步操作任务</returns>
Task SaveAsync<T>(T section) where T : class, ISettingsSection;
Task SaveAsync<T>(T section) where T : class, ISettingsData;
/// <summary>
/// 异步检查指定类型的设置是否存在
/// 异步检查指定类型的设置数据是否存在
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口</typeparam>
/// <returns>如果设置节存在则返回true否则返回false</returns>
Task<bool> ExistsAsync<T>() where T : class, ISettingsSection;
Task<bool> ExistsAsync<T>() where T : class, ISettingsData;
/// <summary>
/// 异步删除指定类型的设置
/// 异步删除指定类型的设置数据
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口</typeparam>
/// <returns>异步操作任务</returns>
Task DeleteAsync<T>() where T : class, ISettingsSection;
Task DeleteAsync<T>() where T : class, ISettingsData;
/// <summary>
/// 保存所有设置数据
/// </summary>
Task SaveAllAsync(IEnumerable<ISettingsData> allData);
/// <summary>
/// 加载所有已知类型的设置数据
/// </summary>
Task<IDictionary<Type, ISettingsData>> LoadAllAsync(IEnumerable<Type> knownTypes);
}

View File

@ -8,53 +8,87 @@ namespace GFramework.Game.setting;
/// </summary>
public class SettingsModel : AbstractModel, ISettingsModel
{
private readonly Dictionary<Type, ISettingsSection> _sections = new();
private readonly Dictionary<Type, IApplyAbleSettings> _applicators = new();
private readonly Dictionary<Type, ISettingsData> _dataSettings = new();
/// <summary>
/// 获取指定类型的设置部分实例,如果不存在则创建新的实例
/// 获取或创建数据设置
/// </summary>
/// <typeparam name="T">设置部分的类型必须实现ISettingsSection接口并具有无参构造函数</typeparam>
/// <returns>指定类型的设置部分实例</returns>
public T Get<T>() where T : class, ISettingsSection, new()
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口并具有无参构造函数</typeparam>
/// <returns>指定类型的设置数据实例</returns>
public T GetData<T>() where T : class, ISettingsData, new()
{
var type = typeof(T);
// 尝试从字典中获取已存在的设置部分实例
if (_sections.TryGetValue(type, out var existing))
// 尝试从现有字典中获取已存在的设置数据
if (_dataSettings.TryGetValue(type, out var existing))
return (T)existing;
// 创建新的设置部分实例并存储到字典中
// 创建新的设置数据实例并存储到字典中
var created = new T();
_sections[type] = created;
_dataSettings[type] = created;
return created;
}
/// <summary>
/// 尝试获取指定类型的设置部分实例
/// 注册可应用设置
/// </summary>
/// <param name="type">设置部分的类型</param>
/// <param name="section">输出参数如果找到则返回对应的设置部分实例否则为null</param>
/// <returns>如果找到指定类型的设置部分则返回true否则返回false</returns>
public bool TryGet(Type type, out ISettingsSection section)
=> _sections.TryGetValue(type, out section!);
/// <summary>
/// 获取所有设置部分的集合
/// </summary>
/// <returns>包含所有设置部分的可枚举集合</returns>
public IEnumerable<ISettingsSection> All()
=> _sections.Values;
/// <summary>
/// 注册一个可应用的设置对象到管理器中
/// </summary>
/// <param name="applyAble">要注册的可应用设置对象</param>
public void Register(IApplyAbleSettings applyAble)
/// <typeparam name="T">可应用设置类型必须实现IApplyAbleSettings接口</typeparam>
/// <param name="applicator">要注册的可应用设置实例</param>
public void RegisterApplicator<T>(T applicator) where T : class, IApplyAbleSettings
{
// 获取传入对象的类型信息
var type = applyAble.GetType();
// 尝试将类型和对象添加到线程安全的字典中
_sections.TryAdd(type, applyAble);
var type = typeof(T);
_applicators[type] = applicator;
}
/// <summary>
/// 获取已注册的可应用设置
/// </summary>
/// <typeparam name="T">可应用设置类型必须实现IApplyAbleSettings接口</typeparam>
/// <returns>找到的可应用设置实例如果未找到则返回null</returns>
public T? GetApplicator<T>() where T : class, IApplyAbleSettings
{
var type = typeof(T);
return _applicators.TryGetValue(type, out var applicator)
? (T)applicator
: null;
}
/// <summary>
/// 尝试获取指定类型的设置节
/// </summary>
/// <param name="type">要查找的设置类型</param>
/// <param name="section">输出参数,找到的设置节实例</param>
/// <returns>如果找到设置节则返回true否则返回false</returns>
public bool TryGet(Type type, out ISettingsSection section)
{
// 首先在数据设置字典中查找
if (_dataSettings.TryGetValue(type, out var data))
{
section = data;
return true;
}
// 然后在应用器字典中查找
if (_applicators.TryGetValue(type, out var applicator))
{
section = applicator;
return true;
}
section = null!;
return false;
}
/// <summary>
/// 获取所有设置节的集合
/// </summary>
/// <returns>包含所有设置节的可枚举集合</returns>
public IEnumerable<ISettingsSection> All()
{
// 合并数据设置和应用器设置的所有值
return _dataSettings.Values
.Concat(_applicators.Values.Cast<ISettingsSection>());
}

View File

@ -13,11 +13,11 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
private IStorage _storage = null!;
/// <summary>
/// 异步加载指定类型的设置数据
/// 异步加载指定类型的设置数据
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口</typeparam>
/// <returns>如果存在则返回已保存的设置数据,否则返回新创建的默认设置实例</returns>
public async Task<T> LoadAsync<T>() where T : class, ISettingsSection, new()
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
/// <returns>如果存在则返回的设置数据,否则返回新创建的实例</returns>
public async Task<T> LoadAsync<T>() where T : class, ISettingsData, new()
{
var key = GetKey<T>();
@ -30,32 +30,32 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
}
/// <summary>
/// 异步保存设置数据到存储中
/// 异步保存设置数据到存储中
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口</typeparam>
/// <param name="section">要保存的设置实例</param>
public async Task SaveAsync<T>(T section) where T : class, ISettingsSection
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
/// <param name="section">要保存的设置数据实例</param>
public async Task SaveAsync<T>(T section) where T : class, ISettingsData
{
var key = GetKey<T>();
await _storage.WriteAsync(key, section);
}
/// <summary>
/// 异步检查指定类型的设置节是否存在
/// 检查指定类型的设置数据是否存在
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口</typeparam>
/// <returns>如果设置节存在返回true否则返回false</returns>
public async Task<bool> ExistsAsync<T>() where T : class, ISettingsSection
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
/// <returns>如果存在返回true否则返回false</returns>
public async Task<bool> ExistsAsync<T>() where T : class, ISettingsData
{
var key = GetKey<T>();
return await _storage.ExistsAsync(key);
}
/// <summary>
/// 异步删除指定类型的设置数据
/// 异步删除指定类型的设置数据
/// </summary>
/// <typeparam name="T">设置节类型必须实现ISettingsSection接口</typeparam>
public async Task DeleteAsync<T>() where T : class, ISettingsSection
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
public async Task DeleteAsync<T>() where T : class, ISettingsData
{
var key = GetKey<T>();
_storage.Delete(key);
@ -63,18 +63,66 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
}
/// <summary>
/// 初始化方法,获取存储服务实例
/// 异步保存所有设置数据到存储中
/// </summary>
/// <param name="allData">包含所有设置数据的可枚举集合</param>
public async Task SaveAllAsync(IEnumerable<ISettingsData> allData)
{
foreach (var data in allData)
{
var type = data.GetType();
var key = GetKey(type);
await _storage.WriteAsync(key, data);
}
}
/// <summary>
/// 异步加载所有已知类型的设置数据
/// </summary>
/// <param name="knownTypes">已知设置数据类型的集合</param>
/// <returns>类型与对应设置数据的字典映射</returns>
public async Task<IDictionary<Type, ISettingsData>> LoadAllAsync(IEnumerable<Type> knownTypes)
{
var result = new Dictionary<Type, ISettingsData>();
foreach (var type in knownTypes)
{
var key = GetKey(type);
if (!await _storage.ExistsAsync(key)) continue;
// 使用反射调用泛型方法
var method = typeof(IStorage)
.GetMethod(nameof(IStorage.ReadAsync))!
.MakeGenericMethod(type);
var task = (Task)method.Invoke(_storage, [key])!;
await task;
var loaded = (ISettingsData)((dynamic)task).Result;
result[type] = loaded;
}
return result;
}
protected override void OnInit()
{
_storage = this.GetUtility<IStorage>()!;
}
/// <summary>
/// 获取设置节对应的存储键名
/// 获取指定类型的存储键名
/// </summary>
/// <typeparam name="T">设置节类型</typeparam>
/// <returns>格式为"Settings_类型名称"的键名字符串</returns>
private static string GetKey<T>() where T : ISettingsSection
=> $"Settings_{typeof(T).Name}";
/// <typeparam name="T">设置数据类型</typeparam>
/// <returns>格式为"Settings_类型名称"的键名</returns>
private static string GetKey<T>() where T : ISettingsData
=> GetKey(typeof(T));
/// <summary>
/// 获取指定类型的存储键名
/// </summary>
/// <param name="type">设置数据类型</param>
/// <returns>格式为"Settings_类型名称"的键名</returns>
private static string GetKey(Type type)
=> $"Settings_{type.Name}";
}

View File

@ -1,22 +0,0 @@
namespace GFramework.Godot.setting;
/// <summary>
/// 音频总线映射配置类,用于定义音频系统中不同类型的音频总线名称
/// </summary>
public sealed class AudioBusMap
{
/// <summary>
/// 主音频总线名称,默认值为"Master"
/// </summary>
public string Master { get; init; } = "Master";
/// <summary>
/// 背景音乐音频总线名称,默认值为"BGM"
/// </summary>
public string Bgm { get; init; } = "BGM";
/// <summary>
/// 音效音频总线名称,默认值为"SFX"
/// </summary>
public string Sfx { get; init; } = "SFX";
}

View File

@ -0,0 +1,28 @@
using GFramework.Game.Abstractions.setting;
namespace GFramework.Godot.setting;
/// <summary>
/// 音频总线映射设置
/// 定义了游戏中不同音频类型的总线名称配置
/// </summary>
public class AudioBusMapSettings : ISettingsData
{
/// <summary>
/// 主音频总线名称
/// 默认值为"Master"
/// </summary>
public string Master { get; set; } = "Master";
/// <summary>
/// 背景音乐总线名称
/// 默认值为"BGM"
/// </summary>
public string Bgm { get; set; } = "BGM";
/// <summary>
/// 音效总线名称
/// 默认值为"SFX"
/// </summary>
public string Sfx { get; set; } = "SFX";
}

View File

@ -6,9 +6,10 @@ namespace GFramework.Godot.setting;
/// <summary>
/// Godot音频设置实现类用于应用音频配置到Godot音频系统
/// </summary>
/// <param name="settings">音频设置对象,包含主音量、背景音乐音量和音效音量</param>
/// <param name="busMap">音频总线映射对象,定义了不同音频类型的总线名称</param>
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
/// <param name="audioSettings">音频设置对象,包含主音量、背景音乐音量和音效音量</param>
/// <param name="audioBusMapSettings">音频总线映射对象,定义了不同音频类型的总线名称</param>
public class GodotAudioSettings(AudioSettings audioSettings, AudioBusMapSettings audioBusMapSettings)
: IApplyAbleSettings
{
/// <summary>
/// 应用音频设置到Godot音频系统
@ -16,9 +17,9 @@ public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IA
/// <returns>表示异步操作的任务</returns>
public Task Apply()
{
SetBus(busMap.Master, settings.MasterVolume);
SetBus(busMap.Bgm, settings.BgmVolume);
SetBus(busMap.Sfx, settings.SfxVolume);
SetBus(audioBusMapSettings.Master, audioSettings.MasterVolume);
SetBus(audioBusMapSettings.Bgm, audioSettings.BgmVolume);
SetBus(audioBusMapSettings.Sfx, audioSettings.SfxVolume);
return Task.CompletedTask;
}

View File

@ -4,34 +4,34 @@ using Godot;
namespace GFramework.Godot.setting;
/// <summary>
/// Godot图形设置类继承自GraphicsSettings并实现IApplyAbleSettings接口
/// 用于管理游戏的图形显示设置,包括分辨率、全屏模式等
/// Godot图形设置应用器
/// </summary>
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
/// <param name="settings">图形设置配置对象</param>
public class GodotGraphicsSettings(GraphicsSettings settings) : IApplyAbleSettings
{
/// <summary>
/// 异步应用当前图形设置到游戏窗口
/// 该方法会根据设置的分辨率、全屏状态等参数调整Godot窗口的显示属性
/// 应用图形设置到Godot引擎
/// </summary>
/// <returns>表示异步操作的任务</returns>
/// <returns>异步任务</returns>
public async Task Apply()
{
var size = new Vector2I(ResolutionWidth, ResolutionHeight);
// 创建分辨率向量
var size = new Vector2I(settings.ResolutionWidth, settings.ResolutionHeight);
// 直接调用DisplayServer API不使用异步或延迟
// 1. 设置边框标志
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, Fullscreen);
// 设置窗口边框状态
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, settings.Fullscreen);
// 2. 设置窗口模式
// 设置窗口模式(全屏或窗口化)
DisplayServer.WindowSetMode(
Fullscreen ? DisplayServer.WindowMode.ExclusiveFullscreen : DisplayServer.WindowMode.Windowed
settings.Fullscreen
? DisplayServer.WindowMode.ExclusiveFullscreen
: DisplayServer.WindowMode.Windowed
);
// 3. 窗口化下设置尺寸和位置
if (!Fullscreen)
// 非全屏模式下设置窗口大小和居中位置
if (!settings.Fullscreen)
{
DisplayServer.WindowSetSize(size);
// 居中窗口
var screen = DisplayServer.GetPrimaryScreen();
var screenSize = DisplayServer.ScreenGetSize(screen);
var pos = (screenSize - size) / 2;