GwWuYou f781be22a9 refactor(setting): 重构音频设置系统架构
- 将 GodotAudioSettings 从继承模式改为组合模式
- 移除 GodotAudioApplier 类,统一使用 GodotAudioSettings
- 修改 GodotAudioSettings 构造函数接受 AudioSettings 和 AudioBusMap 参数
- 更新文档中的代码示例和类图关系
- 添加自定义总线映射的平滑过渡功能
- 优化音频设置的应用流程和音量转换逻辑
2026-01-12 22:01:58 +08:00

581 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Godot 设置模块 (Godot Settings Module)
## 概述
Godot 设置模块是 GFramework.Godot 的核心组件之一,专门为 Godot 引擎提供游戏设置系统的实现。该模块将通用的设置框架与 Godot
引擎的特定功能相结合,提供了音频设置和图形设置的完整解决方案。
## 核心类
### 音频设置系统
#### AudioBusMap
音频总线映射配置类,用于定义音频系统中不同类型音频的总线名称。
**属性:**
- `Master` - 主音频总线名称(默认:"Master"
- `Bgm` - 背景音乐音频总线名称(默认:"BGM"
- `Sfx` - 音效音频总线名称(默认:"SFX"
#### GodotAudioApplier
音频设置应用器,负责将音频设置应用到 Godot 引擎的音频总线系统。
**功能:**
- 应用音量设置到指定音频总线
- 处理音量格式转换(线性值到分贝)
- 音频总线存在性检查和警告
#### GodotAudioSettings
Godot 音频设置实现类,接收 AudioSettings 配置并实现 IApplyAbleSettings 接口,负责将音频配置应用到 Godot 音频系统。
**实现关系:**
```
AudioSettings (配置数据)
↓ [组合]
GodotAudioSettings (Godot 特定实现) → IApplyAbleSettings (可应用设置接口)
```
**功能:**
- 接收 AudioSettings 配置对象和 AudioBusMap 总线映射
- 实现 Apply() 方法,将音量设置应用到指定音频总线
- 支持自定义音频总线映射
- 自动处理音量格式转换(线性值到分贝)
### 图形设置系统
#### GodotGraphicsSettings
Godot 图形设置实现类,继承自 GraphicsSettings 并实现 IApplyAbleSettings。
**功能:**
- 分辨率设置和窗口尺寸调整
- 全屏模式切换
- 窗口位置自动居中
- 多显示器支持
## 架构设计
```mermaid
graph TD
A[AudioSettings] --> B[GodotAudioSettings]
C[GraphicsSettings] --> D[GodotGraphicsSettings]
E[IApplyAbleSettings] --> B
E --> D
G[AudioBusMap] --> B
B --> I[AudioServer API]
D --> J[DisplayServer API]
K[SettingsSystem] --> L[Apply Method]
L --> B
L --> D
```
## 使用示例
### 音频设置配置
#### 基本音频设置
```csharp
// 创建音频配置数据
var settings = new AudioSettings
{
MasterVolume = 0.8f, // 80% 主音量
BgmVolume = 0.6f, // 60% 背景音乐音量
SfxVolume = 0.9f // 90% 音效音量
};
// 创建 Godot 音频设置应用器
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
// 应用设置
await audioSettings.Apply();
```
#### 自定义音频总线映射
```csharp
// 自定义音频总线映射
var customBusMap = new AudioBusMap
{
Master = "Master_Bus",
Bgm = "Background_Music",
Sfx = "Sound_Effects"
};
// 创建音频配置
var settings = new AudioSettings
{
MasterVolume = 0.7f,
BgmVolume = 0.5f,
SfxVolume = 0.8f
};
// 使用自定义总线映射应用设置
var audioSettings = new GodotAudioSettings(settings, customBusMap);
await audioSettings.Apply();
```
#### 通过设置系统使用
```csharp
// 注册音频设置到设置模型
var settingsModel = this.GetModel<ISettingsModel>();
var audioSettingsData = settingsModel.Get<AudioSettings>();
audioSettingsData.MasterVolume = 0.8f;
audioSettingsData.BgmVolume = 0.6f;
audioSettingsData.SfxVolume = 0.9f;
// 创建 Godot 音频设置应用器
var godotAudioSettings = new GodotAudioSettings(audioSettingsData, new AudioBusMap());
await godotAudioSettings.Apply();
```
### 图形设置配置
#### 基本图形设置
```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` 属性,创建后不可修改
- 提供合理的默认值
- 支持对象初始化语法
### GodotAudioSettings
```csharp
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
{
public Task Apply();
}
```
**构造函数参数:**
- `settings` - AudioSettings 配置对象,包含音量设置
- `busMap` - AudioBusMap 对象,定义音频总线映射
**Apply 方法实现:**
```csharp
public Task Apply()
{
SetBus(busMap.Master, settings.MasterVolume);
SetBus(busMap.Bgm, settings.BgmVolume);
SetBus(busMap.Sfx, settings.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 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 AudioSettings { MasterVolume = linearVolume };
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
audioSettings.Apply();
}
}
// 使用自定义总线映射的平滑过渡
public class CustomAudioManager : Node
{
private Tween _volumeTween;
private AudioBusMap _customBusMap;
public override void _Ready()
{
_customBusMap = new AudioBusMap
{
Master = "Master_Bus",
Bgm = "Background_Music",
Sfx = "Sound_Effects"
};
}
public async Task SmoothVolumeTransition(float targetMasterVolume, float duration = 1.0f)
{
var settings = new AudioSettings { MasterVolume = targetMasterVolume };
var currentVolume = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex(_customBusMap.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 audioSettingsData = new AudioSettings { MasterVolume = linearVolume };
var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap);
audioSettings.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 窗口管理