存档系统
Save System v1.2.0 - Infinite Slot Architecture
版本: v1.2.0
创建日期: 2026-01-14
更新日期: 2026-02-04
状态: 全部完成 ✅
需求概述
背景问题
原系统存在以下问题:
- 游戏启动时自动加载数据库,用户未点击任何按钮数据就已读入内存
- 退出游戏时直接调用
get_tree().quit(),无保存选项 - 所有修改直接写入原始数据库文件,开发调试时可能损坏数据
目标
- 延迟加载: 启动时不加载数据库,等待用户选择后再加载
- 保存功能: 退出时提供保存选项,创建新的存档文件
- 无限槽位: 每次保存都创建新文件(时间戳命名),永不覆盖
- 原始文件保护: 开发模式下保存到项目目录
res://sqlite_database/saves/
主菜单三按钮设计
| 按钮 | 功能 |
|---|---|
| 开始游戏 | 从原始 game_trade.db 加载,开始全新游戏 |
| 继续游戏 | 从最新存档文件加载(如无存档则等同开始游戏) |
| 加载游戏 | 显示所有存档列表,供玩家选择加载 |
游戏内ESC菜单设计
| 按钮 | 功能 |
|---|---|
| 保存 | 保存当前进度,继续游戏 |
| 保存并退出 | 保存后退出程序 |
| 保存并返回主界面 | 保存后返回主菜单(方便调试) |
| 返回主界面 | 不保存,直接返回主菜单 |
| 返回游戏 | 关闭对话框,继续游戏 |
系统架构
┌─────────────────────────────────────────────────────────────┐
│ StartMenu │
│ [开始游戏] [继续游戏] [加载游戏] [退出游戏] │
└─────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ GlobalData │
│ - start_new_game() │
│ - load_database_from_file(path) │
│ - is_game_loaded() │
│ - unload_game() │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SaveGameManager │
│ - quick_save_to_new_file() → 创建新存档文件 │
│ - save_to_file(path) → 覆盖指定存档 │
│ - get_all_saves() → 获取所有存档列表 │
│ - get_latest_save() → 获取最新存档 │
│ - delete_save_file(path) → 删除指定存档 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MemoryDataManager │
│ - sync_all_dirty_to_database() → 同步脏数据到SQLite │
│ - DirtyTracker → 追踪修改的数据 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SQLite Database │
│ 原始: res://sqlite_database/game_trade.db │
│ 存档: res://sqlite_database/saves/game_save_YYYYMMDD_HHMMSS.db │
└─────────────────────────────────────────────────────────────┘
实施阶段
阶段0:延迟加载重构 ✅ 已完成
目标: 重构 GlobalData 为延迟加载模式,启动时不自动加载数据库
修改文件:
scripts/core/global_data.gdscripts/ui/menus/start_menu.gdscripts/ui/menus/main_menu.gdscripts/core/managers/scene_manager.gdscripts/ui/hud/welcome_ui.gdscripts/database/button_sql.gd
新增API (global_data.gd):
# 状态变量
var _game_loaded: bool = false
var _current_db_path: String = ""
# 常量
const ORIGINAL_DATABASE_PATH = "res://sqlite_database/game_trade.db"
const SAVE_DIR = "user://saves/"
# 检查游戏是否已加载
func is_game_loaded() -> bool
# 获取当前数据库路径
func get_current_db_path() -> String
# 开始新游戏(从原始数据库加载)
func start_new_game() -> bool
# 从指定数据库文件加载游戏
func load_database_from_file(db_path: String) -> bool
# 卸载当前游戏(清理所有数据)
func unload_game()
阶段1:数据同步功能 ✅ 已完成
目标: 增强 MemoryDataManager,添加将脏数据同步到SQLite的功能
## 将所有脏数据同步到SQLite数据库
func sync_all_dirty_to_database() -> bool
## 获取待同步的脏数据统计
func get_pending_sync_stats() -> Dictionary
同步策略:
| 表名 | 同步操作 | 状态 |
|---|---|---|
| traders | UPDATE 指定字段 | ✅ |
| bulk_commodity_holdings | INSERT/UPDATE/DELETE 根据quantity | ✅ |
| mercenaries | UPDATE 状态字段 | ✅ |
| mercenary_mood | UPDATE 心情值 | ✅ |
| equipment_instances | UPDATE 所有者/状态 | ✅ |
| adventure_projects | UPDATE 进度/状态 | ✅ |
| adventure_teams | UPDATE 队伍状态 | ✅ |
| team_members | INSERT/UPDATE/DELETE | ✅ |
阶段2:无限槽位保存系统 ✅ 已完成
存档文件策略:
res://sqlite_database/saves/ # 开发模式保存在项目目录
├── game_save_20260114_103045.db # 时间戳命名
├── game_save_20260114_112233.db # 永不覆盖
├── game_save_20260114_143022.db # 无限数量
└── ...
新增API:
const SAVE_DIR = "res://sqlite_database/saves/"
const SAVE_PREFIX = "game_save_"
const ORIGINAL_DB = "res://sqlite_database/game_trade.db"
## 快速保存游戏到新文件(时间戳命名,永不覆盖)
func quick_save_to_new_file() -> Dictionary
## 保存到指定文件(覆盖现有存档)
func save_to_file(file_path: String) -> Dictionary
## 获取所有存档列表(按时间倒序)
func get_all_saves() -> Array
## 获取最新存档
func get_latest_save() -> Dictionary
## 删除指定存档
func delete_save_file(file_path: String) -> bool
## 获取存档数量
func get_save_count() -> int
阶段3:存档列表UI ✅ 已完成
UI特性:
- 支持加载模式和保存模式两种显示
- 保存模式下顶部显示"创建新存档"(绿色高亮)
- 保存模式下现有存档显示"覆盖"按钮
- 加载模式下显示"加载"按钮
- 两种模式都支持删除存档(带确认对话框)
- 独立扫描功能: 在主菜单时也能直接扫描存档目录
阶段4:退出确认对话框 ✅ 已完成
┌─────────────────────────────────┐
│ 确认退出 │
├─────────────────────────────────┤
│ │
│ 是否在退出前保存游戏? │
│ │
│ ⚠️ 有 X 条未保存的数据 │
│ │
│ [ 保存 ] │
│ [ 保存并退出 ] │
│ [ 保存并返回主界面 ] │
│ [ 返回主界面 ] │
│ [ 返回游戏 ] │
│ │
└─────────────────────────────────┘
阶段5:主菜单逻辑整合 ✅ 已完成
Player.gd ESC键处理:
func _input(event):
# 按Shift+ESC打开经济菜单
if event.is_action_pressed("ui_cancel") and Input.is_key_pressed(KEY_SHIFT):
open_economy_menu()
# 单独按ESC打开退出确认对话框
elif event.is_action_pressed("ui_cancel") and not Input.is_key_pressed(KEY_SHIFT):
open_quit_dialog()
文件结构
gdt/
├── scripts/
│ ├── core/
│ │ ├── global_data.gd # ✅ 阶段0 - 延迟加载
│ │ ├── data_structures/
│ │ │ └── memory_adventure_teams.gd # ✅ 阶段1 - 添加to_sqlite_dict()
│ │ └── managers/
│ │ ├── memory_data_manager.gd # ✅ 阶段1 - 数据同步
│ │ ├── save_game_manager.gd # ✅ 阶段2 - 无限槽位+覆盖保存
│ │ └── scene_manager.gd # ✅ 阶段0 - 加载检查
│ ├── player/
│ │ └── player.gd # ✅ 阶段5 - ESC键退出对话框
│ └── ui/
│ ├── hud/
│ │ └── welcome_ui.gd # ✅ 阶段0 - 移除独立DB
│ ├── dev_console.gd # ✅ 添加保存测试命令
│ └── menus/
│ ├── start_menu.gd # ✅ 阶段5 - 集成存档列表
│ ├── main_menu.gd # ✅ 阶段5 - 集成退出确认
│ ├── save_load_panel.gd # ✅ 阶段3 - 存档列表(双模式)
│ └── quit_confirm_dialog.gd # ✅ 阶段4 - 退出确认(5按钮)
├── scenes/
│ └── ui/
│ └── menus/
│ ├── SaveLoadPanel.tscn # ✅ 阶段3
│ └── QuitConfirmDialog.tscn # ✅ 阶段4
├── sqlite_database/
│ ├── game_trade.db # 原始数据库(只读)
│ └── saves/ # 存档目录(已gitignore)
│ └── game_save_*.db # 时间戳存档文件
└── docs/
└── SAVE_SYSTEM_IMPLEMENTATION-260114.md # 本文档
API参考
GlobalData
| 方法 | 参数 | 返回值 | 描述 |
|---|---|---|---|
is_game_loaded() |
- | bool | 检查游戏是否已加载 |
get_current_db_path() |
- | String | 获取当前数据库路径 |
start_new_game() |
- | bool | 从原始数据库开始新游戏 |
load_database_from_file(path) |
String | bool | 从指定路径加载数据库 |
unload_game() |
- | void | 卸载游戏,清理所有数据 |
SaveGameManager
| 方法 | 参数 | 返回值 | 描述 |
|---|---|---|---|
quick_save_to_new_file() |
- | Dictionary | 创建新存档,返回保存信息 |
save_to_file(path) |
String | Dictionary | 覆盖指定存档 |
get_all_saves() |
- | Array | 获取所有存档列表(按时间倒序) |
get_latest_save() |
- | Dictionary | 获取最新存档信息 |
delete_save_file(path) |
String | bool | 删除指定存档 |
get_save_count() |
- | int | 获取存档数量 |
数据流
启动流程
游戏启动
│
▼
GlobalData._ready()
│ (不加载数据库)
▼
StartMenu 显示
│
├─→ [开始游戏] ─→ GlobalData.start_new_game()
│ │
│ ▼
│ 加载原始 game_trade.db
│ │
│ ▼
│ 初始化所有管理器
│ │
│ ▼
│ SceneManager.quick_switch_to_main_game()
│
└─→ [加载游戏] ─→ 显示 SaveLoadPanel (加载模式)
│
▼
用户选择存档
│
▼
GlobalData.load_database_from_file()
保存流程
用户按ESC / 手动保存
│
▼
QuitConfirmDialog 显示
│
├─→ [保存] / [保存并退出] / [保存并返回主界面]
│ │
│ ▼
│ 显示 SaveLoadPanel (保存模式)
│ │
│ ├─→ [创建新存档] ─→ quick_save_to_new_file()
│ │
│ └─→ [覆盖存档] ─→ save_to_file(path)
│ │
│ ▼
│ MemoryDataManager.sync_all_dirty_to_database()
│ │
│ ▼
│ 复制数据库到存档目录
│ │
│ ▼
│ 根据选择: 继续游戏 / 退出 / 返回主界面
│
├─→ [返回主界面] ─→ SceneManager.quick_switch_to_start_menu()
│
└─→ [返回游戏] ─→ 关闭对话框
后续计划
自动保存功能 (后续实现)
- 定时自动保存(可配置间隔)
- 保存到专门的自动存档槽位
- 最多保留N个自动存档
存档管理增强 (后续实现)
- 存档重命名
- 存档导出/导入
- 存档压缩
- 云存档同步
变更日志
| 日期 | 版本 | 变更内容 |
|---|---|---|
| 2026-01-14 | v0.6.0 | 阶段0完成:延迟加载重构 |
| 2026-01-14 | v0.7.0 | 阶段1完成:数据同步功能 |
| 2026-01-14 | v1.0.0 | 阶段2-5完成:无限槽位保存、存档列表UI、退出确认、主菜单整合 |
| 2026-01-14 | v1.1.0 | 优化:存档目录改为项目目录、ESC菜单增加返回主界面选项 |
| 2026-02-03 | v1.2.0 | 新增 force_reset 机制、user:// 目录迁移、WAL Checkpoint |
2026-02-03 更新
新增功能
- force_reset 机制:
start_new_game()现在会强制重置user://中的数据库,确保"开始新游戏"真正从头开始 - user:// 目录迁移:存档从
res://迁移到user://,解决导出后无法写入问题 - WAL Checkpoint:保存前执行
PRAGMA wal_checkpoint(TRUNCATE)确保数据完整性
API 变更
# GlobalData - 新增 force_reset 参数
func start_new_game() -> bool:
return load_database_from_file(ORIGINAL_DATABASE_PATH, true) # force_reset=true
func load_database_from_file(db_path: String, force_reset: bool = false) -> bool