
A scene file is the bridge between the editor and the runtime. You move a light, save, reopen the project, and the light is back where you left it. That round trip is not magic; it’s a file format. This is what’s actually in it.
The file on disk
Here’s a real .scene.ron from one of the shipped project templates, trimmed to two entities:
(
version: 5,
entities: [
(
local_id: 1,
guid: 0,
parent: None,
name: Some("Cube"),
transform: (
translation: (0.0, 1.0, 0.0),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (1.0, 1.0, 1.0),
),
prefab_instance: None,
prefab_override: None,
components: {
"Materials": "(slots:[(path:Some(\"builtin/materials/default.mat.ron\"))])",
"Mesh": "(source:Some(Asset(path:\"assets/cube.glb\",node_index:0)),cast_shadows:true)",
},
),
(
local_id: 2,
guid: 0,
parent: None,
name: Some("Directional Light"),
transform: (
translation: (0.0, 3.0, 0.0),
rotation: (-0.5302987, 0.29413742, -0.14047563, 0.782645),
scale: (1.0, 1.0, 1.0),
),
prefab_instance: None,
prefab_override: None,
components: {
"Light": "(kind:Directional,color:(1.0,1.0,1.0),intensity:2.0,direction:(0.5,0.70710677,0.5),range:10.0,inner_angle:0.5235988,outer_angle:0.7853982,cast_shadows:true,shadow_bias_constant:0.005,shadow_bias_slope:0.02,shadow_softness:4.0)",
},
),
],
)
The top level is just two fields: a version (currently 5) and a list of entities. Everything else is per entity.
How the components map works
Each entity is a flat record. local_id is a 1-based index assigned in sorted entity-id order, and parent points at another local_id to rebuild the hierarchy. name and transform get their own dedicated fields.
The interesting field is components. It’s a HashMap<String, String>: the key is the component’s type name, the value is the component serialized to a RON string. Not a typed struct, a string. The cube above carries a Mesh and a Materials, each as an opaque RON blob nested inside the outer RON document.
That looks odd at first. Why nest RON inside RON? Because the engine doesn’t know your project’s components at compile time. A scene file has to round-trip components the engine has never heard of, and the only way to do that without a per-component schema is to treat each component’s value as an opaque string keyed by name. The outer document is structural (entities, hierarchy, transforms); the inner strings are payload.
The serializer never enumerates “Mesh, Light, Materials”. It walks the entity’s archetype, skips a handful of engine-internal types (Parent, Children, Name, Transform, GlobalTransform, which are stored as dedicated fields or rebuilt from hierarchy data), and for everything else asks the type registry two questions: can you read this component off the entity (ReflectComponent), and can you serialize it (ReflectSerialize)? If both answers are yes, the component goes into the file. If either is missing, it’s skipped.
Load is the mirror. For each entry in the components map, the loader looks the type up by name, asks for ReflectComponent and ReflectDeserialize, and rebuilds the value from the stored RON string. This is the point of doing serialization through reflection rather than a hand-written per-component switch: a component you add in your own project, register for reflection and serde, round-trips through the scene file with zero engine changes. The engine didn’t need to know about it, and the format didn’t need to change.
What gets deliberately left out
A few categories of component are never written to the file, by design.
The structural ones (Parent, Children, Name, Transform, GlobalTransform) are stored as dedicated SceneEntity fields or rebuilt from the hierarchy, so they don’t also appear in the components map.
The bigger category is system-maintained caches. A SkeletonInstance holds a full pose buffer per joint; resolved motion sets, character state, and collision caches are the same story. None of these are authorable; they’re rebuilt from authored data on load and on play-in-editor start. Capturing them would write megabytes of cache into the file and reintroduce stale state on reload.
The rule is enforced in one direction by convention: those types are simply never registered for reflection, so the capture path never sees them. It’s enforced in the other direction by a test: the engine’s aggregate reflection registration has a drift guard asserting that every type with ReflectComponent also carries ReflectSerialize and ReflectDeserialize, and that the known system-maintained types are absent. Add a reflected engine component without serde and that test fails.
There’s a footgun the test can’t cover: project types. If you register a component for reflection but forget the serde half, the engine types pass their guard, but yours gets silently dropped on capture. The file loads, the entity is there, the component isn’t. No error, no warning. This is documented, not yet fixed.
Prefabs: the root is saved, the children aren’t
A prefab instance in a scene is one entity carrying a prefab_instance field with the prefab’s path. Its children, the ones the prefab asset owns, are deliberately excluded from scene capture. Their data lives in the .prefab.ron asset, not the scene.
So what happens when you move a prefab child in the scene? The edit can’t go on the child, because the child isn’t saved. It goes on the root, as a prefab_override: a list of per-child component overrides keyed by a stable guid, not by the child’s positional local_id. The guid is minted once when the prefab is first captured and persists in the prefab asset, so an override follows the same child even if siblings are reordered or removed. The root is saved; the override rides on the root; the child stays in the prefab.
On load, the scene spawns the root with its prefab_instance and prefab_override, then a deferred system resolves the prefab asset, instantiates its children, and applies the overrides to them.
Deterministic diffs
The components map is a HashMap, and HashMap iteration order is not deterministic. A scene saved twice could produce two files with the same content in a different order, which makes version-control diffs noisy. The serializer sorts the keys before writing, so a scene that hasn’t changed produces a byte-identical file. This matters less for the engine and more for the person reviewing a pull request.
Where things stand today
Scene save and load works today, in the editor and in the browser. This is the format the editor actually writes, not a roadmap item. The browser editor saving a scene to a virtual filesystem and reloading it is the same code path as the native editor saving to disk.
Eresh is pre-alpha, and the scope of the format reflects that. It handles transforms, names, hierarchies, reflected components, and prefab instances with overrides. What it doesn’t handle yet is migration: forward compatibility is carried by #[serde(default)] on individual fields and a top-level version number, so old files with missing fields still load. But there’s no migration system for a breaking format change, just the version tag waiting for one.
What’s next
The next devlog will go deeper into another subsystem as it lands. If you want to see the scene format in action, the editor runs in the browser with no install required.