存档
基于 EasySave3 的 JSON 文件存档:保存玩家船的金币、全部建筑、武器弹药、容器物品和世界探索状态;当前仅在专用测试场景中可手动触发,主游戏场景尚未接入,也没有任何自动存档。
存档系统(SaveSystemManager.cs,位于 Assets/Scripts/Service/SaveSystem/)负责把一艘船的“建造成果 + 物资 + 世界进度”写入磁盘并在读档时原地重建。底层使用第三方插件 EasySave3(ES3),格式为明文 JSON,不加密、不压缩。
当前接入状态(策划必须知道):存档系统的管理器 SaveSystemManager 和触发器 SaveSystemExample 只被放置在专用测试场景 SaveSystemTest.unity 中;主游戏场景 mvp_002.unity 里没有这两个对象,也没有任何代码在运行时创建它们。换句话说——正常游玩主场景时,玩家既不能存档也不能读档。游戏循环(见 游戏循环与胜负)目前是“单次会话”体验,死亡/胜利后不依赖存档。
存档按“船只编号(shipId)“组织。一次保存会在存档目录下写出 5 个 JSON 文件:
| 文件名 | ES3 内部键 | 内容 |
|---|---|---|
{shipId}_info.json |
ShipData | 船只基础信息(金币、阵营等) |
{shipId}_buildings.json |
BuildingsData | 全部建筑(位置、血量、建造进度等) |
{shipId}_weapons.json |
WeaponsData | 全部武器的当前弹药数 |
{shipId}_containers.json |
ContainersData | 全部建筑容器及其中物品 |
{shipId}_world.json |
多个键(见下文) | 世界探索/迷雾/物价/新闻 |
存档目录:持久化数据目录/ShipSaves/。Windows 上即 C:\Users\<用户名>\AppData\LocalLow\<公司名>\<产品名>\ShipSaves\。目录名 ShipSaves 和扩展名 .json 是 SaveSystemManager 的 Unity Inspector 序列化字段,可在场景里改。
存档时机:只有手动,没有自动
Section titled “存档时机:只有手动,没有自动”代码中不存在任何自动存档:没有定时存档、没有过天存档、没有停靠港口存档、没有战斗结束存档、退出游戏时也不保存(全项目无任何“应用退出时写档”逻辑)。SaveSystemExample.cs 里出现的“自动存档完成”字样只是一条日志文案,并无自动触发源。
实际触发方式(全部在 SaveSystemTest.unity 测试场景内):
| 操作 | 触发方式 | 来源 |
|---|---|---|
| 保存 | 按键,代码默认 Insert |
SaveSystemExample.cs Inspector 字段 saveKey |
| 读取 | 按键,代码默认 Home |
同上,字段 loadKey |
| 清空当前所有建筑 | 按键,默认 Delete |
同上,字段 clearKey |
| 保存/读取/清空 | Unity 编辑器组件右键菜单(ContextMenu) | SaveSystemExample.cs |
| 显示存档列表 / 删除全部存档文件 | Unity 编辑器组件右键菜单 | SaveSystemManager.cs |
注意按键的两套值:代码默认值是 Insert/Home(注释说明 F5/F9 已被场景测试工具 ScenarioTestRunner 占用,见 开发者工具),但 SaveSystemTest.unity 场景里序列化保存的仍是旧值 F5 保存 / F9 读取 / Delete 清空(场景文件中 saveKey=286、loadKey=290、clearKey=127,对应 KeyCode 的 F5/F9/Delete)。在该测试场景里实际生效的是场景序列化值。
测试场景中配置的船只编号为 ExampleShip_001(SaveSystemExample Inspector 字段 shipId)。
保存过程是异步的(后台线程写文件),不会暂停游戏;保存期间游戏状态若继续变化,各文件之间可能出现微小不一致(先收集建筑、再收集武器、再收集容器,逐步进行)。
存档内容清单:存什么
Section titled “存档内容清单:存什么”保存时从全局注册表现场收集数据(不是持续记录),五类内容如下。
1. 船只基础信息(ShipData.cs)——只保存玩家船:
| 字段 | 含义 | 备注 |
|---|---|---|
| version | 数据版本号 | 恒为字符串 “1.0.0”,写入后从不校验(见“存档版本与兼容”) |
| shipId | 船只编号 | 保存时被覆盖为本次存档的 shipId |
| isPlayerShip | 是否玩家船 | |
| factionId | 阵营编号 | 默认 “Player” |
| nationality | 国籍 | 默认空 |
| gold | 当前金币 | 玩家经济唯一入档的数值 |
| maxGold | 金币上限 | 代码常量 9999999999(约 100 亿,ShipData.cs) |
2. 建筑(AllBuildingsInfo.cs → BuildingData.cs)——逐栋保存:
| 字段组 | 内容 |
|---|---|
| 标识 | 实例编号(GUID)、建筑名(对应 BuildingConfig.csv 的 Name 列) |
| 位置 | 网格坐标(格)、建筑层级、相对船体的旋转 |
| 状态 | 当前血量、是否建成、建筑状态(框架/建造中/完成等枚举) |
| 建造进度 | 材料进度(0-1)、施工进度(0-1)、总工作量、所需材料清单、已送达材料清单 |
| 占格 | 占用的全部网格坐标、外部紧邻坐标 |
注意:收集范围是全局建筑注册表的所有建筑,并不按船过滤——shipId 只是档案上的标签。在只有一艘船的测试场景中没有问题;若在多船环境(主场景)启用,会把敌船建筑也收进同一份档。
3. 武器(AllWeaponInfo.cs → WeaponData.cs)——每座武器只存两个字段:所属建筑的实例编号 + 当前弹药数(CurrentAmmo)。判定“哪些建筑算武器”的依据是建筑名出现在 WeaponConsumptionConfig.csv 的 WeaponId 列(该表 8 行数据,即 MachineGun、Cannon、Flame、AA_Gun 各自的 Dev/Prod 变体,定义弹匣容量、装填耗时等,详见武器与弹药)。除弹药数以外的武器运行时状态(攻击方式选择、装弹档位、弹药档案、冷却、目标)一律不存。
4. 容器与物品(AllContainerInfo.cs、SerializableContainerData.cs、SerializableItemStack.cs)——逐建筑保存最多 3 个容器(输入/输出/存储),每个容器保存:
| 字段 | 内容 | 备注 |
|---|---|---|
| capacity | 容器容量(格) | 读档时按存档值恢复,不读当前配置 |
| filterItems | 过滤器允许的物品编号列表 | |
| items | 物品堆列表:物品编号、数量、属性列表 | 属性以“属性名 + 类型名 + 字符串值 + 可合并标记”保存 |
| reservations | 预留数据 | 字段存在但写入恒为空列表——预留序列化未实现(代码注释“暂时跳过”) |
只有挂在建筑上的容器会被保存;不在任何建筑容器里的物品(例如搬运途中、地面散落物)不会进入存档。物品与容器机制详见物品与库存。
5. 世界状态(WorldStateInfo.cs,聚合 4 个服务写入同一个 _world.json):
| 服务 | ES3 键 | 内容 | 数值 |
|---|---|---|---|
发现注册表(DiscoveryRegistry.cs) |
DiscoveryCountries / DiscoveryCities / DiscoveryZones | 已发现的国家/城市/海域编号集合 | 发现城市自动连带发现所属国家,连带关系也随存档保留 |
战争迷雾(FogMaskService.cs) |
FogMaskData | 战略图“已探索”位图 | 80×80 格 = 6400 位,打包为 800 字节(格子尺寸 = 世界最长边 ÷ 80,代码常量) |
全局物价倍率(GlobalPriceModifier.cs) |
GlobalPriceModifierData | 物品编号 → 价格倍率字典 | 由“通货变动”新闻累乘产生(±5%/条),默认 1.0 |
新闻调度(EventScheduler.cs) |
EventSchedulerNews / LastBriefingShownDay / LastInvestigatedDay | 全部历史新闻条目、最后一次看简报的天数、最后一次投放新闻的天数 | 新闻每 7 天投放 1 条(代码常量) |
世界状态部分有完整的 PlayMode 往返测试佐证(SaveLoadRoundTripTests.cs:保存 → 销毁全部服务 → 读取 → 四个服务状态完全恢复)。相关玩法见探索与情报、交易、城市与国家。
明确不存什么(同样重要)
Section titled “明确不存什么(同样重要)”以下内容完全不进存档,读档后回到场景初始状态:
| 不保存的内容 | 后果 |
|---|---|
| 船员的一切(属性、需求、心情、关系、职级、位置、任务) | 见船员;读档不影响船员 |
| 游戏时间(当前天数、时刻) | 见时间与调度;天数从场景初始重新计 |
| 船只位置、朝向、速度 | ShipData 不含任何位置信息 |
| 敌船、怪物、遭遇战进度 | 见遭遇与刷怪 |
| 阵营关系、声望 | 见阵营与外交;战争/和平新闻虽存了新闻条目,但读档时不会重放其阵营关系效果 |
| 任务/工作队列 | 见任务与工作 |
| 生产配方的进行中进度 | 容器里的原料/成品会存,正在加工的那一炉不存,见生产与制作 |
| 容器/物品的预留记录 | 序列化结构有字段但恒为空 |
| 武器的攻击方式/装弹档位/弹药档案选择 | 只存当前弹药数 |
| 玩家手动拆除自动船壳的记录 | 记录只在内存(AutoHullGenerator.cs 的清除集合不入档):同一次游戏会话内读档仍然生效(重扫时跳过这些格),但重启游戏后再读档记录丢失,拆掉的外壳会被自动补回 |
| 流体管道、胜利进度、开发者工具状态 | 各自系统无存档对接 |
读档(测试场景按 Home,或场景序列化的 F9)按以下顺序执行(SaveSystemManager.cs、SaveSystemExample.cs):
- 读文件:依次读取 5 个文件。船只信息文件缺失 → 使用默认值(金币 0),不算失败;世界状态文件缺失 → 视为首次启动,正常;建筑/武器/容器三个文件任一读取失败 → 整次读档判定失败,提示“没有找到存档”,什么都不改变。
- 应用船只数据:金币、阵营等覆盖到玩家船。
- 清场:销毁当前场上全部建筑(GameObject 一并删除)。
- 批量重建守卫开启:重建期间挂起自动船壳生成器(防止它在拆墙/建墙事件中插入与存档墙位置冲突的自动墙)。
- 逐栋重建建筑:
- 旧建筑名迁移(见下文“存档版本与兼容”);
- 按建筑名查
BuildingConfig.csv配置 → 实例化对应预制体; - 世界位置按“船当前位置 + 存档网格坐标”重算,世界旋转 = 船体旋转 × 建筑相对旋转(因此船移动过也能正确读档,建筑跟着船走);
- 恢复血量、建造进度等数据并注册;
- 配置缺失或预制体缺少建筑组件 → 静默跳过该栋(计数不含它)。
- 守卫关闭 + 全量补扫:刷新建筑可见性,广播“重建完成”,自动船壳生成器据此重新扫描一遍外圈补墙(见修建)。
- 恢复武器弹药:按建筑实例编号匹配,写回当前弹药数。
- 恢复容器:按建筑实例编号匹配,重建容器对象并把物品逐堆放回——放回时按当前配置的物品定义和堆叠规则执行,放不下的部分丢弃(仅日志警告)。
- 世界状态在第 1 步读取文件时已由 4 个服务各自恢复完毕(迷雾支持“先读档后初始化”的延迟回放)。
建筑血量的恢复规则:当前血量取存档值,最大血量取预制体上的序列化字段(建筑预制体 Inspector 的 _maxHP,默认 100)。
存档版本与兼容
Section titled “存档版本与兼容”每个存档文件都写入了版本号字段 version = "1.0.0"(AllBuildingsInfo.cs、AllWeaponInfo.cs、AllContainerInfo.cs、ShipData.cs 各一份),但全项目没有任何代码读取或比较这个版本号——不存在版本化迁移框架。
唯一的兼容性处理是一条硬编码的建筑名迁移(SaveSystemManager.cs):
旧存档中的
Wall(旧版统一墙,150 血)在读档时自动改名为WallIron(铁墙)。
选择 WallIron 的原因写在代码注释里:旧 Wall 的最大血量 150 与现行 BuildingConfig.csv 中 WallIron 的 MaxHealth=150 一致(其余墙变体:WallPine 30 血 / WallOak 80 血 / WallBrass 100 血),这样老玩家的船体防御力不会因为墙系统拆分而静默下降。该迁移仅当配置表里已不存在名为 “Wall” 的建筑时才执行。
除此之外,任何配置表与存档的不匹配都靠“跳过 + 日志”兜底,没有报错弹窗,详见下一节。
| 项目 | 数值 | 单位 | 来源 |
|---|---|---|---|
| 存档目录名 | ShipSaves | — | Inspector 字段(SaveSystemManager,SaveSystemTest 场景) |
| 存档文件扩展名 | .json | — | 同上 |
| 每艘船的存档文件数 | 5 | 个 | 代码(SaveSystemManager.cs) |
| 存档格式 | ES3 File + JSON,明文 | — | 代码(各 Info 类的 ES3 设置) |
| 版本号字段 | “1.0.0”,写入但从不校验 | — | 代码(4 个数据类) |
| 自动存档 | 无 | — | 代码(全项目无自动触发) |
| 保存按键(代码默认) | Insert | KeyCode | Inspector 字段(SaveSystemExample.cs) |
| 读取按键(代码默认) | Home | KeyCode | 同上 |
| 清空建筑按键(默认) | Delete | KeyCode | 同上 |
| 保存/读取/清空按键(测试场景实际序列化值) | F5 / F9 / Delete | KeyCode | SaveSystemTest.unity 场景序列化 |
| 测试场景船只编号 | ExampleShip_001 | — | Inspector 字段(SaveSystemExample) |
| 金币上限 maxGold | 9999999999 | 金币 | 代码常量(ShipData.cs) |
| 迷雾位图分辨率 | 80×80 = 6400 格,打包 800 字节 | 格/字节 | 代码常量(FogMaskService.cs Grid=80) |
| 迷雾格子尺寸 | 世界最长边 ÷ 80 | 米 | 代码公式(FogMaskService.cs) |
| 迷雾擦除半径 | 世界最长边 ÷ 10 | 米 | 代码公式(FogMaskService.cs) |
| 新闻投放间隔 | 7 | 天 | 代码常量(EventScheduler.cs IntervalDays) |
| 通货变动新闻价格倍率 | 0.95 或 1.05(各 50% 概率,累乘) | 倍 | 代码常量(NewsTemplates.cs) |
| 旧 Wall 迁移目标 | WallIron(MaxHealth 150) | — | 代码(SaveSystemManager.cs)+ CSV(BuildingConfig.csv) |
| 墙变体血量(迁移参照) | WallPine 30 / WallOak 80 / WallBrass 100 / WallIron 150 | 点 | CSV(BuildingConfig.csv,策划可改) |
| 建筑默认最大血量 | 100 | 点 | Inspector 字段(建筑预制体 _maxHP) |
| 容器默认容量(仅异常兜底路径) | 50 | 格 | 代码常量(SerializableContainerData.cs 默认构造) |
配置表改动对旧存档的影响(策划必读)
Section titled “配置表改动对旧存档的影响(策划必读)”改配置表之前,先对照这张表评估对已有存档的破坏。所有不匹配都是静默跳过 + 日志,玩家看不到任何提示。
| 改动 | 对旧存档的影响 |
|---|---|
BuildingConfig.csv 删除某行或改 Name 列 |
旧存档中该名字的建筑读档时直接消失(查不到配置 → 跳过该栋)。唯一例外是 Wall→WallIron 自动迁移。改建筑名前必须评估存档迁移,或仿照 Wall 在代码里加迁移分支(程序工作,SaveSystemManager.cs)。 |
BuildingConfig.csv 改 MaxHealth |
不影响旧档建筑的当前血量(按存档原值恢复);最大血量实际取预制体 Inspector 字段而非该列,是否会出现“当前血量超过新上限”未在存档链路中处理。 |
StaticItemConfig.csv 删除某个 ItemId |
旧档容器里该物品整堆丢失(恢复时查不到物品配置 → 跳过,错误日志)。见物品数据表。 |
StaticItemConfig.csv 调小堆叠上限等入库规则 |
物品按当前规则重新放回容器,放不下的部分丢弃(警告日志)。 |
容器容量类配置(如 BuildingContainerConfig.csv)改容量 |
对旧存档完全无效:容量从存档原值恢复,老建筑的容器维持旧容量,新建的才用新容量。 |
WeaponConsumptionConfig.csv 删除某个 WeaponId |
该建筑此后不再被当作武器保存(保存时按 WeaponId 匹配建筑名);已有旧档中的弹药数据因按实例编号匹配仍可恢复一次。 |
WeaponConsumptionConfig.csv 调小 MagazineCapacity |
旧档的 CurrentAmmo 按原值写回,可能暂时超过新弹匣容量(存档链路不做钳制)。 |
通用原则:存档里记录的是“编号/名字 + 数值”,定义本体永远查当前配置表。所以“改数值”大多安全(读档后按新配置表现),“改编号/删行”一定危险(旧档引用悬空 → 内容静默丢失)。
与其他系统的交互
Section titled “与其他系统的交互”- 修建:读档即“按图重建”,建造中的框架连同材料/施工进度一起恢复;自动船壳生成器在重建期间被守卫挂起,结束后全量补扫。
- 建筑:建筑血量、状态随档恢复;血量上限来自预制体。
- 武器与弹药:仅弹药数入档;武器身份由 WeaponConsumptionConfig.csv 定义。
- 物品与库存:建筑容器及内容物入档;预留记录不入档。
- 探索与情报 / 岛屿与陆地:发现集合与战争迷雾位图入档。
- 交易:全局物价倍率入档,读档后城市报价延续涨跌。
- 阵营与外交:阵营关系不入档;战争新闻条目虽在档内,读档时不重放其效果。
- 时间与调度:当前天数不入档,但新闻系统的“最后投放日/最后简报日”入档——见下节的天数错位问题。
- 船员 / 任务与工作:完全不入档。
- 开发者工具:F5/F9 被场景测试工具占用,是存档默认按键改为 Insert/Home 的原因。
配置与调参指南
Section titled “配置与调参指南”| 想调什么 | 改哪里 |
|---|---|
| 存档目录名 / 文件扩展名 | SaveSystemTest.unity 场景中 SaveSystemManager 对象的 Inspector(字段 saveDirectory / saveFileExtension) |
| 存/读/清按键 | 同场景 SaveSystemExample 对象的 Inspector(saveKey / loadKey / clearKey);注意场景里目前序列化的是 F5/F9/Delete 旧值 |
| 测试用船只编号 | 同上,字段 shipId(当前 ExampleShip_001) |
| 旧建筑名迁移规则 | 代码(SaveSystemManager.cs 中 Wall→WallIron 分支),需程序添加新迁移 |
| 迷雾分辨率 / 新闻间隔 / 金币上限 | 代码常量(分别在 FogMaskService.cs、EventScheduler.cs、ShipData.cs),需程序改 |
| 让主场景支持存档 | 需要程序把 SaveSystemManager(及触发入口)接入 mvp_002.unity,并解决“全局建筑注册表不分船”的收集问题——纯程序工作 |
| 手工查看/删除存档 | 直接打开 AppData\LocalLow\<公司>\<产品>\ShipSaves\ 下的 JSON 明文文件;或在编辑器中右键 SaveSystemManager 组件选“清空所有存档” |
已知限制与注意事项
Section titled “已知限制与注意事项”- 主场景未接入,无自动存档:存档功能目前是测试场景专属能力。死亡/胜利/退出都不会落盘。
- 天数错位:当前天数不入档,但新闻系统的“最后投放日”入档。读档后游戏天数从头计,而档内记录的投放日可能远大于当前天数,导致每 7 天一条的新闻在天数追上存档值之前不再投放;新闻列表里的历史日期也会显得“在未来”。
- 新闻效果不重放:读档恢复的是新闻条目列表本身;战争/和平新闻曾经造成的阵营关系变化不会随读档恢复(阵营关系本身也不入档)。物价倍率因为单独入档,能正确恢复。
- 预留数据未实现:容器与物品的预留(reservation)序列化结构存在但写入恒为空,相关代码注释明确标注“暂时跳过,后续完善”(
SerializableContainerData.cs、SerializableItemStack.cs)。 - 保存非原子:5 个文件依次写出,中途异常可能留下“半新半旧”的文件组合;没有备份/回滚机制。
- 全局收集不分船:保存收集的是全局建筑注册表的所有建筑,shipId 只是标签。多船同场(主场景)下直接启用会把敌船建筑混入玩家档。
- 静默跳过哲学:配置缺失、预制体缺组件、物品放不下,全部静默跳过并打日志,玩家与策划在游戏内均无感知;排查存档内容丢失问题要看日志(前缀 Save / Container / Registry)。
- 物品属性以字符串存取:物品的动态属性按“属性名 + 类型名 + 字符串值”保存,读档时按属性名塞回字典;复杂属性(如自定义对象)会退化为字符串。
- 文档与现状的差异:
Assets/Scripts/Service/SaveSystem/README.md仍写“F5 保存 / F9 加载”,与代码默认值(Insert/Home)不符;README 中“压缩和加密设置”的描述也与实际(无压缩无加密)不符。以本页(即代码现状)为准。 - 版本号是装饰:所有存档文件的 version 字段写死 “1.0.0” 且无任何读取方,未来做格式变更时没有现成的版本判断钩子可用。