refactor(setting): 将Godot音频设置应用逻辑合并到设置类中

- 移除独立的GodotAudioApplier类,将其功能整合到GodotAudioSettings中
- 在GodotAudioSettings类中实现音频总线音量设置逻辑
- 更新项目文件移除对已删除文件的引用
- 添加设置系统和信号连接系统的技术文档
- 完善Godot扩展方法和设置模块的使用说明
This commit is contained in:
GwWuYou 2026-01-12 21:33:42 +08:00
parent 807dbc482e
commit fcac697663
8 changed files with 1832 additions and 55 deletions

View File

@ -0,0 +1,204 @@
# 设置系统 (Settings System)
## 概述
设置系统是 GFramework.Game 的核心组件之一,负责管理游戏中各种设置配置。该系统采用了模型-系统分离的设计模式支持设置部分Section的管理和设置应用器模式。
## 核心类
### SettingsModel
设置模型类,继承自 `AbstractModel` 并实现 `ISettingsModel` 接口。
**主要功能:**
- 管理不同类型的设置部分Settings Section
- 提供类型安全的设置访问
- 支持可应用设置对象的注册
**关键方法:**
- `Get<T>()` - 获取或创建指定类型的设置部分
- `TryGet(Type, out ISettingsSection)` - 尝试获取设置部分
- `Register(IApplyAbleSettings)` - 注册可应用的设置对象
- `All()` - 获取所有设置部分
### SettingsSystem
设置系统类,继承自 `AbstractSystem` 并实现 `ISettingsSystem` 接口。
**主要功能:**
- 应用设置配置到相应系统
- 支持单个或批量设置应用
- 自动识别可应用设置类型
**关键方法:**
- `ApplyAll()` - 应用所有设置配置
- `Apply<T>()` - 应用指定类型的设置
- `Apply(IEnumerable<Type>)` - 应用指定类型集合的设置
## 架构设计
```mermaid
graph TD
A[ISettingsModel] --> B[SettingsModel]
C[ISettingsSystem] --> D[SettingsSystem]
B --> E[Dictionary<Type, ISettingsSection>]
D --> B
F[ISettingsSection] --> G[IApplyAbleSettings]
H[AudioSettings] --> G
I[GraphicsSettings] --> G
E --> H
E --> I
J[Application] --> D
D --> G
```
## 使用示例
### 基本使用
```csharp
// 获取设置模型
var settingsModel = this.GetModel<ISettingsModel>();
// 获取或创建音频设置
var audioSettings = settingsModel.Get<GodotAudioSettings>();
audioSettings.MasterVolume = 0.8f;
audioSettings.BgmVolume = 0.6f;
audioSettings.SfxVolume = 0.9f;
// 注册设置到模型
settingsModel.Register(audioSettings);
```
### 应用设置
```csharp
// 获取设置系统
var settingsSystem = this.GetSystem<ISettingsSystem>();
// 应用所有设置
await settingsSystem.ApplyAll();
// 应用特定类型设置
await settingsSystem.Apply<GodotAudioSettings>();
// 应用多个类型设置
var types = new[] { typeof(GodotAudioSettings), typeof(GodotGraphicsSettings) };
await settingsSystem.Apply(types);
```
### 创建自定义设置
```csharp
/// <summary>
/// 游戏设置类
/// </summary>
public class GameSettings : ISettingsSection
{
public float GameSpeed { get; set; } = 1.0f;
public int Difficulty { get; set; } = 1;
public bool AutoSave { get; set; } = true;
}
// 使用自定义设置
var gameSettings = settingsModel.Get<GameSettings>();
gameSettings.GameSpeed = 1.5f;
```
### 创建可应用设置
```csharp
/// <summary>
/// 游戏设置应用器
/// </summary>
public class GameSettings : ISettingsSection, IApplyAbleSettings
{
public float GameSpeed { get; set; } = 1.0f;
public int Difficulty { get; set; } = 1;
public Task Apply()
{
// 应用游戏速度
Time.timeScale = GameSpeed;
// 应用难度设置
GameDifficulty.Current = Difficulty;
return Task.CompletedTask;
}
}
```
## 接口定义
### ISettingsSection
```csharp
public interface ISettingsSection
{
// 设置部分的标识接口
}
```
### IApplyAbleSettings
```csharp
public interface IApplyAbleSettings : ISettingsSection
{
Task Apply();
}
```
### ISettingsModel
```csharp
public interface ISettingsModel
{
T Get<T>() where T : class, ISettingsSection, new();
bool TryGet(Type type, out ISettingsSection section);
IEnumerable<ISettingsSection> All();
void Register(IApplyAbleSettings applyAble);
}
```
### ISettingsSystem
```csharp
public interface ISettingsSystem
{
Task ApplyAll();
Task Apply<T>() where T : class, ISettingsSection;
Task Apply(Type settingsType);
Task Apply(IEnumerable<Type> settingsTypes);
}
```
## 设计模式
该系统使用了以下设计模式:
1. **Repository Pattern** - SettingsModel 作为设置数据的仓库
2. **Command Pattern** - IApplyAbleSettings 的 Apply 方法作为命令
3. **Factory Pattern** - Get<T>() 方法创建设置实例
4. **Template Method** - AbstractSystem 提供初始化模板
## 最佳实践
1. **设置分类** - 将相关设置组织到同一个设置类中
2. **延迟应用** - 批量修改后再应用,而不是每次修改都应用
3. **类型安全** - 使用泛型方法确保类型安全
4. **可测试性** - 通过接口实现便于单元测试
## 相关链接
- [Godot 设置模块](../../GFramework.Godot/setting/README.md)
- [存储模块](../../GFramework.Godot/storage/README.md)
- [抽象接口定义](../../../GFramework.Core/Abstractions/)

