docs(game): 收口 Game 持久化文档波次

- 重写 Game 数据、存储、序列化与设置专题页,统一为当前运行时采用路径说明

- 补充 DataRepository、SaveRepository、FileStorage、JsonSerializer 与 SettingsModel 的职责边界和最小接入路径

- 更新 documentation-full-coverage-governance 的 RP-016 恢复点与验证结果
This commit is contained in:
gewuyou 2026-04-23 11:54:58 +08:00
parent 2fa19f89b6
commit 4b5a760643
6 changed files with 644 additions and 2288 deletions

View File

@ -12,17 +12,27 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-015`
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-016`
- 当前阶段:`Phase 5 - Governance Maintenance`
- 当前焦点:
- 继续巡检 `Godot` / `Game` 相关 README、landing page、tutorial 与 API reference 的 cross-link 是否回漂
- 保持 `Godot` family 的模块 README、生成器 README 与站内专题页使用同一套 owner / adoption path 叙述
- 重点观察 `storage.md` / `setting.md` 这类子页是否继续沿用当前 applicator / adoption path 口径
- 仅在发现新的入口漂移时补最小修复,不重复改写已经稳定的 landing page
- 保持 `Game` family 的 persistence docs surface 与当前 `README`、源码、`PersistenceTests` 使用同一套 owner / adoption path 叙述
- `data.md``storage.md``serialization.md``setting.md` 视为 `Game` family 当前需要一起巡检的核心页面集,而不是分散的旧 API 手册页
- 重点观察 `DataRepository` / `UnifiedSettingsDataRepository` / `SaveRepository<TSaveData>``FileStorage` / `ScopedStorage``SettingsModel<TRepository>` 的职责边界是否再次回漂
- `Game` runtime public API 或 README 再次变动前,优先做 targeted 巡检,不重复改写已稳定的 landing page
## 当前状态摘要
- 已归档的 `documentation-governance-and-refresh` 仅保留为历史证据,不再作为默认 `boot` 入口
- `2026-04-23``Game` persistence docs wave 新增结论:
- `docs/zh-CN/game/storage.md` 之前仍停留在旧版“通用存储 API 手册”写法,没有反映 `FileStorage` / `ScopedStorage` 与上层 repository 的分工,也没有强调当前同步 API 只是异步阻塞包装
- `docs/zh-CN/game/data.md` 之前缺少 `DataRepository``UnifiedSettingsDataRepository``SaveRepository<TSaveData>` 三层分工,以及 `PersistenceTests` 已覆盖的备份 / 批量事件 / 存档迁移语义
- `docs/zh-CN/game/serialization.md` 之前仍沿用“业务层手工 Serialize 再写回 storage”的旧示例没有反映当前 `FileStorage` 已直接复用注入的 `ISerializer`
- `docs/zh-CN/game/setting.md` 虽然已回到 `ISettingsModel` / `RegisterApplicator(...)` 口径,但缺少 frontmatter且还没有和新的 `Game` persistence docs surface 使用同一套结构
- `2026-04-23``Game` persistence docs wave 治理动作:
- 重写 `docs/zh-CN/game/storage.md`,将其改为 `FileStorage` / `ScopedStorage` 的职责、路径语义、作用域复用与 repository 边界页
- 重写 `docs/zh-CN/game/data.md`,补齐 `DataRepository``UnifiedSettingsDataRepository``SaveRepository<TSaveData>``DataRepositoryOptions` / `SaveConfiguration` 的当前契约
- 重写 `docs/zh-CN/game/serialization.md`,收敛到 `JsonSerializer` 的配置生命周期、运行时类型序列化与和 storage / repository 的分工
- 重写 `docs/zh-CN/game/setting.md`,使其与 `SettingsModel<TRepository>``SettingsSystem`、迁移缓存和统一设置仓库的当前实现保持一致
- 本轮已消化的 PR #271 review follow-up
- 为 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 补齐 WSL worktree 下的显式 Linux Git 绑定,避免 `git.exe` 在当前会话触发 `Exec format error`
- 同步更新 `.agents/skills/gframework-pr-review/SKILL.md`,改为与 `AGENTS.md` 一致的 Git 策略,并把命令示例统一到 `.agents/...` 路径
@ -96,7 +106,7 @@
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | `README / landing / 类型族级 XML inventory 已收口,成员级审计待补齐` | 根 README、模块 README、`docs/zh-CN/core/**``docs/zh-CN/abstractions/core-abstractions.md` 已对齐当前目录与类型族基线 | 进入巡检;如有新 API 变更,再追加成员级 XML 审计 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | `README / landing / generator topic / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Cqrs/README.md``GFramework.Cqrs.Abstractions/README.md``GFramework.Cqrs.SourceGenerators/README.md``docs/zh-CN/core/cqrs.md``docs/zh-CN/source-generators/cqrs-handler-registry-generator.md``docs/zh-CN/api-reference/index.md` 已对齐当前源码与测试 | 转入巡检;下一波切到 `Game` family 的 XML / 教程链路审计 |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `README / landing / abstractions / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Game/README.md``GFramework.Game.Abstractions/README.md``GFramework.Game.SourceGenerators/README.md``docs/zh-CN/game/index.md``docs/zh-CN/abstractions/game-abstractions.md` 已对齐当前源码与目录基线 | 转入巡检;优先抽查 `config-system``scene``ui``source-generators` 交叉链路是否回漂 |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `README / landing / abstractions / persistence topic pages / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Game/README.md``GFramework.Game.Abstractions/README.md``GFramework.Game.SourceGenerators/README.md``docs/zh-CN/game/index.md``docs/zh-CN/abstractions/game-abstractions.md``docs/zh-CN/game/data.md``storage.md``serialization.md``setting.md` 已对齐当前源码、README 与 `PersistenceTests` | 转入巡检;优先观察后续分支是否再次把 `Game` persistence docs 写回旧 API 手册口径 |
| `Godot` / `Godot.SourceGenerators` | `README / 生成器 README / landing / topic / tutorial / API reference 入口已重新对齐,成员级 XML 审计不在本轮范围` | `GFramework.Godot/README.md``GFramework.Godot.SourceGenerators/README.md``docs/zh-CN/godot/index.md``architecture.md``scene.md``ui.md``storage.md``setting.md``signal.md``extensions.md``logging.md``docs/zh-CN/tutorials/godot-integration.md` | 进入巡检周期,优先观察后续分支是否再次把 README / API 入口写回过时边界 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | `README / landing / abstractions / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Ecs.Arch/README.md``GFramework.Ecs.Arch.Abstractions/README.md``docs/zh-CN/ecs/**``docs/zh-CN/abstractions/ecs-arch-abstractions.md` 已对齐当前源码与测试 | 转入巡检;后续仅在运行时公共 API 变动时补成员级 XML 细审 |
| `SourceGenerators.Common``*.SourceGenerators.Abstractions` | `已判定为内部支撑` | `*.csproj` 明确 `IsPackable=false` | 由所属模块 README 与生成器栏目说明 owner不建独立采用页 |
@ -137,7 +147,10 @@
- 结果通过PR `#271` 已关闭latest reviewed commit 为 `df91d3706ba9db71737e803ef2f40f4841ecbbf1`,当前 `2` 条 open thread 都是已被本地文件满足的陈旧信号,不再构成本轮阻塞
- 最新构建结论:
- `2026-04-23` `cd docs && bun run build`
- 结果:通过;在修正 `docs/zh-CN/godot/storage.md``setting.md` 后再次验证通过,仅保留既有 VitePress 大 chunk warning无构建失败
- 结果:通过;在重写 `docs/zh-CN/game/data.md``storage.md``serialization.md``setting.md` 后再次验证通过,仅保留既有 VitePress 大 chunk warning无构建失败
- 最新 `Game` persistence docs wave 结论:
- `2026-04-23` 基于 `GFramework.Game` 源码、`GFramework.Game/README.md``JsonSerializerTests``SettingsModelTests``PersistenceTests`
- 结果:通过;`docs/zh-CN/game/data.md``storage.md``serialization.md``setting.md` 当前已回到同一套 `Game` runtime 持久化采用路径,不再沿用旧版 API 手册叙述
- 最新稳定性巡检结论:
- `2026-04-23` 重新执行 `Godot` docs surface 巡检
- 结果:通过;根入口链路保持稳定,并额外发现 `docs/zh-CN/godot/storage.md``setting.md` 两页存在旧版叙述残留,当前已按源码口径完成最小修复
@ -168,10 +181,15 @@
- `2026-04-23` `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`通过boot 后复核)
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`通过boot 后复核)
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`通过boot 后复核)
- `2026-04-23` `cd docs && bun run build`通过boot 后复核;仅保留既有 VitePress 大 chunk warning
- `2026-04-23` `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Game`:通过
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/data.md`:通过
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/storage.md`:通过
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`:通过
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`:通过
- `2026-04-23` `cd docs && bun run build`:通过(本轮 `Game` persistence docs wave 复核;仅保留既有 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. 若后续分支继续调整 `GFramework.Game` 的 persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md``storage.md``serialization.md``setting.md` landing page 是否仍保持同一套职责边界
2. 当 `Godot` / `Game` family 再次出现交叉入口漂移时,沿用当前 README -> landing -> topic page -> API reference 的最小修复顺序
3. 仅在需要阶段级细节时再读取 `documentation-governance-and-refresh` archive而不是把 archive 重新当作默认 `boot` 入口

View File

@ -481,6 +481,70 @@
`README.md` / landing page 是否仍保持同一套职责边界
2. 当后续分支再修改 `Godot` / `Game` family 的 README、docs 或公共 API 时,回到对应模块追加 targeted 巡检与验证
### 当前恢复点RP-016
- 用户明确要求“继续下一步的文档治理,并形成足够体量的 PR”后当前 topic 不再停留在 validation-only 巡检,
而是切到一个可独立成波次的 `Game` persistence docs surface
- `docs/zh-CN/game/data.md`
- `docs/zh-CN/game/storage.md`
- `docs/zh-CN/game/serialization.md`
- `docs/zh-CN/game/setting.md`
- 先执行 `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Game`,确认 `Game` 的默认 docs surface
包含 `data``storage``serialization``setting``scene``ui``config-system` 与 landing / API fallback
- 结合 `GFramework.Game/README.md``FileStorage.cs``ScopedStorage.cs``DataRepository.cs`
`UnifiedSettingsDataRepository.cs``SaveRepository.cs``JsonSerializer.cs``SettingsModel.cs`
`SettingsSystem.cs``JsonSerializerTests.cs``SettingsModelTests.cs``PersistenceTests.cs`,确认四个旧页面存在持续性漂移:
- `storage.md` 仍按旧版通用 API 手册组织,没有强调 `FileStorage` / `ScopedStorage` 与 repository 的职责边界
- `data.md` 缺少 `DataRepository``UnifiedSettingsDataRepository``SaveRepository<TSaveData>` 三层分工,以及当前备份 /
批量事件 / 存档迁移语义
- `serialization.md` 仍沿用“业务层手工 Serialize 再写回 storage”的旧接法没有反映当前 `FileStorage`
已直接复用注入的 `ISerializer`
- `setting.md` 虽已回到 `ISettingsModel` / `RegisterApplicator(...)` 口径,但结构仍未与当前 `Game` family 的 runtime topic
页面统一,也缺少 frontmatter
- 因此本轮执行的最小但成组修复集是:
- 重写 `docs/zh-CN/game/storage.md`
- 重写 `docs/zh-CN/game/data.md`
- 重写 `docs/zh-CN/game/serialization.md`
- 重写 `docs/zh-CN/game/setting.md`
- 更新 active tracking 的恢复点、治理结论与下一步
### 当前决策RP-016
- 这轮不去扩张到 `Game` tutorial 或 root README而是把同一子领域里仍残留的旧页一次性收口形成清晰的 PR 边界
- `Game` persistence docs surface 统一采用“当前公开入口 -> 最小接入路径 -> 当前边界 -> 继续阅读”的结构,
不再维护分散的伪 API 手册页
- 文档示例只保留可直接映射到当前框架类型、测试行为或已验证 consumer wiring 的内容,避免继续写虚构接线名
### 当前验证RP-016
- 模块扫描:
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Game`:通过
- 文档校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/data.md`:通过
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/storage.md`:通过
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`:通过
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`:通过
- 构建校验:
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning无构建失败
- 代码 / 测试证据:
- `GFramework.Game/README.md`
- `GFramework.Game/Storage/FileStorage.cs`
- `GFramework.Game/Storage/ScopedStorage.cs`
- `GFramework.Game/Data/DataRepository.cs`
- `GFramework.Game/Data/UnifiedSettingsDataRepository.cs`
- `GFramework.Game/Data/SaveRepository.cs`
- `GFramework.Game/Serializer/JsonSerializer.cs`
- `GFramework.Game/Setting/SettingsModel.cs`
- `GFramework.Game/Setting/SettingsSystem.cs`
- `GFramework.Game.Tests/Data/PersistenceTests.cs`
- `GFramework.Game.Tests/Serializer/JsonSerializerTests.cs`
- `GFramework.Game.Tests/Setting/SettingsModelTests.cs`
### 下一步
1. 回填 tracking 的最新验证结果,并按仓库规则提交本轮 `Game` persistence docs wave
2. 若后续分支继续调整 `GFramework.Game` 的 persistence runtime 或 README优先复核这四个 topic page 与 landing page 的一致性
### 当前恢复点RP-015
- 通过 `$gframework-boot` 恢复当前 worktree 后,继续按 `documentation-full-coverage-governance` 的默认下一步执行一次

View File

@ -1,709 +1,189 @@
---
title: 数据与存档系统
description: 数据与存档系统提供统一的数据持久化基础能力,支持多槽位存档、版本化数据和仓库抽象
description: 以当前 GFramework.Game 源码与 PersistenceTests 为准,说明 DataRepository、UnifiedSettingsDataRepository 和 SaveRepository 的职责边界
---
# 数据与存档系统
## 概述
`GFramework.Game` 的数据持久化不是“只有一个万能仓库”。
数据与存档系统是 GFramework.Game 中用于管理游戏数据持久化的核心组件。它提供了统一的数据加载和保存接口,支持多槽位存档管理、版本化数据模式,以及与存储系统、序列化系统的组合使用。
当前更准确的理解是三层分工:
通过数据系统,你可以将游戏数据保存到本地存储,支持多个存档槽位,并在需要时于应用层实现版本迁移。
- `DataRepository`
- 面向“一个 location 对应一份持久化对象”的通用数据仓库
- `UnifiedSettingsDataRepository`
- 面向“多个设置 section 聚合到同一个文件”的设置仓库
- `SaveRepository<TSaveData>`
- 面向“按槽位组织的版本化存档”
**主要特性**
如果先把这三类入口分开理解,后续采用路径会清晰很多。
- 统一的数据持久化接口
- 多槽位存档管理
- 数据版本控制模式
- 异步加载和保存
- 批量数据操作
- 与存储系统集成
## 什么时候用哪个仓库
## 核心概念
### `DataRepository`
### 数据接口
适合:
`IData` 标记数据类型:
- 单份玩家档案
- 单份运行时缓存
- 一条 location 对应一个文件的普通业务数据
默认语义是:
- `IDataLocation` 决定 key
- 一条 location 对应一份对象
- 覆盖保存时可按 `DataRepositoryOptions.AutoBackup` 创建 `<key>.backup`
- `SaveAllAsync(...)` 视为一次批量提交,只发送批量事件,不重复发送单项保存事件
### `UnifiedSettingsDataRepository`
适合:
- 音频、图形、语言等多个设置 section 统一落到一份文件
- 启动时一次性加载所有设置,再交给 `SettingsModel<TRepository>` 编排
默认语义是:
- 底层持久化文件只有一份,默认文件名是 `settings.json`
- 各个设置 section 仍然通过 `IDataLocation` 的 key 区分
- 保存、删除时会整文件回写,而不是只改单个 section 文件
- 开启 `AutoBackup` 时,备份粒度也是整个统一文件,不是单个 section
### `SaveRepository<TSaveData>`
适合:
- 多槽位存档
- 需要版本迁移的 save data
- 需要列举现有槽位和删除槽位
默认语义是:
- 按 `SaveRoot` / `SaveSlotPrefix` / `SaveFileName` 组织目录
- 槽位不存在时,`LoadAsync(slot)` 返回新的 `TSaveData` 实例,而不是 `null`
- `ListSlotsAsync()` 只返回真实存在存档文件的槽位,并按升序排列
- 迁移成功后会把升级后的结果自动回写到槽位文件
## 当前公开入口
### `DataRepository`
`DataRepository` 是最通用的默认实现。当前仓库和测试确认的行为有几条需要特别记住:
- `LoadAsync<T>(location)` 在文件不存在时返回 `new T()`,不是抛异常
- `DeleteAsync(location)` 只有在目标数据真实存在并被删除时才发送删除事件
- `SaveAllAsync(...)` 会抑制逐项 `DataSavedEvent<T>`,只保留一次 `DataBatchSavedEvent`
- `AutoBackup = true` 时,覆盖旧值前会先把旧值写到 `<key>.backup`
最小接法通常是:项目先准备一个 `IDataLocation``IDataLocationProvider`,再把它交给 `DataRepository`
`location -> key` 的映射repository 自己不负责推导业务对象应该落在哪个位置。
### `UnifiedSettingsDataRepository`
当前 `SettingsModel<TRepository>` 依赖的默认设置仓库就是它。
它和普通 `DataRepository` 的关键区别不是接口,而是落盘形态:
- `DataRepository`
- 每个 location 对应一个独立文件
- `UnifiedSettingsDataRepository`
- 所有 section 聚合到同一个统一文件
还有两个容易遗漏的点:
- `LoadAllAsync()` 依赖 `RegisterDataType(location, type)` 建立 section -> 运行时类型映射
- 仓库内部会先把统一文件加载进缓存,再在保存 / 删除时基于快照整文件提交
这就是为什么 `SettingsModel<TRepository>` 会在拿到 `GetData<T>()``RegisterApplicator(...)` 后主动把类型注册回 repository。
### `SaveRepository<TSaveData>`
`SaveRepository<TSaveData>` 用于槽位存档,不直接复用 `IDataLocation`
最重要的公开配置是 `SaveConfiguration`
```csharp
public interface IData
var config = new SaveConfiguration
{
// 标记接口,用于标识可持久化的数据
}
SaveRoot = "saves",
SaveSlotPrefix = "slot_",
SaveFileName = "save.json"
};
```
### 数据仓库
按这个配置,槽位 `1` 的默认文件结构就是:
`IDataRepository` 提供通用的数据操作:
```csharp
public interface IDataRepository : IUtility
{
Task<T> LoadAsync<T>(IDataLocation location) where T : class, IData, new();
Task SaveAsync<T>(IDataLocation location, T data) where T : class, IData;
Task<bool> ExistsAsync(IDataLocation location);
Task DeleteAsync(IDataLocation location);
Task SaveAllAsync(IEnumerable<(IDataLocation, IData)> dataList);
}
```text
saves/
slot_1/
save.json
```
`IDataRepository` 描述的是“仓库语义”,不是固定的落盘格式。实现可以选择每个数据项独立成文件,也可以把多个 section 聚合到同一个文件里。
当前实现内部会先把根存储包装成 `ScopedStorage(storage, config.SaveRoot)`,再按槽位继续加前缀,因此项目层一般不需要手工再拼一次 `"saves/slot_1"`
当前内建实现里:
## 存档迁移的真实语义
- `DataRepository` 采用“每个 location 一份持久化对象”的模型
- `UnifiedSettingsDataRepository` 采用“所有设置聚合到一个统一文件”的模型
`SaveRepository<TSaveData>` 只有在 `TSaveData` 实现了 `IVersionedData` 时,才支持 `RegisterMigration(...)`
两者对外遵守同一套约定:
当前源码和 `PersistenceTests` 明确约束了下面这些行为
- `SaveAllAsync(...)` 视为一次批量提交,只发送 `DataBatchSavedEvent`,不会再为每个条目重复发送 `DataSavedEvent<T>`
- `DeleteAsync(...)` 只有在目标数据真实存在并被删除时才会发送删除事件
- 当 `DataRepositoryOptions.AutoBackup = true` 时,覆盖已有数据前会先保留上一份快照
- 对 `UnifiedSettingsDataRepository` 来说,备份粒度是整个统一文件,而不是单个设置 section
- 非版本化 save type 注册迁移器会直接失败
- 同一个 `FromVersion` 不能重复注册迁移器
- 迁移链缺口会显式抛错,不会静默返回半升级结果
- 迁移器声明的 `ToVersion` 必须与实际返回对象的版本一致
- 如果读到比当前运行时代码更高版本的存档,也会明确失败
- 单次加载会先固定一份迁移表快照,避免并发注册让同一次加载看到变化中的链路
### 存档仓库
也就是说,`SaveRepository<TSaveData>` 的迁移语义更偏“严格升级管线”,而不是“尽量帮你读出来”。
`ISaveRepository<T>` 专门用于管理游戏存档:
```csharp
public interface ISaveRepository<TSaveData> : IUtility
where TSaveData : class, IData, new()
{
ISaveRepository<TSaveData> RegisterMigration(ISaveMigration<TSaveData> migration);
Task<bool> ExistsAsync(int slot);
Task<TSaveData> LoadAsync(int slot);
Task SaveAsync(int slot, TSaveData data);
Task DeleteAsync(int slot);
Task<IReadOnlyList<int>> ListSlotsAsync();
}
```
`ISaveMigration<TSaveData>` 定义单步迁移:
```csharp
public interface ISaveMigration<TSaveData>
where TSaveData : class, IData
{
int FromVersion { get; }
int ToVersion { get; }
TSaveData Migrate(TSaveData oldData);
}
```
### 版本化数据
`IVersionedData` 支持数据版本管理:
```csharp
public interface IVersionedData : IData
{
int Version { get; }
DateTime LastModified { get; }
}
```
## 基本用法
### 定义数据类型
## 最小接入路径
下面是当前 `Game` 层最常见的一套组合方式:
```csharp
using GFramework.Core.Abstractions.Serializer;
using GFramework.Core.Abstractions.Storage;
using GFramework.Game.Abstractions.Data;
// 简单数据
public class PlayerData : IData
{
public string Name { get; set; }
public int Level { get; set; }
public int Experience { get; set; }
}
// 版本化数据
public class SaveData : IVersionedData
{
public int Version { get; set; } = 1;
public PlayerData Player { get; set; }
public DateTime SaveTime { get; set; }
}
```
### 使用存档仓库
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class SaveController : IController
{
public async Task SaveGame(int slot)
{
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
// 创建存档数据
var saveData = new SaveData
{
Player = new PlayerData
{
Name = "Player1",
Level = 10,
Experience = 1000
},
SaveTime = DateTime.Now
};
// 保存到指定槽位
await saveRepo.SaveAsync(slot, saveData);
Console.WriteLine($"游戏已保存到槽位 {slot}");
}
public async Task LoadGame(int slot)
{
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
// 检查存档是否存在
if (!await saveRepo.ExistsAsync(slot))
{
Console.WriteLine($"槽位 {slot} 不存在存档");
return;
}
// 加载存档
var saveData = await saveRepo.LoadAsync(slot);
Console.WriteLine($"加载存档: {saveData.Player.Name}, 等级 {saveData.Player.Level}");
}
public async Task DeleteSave(int slot)
{
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
// 删除存档
await saveRepo.DeleteAsync(slot);
Console.WriteLine($"已删除槽位 {slot} 的存档");
}
}
```
### 注册存档仓库
```csharp
using GFramework.Game.Data;
public class GameArchitecture : Architecture
{
protected override void Init()
{
// 获取存储系统
var storage = this.GetUtility<IStorage>();
// 创建存档配置
var saveConfig = new SaveConfiguration
{
SaveRoot = "saves",
SaveSlotPrefix = "slot_",
SaveFileName = "save.json"
};
// 注册存档仓库
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig);
RegisterUtility<ISaveRepository<SaveData>>(saveRepo);
}
}
```
## 高级用法
### 列出所有存档
```csharp
public async Task ShowSaveList()
{
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
// 获取所有存档槽位
var slots = await saveRepo.ListSlotsAsync();
Console.WriteLine($"找到 {slots.Count} 个存档:");
foreach (var slot in slots)
{
var saveData = await saveRepo.LoadAsync(slot);
Console.WriteLine($"槽位 {slot}: {saveData.Player.Name}, " +
$"等级 {saveData.Player.Level}, " +
$"保存时间 {saveData.SaveTime}");
}
}
```
### 自动保存
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class AutoSaveController : IController
{
private CancellationTokenSource? _autoSaveCts;
public void StartAutoSave(int slot, TimeSpan interval)
{
_autoSaveCts = new CancellationTokenSource();
Task.Run(async () =>
{
while (!_autoSaveCts.Token.IsCancellationRequested)
{
await Task.Delay(interval, _autoSaveCts.Token);
try
{
await SaveGame(slot);
Console.WriteLine("自动保存完成");
}
catch (Exception ex)
{
Console.WriteLine($"自动保存失败: {ex.Message}");
}
}
}, _autoSaveCts.Token);
}
public void StopAutoSave()
{
_autoSaveCts?.Cancel();
_autoSaveCts?.Dispose();
_autoSaveCts = null;
}
private async Task SaveGame(int slot)
{
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
var saveData = CreateSaveData();
await saveRepo.SaveAsync(slot, saveData);
}
private SaveData CreateSaveData()
{
// 从游戏状态创建存档数据
return new SaveData();
}
}
```
### 数据版本迁移
`SaveRepository<TSaveData>` 现在支持注册正式的迁移器,并在 `LoadAsync(slot)` 时自动升级旧版本存档。
迁移规则如下:
- `TSaveData` 需要实现 `IVersionedData`
- 仓库以 `new TSaveData().Version` 作为当前运行时目标版本
- 每个迁移器负责一个 `FromVersion -> ToVersion` 跳转
- 同一个 `FromVersion` 只能注册一个迁移器,且 `ToVersion` 必须严格大于 `FromVersion`
- 加载时仓库会按链路连续执行迁移,并在成功后自动回写升级后的存档
- 迁移器返回数据上的 `Version` 必须与声明的 `ToVersion` 一致
- 如果缺少中间迁移器,或者读到了比当前运行时更高的版本,`LoadAsync` 会抛出异常,避免静默加载错误数据
```csharp
public sealed class SaveData : IVersionedData
{
// 当前运行时代码支持的最新版本
public int Version { get; set; } = 2;
public string PlayerName { get; set; }
public int Level { get; set; }
public int Experience { get; set; }
public DateTime LastModified { get; set; }
}
public sealed class SaveDataMigrationV1ToV2 : ISaveMigration<SaveData>
{
public int FromVersion => 1;
public int ToVersion => 2;
public SaveData Migrate(SaveData oldData)
{
return new SaveData
{
Version = 2,
PlayerName = oldData.PlayerName,
Level = oldData.Level,
Experience = oldData.Level * 100,
LastModified = DateTime.UtcNow
};
}
}
public sealed class SaveModule : AbstractModule
{
public override void Install(IArchitecture architecture)
{
var storage = architecture.GetUtility<IStorage>();
var saveConfig = new SaveConfiguration
{
SaveRoot = "saves",
SaveSlotPrefix = "slot_",
SaveFileName = "save"
};
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig)
.RegisterMigration(new SaveDataMigrationV1ToV2());
architecture.RegisterUtility<ISaveRepository<SaveData>>(saveRepo);
}
}
public async Task<SaveData> LoadGame(int slot)
{
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
// 如果槽位里是 v1仓库会自动迁移到 v2并把新版本重新写回存储。
return await saveRepo.LoadAsync(slot);
}
```
`ISaveMigration<TSaveData>` 接收和返回的是同一个存档类型。也就是说,框架提供的是“当前类型内的版本升级管线”,
而不是跨 CLR 类型的双模型反序列化系统。如果旧版本缺失了新字段,反序列化会先使用当前类型的默认值,再由迁移器补齐。
### 使用数据仓库
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class SettingsController : IController
{
public async Task SaveSettings()
{
var dataRepo = this.GetUtility<IDataRepository>();
var settings = new GameSettings
{
MasterVolume = 0.8f,
MusicVolume = 0.6f,
SfxVolume = 0.7f
};
// 定义数据位置
var location = new DataLocation("settings", "game_settings.json");
// 保存设置
await dataRepo.SaveAsync(location, settings);
}
public async Task<GameSettings> LoadSettings()
{
var dataRepo = this.GetUtility<IDataRepository>();
var location = new DataLocation("settings", "game_settings.json");
// 检查是否存在
if (!await dataRepo.ExistsAsync(location))
{
return new GameSettings(); // 返回默认设置
}
// 加载设置
return await dataRepo.LoadAsync<GameSettings>(location);
}
}
```
### 批量保存数据
```csharp
public async Task SaveAllGameData()
{
var dataRepo = this.GetUtility<IDataRepository>();
var dataList = new List<(IDataLocation, IData)>
{
(new DataLocation("player", "profile.json"), playerData),
(new DataLocation("inventory", "items.json"), inventoryData),
(new DataLocation("quests", "progress.json"), questData)
};
// 批量保存
await dataRepo.SaveAllAsync(dataList);
Console.WriteLine("所有数据已保存");
}
```
`SaveAllAsync(...)` 的事件语义和逐项调用 `SaveAsync(...)` 不同。它代表一次显式的批量提交,因此适合让监听器在收到 `DataBatchSavedEvent` 时统一刷新 UI、缓存或元数据而不是对每个条目单独响应。
### 聚合设置仓库
如果你希望把设置统一保存到单个文件中,可以使用 `UnifiedSettingsDataRepository`
```csharp
using GFramework.Game.Data;
using GFramework.Game.Serializer;
using GFramework.Game.Storage;
public sealed class GameArchitecture : Architecture
{
protected override void Init()
var serializer = new JsonSerializer();
var storage = new FileStorage("GameData", serializer, ".json");
ISettingsDataRepository settingsRepository = new UnifiedSettingsDataRepository(
storage,
serializer,
new DataRepositoryOptions
{
var storage = this.GetUtility<IStorage>();
var serializer = new JsonSerializer();
BasePath = "settings",
AutoBackup = true
});
var settingsRepo = new UnifiedSettingsDataRepository(
storage,
serializer,
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = true
},
"settings.json");
settingsRepo.RegisterDataType(new DataLocation("settings", "graphics"), typeof(GraphicsSettings));
settingsRepo.RegisterDataType(new DataLocation("settings", "audio"), typeof(AudioSettings));
RegisterUtility<ISettingsDataRepository>(settingsRepo);
}
}
```
这个实现依然满足 `IDataRepository` 的通用契约,但有两个实现层面的差异需要明确:
- 它把所有设置 section 缓存在内存中,并在保存或删除时整文件回写
- 开启自动备份时,备份的是整个 `settings.json` 文件,因此适合做“上一次完整设置快照”的恢复,而不是 section 级别回滚
如果你需要 `LoadAllAsync()`,或者希望在同一个统一文件里混合反序列化多个 section必须先为每个 section 注册类型:
```csharp
public async Task PrintSettingsSnapshot()
var saveConfiguration = new SaveConfiguration
{
var repo = this.GetUtility<ISettingsDataRepository>();
var all = await repo.LoadAllAsync();
var graphics = (GraphicsSettings)all["graphics"];
var audio = (AudioSettings)all["audio"];
Console.WriteLine($"Resolution: {graphics.ResolutionWidth}x{graphics.ResolutionHeight}");
Console.WriteLine($"MasterVolume: {audio.MasterVolume}");
}
SaveRoot = "saves",
SaveSlotPrefix = "slot_",
SaveFileName = "save.json"
};
```
最小采用要求
分工应保持清晰:
- 项目需要可用的 `IStorage`
- 项目需要一个可用的序列化器实例,例如 `GFramework.Game.Serializer.JsonSerializer`
- 在注册仓库时,把所有需要参与 `LoadAllAsync()` 或混合 section 反序列化的 location/type 对显式调用一次 `RegisterDataType(...)`
- `storage` 只负责底层文件读写
- `settingsRepository` 负责统一设置文件
- `SaveRepository<TSaveData>` 负责槽位目录和存档迁移
兼容性说明:
## 当前边界
- 现在 `UnifiedSettingsDataRepository.LoadAsync<T>()` 发送的是 `DataLoadedEvent<T>`,而不是 `DataLoadedEvent<IData>`
- 如果你之前监听的是 `DataLoadedEvent<IData>`,需要改成订阅具体类型,例如 `DataLoadedEvent<GraphicsSettings>``DataLoadedEvent<AudioSettings>`
- `DataRepositoryOptions` 描述的是仓库公开行为契约,不是某一种固定落盘格式
- `UnifiedSettingsDataRepository` 不是通用万能仓库,它专门服务“多 section 聚合单文件”的场景
- `SaveRepository<TSaveData>` 不负责业务层的 autosave 策略、云同步或存档选择 UI
- `LoadAsync(...)` 返回新实例的行为适合默认启动路径;如果项目需要“缺档即报错”,应在业务层显式调用 `ExistsAsync(...)`
### 存档备份
## 继续阅读
```csharp
public async Task BackupSave(int slot)
{
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
if (!await saveRepo.ExistsAsync(slot))
{
Console.WriteLine("存档不存在");
return;
}
// 加载原存档
var saveData = await saveRepo.LoadAsync(slot);
// 保存到备份槽位
int backupSlot = slot + 100;
await saveRepo.SaveAsync(backupSlot, saveData);
Console.WriteLine($"存档已备份到槽位 {backupSlot}");
}
public async Task RestoreBackup(int slot)
{
int backupSlot = slot + 100;
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
if (!await saveRepo.ExistsAsync(backupSlot))
{
Console.WriteLine("备份不存在");
return;
}
// 加载备份
var backupData = await saveRepo.LoadAsync(backupSlot);
// 恢复到原槽位
await saveRepo.SaveAsync(slot, backupData);
Console.WriteLine($"已从备份恢复到槽位 {slot}");
}
```
## 最佳实践
1. **使用版本化数据**:为存档数据实现 `IVersionedData`
```csharp
✓ public class SaveData : IVersionedData { public int Version { get; set; } = 1; }
✗ public class SaveData : IData { } // 无法进行版本管理
```
2. **定期自动保存**:避免玩家数据丢失
```csharp
// 每 5 分钟自动保存
StartAutoSave(currentSlot, TimeSpan.FromMinutes(5));
```
3. **保存前验证数据**:确保数据完整性
```csharp
public async Task SaveGame(int slot)
{
var saveData = CreateSaveData();
if (!ValidateSaveData(saveData))
{
throw new InvalidOperationException("存档数据无效");
}
await saveRepo.SaveAsync(slot, saveData);
}
```
4. **处理保存失败**:使用 try-catch 捕获异常
```csharp
try
{
await saveRepo.SaveAsync(slot, saveData);
}
catch (Exception ex)
{
Logger.Error($"保存失败: {ex.Message}");
ShowErrorMessage("保存失败,请重试");
}
```
5. **提供多个存档槽位**:让玩家可以管理多个存档
```csharp
// 支持 10 个存档槽位
for (int i = 1; i <= 10; i++)
{
if (await saveRepo.ExistsAsync(i))
{
ShowSaveSlot(i);
}
}
```
6. **在关键时刻保存**:场景切换、关卡完成等
```csharp
public async Task OnLevelComplete()
{
// 关卡完成时自动保存
await SaveGame(currentSlot);
}
```
## 常见问题
### 问题:如何实现多个存档槽位?
**解答**
使用 `ISaveRepository<T>` 的槽位参数:
```csharp
// 保存到不同槽位
await saveRepo.SaveAsync(1, saveData); // 槽位 1
await saveRepo.SaveAsync(2, saveData); // 槽位 2
await saveRepo.SaveAsync(3, saveData); // 槽位 3
```
### 问题:如何处理数据版本升级?
**解答**
实现 `IVersionedData`,并在仓库初始化阶段注册 `ISaveMigration<TSaveData>`。之后 `LoadAsync(slot)` 会自动执行迁移并回写:
```csharp
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig)
.RegisterMigration(new SaveDataMigrationV1ToV2())
.RegisterMigration(new SaveDataMigrationV2ToV3());
var data = await saveRepo.LoadAsync(slot);
```
### 问题:存档数据保存在哪里?
**解答**
由存储系统决定,通常在:
- Windows: `%AppData%/GameName/saves/`
- Linux: `~/.local/share/GameName/saves/`
- macOS: `~/Library/Application Support/GameName/saves/`
### 问题:如何实现云存档?
**解答**
实现自定义的 `IStorage`,将数据保存到云端:
```csharp
public class CloudStorage : IStorage
{
public async Task WriteAsync(string path, byte[] data)
{
await UploadToCloud(path, data);
}
public async Task<byte[]> ReadAsync(string path)
{
return await DownloadFromCloud(path);
}
}
```
### 问题:如何加密存档数据?
**解答**
在保存和加载时进行加密/解密:
```csharp
public async Task SaveEncrypted(int slot, SaveData data)
{
var json = JsonSerializer.Serialize(data);
var encrypted = Encrypt(json);
await storage.WriteAsync(path, encrypted);
}
public async Task<SaveData> LoadEncrypted(int slot)
{
var encrypted = await storage.ReadAsync(path);
var json = Decrypt(encrypted);
return JsonSerializer.Deserialize<SaveData>(json);
}
```
### 问题:存档损坏怎么办?
**解答**
实现备份和恢复机制:
```csharp
public async Task SaveWithBackup(int slot, SaveData data)
{
// 先备份旧存档
if (await saveRepo.ExistsAsync(slot))
{
var oldData = await saveRepo.LoadAsync(slot);
await saveRepo.SaveAsync(slot + 100, oldData);
}
// 保存新存档
await saveRepo.SaveAsync(slot, data);
}
```
## 相关文档
- [设置系统](/zh-CN/game/setting) - 游戏设置管理
- [场景系统](/zh-CN/game/scene) - 场景切换时保存
- [存档系统实现教程](/zh-CN/tutorials/save-system) - 完整示例
- [Godot 集成](/zh-CN/godot/index) - Godot 中的数据管理
1. [设置系统](./setting.md)
2. [存储系统](./storage.md)
3. [序列化系统](./serialization.md)
4. [Game 入口](./index.md)

