Tainted\\Coders

Bevy Scenes

Bevy version: 0.20Last updated:
  1. Defining a scene
  2. Spawning a scene
  3. Knowing when a scene is ready
  4. Saving a world
  5. Loading a serialized world

Scenes are how we spawn and despawn groups of entities together. A Scene describes a single entity, while a SceneList describes a collection where each entry becomes its own entity.

Bevy 0.19 introduced Bevy Scene Notation (BSN) and with it the planned .bsn asset format. That format is not yet released. Bevy has no asset loader for it, so BSN is currently focused on spawning complex collections of entities in code.

Bevy actually ships two related but separate scene systems:

  1. BSN scenes, the new in-code system built around the bsn! macro. These are not serializable today.
  2. World serialization, the older file-based system that saves and loads whole worlds as .scn / .scn.ron files. It uses reflection to register the types we want to deserialize back into our game world.

This guide covers the BSN concepts and then shows how to save and load the serialized world format.

The BSN scene system is made up of these core concepts:

  1. Scene, a trait that describes what a spawned entity should look like. A bsn! expression and tuples of Scenes both implement it, which is what makes scenes composable. A Scene is always a single root entity.
  2. SceneList, a trait describing a collection of scenes that can be spawned together, where each entry in the list becomes its own entity.
  3. Template, which allows defining scenes without needing to pass in a bunch of their dependencies.
  4. RelatedScenes, for defining relationships between scenes.
  5. SceneComponent, which associates a Scene with a component so that whole subtrees load together.

See the Bevy Scene Notation guide for a deeper look at composition, patching, and scene components.

Defining a scene

We define scenes in code using the bsn! macro.

fn blue_player() -> impl Scene {
  bsn! {
    Team::Blue
    Player { score: 0 }
  }
}

Each type used in a Scene needs to implement Template (or FromTemplate). Bevy auto-implements these for any type that derives both Default and Clone. bsn! uses these templates to build the actual components only when the scene is spawned.

It's important to remember that returning a scene from the blue_player function did not spawn anything or even initialize any components in memory.

Instead, scenes (or groups of scenes) go through a process of being resolved. Bevy's scene plugin calls Scene::resolve, which mutates a ResolvedScene that is then applied to a specific Entity.

We don't resolve them ourselves, instead we use spawn_scene and queue_spawn_scene which does this resolution for us.

Spawning a scene

Scenes can be spawned in one of two ways:

use bevy::prelude::*;

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

#[derive(Component, Clone, Default)]
enum Team {
  #[default]
  Red,
  Blue,
}

fn spawn_scene_immediate(mut commands: Commands) {
  // Spawn a single player on the blue team
  commands.spawn_scene(blue_player());
}

fn spawn_scene_delayed(mut commands: Commands) {
  // Wait for any dependencies to load before spawning the scene
  commands.queue_spawn_scene(blue_player());
}

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

Notice that our Team enum derives Clone and Default. BSN requires those two traits for an enum to be used as a component without us having to implement FromTemplate ourselves.

Knowing when a scene is ready

A scene is resolved and spawned in stages. These stages might depend on an asset that has not finished loading. Or we might queued it to spawn later. That means we cannot assume a scene is complete the moment spawn_scene returns.

Bevy triggers a Ready entity event for each entity in a scene once that entity and all of its descendants have been fully spawned. Unlike the top-down Add lifecycle event, which fires before children exist, Ready runs bottom-up so the entire hierarchy is available. It is the right hook for logic that needs the finished scene:

fn ready_button() -> impl Scene {
  bsn! {
      Node { width: px(100), height: px(50) }
      on(|ready: On<Ready>| {
          info!("the full scene is ready!")
      })
  }
}

You can also register a global observer with App::add_observer to react to scenes becoming ready from outside the scene itself.

Saving a world

The .bsn format is not yet released and has no asset loader, so BSN scenes cannot be written to disk. The following uses Bevy's file-based world serialization format instead.

Worlds can be saved into a .scn or .scn.ron file. The format of the file is based on Rusty Object Notation (RON).

Here is what a basic .scn.ron file looks like:

(
  resources: {
    "scenes::ResourceA": (
      score: 2,
    ),
  },
  entities: {
    4294967296: (
      components: {
        "bevy_transform::components::transform::Transform": (
          translation: (0.0, 0.0, 0.0),
          rotation: (0.0, 0.0, 0.0, 1.0),
          scale: (1.0, 1.0, 1.0),
        ),
        "scenes::ComponentB": (
          value: "hello",
        ),
        "scenes::ComponentA": (
          x: 1.0,
          y: 2.0,
        ),
      },
    ),
    4294967297: (
      components: {
        "scenes::ComponentA": (
          x: 3.0,
          y: 4.0,
        ),
      },
    ),
  }
)

