Tainted\\Coders

Bevy Scenes

Bevy version: 0.14Last updated:

Scenes are file based representations of a particular world setup. Components and Resources are serialized into a file and then reinitialized when the scene is loaded.

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

Scenes can load into an entirely new world, or added to an existing one. If needed they can be spawned onto an entity using a SceneBundle.

Serializing

Scenes are 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,
        ),
      },
    ),
  }
)

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

fn save_scene_system(world: &mut World) {
  let scene = DynamicScene::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();
}

Deserializing

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

We load a scene as an asset from one of our systems. It uses the SceneLoader to deserialize everything:

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(DynamicSceneBundle {
    // Scenes are loaded just like any other asset.
    scene: asset_server.load(SCENE_FILE_PATH),
    ..default()
  });
}

When we load a scene Bevy is creating a new World parallel to your own and copies the data into your main world. You can never access this parallel world but it stays around.

Components registered with App::register_type that are in scenes must implement the Reflect and FromWorld traits.

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

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.