140 lines
4.4 KiB
Markdown
140 lines
4.4 KiB
Markdown
# 存档系统(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 槽"的清理逻辑
|