Tainted\\Coders

Bevy Scene Notation (BSN)

Bevy version: 0.19Last updated:

Scenes are how we spawn and despawn groups of entities together. Bevy Scene Notation (BSN) was introduced in Bevy 0.19 and is how we define these scenes in code.

In 0.19 the new BSN-based scenes live in the bevy_scene crate. The older file-based format (.scn/.scn.ron) for saving and loading worlds from disk still exists, but it was renamed to world serialization (bevy_world_serialization, with types like WorldAsset and DynamicWorld).

BSN solves a few problems with constructing complicated trees of entities:

  1. Composability of smaller scenes into larger ones without duplication
  2. Granular overrides to change a single field without needing to override everything
  3. Avoidance of boilerplate with automatic scene initialization

What's so good about BSN?

Before 0.19 spawning something that actually renders involved bringing in a lot of extra stuff like meshes and materials. So even the most simple stuff looked like this:

fn spawn_cube(
  mut commands: Commands,
  mut meshes: ResMut<Assets<Mesh>>,
  mut materials: ResMut<Assets<StandardMaterial>>,
) {
  let cube = Cuboid::from_length(5.);

  commands
    .spawn((
      Mesh3d(meshes.add(cube)),
      MeshMaterial3d(materials.add(Color::WHITE)),
      Transform::from_xyz(0.0, 0.5, 0.0),
    ))
    .observe(rotate_on_drag);
}

It meant writing many systems which did nothing but spawn groups of components together.

With BSN we can simply declare the template of our entity:

fn cube() -> impl Scene {
  bsn! {
    Mesh3d(asset_value(Cuboid::from_length(5.)))
    MeshMaterial3d<StandardMaterial>(asset_value(Color::WHITE))
    Transform::from_xyz(0.0, 0.5, 0.0)
    on(rotate_on_drag)
  }
}

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

The good thing is that we didn't have to fret about how to get these values on our entities, we simply declared a template for them. Only when we spawn our scene do the actual components get created.

Another good example is writing a custom command that spawns something complex. This is what it looks like without BSN:

struct SpawnPlanetWithoutBSN {
  radius: f32,
  position: Vec2,
}

impl Command for SpawnPlanetWithoutBSN {
  type Out = ();

  fn apply(self, world: &mut World) {
    // Resource scope works by removing the resource from the world
    // and then running our closure. This lets us mutably borrow
    // the world safely multiple times
    let mesh_handle =
      world.resource_scope(|_world, mut meshes: Mut<Assets<Mesh>>| {
        let circle = Circle::new(self.radius);
        meshes.add(circle)
      });

    let color_material = world.resource_scope(
      |_world, mut materials: Mut<Assets<ColorMaterial>>| {
        let blue = Color::srgb(0.0, 0.0, 1.0);
        materials.add(blue)
      },
    );

    world.spawn((
      Planet::new(self.radius),
      Mesh2d(mesh_handle.clone()),
      MeshMaterial2d(color_material.clone()),
      Transform::from_translation(self.position.extend(0.)),
    ));
  }
}

Compare that to a custom command that spawns the same thing with BSN:

struct SpawnPlanetWithBSN {
  radius: f32,
  position: Vec2,
}

fn planet(position: Vec3, radius: f32) -> impl Scene {
  bsn! {
    #Planet
    Transform::from_translation(position)
    Mesh2d(asset_value(Circle::new(radius)))
    MeshMaterial2d<ColorMaterial>(asset_value(Color::srgb(0.0, 0.0, 1.0)))
  }
}

impl Command for SpawnPlanetWithBSN {
  type Out = ();

  fn apply(self, world: &mut World) {
    world
      .spawn_scene(planet(self.position.extend(0.), self.radius))
      .unwrap();
  }
}

BSN defines templates

BSN expressions define templates for components rather than the actual components.

This is what enables us to avoid having to worry about Assets<Mesh> and Assets<StandardMaterial> in the example above.

This works for any Handle<A> field where the component derives FromTemplate.

