# Scene format (`.forge.ron`) Bevy Forge stores scenes as human-readable [RON](https://github.com/ron-rs/ron) documents with the `.forge.ron` extension. A scene file contains **editor data only** - entities, hierarchy, transforms and components. It never contains generated code and is not Bevy's `.scn.ron` dynamic-scene format. BSN or imperative Rust is generated from a scene on demand; see the [BSN export guide](bsn-export.md). This page documents format version **1** as written by the editor and understood by the core crate. Related pages: [architecture](architecture.md), [README](../README.md). ## Files and naming - Scenes live in `/scenes/`. `Project::list_scenes()` lists the `*.forge.ron` files there. - The editor's suggested filename is the scene display name run through `slugify`: ASCII letters/digits are lowercased, whitespace and `-`, `_`, `.` collapse to `-`, and an empty result becomes `scene` (for example `Level 01` -> `level-01.forge.ron`). The name inside the file is separate and can be any UTF-8 string. - The editor refuses to save under any other extension, and `Project::save_scene` refuses paths outside the project root. - Files are UTF-8 RON produced with a two-space pretty printer. Writers use `\n` line endings. RON comments are legal in the format but the editor does not write them. - Whitespace and line breaks are not significant. The editor's writer puts one struct field per line and expands nested vectors/colours the same way; the examples on this page are written more compactly for readability and parse identically. ## Complete example A small 3D scene with two roots, a directional light and a primitive mesh: ```ron ( format_version: 1, name: "Garden", entities: { 1: ( id: 1, name: "Sun", parent: None, children: [], transform: ( translation: (x: -4.0, y: 8.0, z: -6.0), rotation: (x: 45.0, y: 30.0, z: 0.0), scale: (x: 1.0, y: 1.0, z: 1.0), ), components: [ ( type: directional_light, data: ( color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0), illuminance: 10000.0, shadows: true, ), ), ], locked: false, ), 2: ( id: 2, name: "Ground", parent: None, children: [], transform: ( translation: (x: 0.0, y: 0.0, z: 0.0), rotation: (x: 0.0, y: 0.0, z: 0.0), scale: (x: 1.0, y: 1.0, z: 1.0), ), components: [ ( type: mesh3d, data: ( mesh: ( source: primitive, value: cuboid(size: (x: 12.0, y: 0.2, z: 12.0)), ), material: ( base_color: (r: 0.35, g: 0.55, b: 0.3, a: 1.0), metallic: 0.0, perceptual_roughness: 0.9, unlit: false, ), ), ), ], locked: false, ), }, roots: [1, 2], next_id: 3, ) ``` A 2D scene with hierarchy, a sprite, text, a camera, a hidden component and a custom component: ```ron ( format_version: 1, name: "Platformer", entities: { 1: ( id: 1, name: "Camera", parent: None, children: [], transform: ( translation: (x: 0.0, y: 0.0, z: 10.0), rotation: (x: 0.0, y: 0.0, z: 0.0), scale: (x: 1.0, y: 1.0, z: 1.0), ), components: [ (type: camera2d), ], locked: false, ), 2: ( id: 2, name: "Player", parent: None, children: [3], transform: ( translation: (x: 0.0, y: 1.0, z: 0.0), rotation: (x: 0.0, y: 0.0, z: 0.0), scale: (x: 1.0, y: 1.0, z: 1.0), ), components: [ ( type: sprite, data: ( image: Some("textures/player.png"), color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), custom_size: Some((x: 32.0, y: 48.0)), flip_x: false, flip_y: false, ), ), (type: visibility, data: visible), ( type: custom, data: ( type_path: "forge_demo::Orbit", fields: [ (name: "radius", expr: "2.0"), (name: "speed", expr: "-0.5"), ], ), ), ], locked: false, ), 3: ( id: 3, name: "Label", parent: Some(2), children: [], transform: ( translation: (x: 0.0, y: 32.0, z: 0.0), rotation: (x: 0.0, y: 0.0, z: 0.0), scale: (x: 1.0, y: 1.0, z: 1.0), ), components: [ ( type: text2d, data: ( text: "Bevy Forge", font_size: 24.0, color: (r: 0.9, g: 0.9, b: 1.0, a: 1.0), ), ), ], locked: false, ), }, roots: [1, 2], next_id: 4, ) ``` ## Top-level fields `SceneDoc` is a RON struct with these fields, in this order: | Field | Type | Meaning | |---|---|---| | `format_version` | `u32` | Schema version. Currently `1`; see [Versioning](#versioning). | | `name` | `String` | Scene display name, independent of the filename. May be empty (validation warns). | | `entities` | map `u64 -> EntityNode` | All entities, keyed by ID. Map order is insertion order and is stable across saves. | | `roots` | `[u64]` | Root entity IDs in display order. Every entity is either a root or a descendant of one. | | `next_id` | `u64` | Next identity to allocate. Must be greater than every ID in `entities`; IDs are never reused. | ## Entity fields | Field | Type | Meaning | |---|---|---| | `id` | `u64` | Must equal the key in `entities`. | | `name` | `String` | Display name; arbitrary UTF-8. Used for hierarchy labels and generated `Name(...)`. | | `parent` | `Option` | `None` for roots, otherwise the containing entity. | | `children` | `[u64]` | Ordered child IDs. Order is preserved in the hierarchy and in generated code. | | `transform` | `Transform` | Mandatory local transform; see below. | | `components` | `[ComponentData]` | Ordered component list; at most one component per `key()`. | | `locked` | `bool` | Editor-only. `true` blocks viewport transforms and gizmos, not hierarchy edits or deletion. Missing in old files defaults to `false`. | ## Transforms and coordinates `Transform` mirrors Bevy's `Transform`: ```ron transform: ( translation: (x: 1.0, y: 2.0, z: 0.5), rotation: (x: 0.0, y: 45.0, z: 0.0), scale: (x: 1.0, y: 1.0, z: 1.0), ) ``` - `translation` is the local position. 2D scenes use X/Y; Z is used as the draw-order axis by the 2D viewport. 3D scenes use all three axes. - `rotation` is **XYZ Euler angles in degrees**, not radians and not a quaternion. This is the value the inspector edits. Export converts it with `Quat::from_euler(EulerRot::XYZ, x.to_radians(), y.to_radians(), z.to_radians())`. - `scale` is a per-axis multiplier. A zero scale is legal in the file but produces a validation warning. - World transforms are computed by composing local transforms down the parent chain. Parents affect children; moving a parent moves its subtree in the viewports. Scene files always store local transforms, never world transforms. - All numbers are `f32` RON floats. Non-finite values (`NaN`, `inf`) are representable in RON but rejected by validation and code generation. Colours use the four-field `Color` struct with channels in `0.0..=1.0`: ```ron color: (r: 0.35, g: 0.55, b: 0.95, a: 1.0) ``` The editor edits these as sRGB colours; the generated code emits `Color::srgba(r, g, b, a)`. ## Components `components` is an ordered list of serde-tagged values. Every entry has a `type` tag and, except for unit variants, a `data` payload. Tags and enum payloads are **bare RON identifiers**, not strings - write `type: sprite`, not `type: "sprite"`: ```ron ( type: sprite, data: ( image: Some("textures/player.png"), color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), custom_size: Some((x: 32.0, y: 48.0)), flip_x: false, flip_y: false, ), ) ``` The tag values and uniqueness keys are: | Tag | Payload | `key()` | Notes | |---|---|---|---| | `sprite` | `SpriteData` | `sprite` | 2D sprite | | `text2d` | `Text2dData` | `text2d` | world-space text | | `camera2d` | none | `camera2d` | unit: `(type: camera2d)` | | `camera3d` | none | `camera3d` | unit: `(type: camera3d)` | | `point_light` | `PointLightData` | `point_light` | | | `directional_light` | `DirectionalLightData` | `directional_light` | | | `mesh3d` | `Mesh3dData` | `mesh3d` | primitive or asset mesh plus material | | `visibility` | `VisibilityMode` | `visibility` | `inherited`, `visible` or `hidden` | | `custom` | `CustomComponent` | `custom:` | one per custom type path | `Name` and `Transform` are **not** components in this format - they are entity fields. Adding a component whose key already exists is refused; use the inspector's edit controls to change values. ### Sprite ```ron ( type: sprite, data: ( image: Some("textures/player.png"), color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), custom_size: Some((x: 64.0, y: 64.0)), flip_x: false, flip_y: false, ), ) ``` | Field | Type | Meaning | |---|---|---| | `image` | `Option` | Path relative to `assets/`; omit `Some` for a solid-colour sprite | | `color` | `Color` | Tint; defaults to white | | `custom_size` | `Option` | Explicit size; `None` uses the texture's native size | | `flip_x`, `flip_y` | `bool` | Mirror the texture on that axis | A sprite with no image and no custom size renders nothing; validation reports a warning. ### Text2d ```ron ( type: text2d, data: ( text: "Hello\nBevy", font_size: 32.0, color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), ), ) ``` `font_size` is in pixels and must be greater than zero. The text string is escaped as a normal RON string and becomes a Rust string literal on export. Empty text is allowed but produces a warning. ### Cameras ```ron (type: camera2d) ``` ```ron (type: camera3d) ``` A scene can contain both, but the 2D and 3D camera mix produces a warning. Optional cameras are reported as info; more than one camera is also info. Neither variant stores projection fields in this version. ### Point light ```ron ( type: point_light, data: ( color: (r: 1.0, g: 0.8, b: 0.6, a: 1.0), intensity: 800.0, range: 20.0, shadows: true, ), ) ``` `intensity` must be non-negative and `range` must be positive; `shadows` maps to `shadow_maps_enabled` in generated BSN. ### Directional light ```ron ( type: directional_light, data: ( color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0), illuminance: 10000.0, shadows: true, ), ) ``` `illuminance` must be non-negative. The light's direction comes from the entity's rotation. ### Mesh3d ```ron ( type: mesh3d, data: ( mesh: ( source: primitive, value: cuboid(size: (x: 1.0, y: 1.0, z: 1.0)), ), material: ( base_color: (r: 0.45, g: 0.62, b: 0.95, a: 1.0), metallic: 0.0, perceptual_roughness: 0.5, unlit: false, ), ), ) ``` `MeshSource` is tagged `source` / `value`: - `source: primitive` with one of the five `PrimitiveShape` values below. - `source: asset` with a string mesh path, for example `value: "models/robot.gltf#Mesh0/Primitive0"`. Primitive shapes (RON value for `value`): | Shape | RON | |---|---| | Cuboid | `cuboid(size: (x: 2.0, y: 1.0, z: 0.5))` | | Sphere | `sphere(radius: 0.5)` | | Plane | `plane(size: (x: 10.0, y: 10.0))` - width and depth in X/Z | | Cylinder | `cylinder(radius: 0.5, height: 1.0)` | | Capsule | `capsule(radius: 0.5, length: 1.0)` | `MaterialData` fields: | Field | Type | Meaning | |---|---|---| | `base_color` | `Color` | Albedo / base colour | | `metallic` | `f32` | 0 = dielectric, 1 = metal; values outside `0..=1` warn | | `perceptual_roughness` | `f32` | 0 = mirror, 1 = rough; values outside `0..=1` warn | | `unlit` | `bool` | Emit an unlit material | Asset mesh paths are validated for existence and kind when an asset index is available, and are emitted as `Mesh3d("")` in BSN. The material is always emitted inline, so no material asset loader is required. ### Visibility ```ron (type: visibility, data: hidden) ``` The payload is one of the bare RON identifiers `inherited`, `visible` or `hidden`. `hidden` is equivalent to Bevy's `Visibility::Hidden`; toggling the hierarchy eye adds/removes this component. An entity without a visibility component is treated as `inherited`. ### Custom components ```ron ( type: custom, data: ( type_path: "forge_demo::Orbit", fields: [ (name: "radius", expr: "2.0"), (name: "speed", expr: "-0.5"), (name: "axis", expr: "Vec3::Y"), ], ), ) ``` | Field | Type | Meaning | |---|---|---| | `type_path` | `String` | A Rust type path such as `Health`, `my_game::Health` or `my_game::components::Health` | | `fields` | `[CustomField]` | Ordered field initialisers | | `fields[].name` | `String` | Rust field identifier | | `fields[].expr` | `String` | Raw Rust/BSN expression, stored verbatim and emitted after `name:` | Custom components are the escape hatch for project-specific types. The editor validates only the shape of the type path and field names, and that expressions are non-empty; it does not parse Rust. The component type must derive `Component` plus `Default + Clone` or `FromTemplate` - see [custom-component requirements](bsn-export.md#custom-component-requirements). Two custom components with different type paths can coexist on one entity; the same path may appear only once. ## Ids, forests and ordering Every scene is a forest of entities: - Each entity has exactly one parent (`None` for roots) and appears in exactly one place: either `roots` or one parent's `children` list. - `parent` and `children` must agree (`child.parent == Some(parent_id)`). `roots` entries must have `parent: None`. - Cycles are invalid and are rejected by `reparent`; a hand-edited file with a cycle fails structural validation. - `id` is unique and stable for the lifetime of the scene; IDs are never reused after deletion. `next_id` must remain greater than every entity ID. Gaps are fine. - Order matters: `roots` controls root order, each `children` list controls sibling order, and both are preserved by duplicate/reparent and by code generation. - Deleting an entity removes its whole subtree. Duplicating deep-copies the subtree, inserts the copy immediately after the original among its siblings, and suffixes the root copy's name with `" Copy"`. - `locked` is editor metadata and has no effect on export. `SceneDoc::check_integrity()` returns a list of structural problems (empty when valid). Validation includes these checks. ## Versioning `format_version` is the scene schema version, not the Bevy Forge application version. It is currently `1` (`FORMAT_VERSION`). - The editor writes `1`. - On load, version `0` is normalized to `1` in memory and the data is kept (a permissive path for early files). - There is no migration framework yet: the loader otherwise parses the document as the current schema. Treat version `1` as the only supported schema. - Additive fields should be serde-defaultable so older files still load; breaking changes bump `format_version`. - Generated code is not stored in the file and is not versioned by this field. Export behaviour is governed by the codegen contract and the target Bevy version (0.20). ## Asset paths Sprite images and asset meshes use paths **relative to the project's Bevy assets directory**, with `/` separators: ```ron image: Some("textures/player.png") ``` ```ron value: "models/robot.gltf#Mesh0/Primitive0" ``` Rules (violations are validation errors): - No absolute paths. - No `..` path components. - No backslashes. - With an asset index, the file must exist and its kind must match the use (for example, an `.ogg` cannot be used as a sprite image; `.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.tga`, `.ktx2` and `.dds` are images, while `.gltf`, `.glb`, `.obj` and `.fbx` are meshes). Projects with an `assets/` directory use it directly. Projects without one treat the project root as the assets directory. The editor resolves asset paths through the project, never the process working directory. ## Saving and autosave - `Project::save_scene` serializes with the pretty RON printer and writes **atomically**: it writes a temporary `..tmp-` file in the destination directory and renames it over the target. A crash during save leaves the previous file intact. - The editor refuses non-`.forge.ron` filenames and `Project::save_scene` refuses paths outside the project root. Parent directories are created for save-as paths. - Saving marks the session clean; the title dot and quit guard use that flag. - When autosave is enabled (`autosave_secs > 0`, default 60 seconds), a dirty document is written to `/.forge/autosave.forge.ron` at most once per interval. Autosave is best-effort: it does not mark the session saved and is not a substitute for saving. Set `autosave_secs` to 0 to disable it. - Editor preferences are stored separately in `bevy-forge/config.ron` under the platform config directory; that file is not a scene. ## Validation `validate_scene(doc, assets: Option<&AssetIndex>)` returns a `ValidationReport` with `Error`, `Warning` and `Info` issues. `forge validate` prints them and uses the exit code to distinguish "valid" from "has errors"; the editor shows per-entity issues and summary counts. Passing `None` for the asset index disables missing-asset checks. **Errors** - Structural invariant failures: missing parent/child/root, cycles, mismatched node ID, duplicate references. - Non-finite transform, rotation or scale values. - Non-finite light or material values. - Custom component: invalid type path, invalid field name, empty field expression. - Asset reference containing an absolute path, a `..` component or a backslash. - With an asset index: referenced sprite image or mesh asset missing, or the wrong asset kind. - Sprite `custom_size` with a non-positive component. - Text `font_size <= 0`. - `PointLight.intensity < 0` or `range <= 0`. - `DirectionalLight.illuminance < 0`. - An entity is its own parent or introduces a cycle. **Warnings** - Duplicate entity names. - A sprite with no image and no custom size (it renders nothing). - A zero scale component. - Material `metallic` or `perceptual_roughness` outside `0..=1`. - Empty `Text2d` string. - Both 2D and 3D cameras in one scene. - An empty or whitespace-only entity name. **Info** - No camera in the scene. - No light in a scene that contains meshes. - An entity with only a `Transform`. - More than one camera. Validation is deterministic (severity descending, then entity ID, then message) and must not panic on malformed forests. ## Relationship to BSN The `.forge.ron` file is the source of truth in the editor. Exporting walks the forest and emits `Name`, `Transform` and the component list for every entity, converting Euler degrees to a quaternion and choosing asset parameters for mesh scenes. The generated file is portable Rust, not a scene asset that Bevy loads at runtime. See the [BSN export guide](bsn-export.md).