View File

@ -17,9 +17,4 @@
<ProjectReference Include="..\GFramework.Game.Abstractions\GFramework.Game.Abstractions.csproj" PrivateAssets="all"/>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<Compile Remove="extensions\ControlExtensions.cs"/>
<Compile Remove="setting\GodotAudioSettings.cs"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,335 @@
# Godot 扩展方法 (Godot Extensions)
## 概述
Godot 扩展方法模块为 Godot 引擎提供了丰富的便捷扩展方法集合。这些扩展方法简化了常见的 Godot
开发任务,提高了代码的可读性和开发效率。该模块遵循流畅接口设计原则,支持链式调用。
## 模块结构
```mermaid
graph TD
A[Extensions] --> B[GodotPathExtensions]
A --> C[NodeExtensions]
A --> D[SignalFluentExtensions]
A --> E[UnRegisterExtension]
D --> F[SignalBuilder]
B --> G[路径判断扩展]
C --> H[节点生命周期]
C --> I[节点查询]
C --> J[场景树操作]
C --> K[输入控制]
C --> L[调试工具]
D --> M[信号连接系统]
E --> N[事件管理]
```
## 扩展模块详解
### 1. 路径扩展 (GodotPathExtensions)
提供 Godot 虚拟路径的判断和识别功能。
**主要方法:**
- `IsUserPath()` - 判断是否为 `user://` 路径
- `IsResPath()` - 判断是否为 `res://` 路径
- `IsGodotPath()` - 判断是否为 Godot 虚拟路径
**使用示例:**
```csharp
string savePath = "user://save.dat";
string configPath = "res://config.json";
string logPath = "C:/logs/debug.log";
if (savePath.IsUserPath()) Console.WriteLine("用户数据路径");
if (configPath.IsResPath()) Console.WriteLine("资源路径");
if (logPath.IsGodotPath()) Console.WriteLine("Godot 虚拟路径");
else Console.WriteLine("文件系统路径");
```
### 2. 节点扩展 (NodeExtensions)
最丰富的扩展模块,提供全面的节点操作功能。
#### 节点生命周期管理
```csharp
// 安全释放节点
node.QueueFreeX(); // 延迟释放
node.FreeX(); // 立即释放
// 等待节点就绪
await node.WaitUntilReady();
// 检查节点有效性
if (node.IsValidNode()) Console.WriteLine("节点有效");
if (node.IsInvalidNode()) Console.WriteLine("节点无效");
```
#### 节点查询操作
```csharp
// 查找子节点
var sprite = node.FindChildX<Sprite2D>("Sprite");
var parent = node.GetParentX<Control>();
// 获取或创建节点
var panel = parent.GetOrCreateNode<Panel>("MainPanel");
// 遍历子节点
node.ForEachChild<Sprite2D>(sprite => {
sprite.Modulate = Colors.White;
});
```
#### 场景树操作
```csharp
// 获取根节点
var root = node.GetRootNodeX();
// 异步添加子节点
await parent.AddChildX(childNode);
// 设置场景树暂停状态
node.Paused(true); // 暂停
node.Paused(false); // 恢复
```
#### 输入控制
```csharp
// 标记输入事件已处理
node.SetInputAsHandled();
// 禁用/启用输入
node.DisableInput();
node.EnableInput();
```
#### 调试工具
```csharp
// 打印节点路径
node.LogNodePath();
// 打印节点树
node.PrintTreeX();
// 安全延迟调用
node.SafeCallDeferred("UpdateUI");
```
#### 类型转换
```csharp
// 安全的类型转换
var button = node.OfType<Button>();
var sprite = childNode.OfType<Sprite2D>();
```
### 3. 信号扩展 (SignalFluentExtensions)
提供流畅的信号连接 API详见 [signal/README.md](signal/README.md)。
**快速示例:**
```csharp
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
```
### 4. 取消注册扩展 (UnRegisterExtension)
自动管理事件监听器的生命周期。
**主要方法:**
- `UnRegisterWhenNodeExitTree()` - 节点退出场景树时自动取消注册
**使用示例:**
```csharp
var unRegister = eventManager.Subscribe<GameEvent>(OnGameEvent);
unRegister.UnRegisterWhenNodeExitTree(node);
```
## 快速参考
### 常用代码片段
#### 场景节点管理
```csharp
public class GameManager : Node
{
private Node _uiRoot;
public override void _Ready()
{
_uiRoot = GetNode<Node>("UI");
// 创建游戏面板
var gamePanel = _uiRoot.GetOrCreateNode<Panel>("GamePanel");
// 安全添加子节点
var player = new Player();
await AddChildX(player);
// 查找并配置玩家
var sprite = player.FindChildX<Sprite2D>("Sprite");
if (sprite.IsValidNode()) sprite.Modulate = Colors.Red;
}
public void Cleanup()
{
// 安全释放所有子节点
ForEachChild<Node>(child => child.QueueFreeX());
}
}
```
#### UI 事件处理
```csharp
public class MainMenu : Control
{
private Button _startButton;
private Button _quitButton;
public override void _Ready()
{
_startButton = FindChildX<Button>("StartButton");
_quitButton = FindChildX<Button>("QuitButton");
// 流畅的信号连接
_startButton.Signal(BaseButton.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnStartPressed)));
_quitButton.Signal(BaseButton.SignalName.Pressed)
.To(new Callable(this, nameof(OnQuitPressed)));
}
private void OnStartPressed()
{
GetTree().ChangeSceneToFile("res://scenes/game.tscn");
}
private void OnQuitPressed()
{
GetTree().Quit();
}
}
```
#### 异步场景管理
```csharp
public class SceneManager : Node
{
public async Task<T> LoadSceneAsync<T>(string scenePath) where T : Node
{
var packedScene = GD.Load<PackedScene>(scenePath);
var instance = packedScene.Instantiate<T>();
// 等待场景加载完成
await instance.WaitUntilReady();
return instance;
}
public async Task TransitionToScene(string scenePath)
{
var newScene = await LoadSceneAsync<Node>(scenePath);
// 清理当前场景
ForEachChild<Node>(child => child.QueueFreeX());
// 加载新场景
await AddChildX(newScene);
// 设置输入处理
newScene.SetInputAsHandled();
}
}
```
## 设计原则
### 1. 安全性
- 所有节点操作都包含有效性检查
- 提供安全的类型转换方法
- 避免空引用异常
### 2. 便利性
- 流畅的 API 设计
- 支持链式调用
- 减少样板代码
### 3. 一致性
- 统一的命名约定
- 一致的返回类型
- 预测性方法行为
### 4. 性能
- 避免不必要的节点查找
- 最小化内存分配
- 优化常见操作
## 最佳实践
### 1. 节点生命周期
```csharp
// ✅ 推荐:使用安全释放
node.QueueFreeX();
// ❌ 避免:直接释放可能导致错误
node.QueueFree();
```
### 2. 节点查询
```csharp
// ✅ 推荐:类型安全的查找
var button = node.FindChildX<Button>("Button");
// ❌ 避免:需要手动类型转换
var button = node.FindChild("Button") as Button;
```
### 3. 异步操作
```csharp
// ✅ 推荐:等待节点就绪
await child.WaitUntilReady();
// ❌ 避免:假设节点已就绪
child.DoSomething();
```
### 4. 事件管理
```csharp
// ✅ 推荐:自动清理事件
var unRegister = eventSystem.Subscribe(eventHandler);
unRegister.UnRegisterWhenNodeExitTree(node);
// ❌ 避免:手动管理事件生命周期
// 可能导致内存泄漏
```
## 相关链接
- [信号连接系统](signal/README.md) - 详细的信号连接 API 文档
- [存储模块](../storage/README.md) - 文件存储系统
- [设置模块](../setting/README.md) - 游戏设置系统
- [设置系统](../../GFramework.Game/setting/README.md) - 通用设置框架