To save a world we first have to create a DynamicWorld from it. This struct stores the resources and entities from the world and allows them to be easily serialized.

// https://docs.rs/bevy/latest/bevy/world_serialization/struct.DynamicWorld.html
pub struct DynamicWorld {
    pub resources: Vec<Box<dyn PartialReflect>>,
    pub entities: Vec<DynamicEntity>,
}

pub struct DynamicEntity {
    pub entity: Entity,
    pub components: Vec<Box<dyn PartialReflect>>,
}

DynamicWorld::from_world extracts every reflect-registered component from the entire world. That is convenient for a demo, but usually too much for a real game. For finer control, build the DynamicWorld with DynamicWorldBuilder, which can select entities and filter components:

let type_registry = world.resource::<AppTypeRegistry>().read();
let scene = DynamicWorldBuilder::from_world(world, &type_registry)
    .extract_entity(player_entity)
    .deny_component::<Visibility>()
    .build();

The example below uses the simple from_world form. We save worlds to a file by using the DynamicWorld::serialize method:

fn save_scene_system(world: &mut World) {
  let dynamic_world = DynamicWorld::from_world(world);

  // Dynamic worlds can be serialized like this:
  let type_registry = world.resource::<AppTypeRegistry>();
  let type_registry = type_registry.read();
  let serialized_world = dynamic_world.serialize(&type_registry).unwrap();

  // Showing the serialized world in the console
  info!("{}", serialized_world);

  // Writing the world to a new file. Using a task to avoid calling the
  // filesystem APIs in a system as they are blocking This can't work in WASM as
  // there is no filesystem access
  #[cfg(not(target_arch = "wasm32"))]
  IoTaskPool::get()
    .spawn(async move {
      // Write the world RON data to file
      File::create(format!("assets/{NEW_SCENE_FILE_PATH}"))
        .and_then(|mut file| file.write(serialized_world.as_bytes()))
        .expect("Error while writing world to file");
    })
    .detach();
}

Loading a serialized world

When Bevy loads the world file, it needs to deserialize it into actual components and entities that it loads into your world.

There are two ways to instantiate a DynamicWorld:

  1. Using the WorldInstanceSpawner resource with spawn_dynamic (deferred), spawn_dynamic_sync (immediate), or spawn_dynamic_as_child.
  2. Adding the DynamicWorldRoot component to an entity, which spawns the world's entities as children of that entity.

The easiest of these is simply spawning a DynamicWorldRoot. The WorldAssetLoader reads the .scn.ron file and produces a DynamicWorld asset, and the WorldInstanceSpawner takes care of deserializing it into your world:

const SCENE_FILE_PATH: &str = "scene.scn.ron";

fn load_scene_system(mut commands: Commands, asset_server: Res<AssetServer>) {
  // Spawning a `DynamicWorldRoot` creates a new entity and spawns instances
  // of the world's entities as children of that entity.
  commands.spawn(DynamicWorldRoot(asset_server.load(SCENE_FILE_PATH)));
}

After the DynamicWorldRoot instance is fully spawned, Bevy triggers a WorldInstanceReady entity event on the parent entity. We can observe it with On<WorldInstanceReady> which gives us the entity and instance_id we spawned. When an instance is spawned through the spawner without a parent, entity is Entity::PLACEHOLDER.

Additionally, a WorldInstance component is added to the entity holding the scene root. It dereferences to the InstanceId of the spawned world, which can be passed to methods such as WorldInstanceSpawner::despawn_instance_sync to interact with the newly loaded world:

fn despawn_scene(
  trigger: On<bevy::world_serialization::WorldInstanceReady>,
  mut spawner: ResMut<WorldInstanceSpawner>,
  world: &mut World,
) {
  spawner.despawn_instance_sync(world, &trigger.instance_id);
}

When we load a world, Bevy deserializes the file into a DynamicWorld asset and then writes its entities and resources into your main world. The asset stays in memory so the same world can be spawned multiple times, or modified and reloaded.

Deriving Reflect on a component registers it with the type registry, which is how Bevy knows how to deserialize it.

When a component is deserialized, Bevy constructs it by trying, in order:

  1. Reflected FromReflect (generated by #[derive(Reflect)])
  2. Reflected Default plus apply
  3. Finally reflected FromWorld

FromWorld is therefore a fallback that lets you customize initialization using the current world's resources, and it only participates if you add #[reflect(FromWorld)] to the type.

impl FromWorld for ComponentB {
  fn from_world(world: &mut World) -> Self {
    let time = world.resource::<Time>();
    ComponentB {
      _time_since_startup: time.elapsed(),
      value: "Default Value".to_string(),
    }
  }
}