mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
- 更新 docs 站点中 abstractions、source-generators、api-reference 的导航文案,使其与 landing page 入口一致 - 补充遗漏的源码生成器 sidebar 条目,并本地化抽象层与 API 参考的侧边栏标签 - 修复 ECS、Game、Godot 文档中的路径式可见链接标签,改为读者可理解的语义化名称
182 lines
5.9 KiB
Markdown
182 lines
5.9 KiB
Markdown
---
|
||
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)
|