Snapshot Quicksave
The snapshot path captures the entire scene stack — every @serializable
entity, component, and scene-level state — into one GameSnapshot. Use it
when you genuinely need to “pause and resume the simulator.” For settings,
save slots, and progression, prefer the Save & Load guide,
which uses typed stores and is far simpler.
SnapshotPlugin Setup
Section titled “SnapshotPlugin Setup”import { SnapshotPlugin } from "@yagejs/save";
engine.use(new SnapshotPlugin({ namespace: "my-game", // storage key prefix (default: "yage") storage: myStorage, // custom SnapshotStorage (default: localStorage)}));Provide storage when you need a backend other than localStorage — e.g.
a Node test harness, an in-memory cache for headless runs, or a custom
synchronous filesystem implementation. It must satisfy the synchronous
SnapshotStorage contract (load, save, delete, list). Async-only
backends like IndexedDB don’t fit this contract directly; they’d need a
sync wrapper (e.g. an in-memory cache that flushes asynchronously).
Bundler setup (Vite 8+)
Section titled “Bundler setup (Vite 8+)”@serializable looks up classes by class.name at restore time. Two flags
in vite.config.ts are required for user code that uses the decorator:
import { defineConfig } from "vite";
export default defineConfig({ oxc: { decorator: { legacy: true }, }, build: { rollupOptions: { output: { keepNames: true } }, },});oxc.decorator.legacy: true enables the stage-2 decorator transform that
TypeScript’s @serializable relies on. output.keepNames: true preserves
class names through oxc’s minifier so the registry key stored in a snapshot
still matches the runtime class.
These flags are only required for user code. @yagejs/* packages are
pre-compiled and unaffected.
The @serializable Decorator
Section titled “The @serializable Decorator”import { serializable } from "@yagejs/core";
@serializableclass Player extends Entity { }
@serializableclass GameScene extends Scene { }Built-in serializable components: Transform, RigidBodyComponent,
ColliderComponent, SpriteComponent, GraphicsComponent. Compose your
entity from these and most state serializes for free.
Custom serialization
Section titled “Custom serialization”Components with non-trivial state implement serialize() + a static
fromSnapshot():
@serializableclass MovingSpike extends Component { serialize() { return { startY: this.startY, speed: this.speed, elapsed: this.elapsed }; }
static fromSnapshot(data: { startY: number; speed: number; elapsed: number }) { const spike = new MovingSpike({ startY: data.startY, speed: data.speed }); spike.elapsed = data.elapsed; return spike; }}afterRestore hooks
Section titled “afterRestore hooks”Re-create non-serializable state (draw callbacks, event listeners, runtime references):
@serializableclass Coin extends Entity { afterRestore(): void { this.get(GraphicsComponent).draw(drawCoin); this.setupTrigger(this.get(ColliderComponent)); }}Pattern: extract shared setup into a method called by both onEnter() and
afterRestore().
SnapshotService API
Section titled “SnapshotService API”import { SnapshotServiceKey } from "@yagejs/save";
const snapshot = this.use(SnapshotServiceKey);
snapshot.saveSnapshot("slot1");await snapshot.loadSnapshot("slot1");snapshot.hasSnapshot("slot1");snapshot.deleteSnapshot("slot1");
// Export as plain object for cloud uploads / file exportconst data = snapshot.exportSnapshot("slot1"); // GameSnapshot | nullawait snapshot.importSnapshot("slot1", data);
// Generic key/value blobs alongside snapshots — the store path is preferred// for new code, but these are still useful for one-off blobs that don't// belong on the entity graph (best-score, highscore tables, etc.).snapshot.saveData("bestScore", { value: 9999 });snapshot.loadData("bestScore"); // T | nullsnapshot.hasData("bestScore"); // booleansnapshot.deleteData("bestScore");snapshot.exportData("bestScore"); // alias for loadDatasnapshot.importData("bestScore", { value: 1 }); // alias for saveDataSnapshot schema
Section titled “Snapshot schema”interface GameSnapshot { version: number; timestamp: number; scenes: SceneSnapshotEntry[]; extras?: Record<string, unknown>; // plugin-contributed extras}
interface SceneSnapshotEntry { type: string; paused: boolean; entities: EntitySnapshotEntry[]; userData?: unknown;}
interface EntitySnapshotEntry { id: number; // save-time id, used to rewire references type: string; components: ComponentSnapshot[]; userData?: unknown; parentId?: number; childName?: string;}
interface ComponentSnapshot { type: string; data: unknown;}GameSnapshot.version is 4. Older saves throw a version-mismatch error at
load time.
Cross-entity references
Section titled “Cross-entity references”When an entity holds a pointer to another entity, store the save-time id
in serialize(). On load, afterRestore() receives a SnapshotResolver
that maps old ids to freshly-restored instances:
@serializableclass Enemy extends Entity { target: Entity | null = null;
serialize() { return { targetId: this.target?.id }; }
afterRestore(data: { targetId?: number }, resolver: SnapshotResolver) { if (data.targetId != null) this.target = resolver.entity(data.targetId); }}Quicksave pattern
Section titled “Quicksave pattern”class GameScene extends Scene { private readonly input = this.service(InputManagerKey); private readonly snapshot = this.service(SnapshotServiceKey);
update(): void { if (this.input.isJustPressed("quicksave")) { this.snapshot.saveSnapshot("quicksave"); } if (this.input.isJustPressed("quickload")) { this.snapshot.loadSnapshot("quicksave"); } }}Snapshot contributors
Section titled “Snapshot contributors”Plugins owning state outside the entity/component model (renderer-scope effects, audio buses, etc.) can contribute extras:
import { SnapshotServiceKey, type SnapshotContributor } from "@yagejs/save";
const svc = context.tryResolve(SnapshotServiceKey);svc?.registerSnapshotExtra("myPlugin", { serialize: () => ({ /* … */ }), restore: (data) => { /* apply data */ },});Every registered contributor is called on loadSnapshot — including with
undefined when the snapshot has no entry for it — so contributors can reset
to baseline. A failing contributor is logged via console.error and the
load continues.
Use svc.unregisterSnapshotExtra(key) to remove a contributor (e.g. when
unloading a feature plugin at runtime). Re-registering with the same key
replaces the previous contributor.
The renderer plugin auto-registers a contributor under "renderer" for
layer/scene/screen-scope effects + masks.
What gets serialized
Section titled “What gets serialized”- Included:
@serializableentities, scenes, and components. The full scene stack, entity hierarchy (parent/child), and component data. - Not included: draw callbacks, event listeners, runtime service
references, non-
@serializableclasses.
If something is missing after a load, you need an afterRestore() hook to
re-create it.