A Template is a fancy constructor that produces another type like a Component.

pub trait Template {
  type Output;
  fn build_template(&self, context: &mut TemplateContext) -> Result<Self::Output>;
  fn clone_template(&self) -> Self;
}

These templates have access to 3 things:

  1. The World
  2. The current entity
  3. The scene spawn context

Having both Default and Clone on a type allows Bevy to auto generate a FromTemplate implementation for it. It's rare but possible that you may have to implement your own FromTemplate implementation.

Looking closer at the syntax we should notice how values implicitly have .into() called on them:

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

bsn! {
  Sprite {
    // "player.png" is implicitly converted to HandleTemplate<Image> via .into()
    image: "player.png",
    tint: Color::WHITE
  }
}

The FromTemplate derive generates a SpriteTemplate companion struct. The string "player.png" auto-converts to a HandleTemplate<Image> via .into(), whose build_template calls AssetServer::load when the scene is spawned.

We use asset_value when we have a Rust value that we want to add to the Assets<T> collection for us. Basically:

Reusing the same template returned by asset_value will re-use the same asset. The asset_value returns a HandleTemplate containing an ArcMutexValue<T> which is turned into a Handle<T> by adding the value to Assets<T> and caching the resulting handle on the template.

Writing BSN

BSN is capable of spawning anything in the ECS. In particular it makes it much easier to write Bevy UI code.

Bevy doesn't ship a .bsn asset format yet, the format is not yet released and there is no asset loader for it. BSN scenes can only be defined in code for now. The Bevy team intends to offer a .bsn asset format in a future release.

Each bsn! call returns a value which implements the Scene trait, describing a single Entity.

#[derive(Component, Clone, Default)]
struct Ship;

#[derive(Component, Clone, Default)]
struct Player {
  score: usize,
}

fn spawn_scene(mut commands: Commands) {
  commands.spawn_scene(bsn! {
      Player
      Ship
  });
}

With BSN you don't specify every field or use ..Default::default(). Fields you don't specify have their defaults placed instead.

We can make our own functions that return scenes:

fn button() -> impl Scene {
  bsn! {
      Button
      Node { width: px(100) }
  }
}

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

Notice that in main we call .spawn() on our function. Any argument-less function that returns a value implementing either the Scene or SceneList trait can be turned into a system that adds the scene to the world.

This example uses the immediate spawn_scene. If a scene depends on an asset that may not be loaded yet, such as a .bsn file, use queue_spawn_scene instead.

Field values can be arbitrary Rust expressions:

fn increment_score(current_points: usize) -> impl Scene {
  bsn! {
      Player { score: {current_points + 10} }
  }
}

Enums are somewhat special. BSN requires a default for every variant, not just the enum as a whole. Deriving VariantDefaults (from bevy::ecs) generates the default_{variant_lower} methods it looks for, or you can define them by hand. Deriving FromTemplate also has this effect.

#[derive(Default)]
enum WheelSize {
  #[default]
  Standard,
  Wide,
}

Patching rules

When combining scenes like this its important to understand that overriding templates act like patches on top of each other. Patches merge field by field, and when two patches set the same field the last write wins.

Two scenes that define the same component are combined, even down to the various fields:

fn button() -> impl Scene {
  bsn! {
      Button
      Node { width: px(100) }
  }
}

fn my_button() -> impl Scene {
  bsn! {
      button()
      Node { height: px(100) }
  }
}

So my_button would create a Node with both a width and height of 100px.

Comma rules

Inside a scene list (Children [...], bsn_list![...]), commas separate entities. Whitespace separates components on the same entity:

// One child with A and B
Children [ A B ]

// Two children, one with A, one with B
Children [ A, B ]

// Two children, clearer with parentheses
Children [ (A B), C ]

Scene Components

It has previously been difficult to ensure trees of components all got loaded up together.

BSN solves the loading problem by associating a Scene with a Component via SceneComponent. These are essentially aggregates that respond to a scene method.

