save-systems
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSave systems
存档系统
A save file is a serialized snapshot of game state that survives restarts.
The hard parts aren't writing bytes — they're choosing what to save, writing it
so a crash mid-save can't corrupt it, and reading old saves after you ship a
patch. Get those three right and the rest is plumbing.
存档文件是游戏状态的序列化快照,可在游戏重启后保留进度。
难点不在于写入字节,而在于选择要保存的内容、确保中途崩溃不会损坏存档,以及在发布补丁后仍能读取旧版存档。解决好这三个问题,剩下的就只是基础实现了。
When to use
适用场景
- Use to persist progress: player stats, inventory, world flags, settings, positions — across sessions and game updates.
- Use to design save slots, quicksave/autosave, and crash-safe writes.
- Use when old save files break after a content/code change (versioning & migration).
When not to use: for Roblox cloud persistence specifics, use
. For the data model the save serializes (resources/SOs), use
/ . For Godot's /
and paths, defer to the Godot engine skill while
applying the patterns here.
roblox-datastoresgodot-resourcesunity-scriptableobjectsFileAccessResourceSaveruser://- 用于持久化进度:玩家属性、物品栏、世界标记、设置、位置——跨游戏会话和版本更新。
- 用于设计存档槽、快速存档/自动存档,以及崩溃安全写入机制。
- 当内容/代码变更导致旧存档失效时(版本控制与迁移)。
不适用场景:针对Roblox云端持久化的具体实现,请使用。针对存档序列化的数据模型(资源/SO),请使用 / 。对于Godot的/和路径,在应用此处的设计模式时,请参考Godot引擎的相关技能。
roblox-datastoresgodot-resourcesunity-scriptableobjectsFileAccessResourceSaveruser://Core workflow
核心工作流
- Decide what state is authoritative. Save the data (hp, position, seed, unlocked flags), not engine objects or scene nodes. You will reconstruct objects from data on load — never serialize live node references.
- Define a versioned schema. Every save embeds a integer. This is the single most important field for a game you intend to patch.
version - Pick a format. JSON/text for readability and debuggability; a binary format for size/speed or mild tamper-resistance. Start with JSON.
- Write atomically. Serialize to a temp file, flush, then rename over the real file. A crash leaves either the old save or the new one — never a half-written one.
- Load defensively. Read version → migrate up to current → validate → instantiate. Keep a backup of the last good save and fall back on parse error.
- Autosave on safe boundaries (level change, checkpoint), throttled, and to a separate slot so it can't clobber a manual save.
- Verify: save, fully quit, relaunch, load — and confirm by inspection that state matches. Test loading a save from the previous version.
- 确定权威状态:保存数据(生命值、位置、随机种子、解锁标记),而非引擎对象或场景节点。加载时需从数据重构对象——绝不要序列化实时节点引用。
- 定义带版本的schema:每个存档都嵌入一个整数。对于计划发布补丁的游戏而言,这是最重要的字段。
version - 选择文件格式:JSON/文本格式便于阅读和调试;二进制格式更节省空间、速度更快,或具备轻度防篡改能力。建议从JSON开始。
- 原子式写入:先序列化到临时文件,刷新到磁盘,再重命名为正式存档文件。崩溃时只会保留旧存档或新存档——绝不会出现半写入的损坏存档。
- 防御式加载:读取版本 → 迁移至当前版本 → 验证 → 实例化。保留最近一次有效存档的备份,解析出错时回退到备份。
- 在安全边界触发自动存档(关卡切换、检查点),并限制触发频率,且写入单独的存档槽,避免覆盖手动存档。
- 验证:保存存档、完全退出游戏、重新启动、加载存档——通过检查确认状态一致。测试加载旧版本的存档。
Patterns
设计模式
1. Serialize state as plain data (not engine objects)
1. 将状态序列化为纯数据(而非引擎对象)
gdscript
undefinedgdscript
undefinedBuild a dictionary of pure data. Each savable object reports its own state.
构建纯数据字典。每个可存档对象自行上报其状态。
func capture_state() -> Dictionary:
return {
"version": SAVE_VERSION, # ALWAYS stamp the schema version
"player": { "hp": player.hp, "pos": [player.position.x, player.position.y] },
"inventory": player.inventory.to_array(), # ids + counts, not Item nodes
"flags": world.flags, # e.g. {"met_guard": true}
"seed": world.seed, # regenerate procedural content
}
func capture_state() -> Dictionary:
return {
"version": SAVE_VERSION, # 务必标记schema版本
"player": { "hp": player.hp, "pos": [player.position.x, player.position.y] },
"inventory": player.inventory.to_array(), # 物品ID + 数量,而非Item节点
"flags": world.flags, # 例如 {"met_guard": true}
"seed": world.seed, # 用于生成程序化内容
}
On load, RECONSTRUCT objects from the data — do not expect live references back.
加载时,从数据重构对象——不要期望还原实时引用。
func apply_state(data: Dictionary) -> void:
player.hp = data["player"]["hp"]
player.position = Vector2(data["player"]["pos"][0], data["player"]["pos"][1])
player.inventory.from_array(data["inventory"])
world.flags = data["flags"]
undefinedfunc apply_state(data: Dictionary) -> void:
player.hp = data["player"]["hp"]
player.position = Vector2(data["player"]["pos"][0], data["player"]["pos"][1])
player.inventory.from_array(data["inventory"])
world.flags = data["flags"]
undefined2. Atomic, crash-safe write (temp + rename)
2. 原子式、崩溃安全写入(临时文件 + 重命名)
gdscript
undefinedgdscript
undefinedRIGHT: write to a temp file, then atomically rename over the target.
正确做法:写入临时文件,再原子式重命名为目标文件。
func save_atomic(path: String, data: Dictionary) -> void:
var tmp := path + ".tmp"
var f := FileAccess.open(tmp, FileAccess.WRITE)
f.store_string(JSON.stringify(data))
f.flush() # ensure bytes hit disk
f.close()
DirAccess.rename_absolute(tmp, path) # replaces the target; atomic on POSIX
func save_atomic(path: String, data: Dictionary) -> void:
var tmp := path + ".tmp"
var f := FileAccess.open(tmp, FileAccess.WRITE)
f.store_string(JSON.stringify(data))
f.flush() # 确保字节写入磁盘
f.close()
DirAccess.rename_absolute(tmp, path) # 替换目标文件;在POSIX系统上是原子操作
WRONG: opening path
directly and writing in place — a crash mid-write leaves a
path错误做法:直接打开path
并原地写入——中途崩溃会留下截断的、无法加载的存档,导致玩家进度丢失。
pathtruncated, unloadable save and destroys the player's progress.
—
Rename-over-target is atomic on POSIX (same volume); on Windows a replace-by-rename
isn't guaranteed atomic, so keep the previous file as `path + ".bak"` before the
rename — that backup is what actually guarantees you can recover from a bad write.
在POSIX系统(同一卷)上,覆盖式重命名是原子操作;在Windows系统上,通过重命名替换文件不保证原子性,因此在重命名前需将原文件备份为`path + ".bak"`——这个备份才是确保从错误写入中恢复的关键。3. Versioned load with migration
3. 带版本迁移的加载机制
python
SAVE_VERSION = 3
def load_save(raw_bytes):
data = parse(raw_bytes) # JSON/binary -> dict
v = data.get("version", 0)
if v > SAVE_VERSION:
raise NewerSaveError(v) # save is from a newer build; refuse
while v < SAVE_VERSION: # apply migrations in order, v -> v+1
data = MIGRATIONS[v](data)
v += 1
data["version"] = v
validate(data) # check required keys / ranges
return datapython
SAVE_VERSION = 3
def load_save(raw_bytes):
data = parse(raw_bytes) # JSON/二进制 -> 字典
v = data.get("version", 0)
if v > SAVE_VERSION:
raise NewerSaveError(v) # 存档来自更新版本的构建;拒绝加载
while v < SAVE_VERSION: # 按顺序应用迁移,从v到v+1
data = MIGRATIONS[v](data)
v += 1
data["version"] = v
validate(data) # 检查必填字段/取值范围
return dataEach migration is a pure function from one version's shape to the next.
每个迁移都是纯函数,将旧版本的数据结构转换为新版本。
def migrate_1_to_2(d):
d["flags"] = {k: True for k in d.pop("completed_quests", [])} # list -> set-map
return d
MIGRATIONS = {1: migrate_1_to_2, 2: migrate_2_to_3}
undefineddef migrate_1_to_2(d):
d["flags"] = {k: True for k in d.pop("completed_quests", [])} # 列表 -> 集合映射
return d
MIGRATIONS = {1: migrate_1_to_2, 2: migrate_2_to_3}
undefined4. Save slots + throttled autosave
4. 存档槽 + 限频自动存档
gdscript
const SLOT_PATH := "user://save_%d.json" # manual slots 0..N
const AUTOSAVE_PATH := "user://autosave.json" # separate file: never clobbers a slot
var _autosave_cooldown := 0.0
func autosave_if_due(dt: float) -> void:
_autosave_cooldown -= dt
if _autosave_cooldown <= 0.0:
save_atomic(AUTOSAVE_PATH, capture_state())
_autosave_cooldown = 60.0 # throttle: at most once a minutegdscript
const SLOT_PATH := "user://save_%d.json" # 手动存档槽0..N
const AUTOSAVE_PATH := "user://autosave.json" # 独立文件:绝不会覆盖手动存档槽
var _autosave_cooldown := 0.0
func autosave_if_due(dt: float) -> void:
_autosave_cooldown -= dt
if _autosave_cooldown <= 0.0:
save_atomic(AUTOSAVE_PATH, capture_state())
_autosave_cooldown = 60.0 # 限频:每分钟最多触发一次Trigger an immediate autosave on checkpoints/level transitions, not mid-combat.
在检查点/关卡切换时触发即时自动存档,不要在战斗中触发。
undefinedundefinedPitfalls
常见陷阱
- Serializing engine objects/node paths ties saves to scene structure; renaming a node breaks every old save. Save data, rebuild objects on load.
- No version field. The day you ship a patch, every existing save is a
guessing game. Stamp from version 1.
version - In-place writes corrupt saves on crash/power loss. Always temp-write then
rename; keep a .
.bak - Trusting the file blindly. Saves get truncated, hand-edited, or cloud-synced stale. Validate on load and fall back to backup on failure.
- Floats and locale. Text serializers can drop precision or use comma decimal separators in some locales. Use a locale-invariant serializer.
- Autosave clobbering manual saves, or firing mid-action and saving an inconsistent state. Use a dedicated autosave slot and save on safe boundaries.
- Storing secrets or trusting client saves in multiplayer. A local save is
player-controlled; never treat it as authoritative for online state. For cloud,
handle the device's data limits and conflicts ().
roblox-datastores
- 序列化引擎对象/节点路径会将存档与场景结构绑定;重命名节点会导致所有旧存档失效。应保存数据,加载时重构对象。
- 缺少版本字段。发布补丁的当天,所有现有存档都会变成无法解析的谜题。从版本1开始就标记字段。
version - 原地写入会在崩溃/断电时损坏存档。务必先写入临时文件再重命名;保留备份。
.bak - 盲目信任存档文件。存档可能被截断、手动编辑,或因云同步而过时。加载时需验证,失败时回退到备份。
- 浮点数与区域设置。文本序列化器可能丢失精度,或在部分区域设置中使用逗号作为小数点分隔符。请使用不受区域设置影响的序列化器。
- 自动存档覆盖手动存档,或在动作执行中途触发并保存不一致状态。使用专门的自动存档槽,并在安全边界触发存档。
- 在多人游戏中存储机密信息或信任客户端存档。本地存档由玩家控制;绝不能将其作为在线状态的权威来源。对于云端存档,请处理设备的数据限制和冲突(参考)。
roblox-datastores
References
参考资料
- — schema evolution strategies, the migration chain, backups/rollback, format trade-offs (JSON vs binary), and a load-time validation checklist.
references/versioning-and-migration.md
- —— schema演进策略、迁移链、备份/回滚、格式权衡(JSON vs二进制),以及加载时的验证清单。
references/versioning-and-migration.md
Related skills
相关技能
- — cloud persistence, request limits, session locking.
roblox-datastores - ,
godot-resources— the data model you serialize.unity-scriptableobjects - — store the seed to regenerate worlds instead of saving them.
procedural-gen - ,
rpg,survival-crafting— genres that compose this skill.visual-novel
- —— 云端持久化、请求限制、会话锁定。
roblox-datastores - ,
godot-resources—— 用于序列化的数据模型。unity-scriptableobjects - —— 存储随机种子以重新生成世界,而非直接保存世界数据。
procedural-gen - ,
rpg,survival-crafting—— 会用到本技能的游戏类型。visual-novel