mcpField
S
save-systems

save-systems

github.com/gamedev-skills开发5.4k8.1k 次安装1 天前更新
代码仓库

关于 save-systems

帮助游戏开发者设计跨会话和版本更新的存档方案。内容涵盖状态序列化、崩溃安全写入、存档槽位及旧存档迁移。

主要功能

  1. 01序列化纯游戏状态数据
  2. 02版本化结构与迁移
  3. 03原子写入并保留备份
  4. 04存档槽位与独立自动存档
  5. 05防御式加载与验证

适用场景

  1. 01跨会话保存玩家进度
  2. 02实现防崩溃存档和快速存档
  3. 03更新后继续读取旧版本存档
  4. 04从损坏或无效存档中恢复

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill save-systems

安装后在 Claude Code 中输入/save-systems确认已加载。

附带文件references/versioning-and-migration.md

详细介绍

存档系统

存档文件是序列化的游戏状态快照,即使重启后也能保留。 难点不在于写入字节,而在于选择要保存什么、确保写入过程中发生崩溃也不会损坏存档,以及发布补丁后仍能读取旧存档。把这三点做好,剩下的就只是常规工作。

适用场景

  • 用于跨会话和游戏更新持久化进度:玩家属性、库存、世界标记、设置、位置等。
  • 用于设计存档槽、快速存档/自动存档和防崩溃写入。
  • 用于处理内容/代码变更后旧存档失效的问题(版本管理与迁移)。

**不适用场景:**如需了解 Roblox 云持久化的具体内容,请使用 roblox-datastores。如需了解存档序列化的数据模型(资源/SO),请使用 godot-resources / unity-scriptableobjects。对于 Godot 的 FileAccess/ ResourceSaver 和 user:// 路径,请参考 Godot 引擎技能,同时应用此处的模式。

核心工作流

  1. **确定哪些状态是权威数据。**保存数据(生命值、位置、种子、解锁标记),而不是引擎对象或场景节点。加载时根据数据重建对象——绝不要序列化实时节点引用。
  2. **定义带版本号的架构。**每个存档都要包含一个 version 整数。对于计划持续更新的游戏,这是最重要的字段。
  3. **选择格式。**为便于阅读和调试,使用 JSON/文本;为减小体积、提升速度或略微防篡改,使用二进制格式。建议从 JSON 开始。
  4. **原子写入。**先序列化到临时文件,刷新数据,然后重命名并覆盖正式文件。发生崩溃时,留下的要么是旧存档,要么是新存档——绝不会是写了一半的存档。
  5. **防御性加载。**读取版本 → 迁移到当前版本 → 验证 → 实例化。保留最近一次正常存档的备份,并在解析出错时回退到备份。
  6. 在安全节点自动存档(关卡切换、检查点),对存档频率进行限制,并使用单独的存档槽,以免覆盖手动存档。
  7. **验证:**保存、完全退出、重新启动、加载——并通过检查确认状态一致。测试加载上一版本的存档。

模式

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.

常见问题

  • 序列化引擎对象/节点路径会使存档依赖场景结构;重命名节点会导致所有旧存档失效。保存数据,并在加载时重建对象。
  • **没有版本字段。**补丁发布当天,每个现有存档都会变成一道猜谜题。从版本 1 开始就写入 version。
  • 原地写入会在崩溃/断电时损坏存档。务必先写入临时文件再重命名;保留一个 .bak。
  • **盲目信任文件。**存档可能被截断、手动编辑,或被云同步覆盖为旧版本。加载时进行验证,失败时回退到备份。
  • **浮点数和区域设置。**文本序列化器可能丢失精度,或在某些区域设置下使用逗号作为小数分隔符。请使用与区域设置无关的序列化器。
  • 自动存档覆盖手动存档,或在操作过程中触发,导致保存了不一致的状态。使用专用自动存档槽,并在安全节点存档。
  • **存储机密信息或信任多人游戏客户端存档。**本地存档由玩家控制;切勿将其视为在线状态的权威数据。对于云存档,请处理设备的数据限制和冲突(roblox-datastores)。

参考资料

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

相关技能

  • roblox-datastores — 云端持久化、请求限制和会话锁定。
  • godot-resources、unity-scriptableobjects — 需要序列化的数据模型。
  • procedural-gen — 保存种子以重新生成世界,而不是保存世界本身。
  • rpg、survival-crafting、visual-novel — 与此技能结合使用的游戏类型。

评论

支持 Markdown