#[derive(SceneComponent, Default, Clone)]
#[scene(CarConfig)]
struct Car {
  boost: f32,
}

impl Car {
  fn scene(props: CarConfig) -> impl Scene {
    let wheels: Box<dyn Scene> = match props.wheels {
      WheelSize::Standard => Box::new(bsn! { SlimWheels }),
      WheelSize::Wide => Box::new(bsn! { WideWheels }),
    };

    bsn! {
      Transform { translation: Vec3 { x: 10. } }
      wheels
      Children [
        FrontWheel,
        BackWheel,
      ]
    }
  }
}

Scene components are spawned with the @ prefix:

fn spawn_car(mut commands: Commands) {
  commands.spawn_scene(bsn! {
    @Car {
      @wheels: WheelSize::Wide,
      boost: 100.,
    }
  });
}

Spawning a scene component directly without the scene will log an error in debug builds. This guarantees that at spawn time, if the component is present the full scene will be there too.

Scene components can also accept "props" for dynamic behavior.

In the example above we added our props by adding #[scene(CarConfig)] to the Car component. This allows us to pass in those fields when spawning the scene using the @field syntax (@wheels in the example above).

#[derive(Default)]
struct CarConfig {
  wheels: WheelSize,
}

#[derive(Default)]
enum WheelSize {
  #[default]
  Standard,
  Wide,
}

In spawn_car above, @wheels is a prop (consumed by Car::scene), while boost is a normal field on the Car component. Props are evaluated before component field patches, so the scene they produce can still be patched by later scenes.

Relationships

BSN supports relationships

fn spawn_scene(mut commands: Commands) {
  commands.spawn_scene(bsn! {
    Player
    Children [
      Sword,
      Shield,
    ]
  });
}

It even works for custom relationships you define:

#[derive(Component, Clone, Default)]
struct Ship;

#[derive(Component, Clone, Default)]
struct Player;

#[derive(Component, Clone, Default)]
struct GunTurret;

#[derive(Component)]
#[relationship(relationship_target = ShipAttachments)]
struct AttachedToShip(Entity);

#[derive(Component)]
#[relationship_target(relationship = AttachedToShip)]
struct ShipAttachments(Vec<Entity>);

fn player_ship() -> impl Scene {
  bsn! {
      player()
      Ship
      // Works with custom relationships too!
      ShipAttachments [
          GunTurret,
      ]
  }
}

References

BSN can name entities by adding a Name component, and we reference such an entity by prefixing its name with #.

If we were defining the Name manually it would look like:

fn player_manually() -> impl Scene {
  bsn! {
      Name("Joe")
      Player
  }
}

Which is shortened with the # prefix like:

fn player() -> impl Scene {
  bsn! {
      #Joe
      Player
  }
}

This lets us reference entities from the same scene by name:

#[derive(Component, FromTemplate)]
struct EmployedBy(Entity);

fn boss() -> impl Scene {
  bsn! {
      #Boss
      Children [
          #Joe EmployedBy(#Boss)
      ]
  }
}

This works the same way for lists:

#[derive(Component, FromTemplate)]
struct ReportsTo(Entity);

fn employees() -> impl SceneList {
  bsn_list! [
      (#Joe ReportsTo(#Jane)),
      (#Jane ReportsTo(#Joe)),
  ]
}

Each bsn! call has its own name scope, so a reference is only visible within the macro invocation that defines it (or passed to another scene as an EntityTemplate). In a bsn_list!, all root entities share a single scope, which is what lets sibling scenes reference each other as above.

Lists

A Scene describes a single entity, but we can also make lists of entities with a SceneList (both of these are traits).

fn players() -> impl SceneList {
  bsn_list! [
      (#Car1 Team::Blue),
      (#Car2 Team::Red),
  ]
}

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

Observers

Entities we make through scenes can be hooked up to observe events

fn button() -> impl Scene {
  bsn! {
      Node { width: px(100), height: px(50) }
      on(|press: On<Pointer<Press>>| {
          info!("button pressed!")
      })
  }
}