diff --git a/GFramework.Game.Abstractions/setting/AudioSettings.cs b/GFramework.Game.Abstractions/setting/AudioSettings.cs new file mode 100644 index 0000000..280b424 --- /dev/null +++ b/GFramework.Game.Abstractions/setting/AudioSettings.cs @@ -0,0 +1,22 @@ +namespace GFramework.Game.Abstractions.setting; + +/// +/// 音频设置类,用于管理游戏中的音频配置 +/// +public class AudioSettings : ISettingsSection +{ + /// + /// 获取或设置主音量,控制所有音频的总体音量 + /// + public float MasterVolume { get; set; } = 1.0f; + + /// + /// 获取或设置背景音乐音量,控制BGM的播放音量 + /// + public float BgmVolume { get; set; } = 0.8f; + + /// + /// 获取或设置音效音量,控制SFX的播放音量 + /// + public float SfxVolume { get; set; } = 0.8f; +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/GraphicsSettings.cs b/GFramework.Game.Abstractions/setting/GraphicsSettings.cs new file mode 100644 index 0000000..4f85685 --- /dev/null +++ b/GFramework.Game.Abstractions/setting/GraphicsSettings.cs @@ -0,0 +1,22 @@ +namespace GFramework.Game.Abstractions.setting; + +/// +/// 图形设置类,用于管理游戏的图形相关配置 +/// +public class GraphicsSettings : ISettingsSection +{ + /// + /// 获取或设置是否启用全屏模式 + /// + public bool Fullscreen { get; set; } = false; + + /// + /// 获取或设置屏幕分辨率宽度 + /// + public int ResolutionWidth { get; set; } = 1920; + + /// + /// 获取或设置屏幕分辨率高度 + /// + public int ResolutionHeight { get; set; } = 1080; +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs b/GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs new file mode 100644 index 0000000..5f74b21 --- /dev/null +++ b/GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace GFramework.Game.Abstractions.setting; + +/// +/// 定义可应用设置的接口,继承自ISettingsSection +/// +public interface IApplyAbleSettings : ISettingsSection +{ + /// + /// 应用当前设置到系统中 + /// + Task Apply(); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/ISettingsModel.cs b/GFramework.Game.Abstractions/setting/ISettingsModel.cs new file mode 100644 index 0000000..30895f3 --- /dev/null +++ b/GFramework.Game.Abstractions/setting/ISettingsModel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using GFramework.Core.Abstractions.model; + +namespace GFramework.Game.Abstractions.setting; + +/// +/// 定义设置模型的接口,提供获取特定类型设置节的功能 +/// +public interface ISettingsModel : IModel +{ + /// + /// 获取指定类型的设置节实例 + /// + /// 设置节的类型,必须是class、实现ISettingsSection接口且具有无参构造函数 + /// 指定类型的设置节实例 + T Get() where T : class, ISettingsSection, new(); + + /// + /// 尝试获取指定类型的设置节实例 + /// + /// 要获取的设置节类型 + /// 输出参数,如果成功则包含找到的设置节实例,否则为null + /// 如果找到指定类型的设置节则返回true,否则返回false + bool TryGet(Type type, out ISettingsSection section); + + /// + /// 获取所有设置节的集合 + /// + /// 包含所有设置节的可枚举集合 + IEnumerable All(); + + /// + /// 注册一个可应用的设置对象 + /// + /// 要注册的可应用设置对象 + void Register(IApplyAbleSettings applyAble); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/ISettingsSection.cs b/GFramework.Game.Abstractions/setting/ISettingsSection.cs new file mode 100644 index 0000000..a6b8916 --- /dev/null +++ b/GFramework.Game.Abstractions/setting/ISettingsSection.cs @@ -0,0 +1,7 @@ +namespace GFramework.Game.Abstractions.setting; + +/// +/// 表示游戏设置的一个配置节接口 +/// 该接口定义了设置配置节的基本契约,用于管理游戏中的各种配置选项 +/// +public interface ISettingsSection; \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/ISettingsSystem.cs b/GFramework.Game.Abstractions/setting/ISettingsSystem.cs new file mode 100644 index 0000000..9912c84 --- /dev/null +++ b/GFramework.Game.Abstractions/setting/ISettingsSystem.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GFramework.Core.Abstractions.system; + +namespace GFramework.Game.Abstractions.setting; + +/// +/// 定义设置系统的接口,提供应用各种设置的方法 +/// +public interface ISettingsSystem : ISystem +{ + /// + /// 应用所有可应用的设置 + /// + Task ApplyAll(); + + /// + /// 应用指定类型的设置 + /// + Task Apply(Type settingsType); + + /// + /// 应用指定类型的设置(泛型版本) + /// + Task Apply() where T : class, ISettingsSection; + + /// + /// 批量应用多个设置类型 + /// + Task Apply(IEnumerable settingsTypes); +} \ No newline at end of file diff --git a/GFramework.Game/setting/SettingsModel.cs b/GFramework.Game/setting/SettingsModel.cs new file mode 100644 index 0000000..5d35785 --- /dev/null +++ b/GFramework.Game/setting/SettingsModel.cs @@ -0,0 +1,67 @@ +using GFramework.Core.model; +using GFramework.Game.Abstractions.setting; + +namespace GFramework.Game.setting; + +/// +/// 设置模型类,用于管理不同类型的应用程序设置部分 +/// +public class SettingsModel : AbstractModel, ISettingsModel +{ + private readonly Dictionary _sections = new(); + + /// + /// 获取指定类型的设置部分实例,如果不存在则创建新的实例 + /// + /// 设置部分的类型,必须实现ISettingsSection接口并具有无参构造函数 + /// 指定类型的设置部分实例 + public T Get() where T : class, ISettingsSection, new() + { + var type = typeof(T); + + // 尝试从字典中获取已存在的设置部分实例 + if (_sections.TryGetValue(type, out var existing)) + return (T)existing; + + // 创建新的设置部分实例并存储到字典中 + var created = new T(); + _sections[type] = created; + return created; + } + + /// + /// 尝试获取指定类型的设置部分实例 + /// + /// 设置部分的类型 + /// 输出参数,如果找到则返回对应的设置部分实例,否则为null + /// 如果找到指定类型的设置部分则返回true,否则返回false + public bool TryGet(Type type, out ISettingsSection section) + => _sections.TryGetValue(type, out section!); + + /// + /// 获取所有设置部分的集合 + /// + /// 包含所有设置部分的可枚举集合 + public IEnumerable All() + => _sections.Values; + + /// + /// 注册一个可应用的设置对象到管理器中 + /// + /// 要注册的可应用设置对象 + public void Register(IApplyAbleSettings applyAble) + { + // 获取传入对象的类型信息 + var type = applyAble.GetType(); + // 尝试将类型和对象添加到线程安全的字典中 + _sections.TryAdd(type, applyAble); + } + + + /// + /// 初始化方法,用于执行模型的初始化逻辑 + /// + protected override void OnInit() + { + } +} \ No newline at end of file diff --git a/GFramework.Game/setting/SettingsSystem.cs b/GFramework.Game/setting/SettingsSystem.cs new file mode 100644 index 0000000..3c07c9d --- /dev/null +++ b/GFramework.Game/setting/SettingsSystem.cs @@ -0,0 +1,89 @@ +using GFramework.Core.extensions; +using GFramework.Core.system; +using GFramework.Game.Abstractions.setting; + +namespace GFramework.Game.setting; + +/// +/// 设置系统,负责管理和应用各种设置配置 +/// +public class SettingsSystem : AbstractSystem, ISettingsSystem +{ + private ISettingsModel _model = null!; + + /// + /// 应用所有设置配置 + /// + /// 完成的任务 + public Task ApplyAll() + { + // 遍历所有设置配置并尝试应用 + foreach (var section in _model.All()) + { + TryApply(section); + } + + return Task.CompletedTask; + } + + /// + /// 应用指定类型的设置配置 + /// + /// 设置配置类型,必须是类且实现ISettingsSection接口 + /// 完成的任务 + public Task Apply() where T : class, ISettingsSection + => Apply(typeof(T)); + + /// + /// 应用指定类型的设置配置 + /// + /// 设置配置类型 + /// 完成的任务 + public Task Apply(Type settingsType) + { + if (!_model.TryGet(settingsType, out var section)) + return Task.CompletedTask; + + TryApply(section); + return Task.CompletedTask; + } + + /// + /// 应用指定类型集合的设置配置 + /// + /// 设置配置类型集合 + /// 完成的任务 + public Task Apply(IEnumerable settingsTypes) + { + // 去重后遍历设置类型,获取并应用对应的设置配置 + foreach (var type in settingsTypes.Distinct()) + { + if (_model.TryGet(type, out var section)) + { + TryApply(section); + } + } + + return Task.CompletedTask; + } + + /// + /// 初始化设置系统,获取设置模型实例 + /// + protected override void OnInit() + { + _model = this.GetModel()!; + } + + /// + /// 尝试应用可应用的设置配置 + /// + /// 设置配置对象 + private static void TryApply(ISettingsSection section) + { + if (section is IApplyAbleSettings applyable) + { + applyable.Apply(); + } + } +} \ No newline at end of file diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj index 3a0e22a..df05907 100644 --- a/GFramework.Godot/GFramework.Godot.csproj +++ b/GFramework.Godot/GFramework.Godot.csproj @@ -19,6 +19,7 @@ + diff --git a/GFramework.Godot/extensions/GodotPathExtensions.cs b/GFramework.Godot/extensions/GodotPathExtensions.cs new file mode 100644 index 0000000..97f0edf --- /dev/null +++ b/GFramework.Godot/extensions/GodotPathExtensions.cs @@ -0,0 +1,22 @@ +namespace GFramework.Godot.extensions; + +public static class GodotPathExtensions +{ + /// + /// 判断是否是 Godot 用户数据路径(user://) + /// + public static bool IsUserPath(this string path) + => !string.IsNullOrEmpty(path) && path.StartsWith("user://"); + + /// + /// 判断是否是 Godot 资源路径(res://) + /// + public static bool IsResPath(this string path) + => !string.IsNullOrEmpty(path) && path.StartsWith("res://"); + + /// + /// 判断是否是 Godot 特殊路径(user:// 或 res://) + /// + public static bool IsGodotPath(this string path) + => path.IsUserPath() || path.IsResPath(); +} \ No newline at end of file diff --git a/GFramework.Godot/setting/AudioBusMap.cs b/GFramework.Godot/setting/AudioBusMap.cs new file mode 100644 index 0000000..6b9ddc0 --- /dev/null +++ b/GFramework.Godot/setting/AudioBusMap.cs @@ -0,0 +1,22 @@ +namespace GFramework.Godot.setting; + +/// +/// 音频总线映射配置类,用于定义音频系统中不同类型的音频总线名称 +/// +public sealed class AudioBusMap +{ + /// + /// 主音频总线名称,默认值为"Master" + /// + public string Master { get; init; } = "Master"; + + /// + /// 背景音乐音频总线名称,默认值为"BGM" + /// + public string Bgm { get; init; } = "BGM"; + + /// + /// 音效音频总线名称,默认值为"SFX" + /// + public string Sfx { get; init; } = "SFX"; +} \ No newline at end of file diff --git a/GFramework.Godot/setting/GodotAudioApplier.cs b/GFramework.Godot/setting/GodotAudioApplier.cs new file mode 100644 index 0000000..1302cad --- /dev/null +++ b/GFramework.Godot/setting/GodotAudioApplier.cs @@ -0,0 +1,46 @@ +using GFramework.Game.Abstractions.setting; +using Godot; + +namespace GFramework.Godot.setting; + +/// +/// Godot音频设置应用器,用于将音频设置应用到Godot引擎的音频总线系统 +/// +/// 音频设置对象,包含主音量、背景音乐音量和音效音量 +/// 音频总线映射对象,定义了不同音频类型的总线名称 +public sealed class GodotAudioApplier(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings +{ + /// + /// 应用音频设置到Godot音频系统 + /// + /// 表示异步操作的任务 + public Task Apply() + { + SetBus(busMap.Master, settings.MasterVolume); + SetBus(busMap.Bgm, settings.BgmVolume); + SetBus(busMap.Sfx, settings.SfxVolume); + return Task.CompletedTask; + } + + /// + /// 设置指定音频总线的音量 + /// + /// 音频总线名称 + /// 线性音量值(0-1之间) + private static void SetBus(string busName, float linear) + { + // 获取音频总线索引 + var idx = AudioServer.GetBusIndex(busName); + if (idx < 0) + { + GD.PushWarning($"Audio bus not found: {busName}"); + return; + } + + // 将线性音量转换为分贝并设置到音频总线 + AudioServer.SetBusVolumeDb( + idx, + Mathf.LinearToDb(Mathf.Clamp(linear, 0.0001f, 1f)) + ); + } +} \ No newline at end of file diff --git a/GFramework.Godot/setting/GodotAudioSettings.cs b/GFramework.Godot/setting/GodotAudioSettings.cs new file mode 100644 index 0000000..dc968e0 --- /dev/null +++ b/GFramework.Godot/setting/GodotAudioSettings.cs @@ -0,0 +1,12 @@ + +using GFramework.Game.Abstractions.setting; + +namespace GFramework.Godot.setting; + +public class GodotAudioSettings: AudioSettings,IApplyAbleSettings +{ + public Task Apply() + { + + } +} \ No newline at end of file diff --git a/GFramework.Godot/setting/GodotGraphicsSettings.cs b/GFramework.Godot/setting/GodotGraphicsSettings.cs new file mode 100644 index 0000000..4c3c34e --- /dev/null +++ b/GFramework.Godot/setting/GodotGraphicsSettings.cs @@ -0,0 +1,43 @@ +using GFramework.Game.Abstractions.setting; +using Godot; + +namespace GFramework.Godot.setting; + +/// +/// Godot图形设置类,继承自GraphicsSettings并实现IApplyAbleSettings接口 +/// 用于管理游戏的图形显示设置,包括分辨率、全屏模式等 +/// +public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings +{ + /// + /// 异步应用当前图形设置到游戏窗口 + /// 该方法会根据设置的分辨率、全屏状态等参数调整Godot窗口的显示属性 + /// + /// 表示异步操作的任务 + public async Task Apply() + { + var size = new Vector2I(ResolutionWidth, ResolutionHeight); + + // 直接调用DisplayServer API,不使用异步或延迟 + // 1. 设置边框标志 + DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, Fullscreen); + + // 2. 设置窗口模式 + DisplayServer.WindowSetMode( + Fullscreen ? DisplayServer.WindowMode.ExclusiveFullscreen : DisplayServer.WindowMode.Windowed + ); + + // 3. 窗口化下设置尺寸和位置 + if (!Fullscreen) + { + DisplayServer.WindowSetSize(size); + // 居中窗口 + var screen = DisplayServer.GetPrimaryScreen(); + var screenSize = DisplayServer.ScreenGetSize(screen); + var pos = (screenSize - size) / 2; + DisplayServer.WindowSetPosition(pos); + } + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/GFramework.Godot/storage/GodotFileStorage.cs b/GFramework.Godot/storage/GodotFileStorage.cs new file mode 100644 index 0000000..6f8cde3 --- /dev/null +++ b/GFramework.Godot/storage/GodotFileStorage.cs @@ -0,0 +1,266 @@ +using System.Collections.Concurrent; +using System.Text; +using GFramework.Core.Abstractions.storage; +using GFramework.Game.Abstractions.serializer; +using GFramework.Godot.extensions; +using FileAccess = Godot.FileAccess; + +namespace GFramework.Godot.storage; + +/// +/// Godot 特化的文件存储实现,支持 res://、user:// 和普通文件路径 +/// 支持按 key 细粒度锁保证线程安全 +/// +public sealed class GodotFileStorage : IStorage +{ + /// + /// 每个 key 对应的锁对象 + /// + private readonly ConcurrentDictionary _keyLocks = new(); + + private readonly ISerializer _serializer; + + /// + /// 初始化 Godot 文件存储 + /// + /// 序列化器实例 + public GodotFileStorage(ISerializer serializer) + { + _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + } + + #region Delete + + /// + /// 删除指定键对应的文件 + /// + /// 存储键 + public void Delete(string key) + { + throw new NotImplementedException(); + } + + #endregion + + #region Helpers + + /// + /// 清理路径段中的无效字符,将无效文件名字符替换为下划线 + /// + /// 要清理的路径段 + /// 清理后的路径段 + private static string SanitizeSegment(string segment) + => Path.GetInvalidFileNameChars().Aggregate(segment, (current, c) => current.Replace(c, '_')); + + /// + /// 将存储键转换为绝对路径,处理 Godot 虚拟路径和普通文件系统路径 + /// + /// 存储键 + /// 绝对路径字符串 + private static string ToAbsolutePath(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("Storage key cannot be empty", nameof(key)); + + key = key.Replace('\\', '/'); + + if (key.Contains("..")) + throw new ArgumentException("Storage key cannot contain '..'", nameof(key)); + + // Godot 虚拟路径直接使用 FileAccess 支持 + if (key.IsGodotPath()) + return key; + + // 普通文件系统路径 + var segments = key.Split('/', StringSplitOptions.RemoveEmptyEntries) + .Select(SanitizeSegment) + .ToArray(); + + if (segments.Length == 0) + throw new ArgumentException("Invalid storage key", nameof(key)); + + var dir = Path.Combine(segments[..^1]); + var fileName = segments[^1]; + + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + + return Path.Combine(dir, fileName); + } + + /// + /// 获取指定路径对应的锁对象,如果不存在则创建新的锁对象 + /// + /// 文件路径 + /// 对应路径的锁对象 + private object GetLock(string path) => _keyLocks.GetOrAdd(path, _ => new object()); + + #endregion + + #region Exists + + /// + /// 检查指定键对应的文件是否存在 + /// + /// 存储键 + /// 文件存在返回 true,否则返回 false + public bool Exists(string key) + { + var path = ToAbsolutePath(key); + var keyLock = GetLock(path); + + lock (keyLock) + { + if (!path.IsGodotPath()) return File.Exists(path); + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + return file != null; + } + } + + /// + /// 异步检查指定键对应的文件是否存在 + /// + /// 存储键 + /// 表示异步操作的任务,结果为布尔值表示文件是否存在 + public Task ExistsAsync(string key) + => Task.FromResult(Exists(key)); + + #endregion + + #region Read + + /// + /// 读取指定键对应的序列化数据并反序列化为指定类型 + /// + /// 要反序列化的类型 + /// 存储键 + /// 反序列化后的对象实例 + /// 当指定键对应的文件不存在时抛出 + public T Read(string key) + { + var path = ToAbsolutePath(key); + var keyLock = GetLock(path); + + lock (keyLock) + { + string content; + + if (path.IsGodotPath()) + { + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + if (file == null) throw new FileNotFoundException($"Storage key not found: {key}", path); + content = file.GetAsText(); + } + else + { + if (!File.Exists(path)) + throw new FileNotFoundException($"Storage key not found: {key}", path); + content = File.ReadAllText(path, Encoding.UTF8); + } + + return _serializer.Deserialize(content); + } + } + + /// + /// 读取指定键对应的序列化数据,如果文件不存在则返回默认值 + /// + /// 要反序列化的类型 + /// 存储键 + /// 当文件不存在时返回的默认值 + /// 反序列化后的对象实例或默认值 + public T Read(string key, T defaultValue) + { + var path = ToAbsolutePath(key); + var keyLock = GetLock(path); + + lock (keyLock) + { + if (path.IsGodotPath() && !FileAccess.FileExists(path) || !path.IsGodotPath() && !File.Exists(path)) + return defaultValue; + + return Read(key); + } + } + + /// + /// 异步读取指定键对应的序列化数据并反序列化为指定类型 + /// + /// 要反序列化的类型 + /// 存储键 + /// 表示异步操作的任务,结果为反序列化后的对象实例 + public async Task ReadAsync(string key) + { + var path = ToAbsolutePath(key); + var keyLock = GetLock(path); + + return await Task.Run(() => + { + lock (keyLock) + { + string content; + + if (path.IsGodotPath()) + { + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + if (file == null) throw new FileNotFoundException($"Storage key not found: {key}", path); + content = file.GetAsText(); + } + else + { + if (!File.Exists(path)) + throw new FileNotFoundException($"Storage key not found: {key}", path); + content = File.ReadAllText(path, Encoding.UTF8); + } + + return _serializer.Deserialize(content); + } + }); + } + + #endregion + + #region Write + + /// + /// 将指定对象序列化并写入到指定键对应的文件中 + /// + /// 要序列化的对象类型 + /// 存储键 + /// 要写入的对象实例 + public void Write(string key, T value) + { + var path = ToAbsolutePath(key); + var keyLock = GetLock(path); + + lock (keyLock) + { + var content = _serializer.Serialize(value); + if (path.IsGodotPath()) + { + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write); + if (file == null) throw new IOException($"Cannot write file: {path}"); + file.StoreString(content); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, content, Encoding.UTF8); + } + } + } + + /// + /// 异步将指定对象序列化并写入到指定键对应的文件中 + /// + /// 要序列化的对象类型 + /// 存储键 + /// 要写入的对象实例 + /// 表示异步操作的任务 + public async Task WriteAsync(string key, T value) + { + await Task.Run(() => Write(key, value)); + } + + #endregion +} \ No newline at end of file