Tainted\\Coders

Bevy Scenes

Bevy version: 0.19Last updated:

Scenes represent collections of entities and their components that can be spawned and despawned from the world as a single group.

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.

A key goal of scenes is that they can be serialized into file based representations of a particular world. Everything is serialized into a file and then reinitialized when the scene is loaded.

To accomplish this, Bevy uses reflection to store the types we register and determine how to deserialize the scene files into our world.

Scenes are made up of 4 core concepts:

  1. Scene, a trait that describes what a spawned entity should look like. Both the bsn! macro and tuples of Scenes implement it, which is what makes scenes composable.
  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

Defining a scene

We define scenes in code using the bsn! macro.

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

You can read more about the full bsn! syntax on the Bevy Scene Notation guide.

This macro is defining a corresponding Template type for each type we use in our Scene.

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 is going to call Scene::resolve which produces a ResolvedScene that can 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:

  1. Immediately with spawn_scene which returns an error if any dependencies are not yet loaded
  2. Queued with queue_spawn_scene which will wait for all dependencies ( to be loaded before spawning
use bevy::ecs::VariantDefaults;
use bevy::prelude::*;

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

#[derive(Component, Clone, Default, VariantDefaults)]
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 we derived the trait VariantDefaults for our enum. If you don't then you need to manually create a default_red and default_blue static methods, one for each member of the enum.

Saving a scene

Even though the Bevy team is working on the .bsn format it is not yet released, so there is no asset loader for it. The following is referencing the older, officially supported scene format.

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

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

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

To save a scene we first have to create a DynamicWorld from the world. 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>,
}

We save scenes to a file by using the DynamicWorld::serialize method:

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

  // Scenes can be serialized like this:
  let type_registry = world.resource::<AppTypeRegistry>();
  let type_registry = type_registry.read();
  let serialized_scene = scene.serialize(&type_registry).unwrap();

  // Showing the scene in the console
  info!("{}", serialized_scene);

  // Writing the scene 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 scene RON data to file
      File::create(format!("assets/{NEW_SCENE_FILE_PATH}"))
        .and_then(|mut file| file.write(serialized_scene.as_bytes()))
        .expect("Error while writing scene to file");
    })
    .detach();
}

Loading a serialized scene

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

There are 3 ways to spawn scenes:

  1. Using WorldInstanceSpawner::spawn_dynamic
  2. Adding the DynamicWorldRoot component to an entity
  3. Using the DynamicWorldBuilder to construct a DynamicWorld from a World

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.ron";

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

After the DynamicWorldRoot is fully loaded it will publish a WorldInstanceReady event that you can listen to and trigger behavior.

Additionally, a WorldInstance component is added to the entity holding the scene root. It wraps the InstanceId of the spawned scene which can be used with the WorldInstanceSpawner to interact with the newly loaded scene:

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 scene, 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 scene can be spawned multiple times, or modified and reloaded.

Components that are Reflect are automatically registered to the type registry which is what let's Bevy figure out how to deserialize your components. This can be controlled through the reflect_auto_register feature flag. You can turn it off with the reflect_auto_register_static flag which will overwrite it.

The FromWorld trait determines how your component is constructed when it loads into the World.

Implementing FromWorld on a component will let you customize initialization using the current Worlds resources:

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(),
    }
  }
}

The #[reflect(skip_serializing)] attribute can be used for fields which should not be serialized in the scene.

Reflect on the other hand is providing Bevy with the ability to take dynamic types defined in your scene and downcasting them into concrete types at runtime when your scene loads.