BSN: scene.spawn() system ergonomics (#23868)

# Objective

Spawning a scene on startup is a common pattern. Lets make it easier to
do so!

## Solution

- Add `SpawnSystem` and `SpawnListSystem` traits that are implemented
for functions that return scenes / scene lists, and return a system that
spawns the scene / handles errors.

### Before
```rust
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(world: &mut World) -> Result {
    world.spawn_scene_list(bsn_list![Camera2d, ui()])?;
    Ok(())
}
```

### After
```rust
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, scene.spawn())
        .run();
}

fn scene() -> impl SceneList {
    bsn_list![Camera2d, ui()]
}
```

This cuts out some boilerplate. It also further encourages people to
define standalone "scene functions" rather than embedding them in code,
which is generally a good pattern.
This commit is contained in:
Carter Anderson
2026-04-19 10:25:21 -07:00
committed by GitHub
parent c847aafdbb
commit aa5322ed0f
6 changed files with 50 additions and 19 deletions
+3 -1
View File
@@ -509,7 +509,7 @@ pub mod prelude {
pub use crate::{
bsn, bsn_list, on, template_value, CommandsSceneExt, EntityCommandsSceneExt,
EntityWorldMutSceneExt, PatchFromTemplate, PatchTemplate, Scene, SceneList,
ScenePatchInstance, WorldSceneExt,
ScenePatchInstance, SpawnListSystem, SpawnSystem, WorldSceneExt,
};
}
@@ -523,6 +523,7 @@ mod scene;
mod scene_list;
mod scene_patch;
mod spawn;
mod spawn_system;
pub use bevy_scene_macros::*;
pub use resolved_scene::*;
@@ -530,6 +531,7 @@ pub use scene::*;
pub use scene_list::*;
pub use scene_patch::*;
pub use spawn::*;
pub use spawn_system::*;
use bevy_app::{App, Plugin, SceneSpawnerSystems, SpawnScene};
use bevy_asset::AssetApp;
+35
View File
@@ -0,0 +1,35 @@
use crate::{Scene, SceneList, WorldSceneExt};
use bevy_ecs::{error::Result, world::World};
/// Returns a system that spawns the given [`Scene`]. This should generally only be added to
/// schedules that run once, such as [`Startup`](bevy_app::Startup).
pub trait SpawnSystem {
/// Returns a system that spawns the given [`Scene`]. This should generally only be added to
/// schedules that run once, such as [`Startup`](bevy_app::Startup).
fn spawn(self) -> impl FnMut(&mut World) -> Result;
}
impl<F: FnMut() -> S + Send + Sync + 'static, S: Scene> SpawnSystem for F {
fn spawn(mut self) -> impl FnMut(&mut World) -> Result {
move |world: &mut World| -> Result {
world.spawn_scene(self())?;
Ok(())
}
}
}
/// Returns a system that spawns the given [`SceneList`]. This should generally only be added to
/// schedules that run once, such as [`Startup`](bevy_app::Startup).
pub trait SpawnListSystem {
/// Returns a system that spawns the given [`SceneList`]. This should generally only be added to
/// schedules that run once, such as [`Startup`](bevy_app::Startup).
fn spawn(self) -> impl FnMut(&mut World) -> Result;
}
impl<F: FnMut() -> S + Send + Sync + 'static, S: SceneList> SpawnListSystem for F {
fn spawn(mut self) -> impl FnMut(&mut World) -> Result {
move |world: &mut World| -> Result {
world.spawn_scene_list(self())?;
Ok(())
}
}
}
+3 -4
View File
@@ -4,13 +4,12 @@ use bevy::{prelude::*, text::FontSourceTemplate};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Startup, scene.spawn())
.run();
}
fn setup(world: &mut World) -> Result {
world.spawn_scene_list(bsn_list![Camera2d, ui()])?;
Ok(())
fn scene() -> impl SceneList {
bsn_list![Camera2d, ui()]
}
fn ui() -> impl Scene {
+3 -5
View File
@@ -10,7 +10,6 @@ use bevy::{
tokens, FeathersPlugins,
},
prelude::*,
scene::prelude::Scene,
ui_widgets::Activate,
};
@@ -31,7 +30,7 @@ fn main() {
// Configure feathers to use the dark theme
.insert_resource(UiTheme(create_dark_theme()))
.insert_resource(Counter(0))
.add_systems(Startup, setup)
.add_systems(Startup, scene.spawn())
.add_systems(
Update,
update_counter_text.run_if(resource_changed::<Counter>),
@@ -39,9 +38,8 @@ fn main() {
.run();
}
fn setup(world: &mut World) -> Result {
world.spawn_scene_list(bsn_list![Camera2d, demo_root()])?;
Ok(())
fn scene() -> impl SceneList {
bsn_list![Camera2d, demo_root()]
}
fn demo_root() -> impl Scene {
+3 -5
View File
@@ -26,7 +26,6 @@ use bevy::{
},
input_focus::{tab_navigation::TabGroup, AutoFocus, InputFocus},
prelude::*,
scene::prelude::Scene,
text::{EditableText, TextEdit, TextEditChange},
ui::{Checked, InteractionDisabled},
ui_widgets::{
@@ -64,14 +63,13 @@ fn main() {
rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7),
hsl_color: palettes::tailwind::AMBER_800.into(),
})
.add_systems(Startup, setup)
.add_systems(Startup, scene.spawn())
.add_systems(Update, update_colors)
.run();
}
fn setup(world: &mut World) -> Result {
world.spawn_scene_list(bsn_list![Camera2d, demo_root()])?;
Ok(())
fn scene() -> impl SceneList {
bsn_list![Camera2d, demo_root()]
}
fn demo_root() -> impl Scene {
+3 -4
View File
@@ -15,7 +15,7 @@ fn main() {
App::new()
.add_plugins((DefaultPlugins, FeathersPlugins))
.insert_resource(UiTheme(create_dark_theme()))
.add_systems(Startup, setup)
.add_systems(Startup, scene.spawn())
.run();
}
@@ -23,9 +23,8 @@ fn on_virtual_key_pressed(virtual_key_pressed: On<VirtualKeyPressed<&'static str
println!("key pressed: {}", virtual_key_pressed.key);
}
fn setup(world: &mut World) -> Result {
world.spawn_scene_list(bsn_list![Camera2d, keyboard()])?;
Ok(())
fn scene() -> impl SceneList {
bsn_list![Camera2d, keyboard()]
}
fn keyboard() -> impl Scene {