3 unstable releases
0.2.0 | Nov 19, 2022 |
---|---|
0.1.1 | Aug 21, 2022 |
0.1.0 | Aug 18, 2022 |
#1535 in Game dev
31KB
329 lines
Helpers for working with Bevy Scenes
Version compatibility table:
Bevy Version | Crate Version |
---|---|
0.9 |
0.2 |
0.8 |
0.1 |
What is this about?
For the uninitiated: Bevy Scenes are a way to store some predefined Bevy ECS data (arbitrary entities with components on them) and be able to instantiate them later, as many times as you want!
You can use Scenes for many use cases:
- Loading your game levels/maps (or parts of them)
- Preconfigured game units/modules (some other engines call this "prefabs")
- Saving game state
- …
Until now, creating Bevy scenes, and working with the Bevy scene format,
was very unapproachable. While Bevy makes it easy to use existing scenes in
your game (just spawn them with DynamicSceneBundle
), there was no easy
way to create them. Bevy offers nothing built-in for easily exporting things
into a scene, and no APIs to help you create your scenes.
Thanks to this crate, you can now easily create your own scenes, containing whatever you want, by exporting a custom selection of things from any Bevy app!
Scene Export
You can create Bevy DynamicScene
s that include whatever exact selection
of entities and components you want!
The selections can be done with a syntax similar to Bevy Queries.
This create will then copy the relevant data, based on your selections,
from your World
, and create a scene from it!
There are two "modes" for component selection:
- "all components": when you just select entities, without specifying components. When generating the scene, each entity will be scanned to autodetect all compatible components and include them in the scene
- "explicit component list": you specify exactly what components to include (they may be required or optional), and only those will be exported (incompatible types will be skipped)
// quick: make a scene with all entities that match a given query filter
// (all components will be included)
let my_scene = scene_from_query_filter::<(
With<ComponentA>,
Without<ComponentB>,
)>(&mut world);
// quick: same thing, but only with specific components
let my_scene = scene_from_query_components::<
// the components to include
// (require A and B, only select entities that have them)
// (C is optional, include it if it is present)
(&ComponentA, &ComponentB, Option<&ComponentC>),
// additional filter, to select only specific entities
(With<IWantInMyScene>, Without<DevOnlyDoNotExport>),
>(&mut world);
If you want more flexibility, you can use SceneBuilder
, which lets you
accumulate multiple selections incrementally, and then create a scene with
everything you added. The component selection can be controlled with
per-entity granularity.
let mut builder = SceneBuilder::new(&mut world);
// include entities using query filter:
// all entities with `GameItem`
// all of their components will be included
builder.add_from_query_filter::<With<GameItem>>();
// only specific components for these entities
builder.add_with_components::<
// the components to select
(&Transform, &Health, &BaseStats, Option<&SpecialAbility>),
// query filter to select entities
Or<(With<Player>, With<Enemy>)>
>();
// also add some special entities
builder.add_entity(e);
builder.add_entities(&[magic1, magic2, magic3]);
builder.add_components_to_entities::<
&Transform
>(special_entities.iter());
// we can ignore some components;
// they will never be implicitly included, unless they were
// explicitly selected for specific entities
builder.ignore_components::<(&GlobalTransform, &ComputedVisibility)>();
// now that we have selected everything, make a scene from it!
let my_scene = builder.build_scene();
Exporting to scene asset files
The above examples will create a DynamicScene
instance. However, if you
are simply interested in creating asset files, there are convenience methods
for exporting directly to a Bevy Scene RON Asset file:
// the standalone (simple) functions:
// like `scene_from_query_components`, but takes a file path
scene_file_from_query_components::</* … */>(world, "my_scene.scn.ron")
.expect("Scene file output failed");
// like `scene_from_query_filter`, but takes a file path
scene_file_from_query_filter::</* … */>(world, "my_scene2.scn.ron")
.expect("Scene file output failed");
// for `SceneBuilder`:
let mut builder = SceneBuilder::new(world);
// ... add stuff ...
// instead of `.build_scene()`:
builder.export_to_file("fancy_scene.scn.ron")
.expect("Scene file output failed");
All of the above methods also return the DynamicScene
in the Ok
result,
if the export was successful, in case you also want to do anything else with
the generated scene.
If you prefer not to use the convenience file export methods, you can output to a scene asset file manually like this:
// create the scene, using any of the methods shown before
let my_scene = /* ... */;
// need the type registry
let type_registry = world.resource::<TypeRegistry>();
// output the contents as a String
let data = my_scene.serialize_ron(type_registry)
.expect("Scene serialization failed");
// create a scene file (ending in `.scn.ron`)
std::fs::write("file.scn.ron", &data)
.expect("Writing to file failed");
Directly using a generated scene
If you want to generate a scene and use it straight away, without exporting/loading asset files, here is how.
To use the generated scene in your app, it needs to be added to the app's
assets (the Assets<DynamicScene>
resource), to get a handle.
There are convenience methods to do this for you, which return
Handle<DynamicScene>
instead of the bare DynamicScene
.
// the standalone (simple) functions:
// like `scene_from_query_components`, but adds it to the app for you
let handle = add_scene_from_query_components::</* … */>(world);
// like `scene_from_query_filter`, but adds it to the app for you
let handle = add_scene_from_query_filter::</* … */>(world);
// for `SceneBuilder`:
let mut builder = SceneBuilder::new(world);
// … add stuff …
// instead of `.build_scene()`:
let handle = builder.build_scene_and_add();
If you want to do it manually without the helper functions:
// get the `Assets<DynamicScene>` resource:
// (if we are in an exclusive system)
let mut assets = world.resource_mut::<Assets<DynamicScene>>();
// (in a regular system, you can use `ResMut<Assets<DynamicScene>>`)
// add it
let handle = assets.add(my_scene);
Later, you can spawn your scene from anywhere.
From a regular system:
commands.spawn_bundle(DynamicSceneBundle {
scene: handle,
..default()
});
With direct World access:
world.spawn().insert_bundle(DynamicSceneBundle {
scene: handle,
..default()
});
Warning
Warning! You must ensure that your component types:
- impl
Reflect
- reflect
Component
- are registered in the type registry
Otherwise, they will be silently ignored, and will be missing from your scene!
If you are serializing your scenes to asset files, you probably also want
FromReflect
, or otherwise you will not be able to load your scenes later!
#[derive(Component, Default, Reflect, FromReflect)]
#[reflect(Component)]
struct MyComponent;
(note: Bevy requires either a FromWorld
or a Default
impl, to derive Reflect
)
app.register_type::<MyComponent>();
This is required boilerplate, for all components that you want to use with scenes! Otherwise, things will silently not work.
"Blueprints" Pattern
This is a recommendation for how to make your workflow more flexible, and get the most usefulness out of Bevy scenes.
There are many component types in Bevy that represent internal state
computed at runtime, such as: GlobalTransform
, ComputedVisibility
,
Interaction
, etc….
Their values don't need to be persisted in scenes. You might want to omit them. This will also help your scenes be less bloated.
You might also want to omit other components of your choice, if you prefer to set them up using code, or initialize them to defaults.
let mut builder = SceneBuilder::new(world);
// add our game entities
builder.add_from_query_filter::<With<Enemy>>();
builder.add_from_query_filter::<With<Player>>();
builder.add_from_query_filter::<With<Powerup>>();
// …
// for our UI Nodes, only persist hierarchy + `Style`, `UiColor`, `Text`, `Button`
builder.add_with_components::<
(
(Option<&Parent>, Option<&Children>),
(&Style, &UiColor, Option<&Text>, Option<&Button>),
),
With<Node>
>();
// never include these components in any entity
builder.ignore_components::<
(&GlobalTransform, &Visibility, &ComputedVisibility, &CalculatedSize)
>();
let my_scene = builder.build_scene();
If you are creating such a "sparse" scene (we can call it "blueprint"), that only has some of the components and is missing others, you can write some code to populate the entities to "complete" their setup.
This is easily done using a system with an Added
query filter. This way,
you detect when such entities are spawned into the world, and you can do
any additional setup on them using code.
// ensure everything with a transform has all the transform/visibility stuff
fn setup_spatial(
mut commands: Commands,
// detect anything that was just added and needs setup
q_new: Query<
(Entity, &Transform),
(Added<Transform>, Without<GlobalTransform>)
>,
) {
for (e, transform) in q_new.iter() {
commands.entity(e).insert_bundle(SpatialBundle {
// preserve the transform
transform,
..Default::default()
});
}
}
/// complete the setup of our UI
/// (btw, this could be the starting point for the development
/// of a nice automatic theming system ;) hehe)
fn setup_ui(
mut commands: Commands,
// detect anything that was just added and needs setup
q_new: Query<
(Entity, &Style, Option<&UiColor>, Option<&Text>, Option<&Button>),
(Added<Style>, Without<Node>)
>,
) {
for (e, style, color, text, button) in q_new.iter() {
if let Some(text) = text {
commands.entity(e).insert_bundle(TextBundle {
text: text.clone(),
style: style.clone(),
..Default::default()
});
} else if let Some(_button) = button {
// (`Button` is just a marker)
commands.entity(e).insert_bundle(ButtonBundle {
style: style.clone(),
color: color.cloned().unwrap_or(UiColor(Color::NONE)),
..Default::default()
});
} else {
// this is a generic ui node
commands.entity(e).insert_bundle(NodeBundle {
style: style.clone(),
color: color.cloned().unwrap_or(UiColor(Color::NONE)),
..Default::default()
});
}
}
}
Dependencies
~29–43MB
~720K SLoC