19 KiB
Scene format (.forge.ron)
Bevy Forge stores scenes as human-readable 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.
This page documents format version 1 as written by the editor and understood by the core crate. Related pages: architecture, README.
Files and naming
- Scenes live in
<project>/scenes/.Project::list_scenes()lists the*.forge.ronfiles 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 becomesscene(for exampleLevel 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_scenerefuses paths outside the project root. - Files are UTF-8 RON produced with a two-space pretty printer. Writers use
\nline 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:
(
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:
(
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. |
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<u64> |
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:
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),
)
translationis 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.rotationis XYZ Euler angles in degrees, not radians and not a quaternion. This is the value the inspector edits. Export converts it withQuat::from_euler(EulerRot::XYZ, x.to_radians(), y.to_radians(), z.to_radians()).scaleis 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
f32RON 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:
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":
(
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:<type_path> |
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
(
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<String> |
Path relative to assets/; omit Some for a solid-colour sprite |
color |
Color |
Tint; defaults to white |
custom_size |
Option<Vec2> |
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
(
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
(type: camera2d)
(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
(
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
(
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
(
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: primitivewith one of the fivePrimitiveShapevalues below.source: assetwith a string mesh path, for examplevalue: "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("<path>") in BSN. The material is always emitted inline, so no material asset
loader is required.
Visibility
(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
(
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. 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 (
Nonefor roots) and appears in exactly one place: eitherrootsor one parent'schildrenlist. parentandchildrenmust agree (child.parent == Some(parent_id)).rootsentries must haveparent: None.- Cycles are invalid and are rejected by
reparent; a hand-edited file with a cycle fails structural validation. idis unique and stable for the lifetime of the scene; IDs are never reused after deletion.next_idmust remain greater than every entity ID. Gaps are fine.- Order matters:
rootscontrols root order, eachchildrenlist 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". lockedis 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
0is normalized to1in 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
1as 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:
image: Some("textures/player.png")
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
.oggcannot be used as a sprite image;.png,.jpg,.jpeg,.webp,.bmp,.tga,.ktx2and.ddsare images, while.gltf,.glb,.objand.fbxare 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_sceneserializes with the pretty RON printer and writes atomically: it writes a temporary.<filename>.tmp-<pid>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.ronfilenames andProject::save_scenerefuses 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<project>/.forge/autosave.forge.ronat most once per interval. Autosave is best-effort: it does not mark the session saved and is not a substitute for saving. Setautosave_secsto 0 to disable it. - Editor preferences are stored separately in
bevy-forge/config.ronunder 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_sizewith a non-positive component. - Text
font_size <= 0. PointLight.intensity < 0orrange <= 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
metallicorperceptual_roughnessoutside0..=1. - Empty
Text2dstring. - 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.