When to use this skill
- Gameplay state must remain loadable across ECS/content/code refactors.
- Large worlds need base snapshots, sparse deltas, compaction, and recovery.
- Native and browser builds need one asynchronous save-service contract.
- Reflected world serialization is being considered as a durable save format.
A save file is a long-lived product API. Serialize an explicit, versioned data model
and convert it to/from the live ECS. Runtime Entity values, dense palette indices,
query order, Rust enum discriminants, and reflected component layout are not stable
persistence identities.
Canonical pattern
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[serde(transparent)] struct SaveId(String);
#[derive(Debug, Deserialize, Serialize)] struct SaveV3 { world_seed: u64, player: SaveId, actors: Vec, }
#[derive(Debug, Deserialize, Serialize)] struct ActorV3 { id: SaveId, archetype: String, position: [f32; 3], }
```text
live Bevy World
-> validate/capture one logical revision
-> current Save DTO with stable IDs
-> encode + checksum envelope
-> platform storage transaction
platform bytes
-> size/magic/checksum validation
-> decode declared old DTO
-> migrate one version at a time
-> validate current DTO
-> allocate entities, then resolve stable-ID references
-> rebuild derived/runtime-only state
Never partially mutate the active world while bytes are still being decoded or migrated. Load into staging, validate, then commit or replace the gameplay world.
Version the envelope and payload
Wrap encoded payload bytes with magic, format version, schema/content version, payload length, codec/compression identifiers, and checksum. A checksum detects accidental corruption; it does not prevent malicious tampering. Use authentication or a signature when trust is part of the requirement.
Keep migrations as explicit V1 -> V2 -> V3 pure functions. Test golden fixtures
from every supported version, including corrupt, truncated, duplicated-ID, unknown-ID,
and future-version files. Never deserialize an old version directly into today's
struct and hope Serde defaults reproduce old semantics.
Stable IDs and two-pass load
Use immutable domain IDs for saved objects, content, quests, items, and voxel block types. During load:
- validate ID uniqueness and all size/count limits;
- spawn/allocate objects and build
SaveId -> Entityor stable-ID -> dense-palette maps; - resolve relationships in a second pass;
- reject or explicitly preserve unknown content according to policy;
- rebuild caches, handles, physics state, tasks, diagnostics, and presentation.
Storage is platform-specific
Native writes should use a temporary file in the destination directory, flush and
sync it as required, then atomically replace/rename the committed slot. Retain a
validated backup or two-slot generation when recovery matters. Do blocking file I/O
on IoTaskPool, and send the result back to the main world.
Browsers have no ordinary save-file path. Use IndexedDB for game saves and blobs; small preferences may use local storage. Handle asynchronous completion, quota, eviction, private modes, tab races, export/import, and page termination. A “save complete” UI state requires committed storage confirmation.
Bevy 0.19's SettingsPlugin targets user settings with native/local-storage
backends; it is not a general campaign/world save schema.
Gotchas: world serialization boundary
bevy_world_serialization and DynamicWorldBuilder can extract reflected entities
and resources with component/resource allow/deny filters. This is useful for authored
world assets, editor tooling, controlled snapshots, and short-lived internal formats.
It does not automatically create stable gameplay IDs, schema migrations, corruption
recovery, transactional storage, or compatibility across component refactors.
Use an explicit DTO for durable player saves. If a filtered DynamicWorld is nested
inside that format, version and migrate the surrounding contract and audit every
reflected type intentionally.
Choose the relevant deep dive
| Problem | Read |
|---|---|
| DTO versions, migrations, stable IDs, validation and golden fixtures | Schema, migrations, and IDs |
| Atomic native writes, browser storage, async state and recovery | Native and browser I/O |
| Full snapshots, sparse deltas, compaction, world-serialization boundary | Snapshots, deltas, and world serialization |
Review checklist
- Save schema types are separate from live ECS/runtime components.
- Every persistent reference uses a documented stable ID.
- Old versions migrate sequentially and have committed fixtures.
- Decoder limits untrusted lengths/counts before allocation/decompression.
- Native power-loss and browser quota/termination failures leave a recoverable slot.
- Success is reported only after the platform commit completes.
- Snapshot/delta generations cannot be mixed accidentally.
- Unknown future versions fail safely without overwriting the original.
See also
bevy-voxel-data— stable block IDs versus dense runtime palettes.bevy-testing— deterministic migration and failure-path tests.bevy-assets— stable asset paths/labels and asset readiness.- Bevy world serialization
DynamicWorldBuilder