View File

@ -1,811 +1,162 @@
---
title: 序列化系统
description: 序列化系统提供了统一的对象序列化和反序列化接口,支持 JSON 格式和运行时类型处理
description: 以当前 GFramework.Game.JsonSerializer 与 JsonSerializerTests 为准,说明 JSON 序列化器的配置生命周期和使用边界
---
# 序列化系统
## 概述
`GFramework.Game` 当前在序列化这一层的默认公开入口只有 `JsonSerializer`
序列化系统是 GFramework.Game 中用于对象序列化和反序列化的核心组件。它提供了统一的序列化接口,支持将对象转换为字符串格式(如
JSON进行存储或传输并能够将字符串数据还原为对象。
它实现的是:
序列化系统与数据存储、配置管理、存档系统等模块深度集成,为游戏数据的持久化提供了基础支持。
- `ISerializer`
- `IRuntimeTypeSerializer`
**主要特性**
它不负责
- 统一的序列化接口
- JSON 格式支持
- 运行时类型序列化
- 泛型和非泛型 API
- 与存储系统无缝集成
- 类型安全的反序列化
- schema 驱动配置生成
- 存档槽位管理
- 文件路径或目录布局
## 核心概念
这些能力分别属于 source generator、repository 和 storage。
### 序列化器接
## 当前公开入
`ISerializer` 定义了基本的序列化操作:
### `JsonSerializer`
```csharp
public interface ISerializer : IUtility
{
// 将对象序列化为字符串
string Serialize&lt;T&gt;(T value);
// 将字符串反序列化为对象
T Deserialize&lt;T&gt;(string data);
}
```
### 运行时类型序列化器
`IRuntimeTypeSerializer` 扩展了基本接口,支持运行时类型处理:
```csharp
public interface IRuntimeTypeSerializer : ISerializer
{
// 使用运行时类型序列化对象
string Serialize(object obj, Type type);
// 使用运行时类型反序列化对象
object Deserialize(string data, Type type);
}
```
### JSON 序列化器
`JsonSerializer` 是基于 Newtonsoft.Json 的实现。它会直接复用构造时提供的
`JsonSerializerSettings``Converters` 集合,因此推荐在组合根统一完成配置,再把同一个实例注册到架构或仓库中复用:
```csharp
public sealed class JsonSerializer : IRuntimeTypeSerializer
{
string Serialize&lt;T&gt;(T value);
T Deserialize&lt;T&gt;(string data);
string Serialize(object obj, Type type);
object Deserialize(string data, Type type);
}
```
## 基本用法
### 注册序列化器
在架构中注册序列化器:
`JsonSerializer` 基于 `Newtonsoft.Json`,既支持泛型 API也支持运行时类型 API
```csharp
using GFramework.Core.Abstractions.Serializer;
using GFramework.Game.Serializer;
public class GameArchitecture : Architecture
{
protected override void Init()
{
// 在启动阶段一次性完成配置,后续将该实例视为只读
var jsonSerializer = new JsonSerializer();
jsonSerializer.Converters.Add(new PlayerDataJsonConverter());
RegisterUtility<ISerializer>(jsonSerializer);
RegisterUtility<IRuntimeTypeSerializer>(jsonSerializer);
}
}
ISerializer serializer = new JsonSerializer();
IRuntimeTypeSerializer runtimeSerializer = new JsonSerializer();
```
### 序列化对象
当前测试覆盖的核心行为包括:
使用泛型 API 序列化对象:
- 普通对象可正常 round-trip
- 注入的 `JsonSerializerSettings` 会直接生效
- `Settings``Converters` 暴露的是同一个活动配置实例
- 运行时类型序列化 / 反序列化可处理 `object + Type`
- 非法 JSON 会抛出带目标类型上下文的 `InvalidOperationException`
- 非法参数(例如空字符串)会保留 `ArgumentException`
- 运行时类型序列化允许 `null`,输出 `"null"`
## 配置生命周期
这部分是当前实现最容易被旧文档说错的地方。
`JsonSerializer` 不会复制你传入的 `JsonSerializerSettings`。它会直接持有并复用这份实例,以及里面的 `Converters` 集合。
这意味着推荐模式是:
1. 在组合根创建序列化器
2. 一次性完成 settings / converters 配置
3. 再把同一个实例注册给存储、repository 或 architecture
推荐写法:
```csharp
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using Newtonsoft.Json;
public class PlayerData
{
public string Name { get; set; }
public int Level { get; set; }
public int Experience { get; set; }
}
[ContextAware]
public partial class SaveController : IController
{
public void SavePlayer()
{
var serializer = this.GetUtility<ISerializer>();
var player = new PlayerData
{
Name = "Player1",
Level = 10,
Experience = 1000
};
// 序列化为 JSON 字符串
string json = serializer.Serialize(player);
Console.WriteLine(json);
// 输出: {"Name":"Player1","Level":10,"Experience":1000}
}
}
```
### 反序列化对象
从字符串还原对象:
```csharp
public void LoadPlayer()
{
var serializer = this.GetUtility<ISerializer>();
string json = "{\"Name\":\"Player1\",\"Level\":10,\"Experience\":1000}";
// 反序列化为对象
var player = serializer.Deserialize<PlayerData>(json);
Console.WriteLine($"玩家: {player.Name}, 等级: {player.Level}");
}
```
### 运行时类型序列化
处理不确定类型的对象:
```csharp
public void SerializeRuntimeType()
{
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
object data = new PlayerData { Name = "Player1", Level = 10 };
Type dataType = data.GetType();
// 使用运行时类型序列化
string json = serializer.Serialize(data, dataType);
// 使用运行时类型反序列化
object restored = serializer.Deserialize(json, dataType);
var player = restored as PlayerData;
Console.WriteLine($"玩家: {player?.Name}");
}
```
### 配置生命周期约束
`JsonSerializer` 不会复制 `JsonSerializerSettings`。这意味着:
- 传给构造函数的 settings 会被原样保留
- `serializer.Settings``serializer.Converters` 返回的都是活动配置对象
- 一旦序列化器实例已经注册给其他模块或开始被并发调用,就不应继续修改这些配置
推荐模式:
```csharp
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
};
settings.Converters.Add(new Vector2JsonConverter());
settings.Converters.Add(new CoordinateConverter());
var serializer = new JsonSerializer(settings);
```
不推荐写法:
```csharp
var serializer = architecture.GetUtility<IRuntimeTypeSerializer>();
// 序列化器已经被多个组件共享后,再继续改 converter容易让并发调用看到不稳定配置。
((JsonSerializer)serializer).Converters.Add(new LateBoundConverter());
```
## 最小接入路径
### 作为底层 serializer 注册
当前更常见的采用方式不是“业务代码直接到处调 serializer”而是把它注册给存储和 repository 复用:
```csharp
using GFramework.Core.Abstractions.Serializer;
using GFramework.Game.Serializer;
var serializer = new JsonSerializer();
architecture.RegisterUtility<ISerializer>(serializer);
architecture.RegisterUtility<IRuntimeTypeSerializer>(serializer);
```
不推荐模式:
然后由:
- `FileStorage`
- `UnifiedSettingsDataRepository`
- 其他依赖 `ISerializer` / `IRuntimeTypeSerializer` 的组件
统一复用这一份实例。
### 直接处理运行时类型
当业务层拿到的是 `object + Type` 组合,而不是静态泛型类型时,再使用运行时 API
```csharp
var serializer = architecture.GetUtility<IRuntimeTypeSerializer>();
var serializer = new JsonSerializer();
// 已经共享给运行时后再修改配置,容易让并发调用得到不稳定行为
((JsonSerializer)serializer).Converters.Add(new LateBoundConverter());
object data = new PlayerState
{
Name = "Runtime",
Level = 11
};
var json = serializer.Serialize(data, data.GetType());
var restored = serializer.Deserialize(json, data.GetType());
```
## 高级用法
## 与存储系统的关系
### 与存储系统集成
`FileStorage` 已经会调用注入的 `ISerializer` 自己完成对象读写,因此当前默认接法里:
序列化器与存储系统配合使用:
- 你可以直接 `storage.WriteAsync("profile/player", profile)`
- 不需要先手工 `serializer.Serialize(profile)` 再把字符串写回存储
```csharp
using GFramework.Core.Abstractions.Storage;
using GFramework.Game.Storage;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
手工显式调用 `Serialize(...)` 更适合这些场景:
[ContextAware]
public partial class DataManager : IController
{
public async Task SaveData()
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
- 需要把 JSON 发到网络或日志
- 需要和外部文本格式做中转
- 需要直接调试序列化输出内容
var gameData = new GameData
{
Score = 1000,
Coins = 500
};
如果目标只是本地持久化,优先让 `IStorage` / repository 复用 serializer。
// 序列化数据
string json = serializer.Serialize(gameData);
## 与配置系统的关系
// 写入存储
await storage.WriteAsync("game_data", json);
}
不要把 `JsonSerializer``Game` 的 YAML 配置系统混在一起:
public async Task<GameData> LoadData()
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
- `JsonSerializer`
- 负责运行时对象 JSON 序列化
- `Game.SourceGenerators + YamlConfigLoader`
- 负责 schema 驱动的配置表生成与 YAML 读取
// 从存储读取
string json = await storage.ReadAsync<string>("game_data");
如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [`config-system.md`](./config-system.md)。
// 反序列化数据
return serializer.Deserialize<GameData>(json);
}
}
```
## 当前边界
### 序列化复杂对象
- 当前公开默认实现只有 JSON没有内建 MessagePack、Binary 或 ProtoBuf 实现
- `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel<TRepository>``SaveRepository<TSaveData>`
- 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters
处理嵌套和集合类型:
## 继续阅读
```csharp
public class InventoryData
{
public List<ItemData> Items { get; set; }
public Dictionary<string, int> Resources { get; set; }
}
public class ItemData
{
public string Id { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
}
public void SerializeComplexData()
{
var serializer = this.GetUtility<ISerializer>();
var inventory = new InventoryData
{
Items = new List<ItemData>
{
new ItemData { Id = "sword_01", Name = "铁剑", Quantity = 1 },
new ItemData { Id = "potion_hp", Name = "生命药水", Quantity = 5 }
},
Resources = new Dictionary<string, int>
{
{ "gold", 1000 },
{ "wood", 500 }
}
};
// 序列化复杂对象
string json = serializer.Serialize(inventory);
// 反序列化
var restored = serializer.Deserialize<InventoryData>(json);
Console.WriteLine($"物品数量: {restored.Items.Count}");
Console.WriteLine($"金币: {restored.Resources["gold"]}");
}
```
### 处理多态类型
序列化继承层次结构:
```csharp
public abstract class EntityData
{
public string Id { get; set; }
public string Type { get; set; }
}
public class PlayerEntityData : EntityData
{
public int Level { get; set; }
public int Experience { get; set; }
}
public class EnemyEntityData : EntityData
{
public int Health { get; set; }
public int Damage { get; set; }
}
public void SerializePolymorphic()
{
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
// 创建不同类型的实体
EntityData player = new PlayerEntityData
{
Id = "player_1",
Type = "Player",
Level = 10,
Experience = 1000
};
EntityData enemy = new EnemyEntityData
{
Id = "enemy_1",
Type = "Enemy",
Health = 100,
Damage = 20
};
// 使用运行时类型序列化
string playerJson = serializer.Serialize(player, player.GetType());
string enemyJson = serializer.Serialize(enemy, enemy.GetType());
// 根据类型反序列化
var restoredPlayer = serializer.Deserialize(playerJson, typeof(PlayerEntityData));
var restoredEnemy = serializer.Deserialize(enemyJson, typeof(EnemyEntityData));
}
```
### 自定义序列化逻辑
虽然 GFramework 使用 Newtonsoft.Json但你可以通过特性控制序列化行为
```csharp
using Newtonsoft.Json;
public class CustomData
{
// 忽略此属性
[JsonIgnore]
public string InternalId { get; set; }
// 使用不同的属性名
[JsonProperty("player_name")]
public string Name { get; set; }
// 仅在值不为 null 时序列化
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string? OptionalField { get; set; }
// 格式化日期
[JsonProperty("created_at")]
[JsonConverter(typeof(IsoDateTimeConverter))]
public DateTime CreatedAt { get; set; }
}
```
### 批量序列化
处理多个对象的序列化:
```csharp
public async Task SaveMultipleData()
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
var dataList = new Dictionary<string, object>
{
{ "player", new PlayerData { Name = "Player1", Level = 10 } },
{ "inventory", new InventoryData { Items = new List<ItemData>() } },
{ "settings", new SettingsData { Volume = 0.8f } }
};
// 批量序列化和保存
foreach (var (key, data) in dataList)
{
string json = serializer.Serialize(data);
await storage.WriteAsync(key, json);
}
Console.WriteLine($"已保存 {dataList.Count} 个数据文件");
}
```
### 错误处理
处理序列化和反序列化错误:
```csharp
public void SafeDeserialize()
{
var serializer = this.GetUtility<ISerializer>();
string json = "{\"Name\":\"Player1\",\"Level\":\"invalid\"}"; // 错误的数据
try
{
var player = serializer.Deserialize<PlayerData>(json);
}
catch (ArgumentException ex)
{
Console.WriteLine($"反序列化失败: {ex.Message}");
// 返回默认值或重新尝试
}
catch (JsonException ex)
{
Console.WriteLine($"JSON 格式错误: {ex.Message}");
}
}
public PlayerData DeserializeWithFallback(string json)
{
var serializer = this.GetUtility<ISerializer>();
try
{
return serializer.Deserialize<PlayerData>(json);
}
catch
{
// 返回默认数据
return new PlayerData
{
Name = "DefaultPlayer",
Level = 1,
Experience = 0
};
}
}
```
### 版本兼容性
处理数据结构变化:
```csharp
// 旧版本数据
public class PlayerDataV1
{
public string Name { get; set; }
public int Level { get; set; }
}
// 新版本数据(添加了新字段)
public class PlayerDataV2
{
public string Name { get; set; }
public int Level { get; set; }
public int Experience { get; set; } = 0; // 新增字段,提供默认值
public DateTime LastLogin { get; set; } = DateTime.Now; // 新增字段
}
public PlayerDataV2 LoadWithMigration(string json)
{
var serializer = this.GetUtility<ISerializer>();
try
{
// 尝试加载新版本
return serializer.Deserialize<PlayerDataV2>(json);
}
catch
{
// 如果失败,尝试加载旧版本并迁移
var oldData = serializer.Deserialize<PlayerDataV1>(json);
return new PlayerDataV2
{
Name = oldData.Name,
Level = oldData.Level,
Experience = oldData.Level * 100, // 根据等级计算经验
LastLogin = DateTime.Now
};
}
}
```
## 最佳实践
1. **优先依赖接口,在组合根统一实例化**:业务代码依赖 `ISerializer`,具体 `JsonSerializer` 在启动阶段配置一次即可
```csharp
✓ var serializer = this.GetUtility<ISerializer>();
✓ var serializer = new JsonSerializer(settings); // 仅在组合根/启动阶段配置
✗ var serializer = new JsonSerializer(); // 避免在业务逻辑中临时创建
```
2. **为数据类提供默认值**:确保反序列化的健壮性
```csharp
public class GameData
{
public string Name { get; set; } = "Default";
public int Score { get; set; } = 0;
public List<string> Items { get; set; } = new();
}
```
3. **处理反序列化异常**:避免程序崩溃
```csharp
try
{
var data = serializer.Deserialize<GameData>(json);
}
catch (Exception ex)
{
Logger.Error($"反序列化失败: {ex.Message}");
return GetDefaultData();
}
```
4. **避免序列化敏感数据**:使用 `[JsonIgnore]` 标记
```csharp
public class UserData
{
public string Username { get; set; }
[JsonIgnore]
public string Password { get; set; } // 不序列化密码
}
```
5. **使用运行时类型处理多态**:保持类型信息
```csharp
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
string json = serializer.Serialize(obj, obj.GetType());
```
6. **验证反序列化的数据**:确保数据完整性
```csharp
var data = serializer.Deserialize<GameData>(json);
if (string.IsNullOrEmpty(data.Name) || data.Score < 0)
{
throw new InvalidDataException("数据验证失败");
}
```
## 性能优化
### 减少序列化开销
```csharp
// 避免频繁序列化大对象
public class CachedSerializer
{
private string? _cachedJson;
private GameData? _cachedData;
public string GetJson(GameData data)
{
if (_cachedData == data && _cachedJson != null)
{
return _cachedJson;
}
var serializer = GetSerializer();
_cachedJson = serializer.Serialize(data);
_cachedData = data;
return _cachedJson;
}
}
```
### 异步序列化
```csharp
public async Task SaveDataAsync()
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
var data = GetLargeData();
// 在后台线程序列化
string json = await Task.Run(() => serializer.Serialize(data));
// 异步写入存储
await storage.WriteAsync("large_data", json);
}
```
### 分块序列化
```csharp
public async Task SaveLargeDataset()
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
var largeDataset = GetLargeDataset();
// 分块保存
const int chunkSize = 100;
for (int i = 0; i < largeDataset.Count; i += chunkSize)
{
var chunk = largeDataset.Skip(i).Take(chunkSize).ToList();
string json = serializer.Serialize(chunk);
await storage.WriteAsync($"data_chunk_{i / chunkSize}", json);
}
}
```
## 常见问题
### 问题:如何序列化循环引用的对象?
**解答**
Newtonsoft.Json 默认不支持循环引用,需要配置:
```csharp
// 注意GFramework 的 JsonSerializer 使用默认设置
// 如需处理循环引用,避免创建循环引用的数据结构
// 或使用 [JsonIgnore] 打破循环
public class Node
{
public string Name { get; set; }
public List<Node> Children { get; set; }
[JsonIgnore] // 忽略父节点引用,避免循环
public Node? Parent { get; set; }
}
```
### 问题:序列化后的 JSON 太大怎么办?
**解答**
使用压缩或分块存储:
```csharp
public async Task SaveCompressed()
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
var data = GetLargeData();
string json = serializer.Serialize(data);
// 压缩 JSON
byte[] compressed = Compress(json);
// 保存压缩数据
await storage.WriteAsync("data_compressed", compressed);
}
private byte[] Compress(string text)
{
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionMode.Compress))
using (var writer = new StreamWriter(gzip))
{
writer.Write(text);
}
return output.ToArray();
}
```
### 问题:如何处理不同平台的序列化差异?
**解答**
使用平台无关的数据类型:
```csharp
public class CrossPlatformData
{
// 使用 string 而非 DateTime避免时区问题
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("O");
// 使用 double 而非 float精度一致
public double Score { get; set; }
// 明确指定编码
public string Text { get; set; }
}
```
### 问题:反序列化失败时如何恢复?
**解答**
实现备份和恢复机制:
```csharp
public async Task<GameData> LoadWithBackup(string key)
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
try
{
// 尝试加载主数据
string json = await storage.ReadAsync<string>(key);
return serializer.Deserialize<GameData>(json);
}
catch
{
// 尝试加载备份
try
{
string backupJson = await storage.ReadAsync<string>($"{key}_backup");
return serializer.Deserialize<GameData>(backupJson);
}
catch
{
// 返回默认数据
return new GameData();
}
}
}
```
### 问题:如何加密序列化的数据?
**解答**
在序列化后加密:
```csharp
public async Task SaveEncrypted(string key, GameData data)
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
// 序列化
string json = serializer.Serialize(data);
// 加密
byte[] encrypted = EncryptString(json);
// 保存
await storage.WriteAsync(key, encrypted);
}
public async Task<GameData> LoadEncrypted(string key)
{
var serializer = this.GetUtility<ISerializer>();
var storage = this.GetUtility<IStorage>();
// 读取
byte[] encrypted = await storage.ReadAsync<byte[]>(key);
// 解密
string json = DecryptToString(encrypted);
// 反序列化
return serializer.Deserialize<GameData>(json);
}
```
### 问题:序列化器是线程安全的吗?
**解答**
`JsonSerializer` 的并发读行为只在“配置不再变化”这个前提下才安全。当前实现会暴露活动中的
`JsonSerializerSettings``Converters` 集合,因此:
- 可以在启动阶段创建并配置一个共享实例
- 可以在配置冻结后把该实例注册为 Utility 或注入到仓库
- 不应在序列化器已经被多个调用方使用时继续修改 settings、contract resolver 或 converters
推荐按下面的方式在启动阶段完成配置,然后只做读操作:
```csharp
// 启动阶段完成全部配置
var serializer = new JsonSerializer(new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
serializer.Converters.Add(new GameDataJsonConverter());
architecture.RegisterUtility<ISerializer>(serializer);
// 运行阶段只复用,不再修改配置
public async Task ParallelSave()
{
var tasks = Enumerable.Range(0, 10).Select(async i =>
{
var serializer = this.GetUtility<ISerializer>();
var data = new GameData { Score = i };
string json = serializer.Serialize(data);
await SaveToStorage($"data_{i}", json);
});
await Task.WhenAll(tasks);
}
```
## 相关文档
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
- [存储系统](/zh-CN/game/storage) - 文件存储
- [设置系统](/zh-CN/game/setting) - 设置数据序列化
- [Utility 系统](/zh-CN/core/utility) - 工具类注册
1. [存储系统](./storage.md)
2. [数据与存档系统](./data.md)
3. [配置系统](./config-system.md)
4. [Game 入口](./index.md)

View File

@ -1,207 +1,204 @@
---
title: 设置系统
description: 以当前 SettingsModel、SettingsSystem 与相关测试为准说明设置数据、applicator、迁移和持久化的真实接法。
---
# 设置系统
设置系统负责管理 `ISettingsData`、持久化加载/保存,以及把设置真正应用到运行时环境。
`GFramework.Game` 的设置系统负责三件事:
当前实现以 `SettingsModel<TRepository>``SettingsSystem` 为核心,已经不是旧文档中的
`Get<T>() / Register(IApplyAbleSettings)` 接口模型。
- 管理 `ISettingsData` 实例的生命周期
- 管理设置 applicator并把设置真正作用到运行时环境
- 在初始化时加载、迁移、保存和重置设置
## 核心概念
当前默认 owner 是:
### ISettingsData
- `SettingsModel<TRepository>`
- `SettingsSystem`
设置数据对象负责保存设置值、提供默认值,并在加载后把外部数据回填到当前实例。
而不是旧文档里那种“只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切”的模型。
## 当前公开入口
### `ISettingsData`
设置数据对象需要同时承担:
- 默认值持有者
- 版本化 section
- 从已加载数据回填到当前实例的入口
当前接口组合是:
```csharp
public interface ISettingsData : IResettable, IVersionedData, ILoadableFrom<ISettingsData>;
```
这意味着一个设置数据类型通常需要实现:
这意味着一个设置数据类型至少要处理
- `Reset()`:恢复默认值
- `Version` / `LastModified`:暴露版本化信息
- `LoadFrom(ISettingsData)`:把已加载或迁移后的数据复制到当前实例
- `Reset()`
- `Version`
- `LastModified`
- `LoadFrom(ISettingsData source)`
### IResetApplyAbleSettings
### `IResetApplyAbleSettings`
应用器负责把设置数据作用到引擎或运行时环境:
applicator 的职责不是保存数据,而是把设置结果作用到实际运行时对象。
它当前需要暴露:
- `Data`
- `DataType`
- `Reset()`
- `ApplyAsync()`
典型场景包括:
- 把音量设置同步到音频系统
- 把画质设置同步到窗口或渲染配置
- 把语言设置同步到本地化服务
### `SettingsModel<TRepository>`
这是当前设置系统的核心编排器。按当前源码,它负责:
- `GetData<T>()`
- 返回某个设置类型的唯一实例
- `RegisterApplicator(...)`
- 注册 applicator并把其 `Data` 一并纳入模型管理
- `RegisterMigration(...)`
- 注册同一设置类型的前进式迁移链
- `InitializeAsync()`
- 从 repository 读取所有设置、执行迁移、回填到当前实例
- `SaveAllAsync()`
- 持久化所有已登记的设置数据
- `ApplyAllAsync()`
- 依次应用所有 applicator
- `Reset<T>() / ResetAll()`
- 重置单个或全部设置
### `SettingsSystem`
`SettingsSystem` 是面向业务代码更直接的一层系统封装:
- `ApplyAll()`
- `Apply<T>()`
- `SaveAll()`
- `Reset<T>()`
- `ResetAll()`
它自己不持有独立设置状态,而是把工作委托给 `ISettingsModel`,并在应用时补发 settings 相关事件。
## 初始化与迁移的真实语义
`SettingsModel<TRepository>.InitializeAsync()` 的当前行为,比旧文档里“加载一下就好”更严格一些:
- 它会先调用 `ISettingsDataRepository.LoadAllAsync()`
- 再逐个匹配当前模型里已经登记的设置类型
- 如果读到了旧版本设置,会以“当前内存实例声明的 `Version`”为目标版本执行迁移
- 迁移完成后通过 `LoadFrom(...)` 回填到现有实例,而不是直接替换对象引用
当前测试还确认了几个关键边界:
- 同一设置类型的同一个 `FromVersion` 不能重复注册迁移器
- 注册新迁移器后,类型级迁移缓存会失效并重建,不会继续使用旧快照
- 如果迁移链缺口导致无法安全升级,模型会保留当前内存中的最新实例,而不是把不完整的旧数据覆盖进来
- 单个设置 section 初始化失败时,模型会记录错误并继续处理其他 section
这套语义更接近“尽量保证运行时实例总是可用”,而不是“任意旧设置都必须成功导入”。
## 最小接入路径
当前最常见的接法是:
1. 准备一个 `IStorage`
2. 准备一个 `IRuntimeTypeSerializer`
3. 注册 `ISettingsDataRepository`
4. 注册 `IDataLocationProvider`
5. 创建并注册 `SettingsModel<TRepository>`
6. 注册 applicator
7. 注册 `SettingsSystem`
示意代码:
```csharp
public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings
{
ISettingsData Data { get; }
Type DataType { get; }
}
using GFramework.Core.Abstractions.Storage;
using GFramework.Game.Abstractions.Data;
using GFramework.Game.Abstractions.Setting;
using GFramework.Game.Data;
using GFramework.Game.Serializer;
using GFramework.Game.Setting;
using GFramework.Game.Storage;
var serializer = new JsonSerializer();
var storage = new FileStorage("GameData", serializer, ".json");
var repository = new UnifiedSettingsDataRepository(
storage,
serializer,
new DataRepositoryOptions
{
BasePath = "settings",
AutoBackup = true
});
architecture.RegisterUtility<IStorage>(storage);
architecture.RegisterUtility<IRuntimeTypeSerializer>(serializer);
architecture.RegisterUtility<ISettingsDataRepository>(repository);
// 此处注册项目侧的 IDataLocationProvider 实现,用于把设置类型映射到 section key。
var settingsModel = new SettingsModel<ISettingsDataRepository>(null, null);
// 在注册到架构前,继续补 applicator 与 migration。
architecture.RegisterModel<ISettingsModel>(settingsModel);
architecture.RegisterSystem<ISettingsSystem>(new SettingsSystem());
```
常见用途包括:
- 把音量设置同步到音频总线
- 把图形设置同步到窗口系统
- 把语言设置同步到本地化管理器
## ISettingsModel
当前 `ISettingsModel` 的主要 API 如下:
启动阶段通常是:
```csharp
public interface ISettingsModel : IModel
{
bool IsInitialized { get; }
T GetData<T>() where T : class, ISettingsData, new();
IEnumerable<ISettingsData> AllData();
ISettingsModel RegisterApplicator<T>(T applicator)
where T : class, IResetApplyAbleSettings;
T? GetApplicator<T>() where T : class, IResetApplyAbleSettings;
IEnumerable<IResetApplyAbleSettings> AllApplicators();
ISettingsModel RegisterMigration(ISettingsMigration migration);
Task InitializeAsync();
Task SaveAllAsync();
Task ApplyAllAsync();
void Reset<T>() where T : class, ISettingsData, new();
void ResetAll();
}
```
行为说明:
- `GetData<T>()` 返回某个设置数据的唯一实例
- `RegisterApplicator<T>()` 注册应用器,并把其 `Data` 纳入模型管理
- `InitializeAsync()``ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移
- `SaveAllAsync()` 持久化当前所有设置数据
- `ApplyAllAsync()` 依次调用所有 applicator 的 `ApplyAsync()`
## SettingsSystem
`SettingsSystem` 是对模型的系统级封装,面向业务代码提供更直接的入口:
```csharp
public interface ISettingsSystem : ISystem
{
Task ApplyAll();
Task Apply<T>() where T : class, IResetApplyAbleSettings;
Task SaveAll();
Task Reset<T>() where T : class, ISettingsData, IResetApplyAbleSettings, new();
Task ResetAll();
}
```
它不会自己保存数据,而是把保存、重置和应用逻辑委托给 `ISettingsModel`
## 基本用法
### 定义设置数据
```csharp
public sealed class GameplaySettings : ISettingsData
{
public float GameSpeed { get; set; } = 1.0f;
public int Version { get; private set; } = 1;
public DateTime LastModified { get; } = DateTime.UtcNow;
public void Reset()
{
GameSpeed = 1.0f;
}
public void LoadFrom(ISettingsData source)
{
if (source is not GameplaySettings settings)
{
return;
}
GameSpeed = settings.GameSpeed;
Version = settings.Version;
}
}
```
### 定义 applicator
```csharp
public sealed class GameplaySettingsApplicator : IResetApplyAbleSettings
{
public GameplaySettingsApplicator(GameplaySettings data)
{
Data = data;
}
public ISettingsData Data { get; }
public Type DataType => typeof(GameplaySettings);
public void Reset()
{
Data.Reset();
}
public Task ApplyAsync()
{
var settings = (GameplaySettings)Data;
TimeScale.Current = settings.GameSpeed;
return Task.CompletedTask;
}
}
```
### 使用模型和系统
```csharp
var settingsModel = this.GetModel<ISettingsModel>();
var gameplayData = settingsModel.GetData<GameplaySettings>();
gameplayData.GameSpeed = 1.25f;
settingsModel.RegisterApplicator(new GameplaySettingsApplicator(gameplayData));
await settingsModel.InitializeAsync();
await settingsModel.SaveAllAsync();
var settingsSystem = this.GetSystem<ISettingsSystem>();
await settingsSystem.ApplyAll();
await settingsModel.ApplyAllAsync();
```
## 迁移
设置系统内建了迁移注册入口:
退出或显式保存时:
```csharp
public interface ISettingsMigration
{
Type SettingsType { get; }
int FromVersion { get; }
int ToVersion { get; }
ISettingsData Migrate(ISettingsData oldData);
}
await settingsModel.SaveAllAsync();
```
`InitializeAsync()` 读取到旧版本设置时,会按已注册迁移链逐步升级,再通过 `LoadFrom` 回填到当前实例。
## `GetData<T>()``RegisterApplicator(...)` 的分工
迁移规则如下
这两个入口经常被混用,但职责不同:
- 同一个设置类型的同一个 `FromVersion` 只能注册一个迁移器
- `ToVersion` 必须严格大于 `FromVersion`
- `InitializeAsync()` 会以当前运行时代码里该设置实例的 `Version` 作为目标版本
- 如果迁移链缺口、迁移结果类型不兼容、迁移结果版本与声明不一致,或者读取到比当前运行时更高的版本,当前设置节不会覆盖内存中的最新实例,并会记录错误日志
- 与 `SaveRepository<TSaveData>` 不同,设置初始化阶段会跳过失败的设置节并继续处理其他设置节,而不是把异常继续向外抛出
- `GetData<T>()`
- 只保证某个设置数据实例存在,并在 repository / location provider 已就绪时把类型注册回去
- `RegisterApplicator(...)`
- 同时注册 applicator 和 applicator 绑定的 `Data`
## 依赖项
如果一个设置类型需要真正作用到运行时对象,推荐让它通过 applicator 进入模型;这样 `ApplyAllAsync()``ResetAll()`
`SettingsSystem` 才能完整覆盖到它。
要让设置系统完整工作,通常需要准备:
## 与 repository 的关系
- `ISettingsDataRepository`
- `IDataLocationProvider`
- 一个具体的存储实现和序列化器
设置系统默认不是直接写文件,而是依赖 `ISettingsDataRepository`
如果使用 `UnifiedSettingsDataRepository`,多个设置节会被合并到单个设置文件中统一保存。
当前仓库里更推荐的默认实现是 `UnifiedSettingsDataRepository`,原因很直接:
- 多个设置 section 会被聚合到同一份统一文件
- 启动时能一次性 `LoadAllAsync()`
- `AutoBackup` 针对整个统一文件生效,更贴近“设置快照”的真实语义
如果你的项目明确需要“一类设置一个独立文件”,才考虑回到通用 `DataRepository` 路径。
## 当前边界
- 设置迁移是内建能力
- 设置持久化是内建能力
- 设置如何应用到具体引擎由 applicator 决定
- 存档系统也支持内建版本迁移,但入口位于 `ISaveRepository<T>.RegisterMigration(...)`,语义是槽位存档升级而不是设置节初始化
- `SettingsModel<TRepository>` 负责数据生命周期,`SettingsSystem` 负责系统级调用入口;两者不要混成一个巨型服务
- applicator 决定“怎么把数据应用到宿主”repository 决定“怎么保存数据”,两层职责不要互相侵入
- 设置迁移和存档迁移是两条不同管线;后者看 [`data.md`](./data.md) 里的 `SaveRepository<TSaveData>`
## 继续阅读
1. [数据与存档系统](./data.md)
2. [存储系统](./storage.md)
3. [Game 入口](./index.md)

View File

@ -1,735 +1,181 @@
---
title: 存储系统详解
description: 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化
title: Game 存储系统
description: 以当前 GFramework.Game 源码与持久化测试为准,说明 FileStorage 与 ScopedStorage 的职责、路径语义和复用方式
---
# 存储系统详解
# Game 存储系统
## 概述
`GFramework.Game` 在存储这一层只提供宿主无关的 `IStorage` 默认实现和作用域包装器。
存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。
当前真正对外需要理解的入口只有两个:
存储系统采用装饰器模式设计,通过 `IStorage` 接口定义统一的存储操作,`FileStorage` 提供基于文件系统的实现,`ScopedStorage`
提供作用域隔离功能。
- `FileStorage`
- 负责 `key -> 文件路径 -> 序列化内容` 的落盘读写
- `ScopedStorage`
- 负责给同一份底层存储加前缀作用域,避免不同子系统直接拼字符串抢同一片键空间
**主要特性**
它们不负责
- 统一的键值对存储接口
- 基于文件系统的持久化
- 作用域隔离和命名空间管理
- 线程安全的并发访问
- 支持同步和异步操作
- 目录和文件列举功能
- 路径安全防护
- 跨平台支持(包括 Godot
- 设置 section 的聚合语义
- 存档槽位目录约定
- 业务数据迁移
## 核心概念
这些都属于上层 repository。
### 存储接
## 当前公开入口
`IStorage` 定义了统一的存储操作:
### `FileStorage`
`FileStorage``IStorage` 的默认文件系统实现。按当前源码,它的职责比较集中:
- 把业务 key 映射到根目录下的层级文件路径
- 通过构造函数注入的 `ISerializer` 负责对象序列化和反序列化
- 对同一目标路径使用 `IAsyncKeyLockManager` 做细粒度串行化
- 写入时先落 `.tmp` 临时文件,再原子替换目标文件
- 自动创建父目录
- 拒绝包含 `..` 的非法 key并清理路径段中的非法文件名字符
默认文件扩展名是 `.dat`,也可以在构造时改成 `.json` 或其他后缀:
```csharp
public interface IStorage : IUtility
{
// 检查键是否存在
bool Exists(string key);
Task<bool> ExistsAsync(string key);
// 读取数据
T Read&lt;T&gt;(string key);
T Read&lt;T&gt;(string key, T defaultValue);
Task&lt;T&gt; ReadAsync&lt;T&gt;(string key);
// 写入数据
void Write&lt;T&gt;(string key, T value);
Task WriteAsync&lt;T&gt;(string key, T value);
// 删除数据
void Delete(string key);
Task DeleteAsync(string key);
// 目录操作
Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "");
Task<IReadOnlyList<string>> ListFilesAsync(string path = "");
Task<bool> DirectoryExistsAsync(string path);
Task CreateDirectoryAsync(string path);
}
```
### 文件存储
`FileStorage` 是基于文件系统的存储实现:
- 将数据序列化后保存为文件
- 支持自定义文件扩展名(默认 `.dat`
- 使用细粒度锁保证线程安全
- 自动创建目录结构
- 防止路径遍历攻击
### 作用域存储
`ScopedStorage` 提供命名空间隔离:
- 为所有键添加前缀
- 支持嵌套作用域
- 透明包装底层存储
- 实现逻辑分组
### 存储类型
`StorageKinds` 枚举定义了不同的存储方式:
```csharp
[Flags]
public enum StorageKinds
{
None = 0,
Local = 1 << 0, // 本地文件系统
Memory = 1 << 1, // 内存存储
Remote = 1 << 2, // 远程存储
Database = 1 << 3 // 数据库存储
}
```
## 基本用法
### 创建文件存储
```csharp
using GFramework.Game.Storage;
using GFramework.Core.Abstractions.Storage;
using GFramework.Game.Serializer;
// 创建序列化器
var serializer = new JsonSerializer();
// 创Windows 示例)
var storage = new FileStorage(@"C:\MyGame\Data", serializer);
// 或使用自定义扩展名
var storage = new FileStorage(@"C:\MyGame\Data", serializer, ".json");
```
### 写入和读取数据
```csharp
// 写入简单类型
storage.Write("player_score", 1000);
storage.Write("player_name", "Alice");
// 写入复杂对象
var settings = new GameSettings
{
Volume = 0.8f,
Difficulty = "Hard",
Language = "zh-CN"
};
storage.Write("settings", settings);
// 读取数据
int score = storage.Read<int>("player_score");
string name = storage.Read<string>("player_name");
var loadedSettings = storage.Read<GameSettings>("settings");
// 读取数据(带默认值)
int highScore = storage.Read("high_score", 0);
```
### 异步操作
```csharp
// 异步写入
await storage.WriteAsync("player_level", 10);
// 异步读取
int level = await storage.ReadAsync<int>("player_level");
// 异步检查存在
bool exists = await storage.ExistsAsync("player_level");
// 异步删除
await storage.DeleteAsync("player_level");
```
### 检查和删除
```csharp
// 检查键是否存在
if (storage.Exists("player_score"))
{
Console.WriteLine("存档存在");
}
// 删除数据
storage.Delete("player_score");
// 异步检查
bool exists = await storage.ExistsAsync("player_score");
```
### 使用层级键
```csharp
// 使用 / 分隔符创建层级结构
storage.Write("player/profile/name", "Alice");
storage.Write("player/profile/level", 10);
storage.Write("player/inventory/gold", 1000);
// 文件结构:
// Data/
// player/
// profile/
// name.dat
// level.dat
// inventory/
// gold.dat
// 读取层级数据
string name = storage.Read<string>("player/profile/name");
int gold = storage.Read<int>("player/inventory/gold");
```
## 作用域存储
### 创建作用域存储
```csharp
using GFramework.Game.Storage;
// 基于文件存储创建作用域存储
var baseStorage = new FileStorage(@"C:\MyGame\Data", serializer);
var playerStorage = new ScopedStorage(baseStorage, "player");
// 所有操作都会添加 "player/" 前缀
playerStorage.Write("name", "Alice"); // 实际存储为 "player/name.dat"
playerStorage.Write("level", 10); // 实际存储为 "player/level.dat"
// 读取时也使用相同的前缀
string name = playerStorage.Read<string>("name"); // 从 "player/name.dat" 读取
var serializer = new JsonSerializer();
IStorage storage = new FileStorage("GameData", serializer, ".json");
```
### 嵌套作用域
### `ScopedStorage`
`ScopedStorage` 不额外实现一套落盘逻辑,只是给底层 `IStorage` 包一层前缀。
它适合做的是:
- 把 `settings/``profiles/``runtime-cache/` 这类键空间隔离开
- 让多个 repository 或 utility 共用同一份根存储
- 避免项目层到处手写 `"settings/xxx"``"save/slot_1/xxx"` 之类的字符串拼接
当前实现还支持继续嵌套:
```csharp
// 创建嵌套作用域
var settingsStorage = new ScopedStorage(baseStorage, "settings");
var graphicsStorage = new ScopedStorage(settingsStorage, "graphics");
// 前缀变为 "settings/graphics/"
graphicsStorage.Write("resolution", "1920x1080");
// 实际存储为 "settings/graphics/resolution.dat"
// 或使用 Scope 方法
var rootStorage = new FileStorage("GameData", new JsonSerializer(), ".json");
var settingsStorage = new ScopedStorage(rootStorage, "settings");
var audioStorage = settingsStorage.Scope("audio");
audioStorage.Write("volume", 0.8f);
// 实际存储为 "settings/audio/volume.dat"
await audioStorage.WriteAsync("master", 0.8f);
```
### 多作用域隔离
最终实际写入的 key 会是 `settings/audio/master`
```csharp
// 创建不同作用域的存储
var playerStorage = new ScopedStorage(baseStorage, "player");
var gameStorage = new ScopedStorage(baseStorage, "game");
var settingsStorage = new ScopedStorage(baseStorage, "settings");
## 路径语义
// 在不同作用域中使用相同的键不会冲突
playerStorage.Write("level", 5); // player/level.dat
gameStorage.Write("level", "forest_area_1"); // game/level.dat
settingsStorage.Write("level", "high"); // settings/level.dat
### key 到文件路径的映射
// 读取时各自独立
int playerLevel = playerStorage.Read<int>("level"); // 5
string gameLevel = gameStorage.Read<string>("level"); // "forest_area_1"
string settingsLevel = settingsStorage.Read<string>("level"); // "high"
`FileStorage` 会把 key 中的 `/` 当成目录分隔符,把最后一段作为文件名,并自动附加扩展名。
例如:
```text
key: profile/player
root: GameData
extension: .json
```
## 高级用法
会落到:
### 目录操作
```text
GameData/profile/player.json
```
这意味着 key 的语义应该保持“逻辑路径”,而不是“完整文件名”。不要在业务层再自己补一遍 `.json`,否则会得到双重后缀。
### 安全边界
当前实现会:
1. 把 `\` 统一成 `/`
2. 拒绝包含 `..` 的 key
3. 清理每个路径段中的非法文件名字符
这套规则能挡住明显的路径逃逸和非法文件名问题,但它不代替业务层做目录规划。哪些 key 属于设置、存档还是缓存,仍应由上层模块统一约定。
### 同步与异步 API
`Read``Write``Exists``Delete` 这些同步方法只是对异步 API 的阻塞包装。
在 UI 线程或带同步上下文的宿主中,优先使用:
- `ReadAsync<T>()`
- `WriteAsync<T>()`
- `ExistsAsync()`
- `DeleteAsync()`
只有在无法继续异步传播时,再退回同步封装。
## 最小接入路径
如果你只想先拿到一个可复用的本地持久化底座,最短路径如下:
```csharp
// 列举子目录
var directories = await storage.ListDirectoriesAsync("player");
foreach (var dir in directories)
using GFramework.Core.Abstractions.Serializer;
using GFramework.Core.Abstractions.Storage;
using GFramework.Game.Serializer;
using GFramework.Game.Storage;
var serializer = new JsonSerializer();
IStorage storage = new FileStorage("GameData", serializer, ".json");
await storage.WriteAsync("profiles/player", new Dictionary<string, int>
{
Console.WriteLine($"目录: {dir}");
}
["level"] = 12
});
// 列举文件
var files = await storage.ListFilesAsync("player/inventory");
foreach (var file in files)
{
Console.WriteLine($"文件: {file}");
}
// 检查目录是否存在
bool exists = await storage.DirectoryExistsAsync("player/quests");
// 创建目录
await storage.CreateDirectoryAsync("player/achievements");
var loaded = await storage.ReadAsync<Dictionary<string, int>>("profiles/player");
```
### 批量操作
如果项目里同时有设置、存档和运行时缓存,推荐先在组合根把作用域拆开:
```csharp
public async Task SaveAllPlayerData(PlayerData player)
{
var playerStorage = new ScopedStorage(baseStorage, $"player_{player.Id}");
var serializer = new JsonSerializer();
var rootStorage = new FileStorage("GameData", serializer, ".json");
// 批量写入
var tasks = new List<Task>
{
playerStorage.WriteAsync("profile", player.Profile),
playerStorage.WriteAsync("inventory", player.Inventory),
playerStorage.WriteAsync("quests", player.Quests),
playerStorage.WriteAsync("achievements", player.Achievements)
};
await Task.WhenAll(tasks);
Console.WriteLine("所有玩家数据已保存");
}
public async Task<PlayerData> LoadAllPlayerData(int playerId)
{
var playerStorage = new ScopedStorage(baseStorage, $"player_{playerId}");
// 批量读取
var tasks = new[]
{
playerStorage.ReadAsync<Profile>("profile"),
playerStorage.ReadAsync<Inventory>("inventory"),
playerStorage.ReadAsync<QuestData>("quests"),
playerStorage.ReadAsync<Achievements>("achievements")
};
await Task.WhenAll(tasks);
return new PlayerData
{
Id = playerId,
Profile = tasks[0].Result,
Inventory = tasks[1].Result,
Quests = tasks[2].Result,
Achievements = tasks[3].Result
};
}
var settingsStorage = new ScopedStorage(rootStorage, "settings");
var saveStorage = new ScopedStorage(rootStorage, "saves");
var cacheStorage = new ScopedStorage(rootStorage, "runtime-cache");
```
### 存储迁移
不过在默认仓库接法里,项目通常不需要直接创建 `saveStorage` 这种 scoped instance因为 `SaveRepository<TSaveData>`
会再根据 `SaveConfiguration` 自己组织槽位目录。
```csharp
public async Task MigrateStorage(IStorage oldStorage, IStorage newStorage, string path = "")
{
// 列举所有文件
var files = await oldStorage.ListFilesAsync(path);
## 与上层 repository 的关系
foreach (var file in files)
{
var key = string.IsNullOrEmpty(path) ? file : $"{path}/{file}";
`FileStorage` / `ScopedStorage` 是持久化最底层,不是最终采用入口。当前更常见的实际分工是:
// 读取旧数据
var data = await oldStorage.ReadAsync<object>(key);
- `DataRepository`
- 每个 `IDataLocation` 对应一份独立持久化对象
- `UnifiedSettingsDataRepository`
- 把多个设置 section 聚合到同一个统一文件里保存
- `SaveRepository<TSaveData>`
- 负责存档槽位、文件名和迁移链
// 写入新存储
await newStorage.WriteAsync(key, data);
也就是说:
Console.WriteLine($"已迁移: {key}");
}
- 业务层如果想保存一份独立数据,优先看 [`data.md`](./data.md)
- 业务层如果想保存设置,优先看 [`setting.md`](./setting.md)
- 业务层如果只是需要底层存储实现,才直接依赖 `IStorage`
// 递归处理子目录
var directories = await oldStorage.ListDirectoriesAsync(path);
foreach (var dir in directories)
{
var subPath = string.IsNullOrEmpty(path) ? dir : $"{path}/{dir}";
await MigrateStorage(oldStorage, newStorage, subPath);
}
}
```
## 当前边界
### 存储备份
- `FileStorage` 已经会通过注入的 `ISerializer` 自动序列化对象;默认接法不需要先手工 `Serialize(...)` 再把字符串写回
- `FileStorage` 负责目录列举与目录创建,但不负责“列出所有存档槽位”的业务语义
- `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制
- 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁
- 原子写入只覆盖单文件替换,不等于多文件事务
```csharp
public class StorageBackupSystem
{
private readonly IStorage _storage;
private readonly string _backupPrefix = "backup";
## 继续阅读
public async Task CreateBackup(string sourcePath)
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var backupPath = $"{_backupPrefix}/{timestamp}";
await CopyDirectory(sourcePath, backupPath);
Console.WriteLine($"备份已创建: {backupPath}");
}
public async Task RestoreBackup(string backupName, string targetPath)
{
var backupPath = $"{_backupPrefix}/{backupName}";
if (!await _storage.DirectoryExistsAsync(backupPath))
{
throw new DirectoryNotFoundException($"备份不存在: {backupName}");
}
await CopyDirectory(backupPath, targetPath);
Console.WriteLine($"已从备份恢复: {backupName}");
}
private async Task CopyDirectory(string source, string target)
{
var files = await _storage.ListFilesAsync(source);
foreach (var file in files)
{
var sourceKey = $"{source}/{file}";
var targetKey = $"{target}/{file}";
var data = await _storage.ReadAsync<object>(sourceKey);
await _storage.WriteAsync(targetKey, data);
}
var directories = await _storage.ListDirectoriesAsync(source);
foreach (var dir in directories)
{
await CopyDirectory($"{source}/{dir}", $"{target}/{dir}");
}
}
}
```
### 缓存层
```csharp
public class CachedStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly ConcurrentDictionary<string, object> _cache = new();
public CachedStorage(IStorage innerStorage)
{
_innerStorage = innerStorage;
}
public T Read&lt;T&gt;(string key)
{
// 先从缓存读取
if (_cache.TryGetValue(key, out var cached))
{
return (T)cached;
}
// 从存储读取并缓存
var value = _innerStorage.Read&lt;T&gt;(key);
_cache[key] = value;
return value;
}
public void Write&lt;T&gt;(string key, T value)
{
// 写入存储
_innerStorage.Write(key, value);
// 更新缓存
_cache[key] = value;
}
public void Delete(string key)
{
_innerStorage.Delete(key);
_cache.TryRemove(key, out _);
}
public void ClearCache()
{
_cache.Clear();
}
}
```
## Godot 集成
### 使用 Godot 文件存储
```csharp
using GFramework.Godot.Storage;
// 创建 Godot 文件存储
var storage = new GodotFileStorage(serializer);
// 使用 user:// 路径(用户数据目录)
storage.Write("user://saves/slot1.dat", saveData);
var data = storage.Read<SaveData>("user://saves/slot1.dat");
// 使用 res:// 路径(资源目录,只读)
var config = storage.Read<Config>("res://config/default.json");
// 普通文件路径也支持
storage.Write("/tmp/temp_data.dat", tempData);
```
### Godot 路径说明
```csharp
// user:// - 用户数据目录
// Windows: %APPDATA%/Godot/app_userdata/[project_name]
// Linux: ~/.local/share/godot/app_userdata/[project_name]
// macOS: ~/Library/Application Support/Godot/app_userdata/[project_name]
storage.Write("user://save.dat", data);
// res:// - 项目资源目录(只读)
var config = storage.Read<Config>("res://data/config.json");
// 绝对路径
storage.Write("/home/user/game/data.dat", data);
```
## 最佳实践
1. **使用作用域隔离不同类型的数据**
```csharp
✓ var playerStorage = new ScopedStorage(baseStorage, "player");
✓ var settingsStorage = new ScopedStorage(baseStorage, "settings");
✗ storage.Write("player_name", name); // 不使用作用域
```
2. **使用异步操作避免阻塞**
```csharp
✓ await storage.WriteAsync("data", value);
✗ storage.Write("data", value); // 在 UI 线程中同步操作
```
3. **读取时提供默认值**
```csharp
✓ int score = storage.Read("score", 0);
✗ int score = storage.Read<int>("score"); // 键不存在时抛异常
```
4. **使用层级键组织数据**
```csharp
✓ storage.Write("player/inventory/gold", 1000);
✗ storage.Write("player_inventory_gold", 1000);
```
5. **处理存储异常**
```csharp
try
{
await storage.WriteAsync("data", value);
}
catch (IOException ex)
{
Logger.Error($"存储失败: {ex.Message}");
ShowErrorMessage("保存失败,请检查磁盘空间");
}
```
6. **定期清理过期数据**
```csharp
public async Task CleanupOldData(TimeSpan maxAge)
{
var files = await storage.ListFilesAsync("temp");
foreach (var file in files)
{
var data = await storage.ReadAsync<TimestampedData>($"temp/{file}");
if (DateTime.Now - data.Timestamp > maxAge)
{
await storage.DeleteAsync($"temp/{file}");
}
}
}
```
7. **使用合适的序列化器**
```csharp
// JSON - 可读性好,适合配置文件
var jsonStorage = new FileStorage(path, new JsonSerializer(), ".json");
// 二进制 - 性能好,适合大量数据
var binaryStorage = new FileStorage(path, new BinarySerializer(), ".dat");
```
## 常见问题
### 问题:如何实现跨平台存储路径?
**解答**
使用 `Environment.GetFolderPath` 获取平台特定路径:
```csharp
public static string GetStoragePath()
{
var appData = Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData);
return Path.Combine(appData, "MyGame", "Data");
}
var storage = new FileStorage(GetStoragePath(), serializer);
```
### 问题:存储系统是否线程安全?
**解答**
是的,`FileStorage` 使用细粒度锁机制保证线程安全:
```csharp
// 不同键的操作可以并发执行
Task.Run(() => storage.Write("key1", value1));
Task.Run(() => storage.Write("key2", value2));
// 相同键的操作会串行化
Task.Run(() => storage.Write("key", value1));
Task.Run(() => storage.Write("key", value2)); // 等待第一个完成
```
### 问题:如何实现存储加密?
**解答**
创建加密存储包装器:
```csharp
public class EncryptedStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly IEncryption _encryption;
public void Write&lt;T&gt;(string key, T value)
{
var json = JsonSerializer.Serialize(value);
var encrypted = _encryption.Encrypt(json);
_innerStorage.Write(key, encrypted);
}
public T Read&lt;T&gt;(string key)
{
var encrypted = _innerStorage.Read<byte[]>(key);
var json = _encryption.Decrypt(encrypted);
return JsonSerializer.Deserialize&lt;T&gt;(json);
}
}
```
### 问题:如何限制存储大小?
**解答**
实现配额管理:
```csharp
public class QuotaStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly long _maxSize;
private long _currentSize;
public void Write&lt;T&gt;(string key, T value)
{
var data = Serialize(value);
var size = data.Length;
if (_currentSize + size > _maxSize)
{
throw new InvalidOperationException("存储配额已满");
}
_innerStorage.Write(key, value);
_currentSize += size;
}
}
```
### 问题:如何实现存储压缩?
**解答**
使用压缩序列化器:
```csharp
public class CompressedSerializer : ISerializer
{
private readonly ISerializer _innerSerializer;
public string Serialize&lt;T&gt;(T value)
{
var json = _innerSerializer.Serialize(value);
var bytes = Encoding.UTF8.GetBytes(json);
var compressed = Compress(bytes);
return Convert.ToBase64String(compressed);
}
public T Deserialize&lt;T&gt;(string data)
{
var compressed = Convert.FromBase64String(data);
var bytes = Decompress(compressed);
var json = Encoding.UTF8.GetString(bytes);
return _innerSerializer.Deserialize&lt;T&gt;(json);
}
private byte[] Compress(byte[] data)
{
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionMode.Compress))
{
gzip.Write(data, 0, data.Length);
}
return output.ToArray();
}
private byte[] Decompress(byte[] data)
{
using var input = new MemoryStream(data);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
gzip.CopyTo(output);
return output.ToArray();
}
}
```
### 问题:如何监控存储操作?
**解答**
实现日志存储包装器:
```csharp
public class LoggingStorage : IStorage
{
private readonly IStorage _innerStorage;
private readonly ILogger _logger;
public void Write&lt;T&gt;(string key, T value)
{
var stopwatch = Stopwatch.StartNew();
try
{
_innerStorage.Write(key, value);
_logger.Info($"写入成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
}
catch (Exception ex)
{
_logger.Error($"写入失败: {key}, 错误: {ex.Message}");
throw;
}
}
public T Read&lt;T&gt;(string key)
{
var stopwatch = Stopwatch.StartNew();
try
{
var value = _innerStorage.Read&lt;T&gt;(key);
_logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
return value;
}
catch (Exception ex)
{
_logger.Error($"读取失败: {key}, 错误: {ex.Message}");
throw;
}
}
}
```
## 相关文档
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
- [序列化系统](/zh-CN/game/serialization) - 数据序列化
- [Godot 集成](/zh-CN/godot/index) - Godot 中的存储
- [存档系统教程](/zh-CN/tutorials/save-system) - 完整示例
1. [数据与存档系统](./data.md)
2. [设置系统](./setting.md)
3. [序列化系统](./serialization.md)
4. [Game 入口](./index.md)