Files
Seek-Battle-Evacuate/Docs/Tech/02_SaveData.md
T
2026-08-30 22:25:23 +08:00

147 lines
6.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.
# 存档数据结构
> 定义游戏存档中需要持久化的数据。仅覆盖局外状态与已结束单局的结算记录;进行中的单局使用临时状态,不写入存档。
> 相关设计:`Docs/GameDesign/06_PrototypeScope.md`(存档相关验收项)、`Docs/GameDesign/05_LootAndProgression.md`(物品归属)。
> 代码位置:`Assets/GameMain/Scripts/Base/Definition/DataStruct/`(`SaveData`、`ItemStack`、`CharacterSave`、`LoadoutSave`、`RunRecord`),枚举复用 Luban 生成的 `RunResultType`、`DifficultyTier`。
## 设计原则
- 存档只保存局外状态:主仓库内容、角色装备、当前战备配置、已结束单局的结算记录。
- 背包、保险箱、单局进度均为进入单局时创建的临时状态,不落盘;异常关闭直接丢弃。
- 数值上限(堆叠上限、背包格数等)来自 Luban 配表(`ItemConfig.StackLimit`、`GlobalConfig.BackpackSlotCount` 等),不写死在存档中。
- 存档使用 JSON 序列化,便于调试与手动修改;`SaveData.ToJson()` 序列化,`SaveData.FromJson(string)` 反序列化并补齐缺失字段(空列表/空数组)。
## 序列化格式
枚举在 JSON 中以整数值存储:`RunResultType`(`Extracted=1`、`Defeated=2`、`TimedOut=3`、`Quit=4`)、`DifficultyTier`(`Tier1=1`、`Tier2=2`、`Tier3=3`)。
```json
{
"version": 1,
"updatedAt": 1756540000000,
"mainWarehouse": [
{ "itemId": 1001, "count": 3 },
{ "itemId": 2005, "count": 1 }
],
"characters": [
{ "characterId": 1, "weaponItemId": 1001, "armorItemId": 0 },
{ "characterId": 2, "weaponItemId": 0, "armorItemId": 0 }
],
"loadout": {
"partyCharacterIds": [1, 2],
"carriedItems": [
{ "itemId": 3002, "count": 5 }
],
"difficultyId": 1
},
"runHistory": [
{
"outcome": 1,
"difficultyId": 1,
"seed": 2026083001,
"startedAt": 1756540000000,
"endedAt": 1756541200000
}
]
}
```
## 结构定义
### SaveData(存档根)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `version` | int | 存档结构版本,当前为 1;结构变更时递增并处理迁移 |
| `updatedAt` | long | 最后写入时间,Unix 毫秒 |
| `mainWarehouse` | `ItemStack[]` | 主仓库内容,格子数上限由 `GlobalConfig.WarehouseSlotCount` 配置,可空 |
| `characters` | `CharacterSave[]` | 拥有的角色,数组顺序即角色入队顺序(速度并列时按此顺序行动) |
| `loadout` | `LoadoutSave` | 当前战备配置 |
| `runHistory` | `RunRecord[]` | 已结束单局的结算记录,可空 |
### ItemStack(物品堆叠)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `itemId` | int | 物品 ID,对应 `ItemConfig.Id` |
| `count` | int | 数量,不超过配表 `ItemConfig.StackLimit` |
所有物品容器统一使用堆叠列表存储,不记录格子在背包中的位置。
### CharacterSave(角色)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `characterId` | int | 角色 ID,对应 `CharacterConfig.Id` |
| `weaponItemId` | int | 武器栏物品 ID,0 表示空栏 |
| `armorItemId` | int | 防具栏物品 ID,0 表示空栏 |
装备穿戴在角色装备栏,不占用共享背包格子;成功撤离与任何非全员阵亡的战斗后均保持穿戴,死亡结算时随角色丢失,结算逻辑负责处理,存档本身不记录"阵亡"等局内状态。
### LoadoutSave(战备配置)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `partyCharacterIds` | int[] | 出战角色 ID 及顺序,1~4 人,顺序决定同速时的行动次序 |
| `carriedItems` | `ItemStack[]` | 携带进入地图的物品,开局时填充共享背包;首版无局内效果 |
| `difficultyId` | `DifficultyTier` | 本局难度,对应 `DifficultyConfig` 主键 |
随机数(seed)不属于战备配置:每次进入单局时输入,只写入该局对应的 `RunRecord`,不持久化到 `loadout`。
### RunRecord(结算记录)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `outcome` | `RunResultType` | 结算结果 |
| `difficultyId` | `DifficultyTier` | 本局难度 |
| `seed` | long | 本局使用的随机数 |
| `startedAt` | long | 进入单局时间,Unix 毫秒 |
| `endedAt` | long | 结算时间,Unix 毫秒 |
结算结果枚举复用 Luban 生成的 `RunResultType`:
| 值 | 说明 |
| --- | --- |
| `Extracted` | 成功撤离 |
| `Defeated` | 全员阵亡,单局失败 |
| `TimedOut` | 25 分钟上限未撤离,撤离失败 |
| `Quit` | 玩家主动退出单局,按撤离失败结算 |
`Defeated`、`TimedOut`、`Quit` 均只保留保险箱内容,仅用于结算记录的区分。
## 生命周期
- **新存档**:`characters` 由 `GlobalConfig.NewGameCharacterIds` 生成,初始装备取 `CharacterConfig.WeaponItemId` / `ArmorItemId`;`mainWarehouse`、`loadout.partyCharacterIds`、`loadout.carriedItems`、`runHistory` 为空。
- **进入单局**:根据存档中的局外状态创建仅供本局使用的临时状态(背包、保险箱、单局进度),单局进行中只修改临时状态,不写入存档。
- **正常结算**(撤离成功 / 死亡 / 超时 / 主动退出):按结算规则把物品写入主仓库或丢弃,追加一条 `RunRecord`,一次性写盘。
- **异常关闭**:临时状态丢弃,存档保持进入该局前的状态,不产生结算记录。
## 读写组件
> 代码位置:`Assets/GameMain/Scripts/Runtime/CustomComponent/Save/SaveComponent.cs`,通过 `GameEntry.Save` 访问,挂在 Launcher 场景 `Customs/Save` 物体上。
| API | 说明 |
| --- | --- |
| `Data` | 当前内存中的存档数据,未加载或未创建时为 null |
| `HasSave` | 磁盘上是否已存在存档文件 |
| `IsReady` | 存档数据是否可用 |
| `Load()` | 从磁盘读取存档;文件不存在返回 true 且 `HasSave` 为 false,解析失败返回 false |
| `CreateNewGame()` | 依据配表创建新存档数据(不写盘) |
| `Save()` | 将当前存档写入磁盘,自动更新 `updatedAt`;先写临时文件再替换,避免写入中断损坏存档 |
- 存档文件路径为 `Application.persistentDataPath` 下的 `save.json`(`SaveComponent` 的 `_fileName` 可配置)。
- 加载时若 `version` 与 `SaveData.CurrentVersion` 不一致,记录警告并继续加载,迁移逻辑后续需要时补充。
## 验收对应
对应 `Docs/GameDesign/06_PrototypeScope.md` 存档相关验收项:
- [ ] 退出并重新打开游戏后,局外主仓库、角色装备和战备配置能够从本地存档恢复。
- [ ] 已结束单局的结算数据能够写入本地存档并在重新打开游戏后读取。
- [ ] 重新打开游戏时不会恢复尚未结束的单局。
- [ ] 单局中主动退出时能够按撤离失败结算,保存保险箱内容和本局失败记录。
- [ ] 异常关闭的未结算单局不会修改局外存档,也不会生成结算记录。
## 还没决定的问题
暂无。存档的加密、校验与多存档位支持不在原型范围内,后续需要时再补充。