Files

20 KiB

BSN export

Bevy Forge's primary export target is Bevy 0.20 BSN: Rust source code written with the bsn! and bsn_list! macros from bevy::scene. This is the modern scene notation in Bevy 0.20 - not the legacy DynamicScene / .scn.ron format. The output is ordinary Rust that your game compiles; there is no runtime .bsn asset to load.

Related pages: scene format, architecture, README.

Where the code comes from

  • Editor: File > Export > BSN scene. The dialog shows a preview with the generated function name, lets you copy it to the clipboard, and can write it to a file (default export/<function>.rs).
  • CLI:
cargo run -p bevy_forge_cli -- export examples/demo-project/scenes/garden.forge.ron \
  --format bsn -o /tmp/garden.rs

Without -o, the code is printed to stdout. --function <name> sets the generated function name; the default is scene. The name must be a valid Rust identifier that is not a Rust keyword, otherwise the exporter returns an error.

Generated files begin with use bevy::prelude::*; (plus a macro import when needed). With CodegenOptions::include_header enabled - the editor's default - a short "generated by Bevy Forge" comment precedes the imports; the CLI exports without that header, so its stdout and -o files start directly with the import. Generated files end with exactly one newline. Generation is deterministic for a given scene and options.

Output shape

One root scene:

// Generated by Bevy Forge. Paste into a Bevy 0.20 project.
use bevy::prelude::*;

pub fn scene() -> impl Scene {
    bsn! {
        Name("Player")
        Transform {
            translation: Vec3::new(0.0, 0.0, 0.0),
            rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
            scale: Vec3::new(1.0, 1.0, 1.0),
        }
        Sprite { image: "textures/player.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(64.0, 64.0)) }, flip_x: false, flip_y: false }
        Children [
            Name("Hat")
            Transform {
                translation: Vec3::new(0.0, 1.0, 0.0),
                rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
                scale: Vec3::new(1.0, 1.0, 1.0),
            }
            Sprite { color: Color::srgba(1.0, 0.5, 0.2, 1.0), flip_x: false, flip_y: false }
        ]
    }
}

Multiple roots use bsn_list! and return impl SceneList. Roots (and children) are separated by --:

// Generated by Bevy Forge. Paste into a Bevy 0.20 project.
use bevy::prelude::*;
use bevy::scene::bsn_list;

pub fn scene() -> impl SceneList {
    bsn_list! {
        Name("Left")
        Transform {
            translation: Vec3::new(-2.0, 0.0, 0.0),
            rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
            scale: Vec3::new(1.0, 1.0, 1.0),
        }
        Sprite { color: Color::srgba(0.4, 0.7, 1.0, 1.0), flip_x: false, flip_y: false }
        --
        Name("Right")
        Transform {
            translation: Vec3::new(2.0, 0.0, 0.0),
            rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
            scale: Vec3::new(1.0, 1.0, 1.0),
        }
        Sprite { color: Color::srgba(1.0, 0.6, 0.4, 1.0), flip_x: false, flip_y: false }
    }
}

Rules the generator follows:

  • bsn! accepts exactly one root. Two roots inside one bsn! are a parse error; use bsn_list!.
  • -- separates sibling scenes inside bsn_list! and Children [ ... ]. The editor's scene order is preserved.
  • Every entity emits Name("<escaped display name>") and a complete Transform block with all three fields. Display names are emitted as escaped Rust strings, never as #Identifier references.
  • Rotation is converted from stored XYZ Euler degrees to a quaternion: Quat::from_euler(EulerRot::XYZ, 45.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()). The f32 suffix is part of the generated output; it keeps the literal type unambiguous.
  • Components are emitted in stored order after Transform; children appear as a nested Children block.
  • Macro imports are normal use statements. Do not fully qualify the macros (bevy::scene::bsn_list! { ... } compiles but prints a build-time macro-root warning).

Component templates

Name

Name("Player")

The string is Rust-escaped. Name takes a string-like value; Name(42) does not compile.

Transform

Transform {
    translation: Vec3::new(1.0, 0.5, 0.0),
    rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 45.0f32.to_radians(), 0.0f32.to_radians()),
    scale: Vec3::new(1.0, 1.0, 1.0),
}

All three fields are always present. (Here 45.0f32.to_radians() is 45 degrees.) BSN component patches do not support struct-update syntax: Transform { ..default() } fails with a type inference error, so the generator writes every field.

Sprite

