今日速览
- 存档的难点不是「存一个数字」,而是一套能随游戏复杂度长大的结构:对象会变多、关卡会变多、变量会变多。
- Godot 官方推荐的做法:把需要持久化的节点加进
Persist分组,遍历分组逐个调用各自的save(),再用JSON.stringify()一行一个对象写进存档文件。 - 存档必须写到
user://,不能写res://——导出后res://是只读的,只有user://保证可写。 Vector2等内置类型不能直接被 JSON 序列化,必须先拆成pos_x / pos_y两个浮点数。- 读档前要先清空场景里的旧对象(
queue_free()),否则会复制出两套对象。
核心学习内容
1. 谁需要被存档?用「分组」而不是「清单」
新手最容易犯的错,是写一个巨大的 save_game(),里面硬编码 player.position、enemy.hp、boss.hp……每加一种对象就要改一次存档代码。
官方的思路是让对象自己负责自己:给需要存档的节点加上 persist 分组(编辑器里 Node → Groups 勾选,或代码 add_to_group("persist"))。存档时只做一件事:
var save_nodes = get_tree().get_nodes_in_group("persist")
for node in save_nodes:
if node.scene_file_path.is_empty(): # 不是实例化场景,跳过
continue
if not node.has_method("save"): # 没有 save() 方法,跳过
continue
save_file.store_line(JSON.stringify(node.call("save")))
每个节点自己实现 save(),返回一个字典。加新对象时,只要给它加分组和 save(),存档主流程一行都不用改——这就是「能长大」的含义。
2. 序列化:为什么是一行一个 JSON 对象
JSON.stringify(dict) 把字典转成字符串,store_line() 让它占一行。这样存档文件就是天然的「一行一个对象」,读档时 get_line() 逐行解析即可,坏了某一行也不影响其他行,便于调试和容错。
注意:JSON 只认识数字、字符串、布尔、数组、字典。Vector2、Color 这类引擎类型必须手动展开:
func save() -> Dictionary:
return {
"filename": get_scene_file_path(),
"parent": get_parent().get_path(),
"pos_x": position.x, # Vector2 不支持 JSON
"pos_y": position.y,
"current_health": current_health,
"level": level,
}
3. 读档:先清场,再重建
读档的逻辑是「反着来」:先删掉当前场景里所有 persist 对象,再按行读文件、load(filename).instantiate() 重建对象、设置位置和变量。
for node in get_tree().get_nodes_in_group("persist"):
node.queue_free()
var f = FileAccess.open("user://savegame.save", FileAccess.READ)
while f.get_position() < f.get_length():
var json = JSON.new()
if json.parse(f.get_line()) != OK:
continue
var d = json.data
var obj = load(d["filename"]).instantiate()
get_node(d["parent"]).add_child(obj)
obj.position = Vector2(d["pos_x"], d["pos_y"])
for k in d.keys():
if k in ["filename", "parent", "pos_x", "pos_y"]:
continue
obj.set(k, d[k])
三个工程细节:① 路径固定在 user://savegame.save(user:// 目录自动创建且保证可写);② 存档前先确认 FileAccess.file_exists(),避免空档报错;③ 想存「设置」而非「进度」时用 ConfigFile,别混进存档文件。
4. 让它更像成品:多存档槽与版本号
上架 Steam 的游戏至少要有「继续 / 新游戏」。把路径改成 user://save_slot_%d.save 就能扩展成三个槽位;在字典里加一个 "version": 1 字段,将来格式变了可以按版本号做迁移,而不是让老玩家存档直接崩掉。压缩方面,FileAccess.open_compressed() 可以在存档大时顺手压一下。
🎯 DeepSeek 提示词
① 让 DeepSeek 生成整套存档系统骨架(适合:项目刚起步,要从零搭)
你是一名资深 Godot 4.x (GDScript) 游戏程序员。
任务:为一个 2D 冒险游戏实现「存档 / 读档」系统。
约束:
- 使用官方推荐模式:需要持久化的节点加入 "persist" 分组,各自实现 save() 返回 Dictionary。
- 用 JSON.stringify + FileAccess.store_line 写入 user://savegame.save(一行一个 JSON 对象)。
- 读档时先 queue_free 掉现有 persist 节点,再按行重建。
- 提供:SaveManager.gd(Autoload 单例,含 save_game()/load_game()/has_save())、
一个示例 Player.gd 的 save()/load() 实现。
- 处理场景路径为空、没有 save() 方法、文件不存在这三种边界情况,并打印明确日志。
输出格式:完整 GDScript 代码 + 每段上方一句中文注释说明用途。
不要使用 res:// 作为写入路径。
② 让 DeepSeek 升级成多槽位 + 版本迁移(适合:已有存档,要扩充)
你是一名 Godot 4.x 程序员。下面是我现有的存档代码(贴出 SaveManager.gd 全文)。
任务:在不破坏旧存档的前提下,把它改造为:
1) 支持 3 个存档槽,路径 user://save_slot_{n}.save,n 为 1..3;
2) 存档字典新增 "version" 字段,并写一个 _migrate(data) 函数:
当 version < CURRENT_VERSION 时补齐缺失字段,保证读档不崩;
3) 提供 list_saves() 返回每个槽位的 {存在与否, 保存时间, 玩家等级}。
约束:保持原有函数签名 save_game()/load_game() 可用;改动处用注释标出 `# CHANGED`。
输出:完整改造后的代码 + 一份 3 行以内的变更说明。
③ 让 DeepSeek 帮我排查「存档丢失」类 bug(适合:存了读不到)
你是一名 Godot 4.x 调试专家。现象:游戏里点「保存」没报错,但重启后 load 不到进度。
已知环境:Windows 导出包,存档路径写的是 user://savegame.save。
请按下面格式输出诊断清单:
- 可能原因(按概率从高到低,每条一句话解释原理)
- 每个原因对应的「一行验证命令 / 日志打印」
- 修复方案(给出可直接替换的代码片段)
重点排查:写入路径是否为 res://、是否漏了 flush/close、JSON 里的 Vector2 未展开、
读档时是否在错误时机(节点未就绪)调用。
学习心得
存档系统是「玩家愿不愿意玩下去」的隐形地基:一次丢档就足以让玩家退款+差评。它也是典型的用 AI 一次就能生成、但必须自己验证的模块——DeepSeek 很擅长写这套分组 + JSON 的样板代码,但 user:// 路径、Vector2 展开、读档先清场这三个坑,它偶尔会漏。所以正确的用法是:让 DeepSeek 出骨架,自己按官方文档核对这三处,再让它按「诊断清单」格式帮排查。
对我们「用 DeepSeek 做一款 Steam 游戏」的目标来说,今天这套模式价值很高——只要有官方文档作锚,DeepSeek 就能把样板代码量最大的系统(存档、设置、本地化)一次性铺好,我们把省下的时间花在玩法验证上。
明日关注
- 把「设置」与「存档」拆开:用
ConfigFile存音量/分辨率/键位,验证导出后能正确读写。 - 给存档加一个
flush()/显式关闭,测试「保存后立刻杀进程」是否仍然读得到。 - 尝试让 DeepSeek 为存档写一份「假数据生成器」,用于自动化测试读档流程。
本文由自动化学习流水线每日生成,坚持即学即记。
评论