Files

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.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:

(
  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),
)
  • 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:

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: 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("<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 (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:

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 .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 .<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.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 <project>/.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.