View File

@ -0,0 +1,427 @@
# 信号连接系统 (Signal Connection System)
## 概述
信号连接系统是 Godot 扩展方法模块中的一个专门子模块,提供流畅、类型安全的 Godot 信号连接 API。该系统采用构建器模式Builder
Pattern和流畅接口设计Fluent Interface大大简化了信号订阅代码提高了代码的可读性和可维护性。
## 核心类
### SignalBuilder
信号连接构建器,负责构建和执行信号连接操作。
**特性:**
- 支持链式调用
- 可配置连接标志
- 支持连接后立即调用
- 返回原始对象以便继续操作
### SignalFluentExtensions
`GodotObject` 提供信号连接扩展方法,创建 `SignalBuilder` 实例。
## 架构设计
```mermaid
graph TD
A[GodotObject] --> B[SignalFluentExtensions]
B --> C[Signal Extension Method]
C --> D[SignalBuilder]
D --> E[WithFlags]
D --> F[To]
D --> G[ToAndCall]
D --> H[End]
F --> I[Connect Signal]
G --> J[Connect + Call]
H --> K[Return GodotObject]
L[ConnectFlags] --> E
M[Callable] --> F
M --> G
```
## 使用示例
### 基本信号连接
```csharp
// 基本连接
button.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnButtonPressed)));
// 带连接标志
timer.Signal(Timer.SignalName.Timeout)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnTimerTimeout)));
```
### 连接并立即调用
```csharp
// 连接信号并立即调用一次
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
// 连接带参数的信号并立即调用
area2D.Signal(Area2D.SignalName.BodyEntered)
.ToAndCall(new Callable(this, nameof(OnBodyEntered)), new Variant[] { node });
```
### 复杂的连接链
```csharp
// 设置连接标志并连接
player.Signal(Player.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnHealthChanged)));
// 连接多个信号
var button = GetNode<Button>("StartButton");
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnGameStarted)));
```
## API 详细说明
### SignalBuilder 构造函数
```csharp
public SignalBuilder(GodotObject target, StringName signal)
```
**参数:**
- `target` - 要连接信号的 Godot 对象
- `signal` - 要连接的信号名称
### SignalBuilder 方法
#### WithFlags
设置连接标志。
```csharp
public SignalBuilder WithFlags(GodotObject.ConnectFlags flags)
```
**参数:**
- `flags` - Godot 连接标志枚举值
**常用的连接标志:**
- `ConnectFlags.Deferred` - 延迟调用
- `ConnectFlags.OneShot` - 一次性连接
- `ConnectFlags.ConnectPersisted` - 连接持久化
- `ConnectFlags.ReferenceCounted` - 引用计数
#### To
连接信号到指定的可调用对象。
```csharp
public SignalBuilder To(Callable callable, GodotObject.ConnectFlags? flags = null)
```
**参数:**
- `callable` - 要连接的可调用对象
- `flags` - 可选的连接标志,覆盖之前设置的标志
#### ToAndCall
连接信号并立即调用一次。
```csharp
public SignalBuilder ToAndCall(Callable callable, GodotObject.ConnectFlags? flags = null, params Variant[] args)
```
**参数:**
- `callable` - 要连接的可调用对象
- `flags` - 可选的连接标志
- `args` - 调用时传递的参数
#### End
显式结束构建,返回原始对象。
```csharp
public GodotObject End()
```
### SignalFluentExtensions 扩展方法
#### Signal
为 Godot 对象创建信号构建器。
```csharp
public static SignalBuilder Signal(this GodotObject @object, StringName signal)
```
**参数:**
- `@object` - 扩展方法的目标对象
- `signal` - 要连接的信号名称
## 实际应用场景
### UI 事件处理
```csharp
public class MainMenu : Control
{
public override void _Ready()
{
var startButton = GetNode<Button>("StartButton");
var quitButton = GetNode<Button>("QuitButton");
var settingsButton = GetNode<Button>("SettingsButton");
// 开始按钮 - 一次性连接并立即禁用
startButton.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.ToAndCall(new Callable(this, nameof(OnStartPressed)));
// 退出按钮
quitButton.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnQuitPressed)));
// 设置按钮 - 延迟调用避免嵌套问题
settingsButton.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnSettingsPressed)));
}
private void OnStartPressed()
{
GetTree().ChangeSceneToFile("res://scenes/game.tscn");
}
private void OnQuitPressed()
{
GetTree().Quit();
}
private void OnSettingsPressed()
{
// 打开设置面板
GetNode<Control>("SettingsPanel").Show();
}
}
```
### 游戏逻辑事件
```csharp
public class Player : CharacterBody2D
{
private HealthComponent _health;
private AnimationPlayer _animPlayer;
public override void _Ready()
{
_health = GetNode<HealthComponent>("HealthComponent");
_animPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
// 生命值变化 - 延迟处理避免在动画中修改状态
_health.Signal(HealthComponent.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnHealthChanged)));
// 死亡事件 - 一次性连接
_health.Signal(HealthComponent.SignalName.Died)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnDied)));
}
private void OnHealthChanged(float newHealth, float maxHealth)
{
// 更新UI或状态
UpdateHealthBar(newHealth / maxHealth);
// 播放受伤动画
if (newHealth < _health.PreviousHealth)
{
_animPlayer.Play("hurt");
}
}
private void OnDied()
{
// 播放死亡动画
_animPlayer.Play("death");
// 游戏结束
GetTree().CallDeferred(SceneTree.MethodName.Quit);
}
}
```
### 音频管理
```csharp
public class AudioManager : Node
{
private AudioStreamPlayer _bgmPlayer;
private AudioStreamPlayer _sfxPlayer;
public override void _Ready()
{
_bgmPlayer = GetNode<AudioStreamPlayer>("BGMPlayer");
_sfxPlayer = GetNode<AudioStreamPlayer>("SFXPlayer");
// 背景音乐播放完成
_bgmPlayer.Signal(AudioStreamPlayer.SignalName.Finished)
.To(new Callable(this, nameof(OnBGMFinished)));
// 音效播放完成 - 延迟清理
_sfxPlayer.Signal(AudioStreamPlayer.SignalName.Finished)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(OnSFXFinished)));
}
private void OnBGMFinished()
{
// 循环播放背景音乐
PlayBGM(_currentBGM);
}
private void OnSFXFinished()
{
// 清理音效资源或播放队列中的下一个音效
CleanupSFXResources();
}
}
```
## 设计模式分析
### Builder Pattern
SignalBuilder 实现了构建器模式:
- 分步构建复杂的信号连接
- 支持链式调用
- 延迟执行到最终调用时
### Fluent Interface
流畅接口设计:
- 方法链式调用
- 可读性强
- 表达力强
### Extension Method Pattern
扩展方法模式:
- 为现有类型添加功能
- 不修改原始类
- 保持向后兼容
## 与原生 API 对比
### 原生 Godot API
```csharp
// 传统方式
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)));
// 带标志的方式
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)), (uint)GodotObject.ConnectFlags.OneShot);
// 连接并立即调用
button.Connect(Button.SignalName.Pressed, new Callable(this, nameof(OnButtonPressed)));
new Callable(this, nameof(OnButtonPressed)).Call();
```
### 信号连接系统 API
```csharp
// 流畅方式
button.Signal(Button.SignalName.Pressed)
.To(new Callable(this, nameof(OnButtonPressed)));
// 带标志的方式
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(new Callable(this, nameof(OnButtonPressed)));
// 连接并立即调用
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed)));
```
## 性能考虑
### 内存分配
- SignalBuilder 是轻量级对象
- 创建开销很小
- 使用后可被垃圾回收
### 调用开销
- 与原生 API 性能基本相同
- 主要开销在方法链调用
- 运行时性能无差异
### 推荐做法
- 避免在热循环中创建大量 SignalBuilder
- 适合 UI 事件、游戏逻辑等场景
- 可以放心使用,性能影响可忽略
## 最佳实践
### 1. 选择合适的连接标志
```csharp
// UI 事件通常使用延迟调用
button.Signal(Button.SignalName.Pressed)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(callable);
// 一次性事件使用一次性标志
dialog.Signal(CustomDialog.SignalName.Accepted)
.WithFlags(GodotObject.ConnectFlags.OneShot)
.To(callable);
```
### 2. 合理使用 ToAndCall
```csharp
// ✅ 适合:初始化时立即触发
settingsSlider.Signal(Slider.SignalName.ValueChanged)
.ToAndCall(new Callable(this, nameof(OnSettingsChanged)), initialSliderValue);
// ❌ 避免:重复连接并调用
button.Signal(Button.SignalName.Pressed)
.ToAndCall(new Callable(this, nameof(OnButtonPressed))); // 可能不必要
```
### 3. 链式调用可读性
```csharp
// ✅ 推荐:清晰的链式调用
player.Signal(Player.SignalName.HealthChanged)
.WithFlags(GodotObject.ConnectFlags.Deferred)
.To(new Callable(this, nameof(UpdateHealthUI)));
// ❌ 避免:过度嵌套
node.Signal(CustomSignal.Signal1).WithFlags(Flags1).To(callable1)
.Signal(CustomSignal.Signal2).WithFlags(Flags2).To(callable2);
```
## 相关链接
- [Godot 扩展方法](../README.md) - 扩展方法总览
- [节点扩展](../README.md#nodeextensions) - 更多节点操作方法
- [取消注册扩展](../README.md#unregisterextension) - 事件生命周期管理
- [Godot 官方信号文档](https://docs.godotengine.org/en/stable/tutorials/scripting_signals.html) - Godot 信号系统基础

View File

@ -1,46 +0,0 @@
using GFramework.Game.Abstractions.setting;
using Godot;
namespace GFramework.Godot.setting;
/// <summary>
/// Godot音频设置应用器用于将音频设置应用到Godot引擎的音频总线系统
/// </summary>
/// <param name="settings">音频设置对象,包含主音量、背景音乐音量和音效音量</param>
/// <param name="busMap">音频总线映射对象,定义了不同音频类型的总线名称</param>
public sealed class GodotAudioApplier(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
{
/// <summary>
/// 应用音频设置到Godot音频系统
/// </summary>
/// <returns>表示异步操作的任务</returns>
public Task Apply()
{
SetBus(busMap.Master, settings.MasterVolume);
SetBus(busMap.Bgm, settings.BgmVolume);
SetBus(busMap.Sfx, settings.SfxVolume);
return Task.CompletedTask;
}
/// <summary>
/// 设置指定音频总线的音量
/// </summary>
/// <param name="busName">音频总线名称</param>
/// <param name="linear">线性音量值0-1之间</param>
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))
);
}
}

