Scene Serialization
SafiEngine can save the current ECS world to a JSON file, load it back, and capture in-memory snapshots for things like Play→Stop. The serializer walks every entity with SafiName, queries the component registry for each component's serialize/deserialize callbacks, and writes structured JSON via cJSON.
File-based API
safi_scene_savewrites all named entities + their serializable components topath. Entities taggedSafiEngineOwned(editor fly-cam, future debug billboards) are excluded.safi_scene_loadclears the scene first, then creates entities from the JSON file.safi_scene_cleardeletes every entity withSafiName, except those taggedSafiEngineOwnedwhich are editor infrastructure and must survive a reload.
In-memory snapshot / restore
safi_scene_snapshot_entitiesserializes a specific set of entities into a heap-owned cJSON object (caller frees withcJSON_Delete). Entities withoutSafiNameare skipped.safi_scene_snapshot_allis a shorthand for the full world — same shape, same callbacks assafi_scene_save.safi_scene_restore_snapshotapplies a snapshot back onto existing entities. Matching is bySafiStableIdfirst (the 128-bit GUID every named entity carries), and falls back toSafiNamefor legacy data. It never creates or deletes entities, soecs_entity_thandles held elsewhere remain valid. Missing ids are logged as warnings and skipped; parent/child pairs in the snapshot are ignored (reparenting during Play is out of scope for M1).
This asymmetry is the whole point: safi_scene_load is destructive and rebuilds ids (correct for "open a file"), while safi_scene_restore_snapshot is non-destructive (correct for "leave Play mode and rewind").
Entity lookup
Linear scans over every entity carrying SafiName / SafiStableId. Return 0 when there is no match. Useful after safi_scene_load, which destroys and recreates entities and invalidates any cached ids — look the well-known names (or stored ids) back up to refresh your handles. Stable-id lookup is preferred for anything persisted outside the world (undo history, cross-scene references) because it survives renames.
Scene file format (v1)
Key decisions
- Stable IDs are the lookup key. Every entity carries a 128-bit random
SafiStableId(auto-attached by anOnAddobserver onSafiName). Snapshots and scene files serialize it as 32 hex characters, and restore matches on it first — names are just labels and safe to rename. - Parent references use the parent entity's
SafiName.valuestring (not a numeric id) in v1. Load wires parents throughsafi_entity_set_parent, which rejects hand-edited JSON that would close a cycle. - Asset paths are project-root-relative.
SafiMeshRenderer.model_pathstores e.g."models/player.glb"anchored toSafiAppDesc.project_root; the resolver joins on load so absolute paths from older files still load. SafiMeshRenderer.modelandSafiPrimitive.textureare serialized as path strings. On load, paths resolve through the asset registry viasafi_assets_load_model_lit/safi_assets_load_texture; refcounts are handled by component hooks so no code path leaks.SafiPrimitiveserializes shape, dims, color, and texture_path. The engine'sprimitive_systemrebuilds the GPU mesh automatically on the next frame.SafiGlobalTransformis never serialized — it's computed each frame by the transform propagation system. The loader auto-adds it when aSafiTransformis deserialized.- Physics private fields (
_body_id,_registered) are skipped. Jolt re-registers bodies when the components are attached.
Keybinds
Engine-owned (via Editor Shortcuts):
- F6 —
safi_scene_snapshot_all(world)(stashes a cJSON blob in memory) - F7 —
safi_scene_restore_snapshot(world, blob)(resets components to the last snapshot)
Demo-owned in gltf_viewer (scene file IO needs app-specific handle refresh, so it stays in user code):
- F5 —
safi_scene_save(world, "scene.json") - F9 —
safi_scene_load(world, "scene.json")(refreshes cachedg_demohandles)
Edit scene.json by hand (move an entity, change a color), then press F9 to see the changes.
safi_scene_load
safi_scene_load calls safi_scene_clear before loading, so every ecs_entity_t held outside the world (e.g. in a demo-state struct or the Inspector's selection) goes stale. Use safi_scene_find_entity_by_name to refresh well-known handles, and guard dereferences with ecs_is_alive. Snapshot restore does not have this problem — ids stay stable.
Entity presets
safi/scene/presets.h exposes factory helpers the editor's M5 "Create" menu calls (and tests use today). Each preset spawns an entity, attaches SafiName + SafiStableId + SafiTransform/SafiGlobalTransform, and invokes per-component default_init callbacks so the component's own defaults are the single source of truth.
Callers that want to tweak a preset (e.g. position a light) just call ecs_set on the returned entity after construction.
See also
- Component Registry — how serialize/deserialize/default_init callbacks are registered
- Asset System — model/texture path round-tripping
- Stock Components — what gets serialized (including
SafiStableIdandSafiEngineOwned)