Sprite { image: "textures/player.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(64.0, 32.0)) }, flip_x: false, flip_y: false }
  • image is omitted when the sprite has no image; custom_size is omitted when it is None.
  • color is always emitted.
  • Some(...) must be braced inside a BSN patch; bare Some(..) is a parse error. Bare None is accepted, but the generator simply omits optional fields instead.
  • For a solid-colour sprite the output is Sprite { color: ..., flip_x: false, flip_y: false }.

Text2d

Text2d("Hello, Bevy!")
TextFont { font_size: FontSize::Px(32.0) }
TextColor(Color::srgba(1.0, 1.0, 1.0, 1.0))

Text is a Rust string literal; newlines and quotes are escaped normally. font_size is in pixels.

Cameras

Camera2d
Camera3d::default()

Camera2d is emitted bare. Camera3d is a constructor call, not a bare identifier.

Point light

PointLight { color: Color::srgba(1.0, 0.8, 0.6, 1.0), intensity: 800.0, range: 20.0, shadow_maps_enabled: true }

Directional light

DirectionalLight { color: Color::srgba(1.0, 0.95, 0.85, 1.0), illuminance: 10000.0, shadow_maps_enabled: true }

Negative literal caveat: BSN patches do not accept a leading - in a direct field value. A negative number must be wrapped in a block and suffixed: intensity: { -5.0f32 }. Values produced by calls or paths (Vec3::new(-1.0, 0.0, 0.0), f32::NEG_INFINITY) are fine. The generator uses the braced form when a direct literal is negative.

Visibility

Visibility::Inherited
Visibility::Visible
Visibility::Hidden

An entity without a visibility component emits nothing; Bevy treats it as inherited.

Mesh3d and materials

Primitive meshes and materials need asset handles. The generated function takes parameters for the asset collections the scene uses:

pub fn scene(
    meshes: &mut Assets<Mesh>,
    materials: &mut Assets<StandardMaterial>,
) -> impl Scene {
    bsn! {
        Name("Ground")
        Transform {
            translation: Vec3::new(0.0, 0.0, 0.0),
            rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
            scale: Vec3::new(1.0, 1.0, 1.0),
        }
        Mesh3d({ meshes.add(Cuboid::new(12.0, 0.2, 12.0)) })
        MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
            base_color: Color::srgba(0.35, 0.55, 0.3, 1.0),
            metallic: 0.0,
            perceptual_roughness: 0.9,
            unlit: false,
            ..default()
        }) })
    }
}
  • Primitive mesh handles must be braced because method calls are not accepted as direct BSN values: Mesh3d({ meshes.add(..) }), not Mesh3d(meshes.add(..)).
  • MeshMaterial3d requires the turbofish ::<StandardMaterial>; without it the compiler reports missing generics.
  • ..default() is accepted inside the raw Rust StandardMaterial literal (it is a constructor argument), but not inside a BSN component patch.
  • The generator includes meshes only when a primitive mesh exists, and materials whenever a Mesh3d exists. A scene with no meshes has no asset parameters.
  • Primitive constructors (each wrapped in Mesh3d({ meshes.add(...) })):
    • Cuboid: Cuboid::new(x, y, z)
    • Sphere: Sphere::new(radius)
    • Plane: Plane3d::new(Vec3::Y, Vec2::new(width * 0.5, height * 0.5)) - the scene stores the full size, while Bevy's Plane3d takes half extents along X and Z
    • Cylinder: Cylinder::new(radius, height)
    • Capsule: Capsule3d::new(radius, length)

An asset mesh is addressed by path instead of an added handle:

Mesh3d("models/robot.gltf#Mesh0/Primitive0")
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
    base_color: Color::srgba(0.8, 0.8, 0.85, 1.0),
    metallic: 0.1,
    perceptual_roughness: 0.4,
    unlit: false,
    ..default()
}) })

An asset mesh still needs materials, but not meshes. The path is resolved by Bevy's AssetServer at spawn time; see runtime asset caveats.

Custom components

forge_demo::Orbit {
    radius: 2.0,
    speed: { -0.5f32 },
}

A custom component exports as a struct literal: the stored type_path, then each name: expr pair. A custom component with zero fields is emitted as the bare type path:

forge_demo::Marker

Field expressions are copied from the scene file. The generator wraps an expression in { ... } when it starts with - or is a bare enum-variant call such as Some(..), because neither form is accepted as a direct BSN field value. Duplicate field names are rejected. In the example above the scene expression is -0.5f32, so the f32 suffix survives bracing.

