본문으로 건너뛰기
cccskills
무료GitHub에 공개

game-save-and-persistence

Use when adding or changing save games, settings or progression storage - a versioned save format with a tested migration chain, atomic writes with backup and checksum, per-engine storage locations, safe serializers, and the round-trip/migration/corruption tests.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md5.7 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Game Save and Persistence

Overview

A save file outlives the code that wrote it. Players carry saves across updates, crashes happen mid-write, and a lost 40-hour save is the bug players remember. Every save has a version, every older version has a tested migration, and a write never destroys the last good save.

Core principle: Versioned, migrated, written atomically, checked on load — and never deserialized with a format that can execute code.

The format

{
  "version": 3,
  "savedAtUtc": "2026-10-08T12:00:00Z",
  "player": { "levelId": "forest_02", "position": [12.5, 0, -4.0], "hp": 72 },
  "inventory": [{ "itemId": "sword_iron", "count": 1 }],
  "flags": ["met_blacksmith", "boss_forest_defeated"]
}
  • A plain DTO, separate from engine objects: no GameObject, Node or AActor references, no array indices into data tables — stable IDs only (data-driven-gameplay-design).
  • Settings (volume, bindings, accessibility) in a separate file from progress, so resetting one never touches the other.
  • JSON is readable and diff-able; a compact binary format (MessagePack, protobuf, Godot's store_var without objects) is fine when size matters — the version field stays first.

Migrations

type Migration = (save: any) => any;
const migrations: Record<number, Migration> = {
  1: (s) => ({ ...s, version: 2, inventory: s.items.map((id: string) => ({ itemId: id, count: 1 })) }),
  2: (s) => ({ ...s, version: 3, flags: Object.keys(s.questState ?? {}).filter((k) => s.questState[k] === "done") }),
};

export function upgrade(raw: any): SaveV3 {
  if (typeof raw?.version !== "number") throw new SaveError("unversioned");
  if (raw.version > CURRENT) throw new SaveError("from a newer game version");  // refuse, never guess
  let s = raw;
  while (s.version < CURRENT) s = migrations[s.version](s);
  return SaveV3Schema.parse(s);
}
  • One migration per version step, never edited after release; each tested with a fixture file captured from that version (tests/fixtures/saves/v1.json, v2.json).
  • A save from a newer version is refused with a clear message, not loaded and overwritten.
  • Changing a saved field's meaning without bumping the version is the bug this exists to prevent.

Writing safely

  1. Serialize to bytes in memory.
  2. Write to save.json.tmp, flush to disk.
  3. Keep the previous file as save.json.bak.
  4. Rename the temp file over save.json (atomic on the same volume).
  5. Store a checksum (CRC32 or SHA-256 of the payload) in a header or a sidecar.

On load: verify the checksum and parse; on failure fall back to .bak, tell the player, and never crash or silently start a new game over the old one.

Serialize on the main thread if the engine requires it, but do file I/O off it (or between frames at a checkpoint) — a save must not hitch gameplay. Autosave at checkpoints, not every frame.

Where saves live

EngineLocation / API
UnityApplication.persistentDataPath + System.IO; PlayerPrefs only for tiny settings
Godotuser:// with FileAccess; ConfigFile for settings
UnrealUSaveGame subclass + UGameplayStatics::AsyncSaveGameToSlot / LoadGameFromSlot; keep a version UPROPERTY in it
WebIndexedDB for saves (async, larger; e.g. idb-keyval), localStorage only for small settings — it is synchronous, ~5 MB and can throw in private mode or when full
Bevy / Rustserde to the platform data dir (directories crate)
pygame / MonoGamethe OS user data directory, never the install folder

Consoles and some stores impose their own save APIs and size limits — follow the platform layer the repository already has.

Serializers that execute code — never for save data

  • .NET BinaryFormatter: obsolete and insecure; do not use it in Unity, MonoGame or Godot C#.
  • Godot: bytes_to_var_with_objects / str_to_var on untrusted data, and loading a user-supplied .tres/.res, can instantiate scripts — use JSON, ConfigFile, or FileAccess.get_var(false) (objects disallowed).
  • Python pickle for pygame saves: a shared or downloaded save becomes code execution — use JSON.
  • Treat every save file as untrusted input: players edit them, and mods share them.

Tests

  • Round trip: state → save → load → equal state.
  • Migration: each fixture vN.json → upgrade → matches the expected current-version object.
  • Corruption: truncated file, bad checksum, garbage bytes → falls back to .bak and reports; no exception escapes.
  • Newer version: refused, original file untouched.
  • Atomicity: the write path is tested with a fake file system that fails between steps — the previous save is still loadable.

Common Mistakes

  • Serializing engine objects or private implementation classes directly, so every refactor breaks old saves.
  • No version field; "we'll add one when we need it".
  • Writing in place over the only copy.
  • Saving inventory by data-table index; reordering the table corrupts every save.
  • Blocking the main thread with file I/O mid-gameplay.

Red Flags

  • A change to a saved type with no version bump and no migration test.
  • BinaryFormatter, pickle, or object-enabled Godot variant decoding in a save path.
  • A load path with no fallback when parsing fails.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

makifbaysal의 스킬 모두 보기

이 스킬의 문제 신고