今日速览
- 存档路径只能用
user://,res://在导出后是只读的;写错位置 = 玩家重启就回档。 - 存档文件里必须埋一个
schema_version,否则你第一次改数据结构就会把老玩家的档读崩。 - 写档用「写临时文件 → 校验 → 原子改名」,再加一份
.bak轮转,才能扛住断电和强杀进程。 - 设置(音量/分辨率)和进度(关卡/金币)分两个文件,用
ConfigFile存设置,别混在一起。 - 想用 Steam Cloud 同步,就把
application/config/use_custom_user_dir打开,让存档落到系统标准位置,路径才好配。
核心学习内容
1. 放哪:user:// 是唯一正确选项
Godot 里 res:// 指向项目目录,导出成 exe 后一般不可写(安装目录可能还没有写权限)。所有运行时产生的数据——存档、设置、日志、截图——都应该走 user://。
默认情况下 user:// 藏在 Godot 编辑器数据目录下的 app_userdata/[项目名]/,这对原型很方便,但上架 Steam 就不合适:Steam Cloud 要按路径匹配文件,标准位置才好配。做法是打开项目设置:
application/config/use_custom_user_dir = true
application/config/custom_user_dir_name = "MyGame"
这样 Windows 下会落到 %APPDATA%/MyGame/,Linux 下落到 ~/.local/share/MyGame/,Steamworks 后台的 Cloud 路径直接填相对路径(如 save_01.json)就能对上。
调试时想快速看到存档在哪,加一句:
print(ProjectSettings.globalize_path("user://"))
2. 存什么:设置与进度分开
| 文件 | 内容 | 格式 | 需要迁移 |
|---|---|---|---|
settings.cfg | 音量、画质、键位、语言 | ConfigFile | 很少 |
save_01.json | 关卡、坐标、背包、任务标记 | JSON | 经常 |
save_01.bak | 上一份可用存档 | JSON | 同主档 |
meta.json | 存档列表预览(时间、进度、缩略图路径) | JSON | 少 |
关键取舍:JSON 不认 Vector2,官方文档明确要求拆成 pos_x / pos_y。更省事的做法是所有 Vector2/Vector3/Color 在 save() 里显式转成数组或字典,load() 时再组装回来。别指望 var_to_str 一把梭——它能把整个对象图序列化出来,但一旦你重命名类或属性,老档就全废了。
3. 怎么写:原子写入 + 备份轮转
最容易被忽略的一步。直接 FileAccess.open(path, WRITE) 会在打开瞬间把旧档清空——如果这时游戏崩溃或玩家 Alt+F4,档案就是 0 字节。正确顺序:
- 写到
save_01.tmp flush()+ 关闭,可选做一次 JSON 回读校验- 若已有
save_01.json,改名成save_01.bak tmp改名为save_01.json(DirAccess.rename_absolute())
这样任何时刻磁盘上至少有一份完整可读的档。移动端和 Steam Deck 上被系统强杀的概率比你想的高,这一步不能省。
4. 怎么升级:schema_version + 迁移链
存档格式一定会变。约定一个整数版本号,读档时按版本逐级升级,而不是写一堆 if has("new_field"):
v1 → v2:加 skills 字段
v2 → v3:position 从 {x,y} 改成 [x,y]
v3 → v4:level 重命名为 chapter_index
每一级只写一个 _migrate_v1_to_v2(data) 函数,读档时循环调用,直到版本号等于当前 CURRENT_VERSION。这样第 20 次改结构时,你只需要再补一个函数,老档依然能读——这是「玩家三年后回来打开游戏,档还在」的唯一可靠办法。
至于防作弊,别在这上面花太多力气:单机游戏的存档加密只是抬高门槛,FileAccess.open_encrypted_with_pass() 可以用,但要接受密钥在客户端里一定能被挖出来。真正需要防的是联机/排行榜,那是服务端的事。
🎯 DeepSeek 提示词
提示词 1:让 DeepSeek 写一个完整的存档管理器
适用场景:项目还没有存档系统,想一次性拿到能直接用的骨架。
你是 Godot 4 资深开发者,代码风格遵循官方文档(GDScript 静态类型优先)。
任务:为我的 2D 单机游戏写一个自动加载单例 SaveManager(autoload 名 SaveManager)。
必须实现:
1. SAVE_PATH = "user://save_%02d.json"(槽位 1-3),每个槽位配套 .bak 备份。
2. save_game(slot:int, data:Dictionary) -> bool:
- 写入前把 data["schema_version"] 设为常量 CURRENT_VERSION
- 先写 .tmp,flush 后回读校验 JSON 可解析
- 已有主档先改名为 .bak,再把 .tmp 改名为主档(用 DirAccess,不要用 OS.execute)
- 全程 try 风格检查返回值,失败时 push_error 并返回 false
3. load_game(slot:int) -> Dictionary:文件不存在返回 {};解析失败自动回退读 .bak;读到旧版本自动执行迁移链。
4. has_save(slot:int) -> bool、delete_save(slot:int) -> void、list_saves() -> Array(返回每档的修改时间与体积)。
5. 迁移链:_migrate(data) 内部按 schema_version 逐级调用 _migrate_v1_to_v2 等空实现,留好 TODO 注释。
约束:
- 只用 Godot 4.x 内置类,不引入第三方库
- 每个函数写中文 docstring,说明参数与失败情形
- 不要省略错误处理,不要用 assert 代替运行时检查
输出格式:单个 .gd 完整文件 + 末尾一段 5 行以内的接入示例(如何在暂停菜单里调用)。
提示词 2:让 DeepSeek 审查并加固已有存档代码
适用场景:已经能存能读,但怀疑有坑(丢档、卡顿、老档读不出)。
你是代码审查者,专长是 Godot 4 的持久化与容错。
这是我的存档代码(下面粘贴):
【在此粘贴你的 save/load 代码】
请按以下清单逐项检查并打分(通过/风险/严重):
1. 路径是否使用 user://,导出后是否可写
2. 是否存在「先清空再写入」导致中断即毁档的问题
3. 是否有备份与损坏回退机制
4. 是否序列化了 Vector2/Color 等 JSON 不支持的类型
5. 读档时是否假设字段一定存在(缺字段会不会崩)
6. 是否有 schema_version 与迁移路径
7. 大存档(>2MB)是否在主线程同步读写导致卡顿
8. 存档里是否写入了节点路径/资源路径等脆弱引用
输出格式:
- 问题表格:严重度 | 行号/函数 | 问题 | 修复建议(给出替换代码片段)
- 最后给出加固后的完整函数,保持我原有的函数签名不变
约束:不要重写整个架构,只做最小必要修改;不确定的地方标注「需你确认」而不是猜。
提示词 3:让 DeepSeek 设计存档数据结构
适用场景:动手写代码前,先把「要存什么」想清楚,避免边写边加字段。
你是游戏系统设计师。我的游戏是:(一句话描述,例:2D 横版动作 roguelite,单机,预计 8 小时通关)
请设计存档数据结构,要求:
1. 分成 3 个 JSON 对象:player(角色成长)、world(世界状态)、progress(流程进度),说明为什么这样切分(哪些需要频繁读写、哪些只在存档点写)。
2. 每个字段给出:字段名(snake_case)、类型、示例值、是否必填、默认值、以及「如果缺失时如何兼容」。
3. 明确指出哪些字段绝对不能存(节点引用、资源路径、临时状态、可推导数据)。
4. 给出 schema_version 的初始值,以及未来 3 个最可能发生的结构变更,各写一条迁移思路。
5. 给一个完整的示例 JSON(带真实感的假数据,不要 xxx 占位符)。
输出格式:先 200 字设计说明,再字段表,再示例 JSON,最后「未来变更清单」。
约束:字段总数控制在 40 个以内,宁少勿多;每个字段都要有存在理由。
学习心得
存档系统是那种「做对了没人夸、做错了全是差评」的东西。它也是最适合交给 DeepSeek 的一类工作:需求边界清楚、模式固定、检查项可枚举。上面三条提示词其实对应三个阶段——先让它产出骨架,再让它审查加固,最后让它帮你把数据结构想清楚。模型在这类「有标准答案的工程活」上表现很稳,比让它设计玩法靠谱得多。
对「一个人用 AI 做 Steam 游戏」来说,这意味着:玩法可以慢慢磨,但存档、设置、输入这些基础设施必须一次做对,因为你没有 QA 团队帮你测断电重开。今天花两小时把 user://、备份轮转和版本迁移三件事做扎实,能省掉未来一百条「我存档没了」的差评。
明日关注
- 把
use_custom_user_dir打开,确认导出后存档落在系统标准路径,并记录实际路径 - 给现有项目补一个
schema_version = 1和空迁移函数,先把坑位占住 - 用手动 Alt+F4 /
kill -9测试写入中断,验证.bak回退是否真的生效
本文由自动化学习流水线每日生成,坚持即学即记。
评论