Bevy Scenes
Scenes represent collections of entities that can be spawned and despawned from the world as a single group.
Scenes can be serialized into file based representations of a particular world setup. Everything is 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 DynamicSceneRoot
component.
Saving a scene
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,
),
},
),
}
)
To save a scene we first have to create a DynamicScene
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/scene/prelude/struct.DynamicScene.html
pub struct DynamicScene {
pub resources: Vec<Box<dyn PartialReflect>>,
pub entities: Vec<DynamicEntity>,
}
We 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();
}
Loading a 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:
- Using
SceneSpawner::spawn_dynamic
- Adding the
DynamicSceneRoot
component to an entity - Using the
DynamicSceneBuilder
to construct aDynamicScene
from aWorld
The easiest of these is simply spawning a DynamicSceneRoot
. 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.
let scene = asset_server.load(SCENE_FILE_PATH);
commands.spawn(DynamicSceneRoot(scene));
}
Once the scene has been loaded, a SceneInstance
component is added to the component which can be used with the SceneSpawner
to interact with the scene.
For example we could despawn all our loaded scenes:
use bevy::scene::SceneInstance;
fn despawn_all_scenes(
query: Query<&SceneInstance>,
mut spawner: ResMut<SceneSpawner>,
world: &mut World,
) {
// Despawning the scene root entity will also despawn all of its children
for instance in &query {
spawner.despawn_instance_sync(world, instance);
}
}
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 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.