Files
ImmortalSect/doc/SaveSystem.md
T

140 lines
4.4 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.
# 存档系统(SaveSystem)使用说明
Autoload 单例,路径 `res://src/Core/SaveSystem.gd`,已在 `project.godot` 注册。
## 架构
两级存档:
```
存档组 SaveGroup(一次轮回/一个角色) profiles.json 中一条记录
└── 存档槽 SaveSlot(进度快照) user://saves/profile_{组id}/slot_{时间戳}.json
```
- 每个"新游戏"创建一个组,组 id 由 SaveSystem 分配(1、2、3…)
- 每次 `save()` 在组下**新建**一个槽,槽与组数量无上限
- 存档文件为 JSON,含 `version``date``data` 三段,`data` 为各系统数据合并后的字典
## 核心接口
| 接口 | 说明 |
| --- | --- |
| `create_profile() -> int` | 新建存档组,返回组 id。主菜单"新游戏"调用 |
| `get_profiles() -> Array[Dictionary]` | 组列表,每条含 `id / name / created / last_date` |
| `save(profile_id, data = {}) -> String` | 保存游戏,返回槽文件名。`data` 缺省时自动收集各系统数据 |
| `load(profile_id, slot_name) -> Dictionary` | 读取槽位数据,槽不存在或版本高于当前返回 `{}` |
| `list_slots(profile_id) -> Array[Dictionary]` | 组下槽列表(`name / date`),最新在前 |
| `delete_slot(profile_id, slot_name)` | 删除槽位 |
| `register_saver(callable) -> void` | 注册数据提供者(见下) |
| `collect_data() -> Dictionary` | 手动收集所有注册系统的数据 |
## 各系统如何接入存档
新增需要存档的系统只需两步,**无需改动 SaveSystem**
```gdscript
# 例:SectSystem.gd
extends Node
func _ready() -> void:
SaveSystem.register_saver(_to_save_dict)
## 提供存档数据,返回字典,key 用系统名避免冲突
func _to_save_dict() -> Dictionary:
return {
"sect": {
"name": sect_name,
"resources": resources,
"members": members,
},
}
## 读档时恢复(由 MainGame 在 load 后按 key 分发调用)
func load_from_dict(d: Dictionary) -> void:
if not d.has("sect"):
return
var sect: Dictionary = d["sect"]
sect_name = sect.get("name", "")
...
```
约定:
- 每个系统实现 `_to_save_dict()`(提供数据)和 `load_from_dict()`(恢复状态)
- key 用系统名(如 `"sect"`),避免系统间冲突
- 读档用 `d.get(key, {})` / `.get(field, 默认值)` 容错,保证旧档兼容
- 只把**游戏内数据**放进去;会话参数(当前 mode、profile_id 等)由 GameState 管理,不落档
### 示例:GameState 保存游戏时间
GameState 是 Autoload(持久对象),在其 `_ready` 中注册:
```gdscript
# GameState.gd
extends Node
var year: int = 1
var month: int = 1
func _ready() -> void:
SaveSystem.register_saver(_to_save_dict)
## 提供存档数据:key 为 "game_time",值是游戏时间字段
func _to_save_dict() -> Dictionary:
return {
"game_time": {
"year": year,
"month": month,
},
}
## 读档恢复(MainGame 在 load 后调用 GameState.load_from_dict(data)
func load_from_dict(d: Dictionary) -> void:
var t: Dictionary = d.get("game_time", {})
year = int(t.get("year", 1))
month = int(t.get("month", 1))
```
存档后 JSON 中的形态:
```json
"game_time": { "year": 12, "month": 7 }
```
要点:
- `register_saver` 只接收无参 Callable,方法名直接传,**不要**写成 `register_saver(_to_save_dict())`(那是调用而不是注册)
- `year`/`month` 读档用 `get()` 带默认值,旧存档没有该字段时也能启动
## 存档与读档流程
### 存档(回合结算 / 月度结算 / 关窗前)
```gdscript
# TimeSystem 月度结算处
var slot := SaveSystem.save(GameState.profile_id)
print("已保存:%s" % slot)
```
### 读档(主菜单进入游戏)
```gdscript
# MainGame 场景启动时
var data := SaveSystem.load(GameState.profile_id, GameState.save_slot)
if data.is_empty():
push_error("存档读取失败")
return
# 按 key 分发给各系统恢复
SectSystem.load_from_dict(data)
...
# 主菜单:列出组和槽供选择
var profiles := SaveSystem.get_profiles()
var slots := SaveSystem.list_slots(pid) # 选中某组后
GameState.request_load_game(pid, slot["name"])
```
## 存档时机建议(回合制)
- **每月结算时**自动 `save()` 一次,覆盖当前组新建槽
- **关窗前**`NOTIFICATION_WM_CLOSE_REQUEST`)补存一次,避免丢失当月操作
- 不需要手动存档 UI;如担心槽无限增长,可加"保留最近 N 槽"的清理逻辑