6.9 KiB
存档数据结构
定义游戏存档中需要持久化的数据。仅覆盖局外状态与已结束单局的结算记录;进行中的单局使用临时状态,不写入存档。 相关设计:
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)。
{
"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 存档相关验收项:
- 退出并重新打开游戏后,局外主仓库、角色装备和战备配置能够从本地存档恢复。
- 已结束单局的结算数据能够写入本地存档并在重新打开游戏后读取。
- 重新打开游戏时不会恢复尚未结束的单局。
- 单局中主动退出时能够按撤离失败结算,保存保险箱内容和本局失败记录。
- 异常关闭的未结算单局不会修改局外存档,也不会生成结算记录。
还没决定的问题
暂无。存档的加密、校验与多存档位支持不在原型范围内,后续需要时再补充。