返回技能市场
开发运维 安全

save-systems

@admin/save-systems

设计游戏存读档,选择序列化内容和格式,处理存档槽、原子写入、版本迁移、损坏风险及自动保存。

admin 热度 451v0.0.1

存档系统

存档文件是一个游戏状态的序列化快照,可在重启后继续存在。 难点不在于写入字节——而在于选择*要保存什么*、确保保存中途崩溃也不会损坏文件, 以及发布补丁后仍能读取*旧*存档。把这三件事做好,其余都是管道工作。

何时使用

  • 用于持久化进度:玩家属性、物品栏、世界标记、设置、
  • 位置——跨会话和游戏更新。

  • 用于设计存档槽位、快速存档/自动存档,以及崩溃安全写入。
  • 用于内容/代码变更后旧存档损坏的情况(版本控制与
  • 迁移)。

不适用的情况:对于 Roblox 云持久化具体细节,使用 roblox-datastores。对于存档所序列化的数据模型(resources/SOs),使用 godot-resources / unity-scriptableobjects。对于 Godot 的 FileAccess/ ResourceSaveruser:// 路径,在应用这里模式的同时,请遵循 Godot 引擎技能。

核心工作流

  1. 决定哪些状态是权威的。保存*数据*(hp、position、seed、
  2. 解锁标记),而不是引擎对象或场景节点。加载时你将 从数据重建对象——绝不序列化活动节点引用。

  3. 定义带版本号的 schema。每个存档都嵌入一个 version 整数。对于打算持续补丁的游戏,这是最重要的字段。
  4. 选择格式。JSON/文本便于阅读和调试;二进制格式
  5. 用于大小/速度或轻度防篡改。从 JSON 开始。

  6. 原子写入。序列化到临时文件,刷新,然后重命名覆盖
  7. 真实文件。崩溃只会留下旧存档或新存档——绝不会留下 半写入的文件。

  8. 防御式加载。读取 version → 迁移到当前版本 → 校验 →
  9. 实例化。保留最后一次有效存档的备份,并在解析错误时回退。

  10. 在安全边界自动存档(关卡切换、检查点),并进行限频,且写入
  11. 单独槽位,以免覆盖手动存档。

  12. 验证:保存、完全退出、重新启动、加载——并通过检查确认
  13. 状态匹配。测试加载上一版本的存档。

模式

1. 将状态序列化为纯数据(而非引擎对象)

# Build 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
    }

# 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"]

2. 原子且崩溃安全的写入(临时文件 + 重命名)

# RIGHT: 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
# WRONG: opening `path` directly and writing in place — a crash mid-write leaves a
# truncated, unloadable save and destroys the player's progress.

在 POSIX(同一卷)上,重命名覆盖目标文件是原子的;在 Windows 上,通过重命名替换 不保证原子性,因此在重命名之前将旧文件保留为 path + ".bak"——该备份才是真正保证你能从错误写入中恢复的因素。

3. 带迁移的版本化加载

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 data

# Each 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}

4. 存档槽位 + 限频自动存档

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 minute
# Trigger an immediate autosave on checkpoints/level transitions, not mid-combat.

常见陷阱

  • 序列化引擎对象/节点路径会使存档绑定到场景结构;
  • 重命名节点会破坏所有旧存档。保存数据,加载时重建对象。

  • 没有 version 字段。当你发布补丁的那天,每个现有存档都成了
  • 猜测游戏。从 version 1 开始标记 version

  • 原地写入会在崩溃/断电时损坏存档。始终先写入临时文件再
  • 重命名;保留 .bak

  • 盲目信任文件。存档可能被截断、手动编辑,或
  • 云同步为过期数据。加载时校验,失败时回退到备份。

  • 浮点数与区域设置。文本序列化器可能丢失精度,或在某些区域设置中使用逗号作为
  • 小数分隔符。使用区域设置无关的序列化器。

  • 自动存档覆盖手动存档,或在动作中途触发并保存不一致
  • 状态。使用专用自动存档槽位,并在安全边界保存。

  • 在多人游戏中存储秘密或信任客户端存档。本地存档由
  • 玩家控制;绝不能将其视为在线状态的权威。对于云端, 处理设备数据限制和冲突(roblox-datastores)。

参考

  • references/versioning-and-migration.md — schema 演进策略、
  • 迁移链、备份/回滚、格式权衡(JSON 与二进制),以及 加载时校验清单。

相关技能

  • roblox-datastores — 云持久化、请求限制、会话锁定。
  • godot-resourcesunity-scriptableobjects — 你要序列化的数据模型。
  • procedural-gen — 存储 seed 以重新生成世界,而不是保存世界。
  • rpgsurvival-craftingvisual-novel — 组合使用本技能的类型。
qianwen skills install @admin/save-systems