今日速览
- 存哪:只读资源用
res://,玩家存档/设置必须写user://——它保证可写,且在导出版里也不会被只读文件系统挡住。 - 存什么:官方推荐思路是「给要持久化的节点打上
Persist组标签」,遍历组内节点、调用各自的save()拿到一个字典,再序列化写盘。 - 序列化选型:字典 ↔ 字符串用
JSON最省事,一行一个对象,读取时逐行parse即可;缺点是Vector2/Color这类类型 JSON 不认,得拆成x/y存。 - 两个必须提前想清楚的问题:① 存档「版本号」与迁移(老存档要能读进新版本);② 多存档槽位与「原子写入」(避免存档写一半断电变砖)。
- 一个人的项目:先跑通「能存能读」,再逐步加版本号、槽位、校验,别一上来就设计过度复杂的存档框架。
核心学习内容
1. res:// 与 user://:先把「存哪」搞对
Godot 里路径用 UNIX 风格正斜杠 /(Windows 上也写 C:/Projects/Game,反而更省心)。两类前缀分工明确:
res://:指向项目根目录(含project.godot的那个文件夹),你的图片、场景、脚本都在这。导出后通常只读,绝不能往里写存档。user://:指向用户设备上的可写目录,自动创建且保证可写,导出后依然有效——这就是存档、设置该待的地方。放心把savegame.save、settings.cfg丢这里。
一句话原则:只读素材走 res://,会变的数据走 user://。搞混这两者,是新手导出后「存档写不进去」的头号原因。
2. 通用存档:Persist 组 + save() 字典 + JSON
官方给的可扩展方案非常清爽:不写死「只存玩家」,而是谁想被存谁就加入 Persist 组。
每个持久化节点自己实现 save(),返回一个字典:
func save() -> Dictionary:
return {
"filename": get_scene_file_path(), # 加载时用来 instantiate
"parent": get_parent().get_path(), # 加载时挂回哪个父节点
"pos_x": position.x, # Vector2 不被 JSON 支持,拆开存
"pos_y": position.y,
"current_health": current_health,
"level": level,
}
存档时遍历组、逐个调用 save()、JSON.stringify 后逐行写入:
func save_game() -> void:
var f := FileAccess.open("user://savegame.save", FileAccess.WRITE)
for node in get_tree().get_nodes_in_group("Persist"):
if node.scene_file_path.is_empty():
continue # 非实例化场景,跳过
if not node.has_method("save"):
continue # 没实现 save(),跳过
f.store_line(JSON.stringify(node.call("save")))
加载时逐行 parse,用 filename/parent 重建对象,再回填其余字段:
while save_file.get_position() < save_file.get_length():
var json := JSON.new()
if json.parse(save_file.get_line()) != OK:
print("存档解析失败: ", json.get_error_message())
continue
var d: Dictionary = 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])
这套模式的价值:以后新增任何敌人、箱子、NPC,只要给它加个 Persist 组 + save(),存档系统一行都不用改。
3. 两个真实的坑:版本迁移与原子写入
跑通之后,真正会让你在 Steam 上挨差评的是这两件事:
- 存档版本迁移:玩家用 v1.0 存了档,你更新到 v1.1 改了数据结构,一加载就崩。做法是在存档里存一个
"save_version": 2字段,加载时判断版本号,低于当前就逐级「升级」补齐缺失字段,而不是直接读崩。 - 原子写入:直接覆写存档文件,万一写到一半崩溃/断电,存档就废了。稳妥做法是先写临时文件,再重命名覆盖;同时保留一份
.bak备份,读取失败时自动回退。
再加一个 ConfigFile 存画质、音量、键位等设置(它天生适合 key-value 配置,不用自己写 JSON)。
🎯 DeepSeek 提示词
提示词 1:生成一个可扩展的存档管理器(推荐先跑这条)
你是 Godot 4.7 的资深开发。请帮我写一个 autoload 单例 SaveManager(GDScript),要求:
1. 支持多存档槽位(如 slot_1 ~ slot_3),路径统一放在 user://saves/ 下;
2. 采用「Persist 组 + 节点 save()/load(data) 方法」的通用模式:
- 保存时遍历 get_tree().get_nodes_in_group("Persist"),逐个调用 save() 拿字典;
- 每个字典第一行写 {"save_version": 1},之后逐行 JSON.stringify 写入;
3. 加载时逐行 JSON.parse,用 filename/parent 重建节点并回填字段;
4. 写入必须「原子」:先写 user://saves/slot_x.tmp,成功后重命名覆盖 .save,并保留一份 .bak;
5. 提供 has_save(slot)、delete_save(slot)、get_save_info(slot)(返回等级/时间等摘要)三个接口。
约束:Vector2/Color 等 JSON 不支持的类型请在 save() 里拆成基础类型再存;代码加中文注释。
输出:完整 GDScript 代码 + 一段「如何接入现有项目」的 3 步说明。
提示词 2:把现有实体接入存档系统
我有一个 Godot 4.7 项目,场景里有一批玩家/敌人/NPC 节点。请帮我把它们接入已有的 SaveManager(假设已存在,模式为 Persist 组 + save()/load())。
给你一段节点脚本:
---
<粘贴你的节点脚本,例如 player.gd,含 position、health、level、inventory 等变量>
---
要求:
1. 给该节点加上成员函数 save() -> Dictionary,返回需要持久化的字段;Vector2 拆成 x/y,数组/字典原样返回;
2. 加上 load_data(d: Dictionary) -> void,负责把字段安全地赋回(含类型转换、缺失字段给默认值);
3. 在 _ready() 里 add_to_group("Persist");
4. 指出哪些字段【不该】存(如缓存、临时状态),并说明原因。
输出:改造后的完整脚本 + 变更点清单。
提示词 3:让 DeepSeek 帮你做存档版本迁移与容错
你是负责长期维护的 Godot 4.7 工程师。我的存档结构从 v1 升到了 v2:v1 只有 {save_version, pos_x, pos_y, level},v2 新增了 inventory(数组) 和 difficulty(字符串),并把 level 重命名为 player_level。
请写一段迁移逻辑(GDScript),要求:
1. 读取存档后先判断 save_version;若为 1,则执行 upgrade_v1_to_v2():补齐默认字段、迁移重命名字段;
2. 遇到无法识别的字段或类型错误时,不要抛异常崩溃,而是记录日志并回退到默认值;
3. 若 JSON 解析失败或版本号缺失,返回一个「空存档 + 警告」而不是直接崩游戏;
4. 迁移完成后升级 save_version 并回写。
输出:带中文注释的完整迁移代码 + 一份「未来加 v3 时该改哪几行」的说明。
学习心得
存档系统是最典型的「平时没人夸、崩一次就全是差评」的模块。今天最大的收获是:官方那套 Persist 组 + JSON 的模式看着朴素,但它把一个会随游戏复杂度爆炸的问题,规约成了「每个节点自己对自己负责」——正好契合我们「用 DeepSeek 做一款 Steam 游戏」的打法:把职责切碎,交给 AI 逐块生成,人只负责定接口和验收。先让 DeepSeek 按提示词 1 产出 SaveManager,再用提示词 2 把现有实体一个个接进去,最后用提示词 3 把「版本迁移 + 容错」补齐——一套能上架、经得起更新迭代的存档就成型了。记住:能存能读是及格线,版本迁移和原子写入才是 Steam 玩家的信任线。
明日关注
- 用提示词 1 跑出一个 SaveManager,在本机 Godot 里存/读一局,确认
user://下真的落盘。 - 给玩家节点接入
save()/load_data(),测试「存档 → 退出 → 重开 → 继续」闭环。 - 故意把存档 JSON 改坏一行,验证容错逻辑不会让游戏崩溃,而是回退默认值。
本文由自动化学习流水线每日生成,坚持即学即记。
评论