View File

@ -1,12 +1,46 @@

using GFramework.Game.Abstractions.setting;
using GFramework.Game.Abstractions.setting;
using Godot;
namespace GFramework.Godot.setting;
public class GodotAudioSettings: AudioSettings,IApplyAbleSettings
/// <summary>
/// Godot音频设置实现类用于应用音频配置到Godot音频系统
/// </summary>
/// <param name="settings">音频设置对象,包含主音量、背景音乐音量和音效音量</param>
/// <param name="busMap">音频总线映射对象,定义了不同音频类型的总线名称</param>
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
{
/// <summary>
/// 应用音频设置到Godot音频系统
/// </summary>
/// <returns>表示异步操作的任务</returns>
public Task Apply()
{
SetBus(busMap.Master, settings.MasterVolume);
SetBus(busMap.Bgm, settings.BgmVolume);
SetBus(busMap.Sfx, settings.SfxVolume);
return Task.CompletedTask;
}
/// <summary>
/// 设置指定音频总线的音量
/// </summary>
/// <param name="busName">音频总线名称</param>
/// <param name="linear">线性音量值0-1之间</param>
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))
);
}
}

View File

@ -0,0 +1,547 @@
# Godot 设置模块 (Godot Settings Module)
## 概述
Godot 设置模块是 GFramework.Godot 的核心组件之一,专门为 Godot 引擎提供游戏设置系统的实现。该模块将通用的设置框架与 Godot
引擎的特定功能相结合,提供了音频设置和图形设置的完整解决方案。
## 核心类
### 音频设置系统
#### AudioBusMap
音频总线映射配置类,用于定义音频系统中不同类型音频的总线名称。
**属性:**
- `Master` - 主音频总线名称(默认:"Master"
- `Bgm` - 背景音乐音频总线名称(默认:"BGM"
- `Sfx` - 音效音频总线名称(默认:"SFX"
#### GodotAudioApplier
音频设置应用器,负责将音频设置应用到 Godot 引擎的音频总线系统。
**功能:**
- 应用音量设置到指定音频总线
- 处理音量格式转换(线性值到分贝)
- 音频总线存在性检查和警告
#### GodotAudioSettings
Godot 音频设置实现类,继承自 AudioSettings 并实现 IApplyAbleSettings。
**继承关系:**
```
AudioSettings (基础设置类)
GodotAudioSettings (Godot 特定实现)
IApplyAbleSettings (可应用设置接口)
```
### 图形设置系统
#### GodotGraphicsSettings
Godot 图形设置实现类,继承自 GraphicsSettings 并实现 IApplyAbleSettings。
**功能:**
- 分辨率设置和窗口尺寸调整
- 全屏模式切换
- 窗口位置自动居中
- 多显示器支持
## 架构设计
```mermaid
graph TD
A[AudioSettings] --> B[GodotAudioSettings]
C[GraphicsSettings] --> D[GodotGraphicsSettings]
E[IApplyAbleSettings] --> B
E --> D
F[AudioBusMap] --> G[GodotAudioApplier]
B --> G
G --> H[AudioServer API]
D --> I[DisplayServer API]
J[SettingsSystem] --> K[Apply Method]
K --> G
K --> D
```
## 使用示例
### 音频设置配置
#### 基本音频设置
```csharp
// 创建音频设置
var audioSettings = new GodotAudioSettings
{
MasterVolume = 0.8f, // 80% 主音量
BgmVolume = 0.6f, // 60% 背景音乐音量
SfxVolume = 0.9f // 90% 音效音量
};
// 应用设置
await audioSettings.Apply();
```
#### 自定义音频总线映射
```csharp
// 自定义音频总线映射
var customBusMap = new AudioBusMap
{
Master = "Master_Bus",
Bgm = "Background_Music",
Sfx = "Sound_Effects"
};
// 创建音频设置应用器
var settings = new GodotAudioSettings
{
MasterVolume = 0.7f,
BgmVolume = 0.5f,
SfxVolume = 0.8f
};
var applier = new GodotAudioApplier(settings, customBusMap);
await applier.Apply();
```
#### 通过设置系统使用
```csharp
// 注册音频设置到设置模型
var settingsModel = this.GetModel<ISettingsModel>();
var audioSettings = settingsModel.Get<GodotAudioSettings>();
audioSettings.MasterVolume = 0.8f;
audioSettings.BgmVolume = 0.6f;
audioSettings.SfxVolume = 0.9f;
// 通过设置系统应用
var settingsSystem = this.GetSystem<ISettingsSystem>();
await settingsSystem.Apply<GodotAudioSettings>();
```
### 图形设置配置
#### 基本图形设置
```csharp
// 创建图形设置
var graphicsSettings = new GodotGraphicsSettings
{
ResolutionWidth = 1920,
ResolutionHeight = 1080,
Fullscreen = true
};
// 应用设置
await graphicsSettings.Apply();
```
#### 窗口模式切换
```csharp
public class DisplayManager : Node
{
private GodotGraphicsSettings _graphicsSettings;
public override void _Ready()
{
_graphicsSettings = new GodotGraphicsSettings();
}
public async Task ToggleFullscreen()
{
_graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen;
await _graphicsSettings.Apply();
}
public async Task SetResolution(int width, int height)
{
_graphicsSettings.ResolutionWidth = width;
_graphicsSettings.ResolutionHeight = height;
_graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏
await _graphicsSettings.Apply();
}
}
```
#### 预设分辨率配置
```csharp
public class ResolutionPresets
{
public static readonly (int width, int height)[] CommonResolutions =
{
(1920, 1080), // Full HD
(2560, 1440), // QHD
(3840, 2160), // 4K
(1280, 720), // HD
(1366, 768), // 常见笔记本分辨率
};
public static async Task ApplyResolution(GodotGraphicsSettings settings, int width, int height)
{
settings.ResolutionWidth = width;
settings.ResolutionHeight = height;
settings.Fullscreen = false;
await settings.Apply();
}
}
```
## API 详细说明
### AudioBusMap
```csharp
public sealed class AudioBusMap
{
public string Master { get; init; } = "Master";
public string Bgm { get; init; } = "BGM";
public string Sfx { get; init; } = "SFX";
}
```
**特点:**
- 使用 `init` 属性,创建后不可修改
- 提供合理的默认值
- 支持对象初始化语法
### GodotAudioApplier
```csharp
public sealed class GodotAudioApplier(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
{
public Task Apply();
}
```
**实现细节:**
- 将线性音量值0-1转换为分贝值
- 使用 `AudioServer.GetBusIndex()` 查找总线
- 使用 `AudioServer.SetBusVolumeDb()` 设置音量
- 自动处理无效总线名称
### GodotAudioSettings
```csharp
public class GodotAudioSettings : AudioSettings, IApplyAbleSettings
{
public Task Apply();
}
```
**音量设置:**
- `MasterVolume` - 主音量控制
- `BgmVolume` - 背景音乐音量控制
- `SfxVolume` - 音效音量控制
**Apply 方法实现:**
```csharp
public Task Apply()
{
SetBusVolume("Master", MasterVolume);
SetBusVolume("BGM", BgmVolume);
SetBusVolume("SFX", SfxVolume);
return Task.CompletedTask;
}
```
### GodotGraphicsSettings
```csharp
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
{
public Task Apply();
}
```
**Apply 方法功能:**
- 设置窗口边框标志
- 切换窗口模式(窗口化/全屏)
- 调整窗口尺寸
- 自动居中窗口
## 技术实现细节
### 音频音量转换
Godot 音频系统使用分贝dB作为音量单位而我们通常使用线性值0-1
```csharp
// 线性值到分贝转换
float linearVolume = 0.5f; // 50% 音量
float dbVolume = Mathf.LinearToDb(linearVolume); // 转换为分贝
// 应用到音频总线
AudioServer.SetBusVolumeDb(busIndex, dbVolume);
```
### 音量限制和保护
为避免完全静音(-inf dB应用了最小音量限制
```csharp
float clampedVolume = Mathf.Clamp(linear, 0.0001f, 1f);
float dbVolume = Mathf.LinearToDb(clampedVolume);
```
### 窗口管理
#### 全屏模式
```csharp
// 设置全屏
DisplayServer.WindowSetMode(DisplayServer.WindowMode.ExclusiveFullscreen);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true);
```
#### 窗口化模式
```csharp
// 设置窗口化
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
DisplayServer.WindowSetSize(newSize);
// 居中窗口
var screen = DisplayServer.GetPrimaryScreen();
var screenSize = DisplayServer.ScreenGetSize(screen);
var position = (screenSize - newSize) / 2;
DisplayServer.WindowSetPosition(position);
```
## 最佳实践
### 1. 音频设置管理
#### 音量变化平滑过渡
```csharp
public class AudioManager : Node
{
private Tween _volumeTween;
public async Task SmoothVolumeTransition(float targetMasterVolume, float duration = 1.0f)
{
var audioSettings = new GodotAudioSettings();
var currentVolume = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("Master"));
var currentLinear = Mathf.DbToLinear(currentVolume);
_volumeTween?.Kill();
_volumeTween = CreateTween();
_volumeTween.TweenMethod(
new Callable(this, nameof(SetMasterVolume)),
currentLinear,
targetMasterVolume,
duration
);
}
private void SetMasterVolume(float linearVolume)
{
var settings = new GodotAudioSettings { MasterVolume = linearVolume };
settings.Apply();
}
}
```
#### 音频设置验证
```csharp
public static class AudioSettingsValidator
{
public static bool ValidateBusNames(AudioBusMap busMap)
{
var masterIndex = AudioServer.GetBusIndex(busMap.Master);
var bgmIndex = AudioServer.GetBusIndex(busMap.Bgm);
var sfxIndex = AudioServer.GetBusIndex(busMap.Sfx);
return masterIndex >= 0 && bgmIndex >= 0 && sfxIndex >= 0;
}
public static void LogMissingBuses(AudioBusMap busMap)
{
if (AudioServer.GetBusIndex(busMap.Master) < 0)
GD.PrintErr($"Master bus not found: {busMap.Master}");
if (AudioServer.GetBusIndex(busMap.Bgm) < 0)
GD.PrintErr($"BGM bus not found: {busMap.Bgm}");
if (AudioServer.GetBusIndex(busMap.Sfx) < 0)
GD.PrintErr($"SFX bus not found: {busMap.Sfx}");
}
}
```
### 2. 图形设置管理
#### 分辨率变更安全检查
```csharp
public static class DisplayValidator
{
public static bool IsResolutionSupported(int width, int height)
{
var screen = DisplayServer.GetPrimaryScreen();
var screenSize = DisplayServer.ScreenGetSize(screen);
return width <= screenSize.x && height <= screenSize.y;
}
public static (int width, int height) GetMaxSafeResolution()
{
var screen = DisplayServer.GetPrimaryScreen();
var screenSize = DisplayServer.ScreenGetSize(screen);
return ((int)screenSize.x, (int)screenSize.y);
}
}
```
#### 图形设置持久化
```csharp
public class GraphicsSettingsManager : Node
{
private const string SettingsKey = "graphics_settings";
private GodotGraphicsSettings _settings;
public override void _Ready()
{
LoadSettings();
}
private void LoadSettings()
{
var storage = new GodotFileStorage(new JsonSerializer());
try
{
_settings = storage.Read<GodotGraphicsSettings>(SettingsKey);
}
catch (FileNotFoundException)
{
_settings = new GodotGraphicsSettings
{
ResolutionWidth = 1920,
ResolutionHeight = 1080,
Fullscreen = false
};
SaveSettings();
}
}
public void SaveSettings()
{
var storage = new GodotFileStorage(new JsonSerializer());
storage.Write(SettingsKey, _settings);
}
public async Task ApplyAndSave()
{
await _settings.Apply();
SaveSettings();
}
}
```
## 性能考虑
### 1. 音频设置应用
- 音频总线查找是 O(1) 操作
- 音量转换计算开销很小
- 建议批量应用多个音量设置
### 2. 图形设置应用
- 窗口操作需要系统调用,相对较慢
- 分辨率变更可能触发窗口重建
- 避免频繁切换显示模式
### 3. 设置持久化
- 使用异步文件 I/O
- 考虑设置变更防抖机制
- 压缩设置文件以减少 I/O 开销
## 故障排除
### 常见问题
#### 1. 音频总线未找到
```
错误Audio bus not found: CustomBGM
解决:确保在 Godot 项目中创建了对应的音频总线
```
#### 2. 分辨率设置无效
```
错误:分辨率无法设置到指定值
解决:检查分辨率是否超出显示器支持范围
```
#### 3. 全屏模式问题
```
错误:全屏切换失败
解决:检查是否在调试器中运行,某些全屏模式在调试时可能不可用
```
### 调试技巧
#### 音频调试
```csharp
// 打印所有音频总线信息
for (int i = 0; i < AudioServer.GetBusCount(); i++)
{
var name = AudioServer.GetBusName(i);
var volume = AudioServer.GetBusVolumeDb(i);
GD.Print($"Bus {i}: {name} ({volume} dB)");
}
```
#### 图形调试
```csharp
// 打印当前显示信息
var screen = DisplayServer.GetPrimaryScreen();
var screenSize = DisplayServer.ScreenGetSize(screen);
var windowSize = DisplayServer.WindowGetSize();
var windowPos = DisplayServer.WindowGetPosition();
var windowMode = DisplayServer.WindowGetMode();
GD.Print($"Screen: {screenSize}");
GD.Print($"Window: {windowSize} at {windowPos}");
GD.Print($"Mode: {windowMode}");
```
## 相关链接
- [设置系统](../../GFramework.Game/setting/README.md) - 通用设置框架
- [存储模块](../storage/README.md) - 设置持久化存储
- [扩展方法](../extensions/README.md) - Godot 扩展功能
- [Godot 音频文档](https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html) - Godot 音频总线系统
- [Godot 显示文档](https://docs.godotengine.org/en/stable/tutorials/rendering/window_management.html) - Godot 窗口管理

View File

@ -0,0 +1,281 @@
# 存储模块 (Storage Module)
## 概述
存储模块是 GFramework.Godot 的核心存储实现,专门为 Godot 引擎设计的文件存储系统。该模块支持 Godot 的虚拟路径系统(如
`res://``user://`),并提供了按键级别的细粒度锁机制来保证线程安全。
## 核心类
### GodotFileStorage
Godot 特化的文件存储实现,实现了 `IStorage` 接口。
**主要特性:**
- ✅ Godot 虚拟路径支持(`res://`, `user://`
- ✅ 线程安全(按键级别的细粒度锁)
- ✅ 同步/异步读写操作
- ✅ 自动创建目录结构
- ❌ 删除操作Delete 方法未实现)
## 功能特性
### 路径处理
该存储系统支持三种路径类型:
#### 1. Godot 资源路径 (`res://`)
- **用途**:存储游戏资源文件
- **特点**:只读,包含在游戏构建中
- **示例**`res://config/game_settings.json`
#### 2. Godot 用户数据路径 (`user://`)
- **用途**:存储用户数据、存档、配置等
- **特点**:可读写,游戏可访问的用户目录
- **示例**`user://saves/save_001.dat`
#### 3. 普通文件系统路径
- **用途**:存储临时文件或调试数据
- **特点**:完整的文件系统访问
- **示例**`C:/Games/MyGame/logs/debug.log`
### 路径验证与清理
```mermaid
graph TD
A[输入路径] --> B{包含 ".." ?}
B -->|是| C[抛出异常]
B -->|否| D{是 Godot 路径?}
D -->|是| E[直接使用]
D -->|否| F[清理路径段]
F --> G[替换无效字符]
G --> H[创建目录结构]
H --> I[返回绝对路径]
C --> J[结束]
E --> J
I --> J
```
### 线程安全机制
每个文件路径都有独立的锁对象,确保:
1. **细粒度锁** - 不同文件可以并发访问
2. **避免死锁** - 锁的获取顺序一致
3. **高性能** - 减少锁竞争
## API 接口
### IStorage 接口
```csharp
public interface IStorage
{
// 读取操作
T Read<T>(string key);
T Read<T>(string key, T defaultValue);
Task<T> ReadAsync<T>(string key);
// 写入操作
void Write<T>(string key, T value);
Task WriteAsync<T>(string key, T value);
// 检查存在性
bool Exists(string key);
Task<bool> ExistsAsync(string key);
// 删除操作(未实现)
void Delete(string key);
}
```
## 使用示例
### 基本使用
```csharp
// 创建存储实例(需要序列化器)
var serializer = new JsonSerializer(); // 或其他序列化器
var storage = new GodotFileStorage(serializer);
// 写入用户数据
var userData = new UserData
{
PlayerName = "Alice",
Level = 5,
Score = 1000
};
storage.Write("user://player.dat", userData);
// 读取用户数据
var loadedData = storage.Read<UserData>("user://player.dat");
Console.WriteLine($"Player: {loadedData.PlayerName}, Level: {loadedData.Level}");
```
### 异步操作
```csharp
// 异步写入游戏配置
var config = new GameConfig
{
Resolution = "1920x1080",
Fullscreen = true,
Volume = 0.8f
};
await storage.WriteAsync("user://config.json", config);
// 异步读取配置
var loadedConfig = await storage.ReadAsync<GameConfig>("user://config.json");
```
### 不同路径类型使用
```csharp
// 读取游戏资源(只读)
var levelData = storage.Read<LevelData>("res://levels/level_001.json");
// 存储用户存档
var saveData = new SaveData { /* ... */ };
storage.Write("user://saves/slot_001.dat", saveData);
// 存储调试信息(普通路径)
var debugLog = new DebugLog { /* ... */ };
storage.Write("logs/debug_" + DateTime.Now.Ticks + ".json", debugLog);
```
### 存在性检查
```csharp
// 检查文件是否存在
if (storage.Exists("user://settings.json"))
{
var settings = storage.Read<AppSettings>("user://settings.json");
// 使用设置...
}
else
{
// 使用默认设置
var defaultSettings = new AppSettings();
storage.Write("user://settings.json", defaultSettings);
}
```
### 带默认值的读取
```csharp
// 尝试读取,如果文件不存在则返回默认值
var settings = storage.Read("user://user_prefs.json", new UserPrefs
{
Language = "en",
Volume = 1.0f,
Difficulty = 1
});
```
## 路径扩展
该模块使用了路径扩展方法:
```csharp
public static class GodotPathExtensions
{
public static bool IsUserPath(this string path);
public static bool IsResPath(this string path);
public static bool IsGodotPath(this string path);
}
```
**使用示例:**
```csharp
string path1 = "user://save.dat";
string path2 = "res://config.json";
string path3 = "C:/temp/file.txt";
Console.WriteLine(path1.IsGodotPath()); // true
Console.WriteLine(path1.IsUserPath()); // true
Console.WriteLine(path2.IsResPath()); // true
Console.WriteLine(path3.IsGodotPath()); // false
```
## 性能考虑
### 1. 锁机制
- 每个文件路径独立锁,减少锁竞争
- 读写操作串行化,避免数据损坏
### 2. 文件访问
- Godot 虚拟路径使用 `FileAccess` API
- 普通路径使用标准 .NET 文件 I/O
- 自动创建目录结构
### 3. 内存使用
- 锁对象使用 `ConcurrentDictionary` 管理
- 锁对象按需创建,避免内存泄漏
## 错误处理
### 常见异常
1. **ArgumentException** - 路径参数无效
- 空路径
- 包含 ".." 的路径
- 无效的存储键
2. **FileNotFoundException** - 文件不存在
- 读取不存在的文件时抛出
3. **IOException** - 文件操作失败
- 写入权限不足
- 磁盘空间不足
### 错误处理示例
```csharp
try
{
var data = storage.Read<UserData>("user://save.dat");
}
catch (FileNotFoundException)
{
Console.WriteLine("存档文件不存在,创建新的存档");
var newSave = new UserData();
storage.Write("user://save.dat", newSave);
}
catch (Exception ex)
{
Console.WriteLine($"读取存档失败: {ex.Message}");
}
```
## 最佳实践
1. **路径选择**
- 游戏资源使用 `res://`
- 用户数据使用 `user://`
- 调试/临时文件使用普通路径
2. **异常处理**
- 总是处理 `FileNotFoundException`
- 使用带默认值的 `Read` 重载方法
3. **性能优化**
- 批量读写时使用异步方法
- 避免频繁的小文件操作
4. **序列化器选择**
- JSON人类可读调试友好
- 二进制:性能更好,文件更小
## 相关链接
- [路径扩展](../extensions/README.md#godotpathextensions)
- [设置系统](../setting/README.md)
- [抽象接口定义](../../../GFramework.Core/Abstractions/storage/)