Custom component requirements

The generated code must compile in the game crate that defines the component. Bevy 0.20's BSN macro needs a FromTemplate implementation, provided either by the blanket Default + Clone + Unpin implementation or by a derived FromTemplate.

Component shape Minimum derives
Simple data fields (f32, Color, Vec3, ...) #[derive(Component, Default, Clone)]
Unit marker #[derive(Component, Default, Clone)]
Fields like Handle<Image> or Entity #[derive(Component, FromTemplate)]
Option<Handle<T>> or other #[template(built_in)] fields #[derive(Component, FromTemplate)] plus #[template(built_in)] on that field

Notes:

  • FromTemplate is in Bevy's prelude. The derive generates a companion template type, so it can be combined with Default + Clone in 0.20.
  • A component with Handle<Image> and only Default + Clone fails to compile because Handle<Image> does not convert from a string path through the blanket template; use FromTemplate.
  • Add #[template(built_in)] in your Rust source where a field type needs it. The editor does not introspect your types, so it cannot infer this option.
  • The exported type_path must be importable where the generated file is compiled. Use a qualified path such as my_game::Health if the type is not in scope.
  • The generated BSN binds no entity references (#Child), so custom components that link to other entities should use whatever handle/entity value your scene code provides; a raw Entity field expression is the author's responsibility.

Example component definitions:

use bevy::prelude::*;

#[derive(Component, Default, Clone)]
pub struct Health {
    pub current: f32,
    pub max: f32,
}

#[derive(Component, FromTemplate)]
pub struct Icon {
    pub image: Handle<Image>,
    pub tint: Color,
}

Pasting generated code into a Bevy app

  1. Export the file, for example into src/scenes/garden.rs in your game.
  2. Declare the module and call the generated function from a startup system:
use bevy::prelude::*;

mod scenes {
    pub mod garden;
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, spawn_garden)
        .run();
}

fn spawn_garden(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn_scene(scenes::garden::scene(&mut meshes, &mut materials));
}

For a scene whose generated signature has no asset parameters:

commands.spawn_scene(scenes::garden::scene());

For multiple roots:

commands.spawn_scene_list(scenes::garden::scene());
  • spawn_scene, spawn_scene_list and the related traits are in Bevy's prelude.
  • Matching the generated parameter list is required: passing unused asset collections is harmless, but omitting a required one does not compile.
  • On World (for example in exclusive systems or tests), use world.spawn_scene(scene) or world.spawn_scene_list(scene); spawn_scene_list returns a Result<Vec<Entity>, _>.
  • commands.queue_spawn_scene(..) (and the list variant) defers the spawn. Handles created by meshes.add / materials.add are evaluated when scene() is called and are owned by the scene, so deferred spawning is safe.
  • Use DefaultPlugins or the specific plugins your components need (ScenePlugin, SpritePlugin, PbrPlugin, ...). A headless test harness needs at least task pool, asset and scene plugins.

Runtime asset caveats

  • Primitives and inline materials work immediately. meshes.add(Cuboid::new(..)) and materials.add(StandardMaterial { .. }) create values when scene() runs; the entity renders as soon as it is spawned and the asset is registered.
  • Sprite images and asset meshes are path handles. A string like "textures/player.png" becomes a path handled by AssetServer at spawn time. If the file is missing, or the relevant asset loader is not enabled in your game (bevy_gltf for .gltf, for example), the handle silently stays Loading or Failed - spawning does not error.
  • Material asset paths need a loader. MeshMaterial3d::<StandardMaterial>("materials/cube.toml") compiles, but unless your project registers a StandardMaterial asset loader it never resolves. This is why Forge emits inline material values instead of material asset paths. Do not replace the generated material expression with a bare path and expect it to render.
  • No runtime BSN scene assets. The generated code is compiled into your crate. Bevy's scene asset/caching features are not used by this exporter.

Determinism and export errors

ExportFormat::generate is pure and deterministic. It returns errors instead of panicking:

Error Cause
EmptyScene The scene has no root entities (including an entirely empty scene); there is nothing to generate.
InvalidIdentifier The function name is not a valid Rust identifier, or is a Rust keyword.
NonFinite A transform, material or light value is NaN or infinite.
InvalidCustomComponent A custom type path or field name is invalid, or a field expression is empty. The BSN generator additionally rejects duplicate field names.

The CLI exits 1 for these codegen errors and 2 for I/O, RON parse or usage errors. The editor shows the error in the export dialog instead of writing a file.

