mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
docs(godot): 刷新存储与设置专题页
- 更新 Godot 存储与设置专题页到当前运行时与 applicator 接线路径 - 补充 Godot 子页巡检结论、验证结果与 RP-014 恢复点记录
This commit is contained in:
parent
71f36c7c20
commit
1a62f337a6
@ -12,11 +12,12 @@
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-013`
|
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-014`
|
||||||
- 当前阶段:`Phase 5 - Governance Maintenance`
|
- 当前阶段:`Phase 5 - Governance Maintenance`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- 继续巡检 `Godot` / `Game` 相关 README、landing page、tutorial 与 API reference 的 cross-link 是否回漂
|
- 继续巡检 `Godot` / `Game` 相关 README、landing page、tutorial 与 API reference 的 cross-link 是否回漂
|
||||||
- 保持 `Godot` family 的模块 README、生成器 README 与站内专题页使用同一套 owner / adoption path 叙述
|
- 保持 `Godot` family 的模块 README、生成器 README 与站内专题页使用同一套 owner / adoption path 叙述
|
||||||
|
- 重点观察 `storage.md` / `setting.md` 这类子页是否继续沿用当前 applicator / adoption path 口径
|
||||||
- 仅在发现新的入口漂移时补最小修复,不重复改写已经稳定的 landing page
|
- 仅在发现新的入口漂移时补最小修复,不重复改写已经稳定的 landing page
|
||||||
|
|
||||||
## 当前状态摘要
|
## 当前状态摘要
|
||||||
@ -42,6 +43,9 @@
|
|||||||
- `2026-04-23` 的 validation-only 巡检新增结论:
|
- `2026-04-23` 的 validation-only 巡检新增结论:
|
||||||
- 根 `README.md`、`docs/zh-CN/godot/index.md`、`docs/zh-CN/tutorials/godot-integration.md`、`docs/zh-CN/source-generators/index.md` 与 `docs/zh-CN/api-reference/index.md` 当前仍保持同一套 `Godot` owner / adoption path 叙述,没有发现新的入口漂移
|
- 根 `README.md`、`docs/zh-CN/godot/index.md`、`docs/zh-CN/tutorials/godot-integration.md`、`docs/zh-CN/source-generators/index.md` 与 `docs/zh-CN/api-reference/index.md` 当前仍保持同一套 `Godot` owner / adoption path 叙述,没有发现新的入口漂移
|
||||||
- `scan_module_evidence.py Godot` 显示 `docs/zh-CN/godot/storage.md` 与 `setting.md` 仍属于 `Godot` runtime docs surface,应并入 active topic 的最小恢复摘要,避免后续 `boot` 漏掉当前 landing page 的关键入口
|
- `scan_module_evidence.py Godot` 显示 `docs/zh-CN/godot/storage.md` 与 `setting.md` 仍属于 `Godot` runtime docs surface,应并入 active topic 的最小恢复摘要,避免后续 `boot` 漏掉当前 landing page 的关键入口
|
||||||
|
- `2026-04-23` 的子页巡检新增结论:
|
||||||
|
- `docs/zh-CN/godot/storage.md` 之前仍停留在旧版 API 手册写法,缺少 frontmatter、`IStorage` / repository 边界和 `GodotYamlConfigLoader` 的分流说明
|
||||||
|
- `docs/zh-CN/godot/setting.md` 之前仍把 `GodotAudioSettings` / `GodotGraphicsSettings` 描述成直接持有设置数据对象的旧构造方式,没有反映当前 `ISettingsModel` + `RegisterApplicator(...)` 接法
|
||||||
- `2026-04-23` 的交叉链接巡检新增结论:
|
- `2026-04-23` 的交叉链接巡检新增结论:
|
||||||
- `GFramework.Godot/README.md` 仍停留在旧版简略描述,缺少当前包关系、子系统地图、最小接入路径与 `docs/zh-CN` 入口
|
- `GFramework.Godot/README.md` 仍停留在旧版简略描述,缺少当前包关系、子系统地图、最小接入路径与 `docs/zh-CN` 入口
|
||||||
- `GFramework.Godot.SourceGenerators/README.md` 虽有示例,但没有覆盖 `AutoScene`、`AutoUiPage`、`AutoRegisterExportedCollections` 等当前生成器分组,也没有把运行时 / 生成器边界说清
|
- `GFramework.Godot.SourceGenerators/README.md` 虽有示例,但没有覆盖 `AutoScene`、`AutoUiPage`、`AutoRegisterExportedCollections` 等当前生成器分组,也没有把运行时 / 生成器边界说清
|
||||||
@ -82,6 +86,8 @@
|
|||||||
- 更新 `docs/zh-CN/api-reference/index.md` 的 `Godot` 模块映射,使 API 参考能直接落到 `Godot` 专用生成器专题页,而不是仅回到总览页
|
- 更新 `docs/zh-CN/api-reference/index.md` 的 `Godot` 模块映射,使 API 参考能直接落到 `Godot` 专用生成器专题页,而不是仅回到总览页
|
||||||
- 修正根 `README.md` 中 `GFramework.Godot.SourceGenerators` 的模块描述,使其与当前生成器职责边界一致
|
- 修正根 `README.md` 中 `GFramework.Godot.SourceGenerators` 的模块描述,使其与当前生成器职责边界一致
|
||||||
- 扩充 `docs/zh-CN/source-generators/index.md` 的 Godot 选包说明,把 Scene / UI 包装与导出集合注册辅助纳入入口摘要
|
- 扩充 `docs/zh-CN/source-generators/index.md` 的 Godot 选包说明,把 Scene / UI 包装与导出集合注册辅助纳入入口摘要
|
||||||
|
- 重写 `docs/zh-CN/godot/storage.md`,补齐 frontmatter、`GodotFileStorage` 的路径语义、repository 分工与 `GodotYamlConfigLoader` 分流边界
|
||||||
|
- 重写 `docs/zh-CN/godot/setting.md`,改回当前 `ISettingsModel` / `RegisterApplicator(...)` 口径,并补上 `LocalizationMap` fallback 与 `CoreGrid` 注册示例
|
||||||
|
|
||||||
## Inventory(第一版)
|
## Inventory(第一版)
|
||||||
|
|
||||||
@ -130,10 +136,10 @@
|
|||||||
- 结果:通过;PR `#271` 已关闭,latest reviewed commit 为 `df91d3706ba9db71737e803ef2f40f4841ecbbf1`,当前 `2` 条 open thread 都是已被本地文件满足的陈旧信号,不再构成本轮阻塞
|
- 结果:通过;PR `#271` 已关闭,latest reviewed commit 为 `df91d3706ba9db71737e803ef2f40f4841ecbbf1`,当前 `2` 条 open thread 都是已被本地文件满足的陈旧信号,不再构成本轮阻塞
|
||||||
- 最新构建结论:
|
- 最新构建结论:
|
||||||
- `2026-04-23` `cd docs && bun run build`
|
- `2026-04-23` `cd docs && bun run build`
|
||||||
- 结果:通过;在 `Godot` docs surface 的 validation-only 巡检后再次验证通过,仅保留既有 VitePress 大 chunk warning,无构建失败
|
- 结果:通过;在修正 `docs/zh-CN/godot/storage.md` 与 `setting.md` 后再次验证通过,仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||||
- 最新稳定性巡检结论:
|
- 最新稳定性巡检结论:
|
||||||
- `2026-04-23` 重新执行 `Godot` docs surface 巡检
|
- `2026-04-23` 重新执行 `Godot` docs surface 巡检
|
||||||
- 结果:通过;`README.md`、`docs/zh-CN/godot/index.md`、`docs/zh-CN/tutorials/godot-integration.md`、`docs/zh-CN/source-generators/index.md` 与 `docs/zh-CN/api-reference/index.md` 仍保持同一套职责边界,没有发现新的入口漂移
|
- 结果:通过;根入口链路保持稳定,并额外发现 `docs/zh-CN/godot/storage.md`、`setting.md` 两页存在旧版叙述残留,当前已按源码口径完成最小修复
|
||||||
- 最新恢复治理结论:
|
- 最新恢复治理结论:
|
||||||
- `2026-04-23` 重新读取 `ai-plan/public/archive/documentation-governance-and-refresh/**`
|
- `2026-04-23` 重新读取 `ai-plan/public/archive/documentation-governance-and-refresh/**`
|
||||||
- 结果:通过;确认 `Godot` family 适合把最小恢复摘要迁回 active topic,但不需要把整段归档历史重新放回默认 `boot` 路径
|
- 结果:通过;确认 `Godot` family 适合把最小恢复摘要迁回 active topic,但不需要把整段归档历史重新放回默认 `boot` 路径
|
||||||
@ -153,6 +159,8 @@
|
|||||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/index.md`:通过
|
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/index.md`:通过
|
||||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`:通过
|
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`:通过
|
||||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`:通过
|
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`:通过
|
||||||
|
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`:通过
|
||||||
|
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`:通过
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
|
|||||||
@ -480,3 +480,49 @@
|
|||||||
1. 若后续分支继续调整 `GFramework.Godot` 运行时入口,优先复核 `docs/zh-CN/godot/storage.md`、`setting.md` 与根
|
1. 若后续分支继续调整 `GFramework.Godot` 运行时入口,优先复核 `docs/zh-CN/godot/storage.md`、`setting.md` 与根
|
||||||
`README.md` / landing page 是否仍保持同一套职责边界
|
`README.md` / landing page 是否仍保持同一套职责边界
|
||||||
2. 当后续分支再修改 `Godot` / `Game` family 的 README、docs 或公共 API 时,回到对应模块追加 targeted 巡检与验证
|
2. 当后续分支再修改 `Godot` / `Game` family 的 README、docs 或公共 API 时,回到对应模块追加 targeted 巡检与验证
|
||||||
|
|
||||||
|
### 当前恢复点:RP-014
|
||||||
|
|
||||||
|
- 继续沿用 `RP-013` 的 `Godot` docs surface 巡检范围,补读:
|
||||||
|
- `docs/zh-CN/godot/storage.md`
|
||||||
|
- `docs/zh-CN/godot/setting.md`
|
||||||
|
- `GFramework.Godot/Storage/GodotFileStorage.cs`
|
||||||
|
- `GFramework.Godot/Setting/GodotAudioSettings.cs`
|
||||||
|
- `GFramework.Godot/Setting/GodotGraphicsSettings.cs`
|
||||||
|
- `GFramework.Godot/Setting/GodotLocalizationSettings.cs`
|
||||||
|
- `GFramework.Godot/Setting/Data/AudioBusMap.cs`
|
||||||
|
- `GFramework.Godot/Setting/Data/LocalizationMap.cs`
|
||||||
|
- `GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs`
|
||||||
|
- `ai-libs/CoreGrid/scripts/module/UtilityModule.cs`
|
||||||
|
- `ai-libs/CoreGrid/scripts/module/ModelModule.cs`
|
||||||
|
- 巡检发现两处新的 topic 级漂移:
|
||||||
|
- `docs/zh-CN/godot/storage.md` 仍按旧版 API 手册组织,缺少 frontmatter、当前 `IStorage` / repository 分工与
|
||||||
|
`GodotYamlConfigLoader` 分流说明
|
||||||
|
- `docs/zh-CN/godot/setting.md` 仍使用过时的“settings data 直接注入 applicator 构造函数”叙述,没有反映当前
|
||||||
|
`ISettingsModel` + `RegisterApplicator(...)` 的真实接线方式
|
||||||
|
- 因此本轮执行最小修复集:
|
||||||
|
- 重写 `docs/zh-CN/godot/storage.md`
|
||||||
|
- 重写 `docs/zh-CN/godot/setting.md`
|
||||||
|
- 更新 active tracking 的恢复点、巡检结论与验证结果
|
||||||
|
|
||||||
|
### 当前决策(RP-014)
|
||||||
|
|
||||||
|
- 继续遵循“README / landing 稳定时,不重写稳定入口;只修新发现的 topic 漂移”的治理节奏
|
||||||
|
- `storage.md` 应强调宿主路径语义与 repository 分工,而不是重复 `Game` 通用存储手册
|
||||||
|
- `setting.md` 应强调 applicator 注册和运行时边界,而不是重新维护一份过时的设置 API 摘要
|
||||||
|
|
||||||
|
### 当前验证(RP-014)
|
||||||
|
|
||||||
|
- 模块扫描:
|
||||||
|
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`:通过
|
||||||
|
- 文档校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`:通过
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`:通过
|
||||||
|
- 构建校验:
|
||||||
|
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||||
|
|
||||||
|
### 下一步
|
||||||
|
|
||||||
|
1. 若后续分支继续调整 `GFramework.Godot` 运行时入口,优先复核 `docs/zh-CN/godot/storage.md`、`setting.md` 与根
|
||||||
|
`README.md` / landing page 是否仍保持同一套职责边界
|
||||||
|
2. 当后续分支再修改 `Godot` / `Game` family 的 README、docs 或公共 API 时,回到对应模块追加 targeted 巡检与验证
|
||||||
|
|||||||
@ -1,603 +1,156 @@
|
|||||||
# Godot 设置模块 (Godot Settings Module)
|
---
|
||||||
|
title: Godot 设置系统
|
||||||
|
description: 以当前 GFramework.Godot 源码、测试与 CoreGrid 接线为准,说明 Godot settings applicator 的职责、注册方式和运行时边界。
|
||||||
|
---
|
||||||
|
|
||||||
## 概述
|
# Godot 设置系统
|
||||||
|
|
||||||
Godot 设置模块是 GFramework.Godot 的核心组件之一,专门为 Godot 引擎提供游戏设置系统的实现。该模块将通用的设置框架与 Godot
|
`GFramework.Godot` 在设置这一层做的事情很克制:它没有重新发明一套设置模型,而是给
|
||||||
引擎的特定功能相结合,提供了音频设置、图形设置和本地化设置的完整解决方案。
|
`GFramework.Game` 的 `ISettingsModel` 提供三个 Godot 宿主 applicator:
|
||||||
|
|
||||||
## 核心类
|
- `GodotAudioSettings`
|
||||||
|
- `GodotGraphicsSettings`
|
||||||
|
- `GodotLocalizationSettings`
|
||||||
|
|
||||||
### 音频设置系统
|
这些类型的职责是“把已经存在的设置数据应用到 Godot 引擎和框架运行时”,不是负责设置 UI、设置持久化或设置迁移。
|
||||||
|
|
||||||
#### AudioBusMap
|
## 当前公开入口
|
||||||
|
|
||||||
音频总线映射配置类,用于定义音频系统中不同类型音频的总线名称。
|
### `GodotAudioSettings`
|
||||||
|
|
||||||
**属性:**
|
`GodotAudioSettings` 从 `ISettingsModel` 读取 `AudioSettings`,再按 `AudioBusMap` 中的总线名把音量写入
|
||||||
|
`AudioServer`。
|
||||||
|
|
||||||
- `Master` - 主音频总线名称(默认:"Master")
|
当前行为有几个关键点:
|
||||||
- `Bgm` - 背景音乐音频总线名称(默认:"BGM")
|
|
||||||
- `Sfx` - 音效音频总线名称(默认:"SFX")
|
|
||||||
|
|
||||||
#### GodotAudioApplier
|
- `Master`、`Bgm`、`Sfx` 三类音量都来自 `AudioSettings`
|
||||||
|
- 应用前会把线性音量限制在 `0.0001f ~ 1f`,再转换成分贝
|
||||||
|
- 如果找不到对应 bus,当前实现只会 `GD.PushWarning(...)`,不会抛异常中断整个设置流程
|
||||||
|
|
||||||
音频设置应用器,负责将音频设置应用到 Godot 引擎的音频总线系统。
|
`AudioBusMap` 默认值是:
|
||||||
|
|
||||||
**功能:**
|
- `Master`
|
||||||
|
- `BGM`
|
||||||
|
- `SFX`
|
||||||
|
|
||||||
- 应用音量设置到指定音频总线
|
如果项目里的 Godot Audio Bus 命名不同,需要在注册 applicator 时替换映射,而不是改写 applicator 本身。
|
||||||
- 处理音量格式转换(线性值到分贝)
|
|
||||||
- 音频总线存在性检查和警告
|
|
||||||
|
|
||||||
#### GodotAudioSettings
|
### `GodotGraphicsSettings`
|
||||||
|
|
||||||
Godot 音频设置实现类,接收 AudioSettings 配置并实现 IApplyAbleSettings 接口,负责将音频配置应用到 Godot 音频系统。
|
`GodotGraphicsSettings` 从 `ISettingsModel` 读取 `GraphicsSettings`,并把结果同步到 `DisplayServer`:
|
||||||
|
|
||||||
**实现关系:**
|
- `Fullscreen = true` 时切到 `ExclusiveFullscreen`
|
||||||
|
- 同时把 `Borderless` flag 设为 `true`
|
||||||
|
- `Fullscreen = false` 时切回窗口模式,设置窗口尺寸,并按主屏尺寸重新居中
|
||||||
|
|
||||||
```
|
当前实现没有扩展到分辨率档位之外的图形质量、渲染后端或平台特定显示策略。本页不再把这些未实现能力写成既成事实。
|
||||||
AudioSettings (配置数据)
|
|
||||||
↓ [组合]
|
|
||||||
GodotAudioSettings (Godot 特定实现) → IApplyAbleSettings (可应用设置接口)
|
|
||||||
```
|
|
||||||
|
|
||||||
**功能:**
|
### `GodotLocalizationSettings`
|
||||||
|
|
||||||
- 接收 AudioSettings 配置对象和 AudioBusMap 总线映射
|
`GodotLocalizationSettings` 负责把 `LocalizationSettings.Language` 同时同步到:
|
||||||
- 实现 `ApplyAsync()` 方法,将音量设置应用到指定音频总线
|
|
||||||
- 支持自定义音频总线映射
|
|
||||||
- 自动处理音量格式转换(线性值到分贝)
|
|
||||||
|
|
||||||
### 图形设置系统
|
- Godot `TranslationServer.SetLocale(...)`
|
||||||
|
- GFramework `ILocalizationManager.SetLanguage(...)`
|
||||||
|
|
||||||
#### GodotGraphicsSettings
|
这一步依赖 `LocalizationMap` 把“用户可见语言值”拆成两套目标值:
|
||||||
|
|
||||||
Godot 图形设置实现类,继承自 GraphicsSettings 并实现 IApplyAbleSettings。
|
- Godot locale,例如 `zh_CN`
|
||||||
|
- 框架语言码,例如 `zhs`
|
||||||
|
|
||||||
**功能:**
|
当前默认映射是:
|
||||||
|
|
||||||
- 分辨率设置和窗口尺寸调整
|
- `简体中文` -> Godot `zh_CN`,框架 `zhs`
|
||||||
- 全屏模式切换
|
- `English` -> Godot `en`,框架 `eng`
|
||||||
- 窗口位置自动居中
|
|
||||||
- 多显示器支持
|
|
||||||
|
|
||||||
### 本地化设置系统
|
`GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs` 已覆盖三条关键边界:
|
||||||
|
|
||||||
#### LocalizationMap
|
- 英文会同步到 `en` / `eng`
|
||||||
|
- 简体中文会同步到 `zh_CN` / `zhs`
|
||||||
|
- 未知语言值会稳定回退到英文,而不是让 Godot locale 与框架语言状态分裂
|
||||||
|
|
||||||
本地化映射配置类,用于把设置系统中保存的用户可见语言值解析为:
|
如果当前架构上下文里解析不到 `ILocalizationManager`,Godot locale 仍会被设置,只是不会额外同步框架语言管理器。
|
||||||
|
|
||||||
- Godot `TranslationServer` 使用的 locale
|
## 最小接入路径
|
||||||
- GFramework `ILocalizationManager` 使用的语言码
|
|
||||||
|
|
||||||
默认映射如下:
|
当前消费者 `ai-libs/CoreGrid` 的接法,是先注册 `SettingsModel<ISettingsDataRepository>`,再把 Godot applicator
|
||||||
|
挂进去:
|
||||||
- `"简体中文"` -> Godot `zh_CN`,框架语言码 `zhs`
|
|
||||||
- `"English"` -> Godot `en`,框架语言码 `eng`
|
|
||||||
|
|
||||||
未知语言值会稳定回退到英文,避免重启后出现设置值与运行时语言状态不一致。
|
|
||||||
|
|
||||||
#### GodotLocalizationSettings
|
|
||||||
|
|
||||||
Godot 本地化设置实现类,负责把 `LocalizationSettings` 同时应用到 Godot 引擎与 GFramework 本地化管理器。
|
|
||||||
|
|
||||||
**功能:**
|
|
||||||
|
|
||||||
- 将语言设置应用到 `TranslationServer.SetLocale(...)`
|
|
||||||
- 同步 `ILocalizationManager.SetLanguage(...)`
|
|
||||||
- 通过统一映射避免 Godot locale 与框架语言码分裂
|
|
||||||
|
|
||||||
## 架构设计
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[AudioSettings] --> B[GodotAudioSettings]
|
|
||||||
C[GraphicsSettings] --> D[GodotGraphicsSettings]
|
|
||||||
E[LocalizationSettings] --> F[GodotLocalizationSettings]
|
|
||||||
G[IApplyAbleSettings] --> B
|
|
||||||
G --> D
|
|
||||||
G --> F
|
|
||||||
|
|
||||||
H[AudioBusMap] --> B
|
|
||||||
I[LocalizationMap] --> F
|
|
||||||
|
|
||||||
B --> J[AudioServer API]
|
|
||||||
D --> K[DisplayServer API]
|
|
||||||
F --> L[TranslationServer API]
|
|
||||||
F --> M[ILocalizationManager]
|
|
||||||
|
|
||||||
N[SettingsSystem] --> O[ApplyAsync Method]
|
|
||||||
O --> B
|
|
||||||
O --> D
|
|
||||||
O --> F
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 音频设置配置
|
|
||||||
|
|
||||||
#### 基本音频设置
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 创建音频配置数据
|
using GFramework.Game.Abstractions.Data;
|
||||||
var settings = new AudioSettings
|
using GFramework.Game.Abstractions.Setting;
|
||||||
{
|
using GFramework.Game.Setting;
|
||||||
MasterVolume = 0.8f, // 80% 主音量
|
using GFramework.Godot.Setting;
|
||||||
BgmVolume = 0.6f, // 60% 背景音乐音量
|
using GFramework.Godot.Setting.Data;
|
||||||
SfxVolume = 0.9f // 90% 音效音量
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建 Godot 音频设置应用器
|
var settingsDataRepository = architecture.Context.GetUtility<ISettingsDataRepository>();
|
||||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
|
||||||
|
|
||||||
// 应用设置
|
architecture.RegisterModel(
|
||||||
await audioSettings.ApplyAsync();
|
new SettingsModel<ISettingsDataRepository>(
|
||||||
|
new SettingDataLocationProvider(),
|
||||||
|
settingsDataRepository)
|
||||||
|
.Also(it =>
|
||||||
|
{
|
||||||
|
it.RegisterApplicator(new GodotAudioSettings(it, new AudioBusMap()))
|
||||||
|
.RegisterApplicator(new GodotGraphicsSettings(it))
|
||||||
|
.RegisterApplicator(new GodotLocalizationSettings(it, new LocalizationMap()));
|
||||||
|
}));
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 自定义音频总线映射
|
这条接法说明了当前边界:
|
||||||
|
|
||||||
|
- 设置数据和生命周期由 `SettingsModel` 管
|
||||||
|
- `GodotAudioSettings` / `GodotGraphicsSettings` / `GodotLocalizationSettings` 只是 applicator
|
||||||
|
- 保存、加载和迁移仍然走 `ISettingsDataRepository`、`SettingsModel.InitializeAsync()`、`SaveAllAsync()` 等 `Game`
|
||||||
|
family 入口
|
||||||
|
|
||||||
|
## 运行时使用方式
|
||||||
|
|
||||||
|
业务代码通常不会直接 new 一次 applicator 然后立即调用,而是通过 `ISettingsSystem` 或 `ISettingsModel` 触发应用:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 自定义音频总线映射
|
using GFramework.Game.Abstractions.Setting;
|
||||||
var customBusMap = new AudioBusMap
|
using GFramework.Godot.Setting;
|
||||||
{
|
|
||||||
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.ApplyAsync();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 通过设置系统使用
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 注册音频设置到设置模型
|
|
||||||
var settingsModel = this.GetModel<ISettingsModel>();
|
var settingsModel = this.GetModel<ISettingsModel>();
|
||||||
var audioSettingsData = settingsModel.Get<AudioSettings>();
|
var audioData = settingsModel.GetData<AudioSettings>();
|
||||||
audioSettingsData.MasterVolume = 0.8f;
|
audioData.MasterVolume = 0.8f;
|
||||||
audioSettingsData.BgmVolume = 0.6f;
|
audioData.BgmVolume = 0.6f;
|
||||||
audioSettingsData.SfxVolume = 0.9f;
|
audioData.SfxVolume = 0.9f;
|
||||||
|
|
||||||
// 创建 Godot 音频设置应用器
|
var settingsSystem = this.GetSystem<ISettingsSystem>();
|
||||||
var godotAudioSettings = new GodotAudioSettings(audioSettingsData, new AudioBusMap());
|
await settingsSystem.Apply<GodotAudioSettings>();
|
||||||
await godotAudioSettings.ApplyAsync();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 图形设置配置
|
对图形和语言设置的调用方式相同,区别只是 applicator 类型不同。
|
||||||
|
|
||||||
#### 基本图形设置
|
## 当前边界
|
||||||
|
|
||||||
```csharp
|
- 这三个类型都不是设置数据对象;它们读取的是 `AudioSettings`、`GraphicsSettings`、`LocalizationSettings`
|
||||||
// 创建图形设置
|
- 它们不负责设置持久化;是否保存到文件由 `ISettingsDataRepository` 和存储层决定
|
||||||
var graphicsSettings = new GodotGraphicsSettings
|
- `ApplyAsync()` 当前都只是同步推进 Godot 引擎调用后返回 `Task.CompletedTask`,不会启动后台工作线程
|
||||||
{
|
- `GodotAudioSettings` 依赖项目里已经存在对应 bus 名称;缺失时只会警告,不会帮你自动创建总线
|
||||||
ResolutionWidth = 1920,
|
- `GodotGraphicsSettings` 当前只覆盖窗口模式、尺寸和居中,不等于一个完整的图形选项系统
|
||||||
ResolutionHeight = 1080,
|
- `GodotLocalizationSettings` 解决的是“用户语言值 -> Godot locale / 框架语言码”双向对齐,不负责翻译资源本身的组织方式
|
||||||
Fullscreen = true
|
|
||||||
};
|
|
||||||
|
|
||||||
// 应用设置
|
## 什么时候应该改看别的入口
|
||||||
await graphicsSettings.ApplyAsync();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 窗口模式切换
|
### 先理解设置模型和仓库
|
||||||
|
|
||||||
```csharp
|
如果你想先理解 `ISettingsData`、`IResetApplyAbleSettings`、`SettingsModel`、`SettingsSystem` 与设置迁移,先看
|
||||||
public class DisplayManager : Node
|
[`../game/setting.md`](../game/setting.md)。
|
||||||
{
|
|
||||||
private GodotGraphicsSettings _graphicsSettings;
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
_graphicsSettings = new GodotGraphicsSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ToggleFullscreen()
|
|
||||||
{
|
|
||||||
_graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen;
|
|
||||||
await _graphicsSettings.ApplyAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetResolution(int width, int height)
|
|
||||||
{
|
|
||||||
_graphicsSettings.ResolutionWidth = width;
|
|
||||||
_graphicsSettings.ResolutionHeight = height;
|
|
||||||
_graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏
|
|
||||||
await _graphicsSettings.ApplyAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 预设分辨率配置
|
### 先理解设置如何被持久化
|
||||||
|
|
||||||
```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.ApplyAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 详细说明
|
- [`../game/storage.md`](../game/storage.md)
|
||||||
|
- [Godot 存储系统](./storage.md)
|
||||||
|
|
||||||
### AudioBusMap
|
本页只补 Godot 宿主如何“应用”设置,不重复维护一份完整设置系统手册。
|
||||||
|
|
||||||
```csharp
|
## 继续阅读
|
||||||
public sealed class AudioBusMap
|
|
||||||
{
|
|
||||||
public string Master { get; init; } = "Master";
|
|
||||||
public string Bgm { get; init; } = "BGM";
|
|
||||||
public string Sfx { get; init; } = "SFX";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**特点:**
|
1. [Godot 运行时集成](./index.md)
|
||||||
|
2. [Game 设置系统](../game/setting.md)
|
||||||
- 使用 `init` 属性,创建后不可修改
|
3. [Godot 存储系统](./storage.md)
|
||||||
- 提供合理的默认值
|
4. [Godot 集成教程](../tutorials/godot-integration.md)
|
||||||
- 支持对象初始化语法
|
|
||||||
|
|
||||||
### GodotAudioSettings
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
|
|
||||||
{
|
|
||||||
public Task ApplyAsync();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**构造函数参数:**
|
|
||||||
|
|
||||||
- `settings` - AudioSettings 配置对象,包含音量设置
|
|
||||||
- `busMap` - AudioBusMap 对象,定义音频总线映射
|
|
||||||
|
|
||||||
**Apply 方法实现:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public Task ApplyAsync()
|
|
||||||
{
|
|
||||||
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 ApplyAsync();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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 async void SetMasterVolume(float linearVolume)
|
|
||||||
{
|
|
||||||
var settings = new AudioSettings { MasterVolume = linearVolume };
|
|
||||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
|
||||||
await audioSettings.ApplyAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用自定义总线映射的平滑过渡
|
|
||||||
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 async void SetMasterVolume(float linearVolume)
|
|
||||||
{
|
|
||||||
var audioSettingsData = new AudioSettings { MasterVolume = linearVolume };
|
|
||||||
var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap);
|
|
||||||
await audioSettings.ApplyAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 音频设置验证
|
|
||||||
|
|
||||||
```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.ApplyAsync();
|
|
||||||
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}");
|
|
||||||
```
|
|
||||||
|
|||||||
@ -1,275 +1,138 @@
|
|||||||
# 存储模块 (Storage Module)
|
---
|
||||||
|
title: Godot 存储系统
|
||||||
|
description: 以当前 GFramework.Godot 源码与 CoreGrid 接线为准,说明 GodotFileStorage 的职责、路径边界和最小接入方式。
|
||||||
|
---
|
||||||
|
|
||||||
## 概述
|
# Godot 存储系统
|
||||||
|
|
||||||
存储模块是 GFramework.Godot 的核心存储实现,专门为 Godot 引擎设计的文件存储系统。该模块支持 Godot 的虚拟路径系统(如
|
`GFramework.Godot` 在存储这一层提供的核心入口只有 `GodotFileStorage`。
|
||||||
`res://` 和 `user://`),并提供了按键级别的细粒度锁机制来保证线程安全。
|
|
||||||
|
|
||||||
## 核心类
|
它实现 `GFramework.Game` 侧统一的 `IStorage` 契约,负责把序列化后的读写、目录列举和路径处理接到 Godot 的
|
||||||
|
`res://`、`user://` 和普通文件系统路径上,而不是另外提供一套独立的“Godot 专属存档框架”。
|
||||||
|
|
||||||
### GodotFileStorage
|
## 当前公开入口
|
||||||
|
|
||||||
Godot 特化的文件存储实现,实现了 `IStorage` 接口。
|
### `GodotFileStorage`
|
||||||
|
|
||||||
**主要特性:**
|
`GodotFileStorage` 的当前职责比较集中:
|
||||||
|
|
||||||
- ✅ Godot 虚拟路径支持(`res://`, `user://`)
|
- 对外暴露 `IStorage` 约定的 `Read`、`Write`、`Exists`、`Delete`、目录列举与目录创建能力
|
||||||
- ✅ 线程安全(按键级别的细粒度锁)
|
- 识别并保留 Godot 虚拟路径:`res://`、`user://`
|
||||||
- ✅ 同步/异步读写操作
|
- 对普通文件系统路径做段级清理,并拒绝包含 `..` 的非法 key
|
||||||
- ✅ 自动创建目录结构
|
- 使用 `IAsyncKeyLockManager` 对“绝对路径 / Godot 路径”做按 key 细粒度串行化
|
||||||
- ❌ 删除操作(Delete 方法未实现)
|
|
||||||
|
|
||||||
## 功能特性
|
构造函数默认会在未注入锁管理器时创建内部 `AsyncKeyLockManager`。这意味着:
|
||||||
|
|
||||||
### 路径处理
|
- 同一个 `GodotFileStorage` 实例内,不同文件可以并发访问
|
||||||
|
- 同一个目标路径的读写 / 删除会被串行化
|
||||||
|
- 锁作用域只限当前进程内的当前实例,不是跨进程文件锁
|
||||||
|
|
||||||
该存储系统支持三种路径类型:
|
## 路径语义
|
||||||
|
|
||||||
#### 1. Godot 资源路径 (`res://`)
|
### `res://`
|
||||||
|
|
||||||
- **用途**:存储游戏资源文件
|
`res://` 更适合作为只读资源或配置源目录。
|
||||||
- **特点**:只读,包含在游戏构建中
|
|
||||||
- **示例**:`res://config/game_settings.json`
|
|
||||||
|
|
||||||
#### 2. Godot 用户数据路径 (`user://`)
|
当前实现不会阻止你把它传给 `ReadAsync`、`ExistsAsync` 之类的方法,但在导出后的 Godot 项目里,`res://`
|
||||||
|
通常不应被当作用户可写存储根目录。存档、设置和运行时缓存应优先落到 `user://`。
|
||||||
|
|
||||||
- **用途**:存储用户数据、存档、配置等
|
### `user://`
|
||||||
- **特点**:可读写,游戏可访问的用户目录
|
|
||||||
- **示例**:`user://saves/save_001.dat`
|
|
||||||
|
|
||||||
#### 3. 普通文件系统路径
|
`user://` 是当前推荐的可写路径:
|
||||||
|
|
||||||
- **用途**:存储临时文件或调试数据
|
- 用户设置
|
||||||
- **特点**:完整的文件系统访问
|
- 存档
|
||||||
- **示例**:`C:/Games/MyGame/logs/debug.log`
|
- 运行时缓存
|
||||||
|
- 导出后仍需要读写的 JSON / YAML / 二进制数据
|
||||||
|
|
||||||
### 路径验证与清理
|
如果调用 `ListDirectoriesAsync()` 或 `ListFilesAsync()` 时传入空字符串,当前实现会默认从 `user://` 根开始列举。
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 线程安全机制
|
当 key 不是 Godot 路径时,`GodotFileStorage` 会:
|
||||||
|
|
||||||
每个文件路径都有独立的锁对象,确保:
|
1. 把 `\` 统一成 `/`
|
||||||
|
2. 拒绝包含 `..` 的 key
|
||||||
|
3. 按路径段清理非法文件名字符
|
||||||
|
4. 在写入或建目录前自动补父目录
|
||||||
|
|
||||||
1. **细粒度锁** - 不同文件可以并发访问
|
这条路径更适合测试、桌面工具链或显式指定外部目录的宿主环境,不建议在项目业务层自己重新拼装一套路径清理逻辑。
|
||||||
2. **避免死锁** - 锁的获取顺序一致
|
|
||||||
3. **高性能** - 减少锁竞争
|
|
||||||
|
|
||||||
## API 接口
|
## 最小接入路径
|
||||||
|
|
||||||
### IStorage 接口
|
当前消费者 `ai-libs/CoreGrid` 的接法是先注册同一个序列化器和存储实例,再让设置仓库、存档仓库等上层组件复用它:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public interface IStorage
|
using GFramework.Core.Abstractions.Serializer;
|
||||||
{
|
using GFramework.Core.Abstractions.Storage;
|
||||||
// 读取操作
|
using GFramework.Game.Abstractions.Data;
|
||||||
T Read<T>(string key);
|
using GFramework.Game.Storage;
|
||||||
T Read<T>(string key, T defaultValue);
|
using GFramework.Godot.Storage;
|
||||||
Task<T> ReadAsync<T>(string key);
|
using Godot;
|
||||||
|
|
||||||
// 写入操作
|
var jsonSerializer = new JsonSerializer();
|
||||||
void Write<T>(string key, T value);
|
architecture.RegisterUtility<ISerializer>(jsonSerializer);
|
||||||
Task WriteAsync<T>(string key, T value);
|
|
||||||
|
var storage = new GodotFileStorage(jsonSerializer);
|
||||||
// 检查存在性
|
architecture.RegisterUtility<IStorage>(storage);
|
||||||
bool Exists(string key);
|
|
||||||
Task<bool> ExistsAsync(string key);
|
architecture.RegisterUtility(new UnifiedSettingsDataRepository(
|
||||||
|
storage,
|
||||||
// 删除操作(未实现)
|
jsonSerializer,
|
||||||
void Delete(string key);
|
new DataRepositoryOptions
|
||||||
}
|
{
|
||||||
|
BasePath = ProjectSettings.GetSetting("application/config/save/setting_path").AsString(),
|
||||||
|
AutoBackup = true
|
||||||
|
}));
|
||||||
|
|
||||||
|
architecture.RegisterUtility<ISaveRepository<GameSaveData>>(new SaveRepository<GameSaveData>(
|
||||||
|
storage,
|
||||||
|
new SaveConfiguration
|
||||||
|
{
|
||||||
|
SaveRoot = ProjectSettings.GetSetting("application/config/save/save_path").AsString(),
|
||||||
|
SaveSlotPrefix = ProjectSettings.GetSetting("application/config/save/save_slot_prefix").AsString(),
|
||||||
|
SaveFileName = ProjectSettings.GetSetting("application/config/save/save_file_name").AsString()
|
||||||
|
}));
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用示例
|
这里的分工是:
|
||||||
|
|
||||||
### 基本使用
|
- `GodotFileStorage` 负责底层 key -> 文件读写
|
||||||
|
- `UnifiedSettingsDataRepository` 负责设置节聚合与持久化
|
||||||
|
- `SaveRepository<TSaveData>` 负责存档结构和保存槽位语义
|
||||||
|
|
||||||
```csharp
|
不要把 `GodotFileStorage` 本身写成“设置系统”或“存档系统”的 owner。
|
||||||
// 创建存储实例(需要序列化器)
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 读取用户数据
|
### 配置 YAML / schema 文本加载
|
||||||
var loadedData = storage.Read<UserData>("user://player.dat");
|
|
||||||
Console.WriteLine($"Player: {loadedData.PlayerName}, Level: {loadedData.Level}");
|
|
||||||
```
|
|
||||||
|
|
||||||
### 异步操作
|
如果你的目标是读取 `res://` 下的 YAML 配置,并在导出态同步到运行时缓存,请优先看
|
||||||
|
[`../game/config-system.md`](../game/config-system.md) 里的 `GodotYamlConfigLoader` 接法。
|
||||||
|
|
||||||
```csharp
|
这类场景的重点不是通用键值存储,而是:
|
||||||
// 异步写入游戏配置
|
|
||||||
var config = new GameConfig
|
|
||||||
{
|
|
||||||
Resolution = "1920x1080",
|
|
||||||
Fullscreen = true,
|
|
||||||
Volume = 0.8f
|
|
||||||
};
|
|
||||||
await storage.WriteAsync("user://config.json", config);
|
|
||||||
|
|
||||||
// 异步读取配置
|
- `res://` 与 `user://` 缓存切换
|
||||||
var loadedConfig = await storage.ReadAsync<GameConfig>("user://config.json");
|
- 生成器表元数据
|
||||||
```
|
- 热重载可用性边界
|
||||||
|
|
||||||
### 不同路径类型使用
|
### 通用存储契约
|
||||||
|
|
||||||
```csharp
|
如果你想先理解 `IStorage`、`ScopedStorage`、`FileStorage` 和统一数据仓库的宿主无关语义,应先看
|
||||||
// 读取游戏资源(只读)
|
[`../game/storage.md`](../game/storage.md)。
|
||||||
var levelData = storage.Read<LevelData>("res://levels/level_001.json");
|
|
||||||
|
|
||||||
// 存储用户存档
|
本页只补 Godot 宿主差异,不重复维护一份跨宿主 API 手册。
|
||||||
var saveData = new SaveData { /* ... */ };
|
|
||||||
storage.Write("user://saves/slot_001.dat", saveData);
|
|
||||||
|
|
||||||
// 存储调试信息(普通路径)
|
## 当前边界
|
||||||
var debugLog = new DebugLog { /* ... */ };
|
|
||||||
storage.Write("logs/debug_" + DateTime.UtcNow.Ticks + ".json", debugLog);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 存在性检查
|
- 同步 `Read` / `Write` / `Delete` / `Exists` 只是对异步方法的阻塞包装;在带同步上下文的宿主里,优先使用异步 API
|
||||||
|
- `GodotFileStorage` 不负责文件扩展名约定、作用域前缀或保存槽位策略,这些属于上层 repository / scoped storage
|
||||||
|
- 路径安全只覆盖当前 key 的格式校验与路径段清理,不代替业务层的目录规划
|
||||||
|
- 当前实现支持目录列举与目录创建,但没有额外的“监视目录变化”或“自动迁移目录结构”能力
|
||||||
|
|
||||||
```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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 带默认值的读取
|
1. [Godot 运行时集成](./index.md)
|
||||||
|
2. [Game 存储系统](../game/storage.md)
|
||||||
```csharp
|
3. [Game 配置系统](../game/config-system.md)
|
||||||
// 尝试读取,如果文件不存在则返回默认值
|
4. [Godot 集成教程](../tutorials/godot-integration.md)
|
||||||
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:人类可读,调试友好
|
|
||||||
- 二进制:性能更好,文件更小
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user