Tainted\\Coders

Bevy Scene Notation (BSN)

Bevy version: 0.19Last updated:

Bevy Scene Notation (BSN) was introduced in Bevy 0.19.

Scenes are how we spawn and despawn groups of entities. They can be saved or loaded from disk or be defined inline in code.

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

Whats so good about BSN?

Before 0.19 spawning something that actually renders involved bringing in a lot of extra stuff like meshes, materials, and the asset_server. 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
    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:

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<Mesh3d> and Assets<MeshMaterial3d> 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: Send + Sync + 'static {
  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.

Its rare but possible that you have to implement your own FromTemplate implementation. asset_value is one example of this. This is a special template that takes in a path to an asset and produces the actual asset once the scene is spawned inside this trait.

Looking closer we can see how values implicitly have .into() called on them:

#[derive(Component, FromTemplate)]
struct Sprite {
    // &str is implicitly converted to Handle<Image> via FromTemplate
    image: Handle<Image>,
    tint: Color,
}

bsn! { Sprite { 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(), which calls AssetServer::load when being spawned.

Writing BSN

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

Bevy does not yet ship a first-party .bsn asset loader, so even though we can output the format there is no built-in reader. Going forward BSN is the recommended default format.

Each bsn! call returns a Scene which defines 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 system. Any system that returns either a Scene or SceneList can be automatically turned into a system that adds the scene to the world.

Field values can be arbitrary rust expressions:

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

Patching rules

When combining scenes like this its important to understand that overriding templates are acting like patches on top of each other. In the case of a conflict the last write wins, same as components.

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 and torn down together.

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

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

impl Car {
  fn scene() -> impl Scene {
    bsn! {
      Transform { translation: Vec3 { x: 10. } }
      Children [
        FrontWheel,
        BackWheel,
      ]
    }
  }
}

Scene components are spawned with the @ prefix:

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

Spawning a scene component directly without the scene will log an error. This guarantees that if the component is present the full scene will be there too.

Scene components can also accept "props" for dynamic behavior. A props struct like CarConfig is passed into the scene function and lets you change what gets spawned:

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

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

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

  bsn! {
    #Car
    wheels
  }
}

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 supports referencing entities via a provided Name component

We reference that entity via its name by prefixing the 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)),
  ]
}

Lists

A Scene corresponds to a single entity, but we can also make lists of entities with a SceneList

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!")
      })
  }
}