GFramework/docs/zh-CN/game/storage.md
gewuyou 094e29ed8b docs(docs): 统一中文文档导航与语义化链接文案
- 更新 docs 站点中 abstractions、source-generators、api-reference 的导航文案,使其与 landing page 入口一致

- 补充遗漏的源码生成器 sidebar 条目,并本地化抽象层与 API 参考的侧边栏标签

- 修复 ECS、Game、Godot 文档中的路径式可见链接标签,改为读者可理解的语义化名称
2026-04-25 14:56:59 +08:00

182 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

---
title: Game 存储系统
description: 以当前 GFramework.Game 源码与持久化测试为准,说明 FileStorage 与 ScopedStorage 的职责、路径语义和复用方式。
---
# Game 存储系统
`GFramework.Game` 在存储这一层只提供宿主无关的 `IStorage` 默认实现和作用域包装器。
当前真正对外需要理解的入口只有两个:
- `FileStorage`
- 负责 `key -> 文件路径 -> 序列化内容` 的落盘读写
- `ScopedStorage`
- 负责给同一份底层存储加前缀作用域,避免不同子系统直接拼字符串抢同一片键空间
它们不负责:
- 设置 section 的聚合语义
- 存档槽位目录约定
- 业务数据迁移
这些都属于上层 repository。
## 当前公开入口
### `FileStorage`
`FileStorage``IStorage` 的默认文件系统实现。按当前源码,它的职责比较集中:
- 把业务 key 映射到根目录下的层级文件路径
- 通过构造函数注入的 `ISerializer` 负责对象序列化和反序列化
- 对同一目标路径使用 `IAsyncKeyLockManager` 做细粒度串行化
- 写入时先落 `.tmp` 临时文件,再原子替换目标文件
- 自动创建父目录
- 拒绝包含 `..` 的非法 key并清理路径段中的非法文件名字符
默认文件扩展名是 `.dat`,也可以在构造时改成 `.json` 或其他后缀:
```csharp
using GFramework.Core.Abstractions.Storage;
using GFramework.Game.Serializer;
using GFramework.Game.Storage;
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 rootStorage = new FileStorage("GameData", new JsonSerializer(), ".json");
var settingsStorage = new ScopedStorage(rootStorage, "settings");
var audioStorage = settingsStorage.Scope("audio");
await audioStorage.WriteAsync("master", 0.8f);
```
最终实际写入的 key 会是 `settings/audio/master`
## 路径语义
### key 到文件路径的映射
`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
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>
{
["level"] = 12
});
var loaded = await storage.ReadAsync<Dictionary<string, int>>("profiles/player");
```
如果项目里同时有设置、存档和运行时缓存,推荐先在组合根把作用域拆开:
```csharp
var serializer = new JsonSerializer();
var rootStorage = new FileStorage("GameData", serializer, ".json");
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` 自己组织槽位目录。
## 与上层 repository 的关系
`FileStorage` / `ScopedStorage` 是持久化最底层,不是最终采用入口。当前更常见的实际分工是:
- `DataRepository`
- 每个 `IDataLocation` 对应一份独立持久化对象
- `UnifiedSettingsDataRepository`
- 把多个设置 section 聚合到同一个统一文件里保存
- `SaveRepository<TSaveData>`
- 负责存档槽位、文件名和迁移链
也就是说:
- 业务层如果想保存一份独立数据,优先看 [数据与存档系统](./data.md)
- 业务层如果想保存设置,优先看 [设置系统](./setting.md)
- 业务层如果只是需要底层存储实现,才直接依赖 `IStorage`
## 当前边界
- `FileStorage` 已经会通过注入的 `ISerializer` 自动序列化对象;默认接法不需要先手工 `Serialize(...)` 再把字符串写回
- `FileStorage` 负责目录列举与目录创建,但不负责“列出所有存档槽位”的业务语义
- `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制
- 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁
- 原子写入只覆盖单文件替换,不等于多文件事务
## 继续阅读
1. [数据与存档系统](./data.md)
2. [设置系统](./setting.md)
3. [序列化系统](./serialization.md)
4. [Game 入口](./index.md)