Troubleshooting compile errors

Compiler message Cause Fix
error: Unexpected input after function name Method call used as a direct BSN value, e.g. Mesh3d(meshes.add(..)) Brace it: Mesh3d({ meshes.add(..) })
error[E0107]: missing generics for struct MeshMaterial3d Missing turbofish Use MeshMaterial3d::<StandardMaterial>(...)
error: expected type, found variant 'Some' Unbraced Some(..) in a patch Use custom_size: { Some(Vec2::new(..)) }
error: expected identifier at a negative number Negative direct field literal Use intensity: { -5.0f32 } (or a call/path value)
error: expected f32, found integer Integer literal in an f32 field Write 10.0, not 10
error[E0282]: type annotations needed ..default() or ..Default::default() inside a BSN component patch Not supported; write the fields or omit them to keep defaults. ..default() is only valid inside raw Rust literals in {} arguments
error: expected HashedStr, found integer Name(42) Name takes a string: Name("42")
error: unexpected token at -- inside bsn! Multiple roots in a single-root macro Use bsn_list! and return impl SceneList
cannot find macro bsn_list Missing import Add use bevy::scene::bsn_list; (or use bevy::prelude::*;)
Unknown macro root! printed to stderr Fully qualified macro path Import the macro with use instead
warning: unused variable: meshes Asset parameter present but unused (e.g. hand-edited) Let the exporter include only needed parameters, or allow the warning
warning: float_literal_f32_fallback Unsuffixed negative float in a bracing block Use an f32 suffix: { -5.0f32 }
Duplicate sibling entities visually merged Missing -- between scenes Separate sibling patches with --

If an exported scene fails to compile, first check that the pasted file is in a crate that depends on Bevy 0.20 and that custom component types and their derives are present. Then compare against the templates above. The repository's compat/bsn-smoke crate compiles representative generated output against real Bevy 0.20 and is the reference for supported syntax.

Imperative Rust alternative

forge export --format rust (or File > Export > Rust spawn code) produces a function that spawns the scene immediately with Commands. It targets the same Bevy 0.20 API surface but does not use BSN.

// Generated by Bevy Forge. Paste into a Bevy 0.20 project.
// Imperative `Commands::spawn` code: call this function from a startup system.

use bevy::prelude::*;

pub fn scene(
    commands: &mut Commands,
    asset_server: &AssetServer,
) {
    commands.spawn((
        Name::new("Player"),
        Transform::from_xyz(0.0, 0.0, 0.0)
            .with_rotation(Quat::from_euler(EulerRot::XYZ, 0.0_f32.to_radians(), 0.0_f32.to_radians(), 0.0_f32.to_radians()))
            .with_scale(Vec3::new(1.0, 1.0, 1.0)),
        Sprite { image: asset_server.load("textures/player.png"), color: Color::srgba(1.0, 1.0, 1.0, 1.0), flip_x: false, flip_y: false, ..default() },
    )).with_children(|parent| {
        parent.spawn((
            Name::new("Hat"),
            Transform::from_xyz(0.0, 1.0, 0.0)
                .with_rotation(Quat::from_euler(EulerRot::XYZ, 0.0_f32.to_radians(), 0.0_f32.to_radians(), 0.0_f32.to_radians()))
                .with_scale(Vec3::new(1.0, 1.0, 1.0)),
            Sprite { color: Color::srgba(1.0, 0.5, 0.2, 1.0), flip_x: false, flip_y: false, ..default() },
        ));
    });
}

Usage in a system:

fn spawn_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
    scene(&mut commands, &asset_server);
}

Rules for the imperative generator:

  • commands is always a parameter. asset_server appears only when a sprite image or asset mesh is used. meshes and materials are both emitted whenever the scene contains any Mesh3d (primitive or asset), because the mesh handle and the inline material are created at the same time.
  • Each entity spawns a tuple of Name::new(..), a Transform, then its components. Omitted Sprite fields use ..default().
  • Hierarchy uses .with_children(|parent| { ... }); sibling order is preserved.
  • Primitive meshes and materials use the same constructors as BSN. Text2d uses Text2d::new, TextFont { font_size: FontSize::Px(..), ..default() } and TextColor(..).
  • Custom components are emitted as struct literals; the type must be imported where the generated code compiles.
  • Empty scenes are an error, exactly as for BSN.

The imperative form is useful when a project cannot use BSN macros or when code must run against an API where a scene is spawned imperatively. BSN remains the primary, recommended export.