From 21dac5d3572ecce9743193fa833ffc96a29494ca Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 16 Mar 2025 14:20:16 -0700 Subject: [PATCH 1/9] Rename all the dynamic scene methods to include a `_dynamic` to make it clear. --- crates/bevy_scene/src/scene_spawner.rs | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 13713fe64c..b63bf47c1f 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -75,10 +75,10 @@ impl InstanceId { pub struct SceneSpawner { pub(crate) spawned_dynamic_scenes: HashMap, HashSet>, pub(crate) spawned_instances: HashMap, - scene_asset_event_reader: EventCursor>, - dynamic_scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, + dynamic_scene_asset_event_reader: EventCursor>, scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, - scenes_to_despawn: Vec>, + dynamic_scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, + dynamic_scenes_to_despawn: Vec>, instances_to_despawn: Vec, scenes_with_parent: Vec<(InstanceId, Entity)>, instances_ready: Vec<(InstanceId, Option)>, @@ -177,8 +177,8 @@ impl SceneSpawner { } /// Schedule the despawn of all instances of the provided dynamic scene. - pub fn despawn(&mut self, id: impl Into>) { - self.scenes_to_despawn.push(id.into()); + pub fn despawn_dynamic(&mut self, id: impl Into>) { + self.dynamic_scenes_to_despawn.push(id.into()); } /// Schedule the despawn of a scene instance, removing all its entities from the world. @@ -196,7 +196,7 @@ impl SceneSpawner { } /// Immediately despawns all instances of a dynamic scene. - pub fn despawn_sync( + pub fn despawn_dynamic_sync( &mut self, world: &mut World, id: impl Into>, @@ -287,7 +287,7 @@ impl SceneSpawner { /// Iterate through all instances of the provided scenes and update those immediately. /// /// Useful for updating already spawned scene instances after their corresponding scene has been modified. - pub fn update_spawned_scenes( + pub fn update_spawned_dynamic_scenes( &mut self, world: &mut World, scene_ids: &[AssetId], @@ -306,10 +306,10 @@ impl SceneSpawner { /// Immediately despawns all scenes scheduled for despawn by despawning their instances. pub fn despawn_queued_scenes(&mut self, world: &mut World) -> Result<(), SceneSpawnError> { - let scenes_to_despawn = core::mem::take(&mut self.scenes_to_despawn); + let scenes_to_despawn = core::mem::take(&mut self.dynamic_scenes_to_despawn); for scene_handle in scenes_to_despawn { - self.despawn_sync(world, scene_handle)?; + self.despawn_dynamic_sync(world, scene_handle)?; } Ok(()) } @@ -473,15 +473,15 @@ pub fn scene_spawner_system(world: &mut World) { let scene_asset_events = world.resource::>>(); - let mut updated_spawned_scenes = Vec::new(); + let mut updated_spawned_dynamic_scenes = Vec::new(); let scene_spawner = &mut *scene_spawner; for event in scene_spawner - .scene_asset_event_reader + .dynamic_scene_asset_event_reader .read(scene_asset_events) { if let AssetEvent::Modified { id } = event { if scene_spawner.spawned_dynamic_scenes.contains_key(id) { - updated_spawned_scenes.push(*id); + updated_spawned_dynamic_scenes.push(*id); } } } @@ -492,7 +492,7 @@ pub fn scene_spawner_system(world: &mut World) { .spawn_queued_scenes(world) .unwrap_or_else(|err| panic!("{}", err)); scene_spawner - .update_spawned_scenes(world, &updated_spawned_scenes) + .update_spawned_dynamic_scenes(world, &updated_spawned_dynamic_scenes) .unwrap(); scene_spawner.set_scene_instance_parent_sync(world); scene_spawner.trigger_scene_ready_events(world); @@ -615,7 +615,7 @@ mod tests { // let's try to delete the scene let mut scene_spawner = app.world_mut().resource_mut::(); - scene_spawner.despawn(&scene_handle); + scene_spawner.despawn_dynamic(&scene_handle); // run the scene spawner system to despawn the scene app.update(); From 82f0eb7470dd4bd496ef635963a0e4578eb83c09 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 16 Mar 2025 14:34:31 -0700 Subject: [PATCH 2/9] Add corresponding methods for all the `_dynamic` methods to handle `Scene`s. --- crates/bevy_scene/src/lib.rs | 7 +++ crates/bevy_scene/src/scene_spawner.rs | 78 ++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 9b0845f80f..a6874f9721 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -91,12 +91,19 @@ impl Plugin for ScenePlugin { app.world_mut() .register_component_hooks::() .on_remove(|mut world, context| { + let Some(handle) = world.get::(context.entity) else { + return; + }; + let id = handle.id(); if let Some(&SceneInstance(scene_instance)) = world.get::(context.entity) { let Some(mut scene_spawner) = world.get_resource_mut::() else { return; }; + if let Some(instance_ids) = scene_spawner.spawned_scenes.get_mut(&id) { + instance_ids.remove(&scene_instance); + } scene_spawner.unregister_instance(scene_instance); } }); diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index b63bf47c1f..688c3287e8 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -73,11 +73,14 @@ impl InstanceId { /// - [`despawn_instance`](Self::despawn_instance) #[derive(Default, Resource)] pub struct SceneSpawner { + pub(crate) spawned_scenes: HashMap, HashSet>, pub(crate) spawned_dynamic_scenes: HashMap, HashSet>, pub(crate) spawned_instances: HashMap, + scene_asset_event_reader: EventCursor>, dynamic_scene_asset_event_reader: EventCursor>, scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, dynamic_scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, + scenes_to_despawn: Vec>, dynamic_scenes_to_despawn: Vec>, instances_to_despawn: Vec, scenes_with_parent: Vec<(InstanceId, Entity)>, @@ -176,6 +179,11 @@ impl SceneSpawner { instance_id } + /// Schedule the despawn of all instances of the provided scene. + pub fn despawn(&mut self, id: impl Into>) { + self.scenes_to_despawn.push(id.into()); + } + /// Schedule the despawn of all instances of the provided dynamic scene. pub fn despawn_dynamic(&mut self, id: impl Into>) { self.dynamic_scenes_to_despawn.push(id.into()); @@ -195,6 +203,20 @@ impl SceneSpawner { self.spawned_instances.remove(&instance_id); } + /// Immediately despawns all instances of a scene. + pub fn despawn_sync( + &mut self, + world: &mut World, + id: impl Into>, + ) -> Result<(), SceneSpawnError> { + if let Some(instance_ids) = self.spawned_scenes.remove(&id.into()) { + for instance_id in instance_ids { + self.despawn_instance_sync(world, &instance_id); + } + } + Ok(()) + } + /// Immediately despawns all instances of a dynamic scene. pub fn despawn_dynamic_sync( &mut self, @@ -263,6 +285,8 @@ impl SceneSpawner { let instance_id = InstanceId::new(); self.spawned_instances .insert(instance_id, InstanceInfo { entity_map }); + let spawned = self.spawned_scenes.entry(id).or_default(); + spawned.insert(instance_id); Ok(instance_id) } @@ -286,7 +310,29 @@ impl SceneSpawner { /// Iterate through all instances of the provided scenes and update those immediately. /// - /// Useful for updating already spawned scene instances after their corresponding scene has been modified. + /// Useful for updating already spawned scene instances after their corresponding scene has been + /// modified. + pub fn update_spawned_scenes( + &mut self, + world: &mut World, + scene_ids: &[AssetId], + ) -> Result<(), SceneSpawnError> { + for id in scene_ids { + if let Some(spawned_instances) = self.spawned_scenes.get(id) { + for instance_id in spawned_instances { + if let Some(instance_info) = self.spawned_instances.get_mut(instance_id) { + Self::spawn_sync_internal(world, *id, &mut instance_info.entity_map)?; + } + } + } + } + Ok(()) + } + + /// Iterate through all instances of the provided dynamic scenes and update those immediately. + /// + /// Useful for updating already spawned scene instances after their corresponding dynamic scene + /// has been modified. pub fn update_spawned_dynamic_scenes( &mut self, world: &mut World, @@ -306,8 +352,11 @@ impl SceneSpawner { /// Immediately despawns all scenes scheduled for despawn by despawning their instances. pub fn despawn_queued_scenes(&mut self, world: &mut World) -> Result<(), SceneSpawnError> { + let scenes_to_despawn = core::mem::take(&mut self.scenes_to_despawn); + for scene_handle in scenes_to_despawn { + self.despawn_sync(world, scene_handle)?; + } let scenes_to_despawn = core::mem::take(&mut self.dynamic_scenes_to_despawn); - for scene_handle in scenes_to_despawn { self.despawn_dynamic_sync(world, scene_handle)?; } @@ -362,6 +411,8 @@ impl SceneSpawner { Ok(_) => { self.spawned_instances .insert(instance_id, InstanceInfo { entity_map }); + let spawned = self.spawned_scenes.entry(scene_handle.id()).or_default(); + spawned.insert(instance_id); // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. @@ -471,13 +522,25 @@ pub fn scene_spawner_system(world: &mut World) { .scenes_to_spawn .retain(|(_, instance, _)| !dead_instances.contains(instance)); - let scene_asset_events = world.resource::>>(); - - let mut updated_spawned_dynamic_scenes = Vec::new(); + let scene_asset_events = world.resource::>>(); + let dynamic_scene_asset_events = world.resource::>>(); let scene_spawner = &mut *scene_spawner; + + let mut updated_spawned_scenes = Vec::new(); + for event in scene_spawner + .scene_asset_event_reader + .read(scene_asset_events) + { + if let AssetEvent::Modified { id } = event { + if scene_spawner.spawned_scenes.contains_key(id) { + updated_spawned_scenes.push(*id); + } + } + } + let mut updated_spawned_dynamic_scenes = Vec::new(); for event in scene_spawner .dynamic_scene_asset_event_reader - .read(scene_asset_events) + .read(dynamic_scene_asset_events) { if let AssetEvent::Modified { id } = event { if scene_spawner.spawned_dynamic_scenes.contains_key(id) { @@ -491,6 +554,9 @@ pub fn scene_spawner_system(world: &mut World) { scene_spawner .spawn_queued_scenes(world) .unwrap_or_else(|err| panic!("{}", err)); + scene_spawner + .update_spawned_scenes(world, &updated_spawned_scenes) + .unwrap(); scene_spawner .update_spawned_dynamic_scenes(world, &updated_spawned_dynamic_scenes) .unwrap(); From 89adaeacc65f6015d8aaa77705b2b1381b3f6d7d Mon Sep 17 00:00:00 2001 From: andriyDev Date: Mon, 17 Mar 2025 19:51:17 -0700 Subject: [PATCH 3/9] Update the doc comments for `SceneSpawner`. --- crates/bevy_scene/src/scene_spawner.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 688c3287e8..56db96f4f5 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -55,11 +55,13 @@ impl InstanceId { /// Handles spawning and despawning scenes in the world, either synchronously or batched through the [`scene_spawner_system`]. /// /// Synchronous methods: (Scene operations will take effect immediately) -/// - [`spawn_dynamic_sync`](Self::spawn_dynamic_sync) /// - [`spawn_sync`](Self::spawn_sync) +/// - [`spawn_dynamic_sync`](Self::spawn_dynamic_sync) /// - [`despawn_sync`](Self::despawn_sync) +/// - [`despawn_dynamic_sync`](Self::despawn_dynamic_sync) /// - [`despawn_instance_sync`](Self::despawn_instance_sync) /// - [`update_spawned_scenes`](Self::update_spawned_scenes) +/// - [`update_spawned_dynamic_scenes`](Self::update_spawned_dynamic_scenes) /// - [`spawn_queued_scenes`](Self::spawn_queued_scenes) /// - [`despawn_queued_scenes`](Self::despawn_queued_scenes) /// - [`despawn_queued_instances`](Self::despawn_queued_instances) @@ -70,6 +72,7 @@ impl InstanceId { /// - [`spawn`](Self::spawn) /// - [`spawn_as_child`](Self::spawn_as_child) /// - [`despawn`](Self::despawn) +/// - [`despawn_dynamic`](Self::despawn_dynamic) /// - [`despawn_instance`](Self::despawn_instance) #[derive(Default, Resource)] pub struct SceneSpawner { From f630a11a6fbd43954ebd17d2b7930836f035a6f4 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Tue, 15 Jul 2025 17:02:52 -0700 Subject: [PATCH 4/9] Despawn scenes before hot reloading them. --- crates/bevy_scene/src/scene_spawner.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 56db96f4f5..a319470af7 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -236,15 +236,21 @@ impl SceneSpawner { /// Immediately despawns a scene instance, removing all its entities from the world. pub fn despawn_instance_sync(&mut self, world: &mut World, instance_id: &InstanceId) { - if let Some(instance) = self.spawned_instances.remove(instance_id) { - for &entity in instance.entity_map.values() { - if let Ok(entity_mut) = world.get_entity_mut(entity) { - entity_mut.despawn(); - }; - } + if let Some(mut instance) = self.spawned_instances.remove(instance_id) { + Self::despawn_instance_internal(world, &mut instance); } } + fn despawn_instance_internal(world: &mut World, instance: &mut InstanceInfo) { + for &entity in instance.entity_map.values() { + if let Ok(entity_mut) = world.get_entity_mut(entity) { + entity_mut.despawn(); + }; + } + // Just make sure if we reuse `InstanceInfo` for something, we don't reuse the despawned entities. + instance.entity_map.clear(); + } + /// Immediately spawns a new instance of the provided dynamic scene. pub fn spawn_dynamic_sync( &mut self, @@ -324,6 +330,10 @@ impl SceneSpawner { if let Some(spawned_instances) = self.spawned_scenes.get(id) { for instance_id in spawned_instances { if let Some(instance_info) = self.spawned_instances.get_mut(instance_id) { + // Despawn the scene before respawning it. This is a very heavy operation, + // but otherwise, entities may be left behind, or be left in an otherwise + // invalid state (e.g., invalid relationships). + Self::despawn_instance_internal(world, instance_info); Self::spawn_sync_internal(world, *id, &mut instance_info.entity_map)?; } } @@ -345,6 +355,10 @@ impl SceneSpawner { if let Some(spawned_instances) = self.spawned_dynamic_scenes.get(id) { for instance_id in spawned_instances { if let Some(instance_info) = self.spawned_instances.get_mut(instance_id) { + // Despawn the scene before respawning it. This is a very heavy operation, + // but otherwise, entities may be left behind, or be left in an otherwise + // invalid state (e.g., invalid relationships). + Self::despawn_instance_internal(world, instance_info); Self::spawn_dynamic_internal(world, *id, &mut instance_info.entity_map)?; } } From 0c953880b372cdb6de7ad639de412e14b9b05652 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Tue, 15 Jul 2025 17:39:45 -0700 Subject: [PATCH 5/9] Write tests for scene hot reloading. --- crates/bevy_scene/src/lib.rs | 296 +++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index a6874f9721..9e6fe16d1a 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -114,3 +114,299 @@ impl Plugin for ScenePlugin { impl Plugin for ScenePlugin { fn build(&self, _: &mut App) {} } + +#[cfg(test)] +mod tests { + use bevy_app::App; + use bevy_asset::{AssetPlugin, Assets}; + use bevy_ecs::{ + component::Component, + hierarchy::{ChildOf, Children}, + reflect::{AppTypeRegistry, ReflectComponent}, + world::World, + }; + use bevy_reflect::Reflect; + + use crate::{ + DynamicScene, DynamicSceneBuilder, DynamicSceneRoot, Scene, ScenePlugin, SceneRoot, + }; + + #[derive(Component, Reflect, PartialEq, Debug)] + #[reflect(Component)] + struct Circle { + radius: f32, + } + + #[derive(Component, Reflect, PartialEq, Debug)] + #[reflect(Component)] + struct Rectangle { + width: f32, + height: f32, + } + + #[derive(Component, Reflect, PartialEq, Debug)] + #[reflect(Component)] + struct Triangle { + base: f32, + height: f32, + } + + #[derive(Component, Reflect)] + #[reflect(Component)] + struct FinishLine; + + #[test] + fn scene_spawns_and_respawns_after_change() { + let mut app = App::new(); + + app.add_plugins((AssetPlugin::default(), ScenePlugin)) + .register_type::() + .register_type::() + .register_type::() + .register_type::(); + + let scene_handle = app + .world_mut() + .resource_mut::>() + .reserve_handle(); + + let scene_entity = app.world_mut().spawn(SceneRoot(scene_handle.clone())).id(); + app.update(); + + assert!(app.world().entity(scene_entity).get::().is_none()); + + let mut scene_1 = Scene { + world: World::new(), + }; + let root = scene_1.world.spawn_empty().id(); + scene_1.world.spawn(( + Rectangle { + width: 10.0, + height: 5.0, + }, + FinishLine, + ChildOf(root), + )); + scene_1.world.spawn((Circle { radius: 7.0 }, ChildOf(root))); + + app.world_mut() + .resource_mut::>() + .insert(&scene_handle, scene_1); + + app.update(); + + let child_root = app + .world() + .entity(scene_entity) + .get::() + .and_then(|children| children.first().cloned()) + .expect("There should be exactly one child on the scene root"); + let children = app + .world() + .entity(child_root) + .get::() + .expect("The child of the scene root should itself have 2 children"); + assert_eq!(children.len(), 2); + + let finish_line = app.world().entity(children[0]); + assert_eq!(finish_line.archetype().component_count(), 3); + let (rectangle, _, child_of) = + finish_line.components::<(&Rectangle, &FinishLine, &ChildOf)>(); + assert_eq!( + rectangle, + &Rectangle { + width: 10.0, + height: 5.0, + } + ); + assert_eq!(child_of.0, child_root); + + let circle = app.world().entity(children[1]); + assert_eq!(circle.archetype().component_count(), 2); + let (circle, child_of) = circle.components::<(&Circle, &ChildOf)>(); + assert_eq!(circle, &Circle { radius: 7.0 }); + assert_eq!(child_of.0, child_root); + + // Now that we know our scene contains exactly what we expect, we will change the scene + // asset and ensure it contains the new scene results. + + let mut scene_2 = Scene { + world: World::new(), + }; + let root = scene_2.world.spawn_empty().id(); + scene_2.world.spawn(( + Triangle { + base: 1.0, + height: 2.0, + }, + ChildOf(root), + )); + + app.world_mut() + .resource_mut::>() + .insert(&scene_handle, scene_2); + + app.update(); + app.update(); + + let child_root = app + .world() + .entity(scene_entity) + .get::() + .and_then(|children| children.first().cloned()) + .expect("There should be exactly one child on the scene root"); + let children = app + .world() + .entity(child_root) + .get::() + .expect("The child of the scene root should itself have 2 children"); + assert_eq!(children.len(), 1); + + let triangle = app.world().entity(children[0]); + assert_eq!(triangle.archetype().component_count(), 2); + let (triangle, child_of) = triangle.components::<(&Triangle, &ChildOf)>(); + assert_eq!( + triangle, + &Triangle { + base: 1.0, + height: 2.0, + } + ); + assert_eq!(child_of.0, child_root); + } + + #[test] + fn dynamic_scene_spawns_and_respawns_after_change() { + let mut app = App::new(); + + app.add_plugins((AssetPlugin::default(), ScenePlugin)) + .register_type::() + .register_type::() + .register_type::() + .register_type::(); + + let scene_handle = app + .world_mut() + .resource_mut::>() + .reserve_handle(); + + let scene_entity = app + .world_mut() + .spawn(DynamicSceneRoot(scene_handle.clone())) + .id(); + app.update(); + + assert!(app.world().entity(scene_entity).get::().is_none()); + + let create_dynamic_scene = |mut scene: Scene, world: &World| { + scene + .world + .insert_resource(world.resource::().clone()); + DynamicSceneBuilder::from_world(&scene.world) + .extract_entities(scene.world.iter_entities().map(|entity| entity.id())) + .build() + }; + + let mut scene_1 = Scene { + world: World::new(), + }; + let root = scene_1.world.spawn_empty().id(); + scene_1.world.spawn(( + Rectangle { + width: 10.0, + height: 5.0, + }, + FinishLine, + ChildOf(root), + )); + scene_1.world.spawn((Circle { radius: 7.0 }, ChildOf(root))); + + let scene_1 = create_dynamic_scene(scene_1, app.world()); + app.world_mut() + .resource_mut::>() + .insert(&scene_handle, scene_1); + + app.update(); + + let child_root = app + .world() + .entity(scene_entity) + .get::() + .and_then(|children| children.first().cloned()) + .expect("There should be exactly one child on the scene root"); + let children = app + .world() + .entity(child_root) + .get::() + .expect("The child of the scene root should itself have 2 children"); + assert_eq!(children.len(), 2); + + let finish_line = app.world().entity(children[0]); + assert_eq!(finish_line.archetype().component_count(), 3); + let (rectangle, _, child_of) = + finish_line.components::<(&Rectangle, &FinishLine, &ChildOf)>(); + assert_eq!( + rectangle, + &Rectangle { + width: 10.0, + height: 5.0, + } + ); + assert_eq!(child_of.0, child_root); + + let circle = app.world().entity(children[1]); + assert_eq!(circle.archetype().component_count(), 2); + let (circle, child_of) = circle.components::<(&Circle, &ChildOf)>(); + assert_eq!(circle, &Circle { radius: 7.0 }); + assert_eq!(child_of.0, child_root); + + // Now that we know our scene contains exactly what we expect, we will change the scene + // asset and ensure it contains the new scene results. + + let mut scene_2 = Scene { + world: World::new(), + }; + let root = scene_2.world.spawn_empty().id(); + scene_2.world.spawn(( + Triangle { + base: 1.0, + height: 2.0, + }, + ChildOf(root), + )); + + let scene_2 = create_dynamic_scene(scene_2, app.world()); + + app.world_mut() + .resource_mut::>() + .insert(&scene_handle, scene_2); + + app.update(); + app.update(); + + let child_root = app + .world() + .entity(scene_entity) + .get::() + .and_then(|children| children.first().cloned()) + .expect("There should be exactly one child on the scene root"); + let children = app + .world() + .entity(child_root) + .get::() + .expect("The child of the scene root should itself have 2 children"); + assert_eq!(children.len(), 1); + + let triangle = app.world().entity(children[0]); + assert_eq!(triangle.archetype().component_count(), 2); + let (triangle, child_of) = triangle.components::<(&Triangle, &ChildOf)>(); + assert_eq!( + triangle, + &Triangle { + base: 1.0, + height: 2.0, + } + ); + assert_eq!(child_of.0, child_root); + } +} From 065043d687e389615c76046da5fe7ad3c0c710b8 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Tue, 15 Jul 2025 17:43:00 -0700 Subject: [PATCH 6/9] Make InstanceInfo private. --- crates/bevy_scene/src/scene_spawner.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index a319470af7..f4720145ad 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -36,9 +36,9 @@ pub struct SceneInstanceReady { /// Information about a scene instance. #[derive(Debug)] -pub struct InstanceInfo { +struct InstanceInfo { /// Mapping of entities from the scene world to the instance world. - pub entity_map: EntityHashMap, + entity_map: EntityHashMap, } /// Unique id identifying a scene instance. @@ -78,7 +78,7 @@ impl InstanceId { pub struct SceneSpawner { pub(crate) spawned_scenes: HashMap, HashSet>, pub(crate) spawned_dynamic_scenes: HashMap, HashSet>, - pub(crate) spawned_instances: HashMap, + spawned_instances: HashMap, scene_asset_event_reader: EventCursor>, dynamic_scene_asset_event_reader: EventCursor>, scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, From 0c687143e379a0a3f6aaa7f4862dcdb17e66ac7e Mon Sep 17 00:00:00 2001 From: andriyDev Date: Tue, 15 Jul 2025 22:05:56 -0700 Subject: [PATCH 7/9] Store the parent of a scene in its `InstanceInfo`. --- crates/bevy_scene/src/scene_spawner.rs | 129 ++++++++++++------------- 1 file changed, 62 insertions(+), 67 deletions(-) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index f4720145ad..187339c8c1 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -39,6 +39,8 @@ pub struct SceneInstanceReady { struct InstanceInfo { /// Mapping of entities from the scene world to the instance world. entity_map: EntityHashMap, + /// The parent to attach this instance to. + parent: Option, } /// Unique id identifying a scene instance. @@ -86,7 +88,6 @@ pub struct SceneSpawner { scenes_to_despawn: Vec>, dynamic_scenes_to_despawn: Vec>, instances_to_despawn: Vec, - scenes_with_parent: Vec<(InstanceId, Entity)>, instances_ready: Vec<(InstanceId, Option)>, } @@ -162,7 +163,6 @@ impl SceneSpawner { let instance_id = InstanceId::new(); self.dynamic_scenes_to_spawn .push((id.into(), instance_id, Some(parent))); - self.scenes_with_parent.push((instance_id, parent)); instance_id } @@ -178,7 +178,6 @@ impl SceneSpawner { let instance_id = InstanceId::new(); self.scenes_to_spawn .push((id.into(), instance_id, Some(parent))); - self.scenes_with_parent.push((instance_id, parent)); instance_id } @@ -261,10 +260,18 @@ impl SceneSpawner { let id = id.into(); Self::spawn_dynamic_internal(world, id, &mut entity_map)?; let instance_id = InstanceId::new(); - self.spawned_instances - .insert(instance_id, InstanceInfo { entity_map }); + self.spawned_instances.insert( + instance_id, + InstanceInfo { + entity_map, + parent: None, + }, + ); let spawned = self.spawned_dynamic_scenes.entry(id).or_default(); spawned.insert(instance_id); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); Ok(instance_id) } @@ -292,10 +299,18 @@ impl SceneSpawner { let id = id.into(); Self::spawn_sync_internal(world, id, &mut entity_map)?; let instance_id = InstanceId::new(); - self.spawned_instances - .insert(instance_id, InstanceInfo { entity_map }); + self.spawned_instances.insert( + instance_id, + InstanceInfo { + entity_map, + parent: None, + }, + ); let spawned = self.spawned_scenes.entry(id).or_default(); spawned.insert(instance_id); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); Ok(instance_id) } @@ -335,6 +350,7 @@ impl SceneSpawner { // invalid state (e.g., invalid relationships). Self::despawn_instance_internal(world, instance_info); Self::spawn_sync_internal(world, *id, &mut instance_info.entity_map)?; + Self::set_scene_instance_parent_sync(world, instance_info); } } } @@ -360,6 +376,7 @@ impl SceneSpawner { // invalid state (e.g., invalid relationships). Self::despawn_instance_internal(world, instance_info); Self::spawn_dynamic_internal(world, *id, &mut instance_info.entity_map)?; + Self::set_scene_instance_parent_sync(world, instance_info); } } } @@ -398,18 +415,15 @@ impl SceneSpawner { match Self::spawn_dynamic_internal(world, handle.id(), &mut entity_map) { Ok(_) => { - self.spawned_instances - .insert(instance_id, InstanceInfo { entity_map }); + let instance_info = InstanceInfo { entity_map, parent }; + Self::set_scene_instance_parent_sync(world, &instance_info); + + self.spawned_instances.insert(instance_id, instance_info); let spawned = self.spawned_dynamic_scenes.entry(handle.id()).or_default(); spawned.insert(instance_id); - - // Scenes with parents need more setup before they are ready. - // See `set_scene_instance_parent_sync()`. - if parent.is_none() { - // We trigger `SceneInstanceReady` events after processing all scenes - // SceneSpawner may not be available in the observer. - self.instances_ready.push((instance_id, None)); - } + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, parent)); } Err(SceneSpawnError::NonExistentScene { .. }) => { self.dynamic_scenes_to_spawn @@ -426,18 +440,16 @@ impl SceneSpawner { match Self::spawn_sync_internal(world, scene_handle.id(), &mut entity_map) { Ok(_) => { - self.spawned_instances - .insert(instance_id, InstanceInfo { entity_map }); + let instance_info = InstanceInfo { entity_map, parent }; + Self::set_scene_instance_parent_sync(world, &instance_info); + + self.spawned_instances.insert(instance_id, instance_info); let spawned = self.spawned_scenes.entry(scene_handle.id()).or_default(); spawned.insert(instance_id); - // Scenes with parents need more setup before they are ready. - // See `set_scene_instance_parent_sync()`. - if parent.is_none() { - // We trigger `SceneInstanceReady` events after processing all scenes - // SceneSpawner may not be available in the observer. - self.instances_ready.push((instance_id, None)); - } + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, parent)); } Err(SceneSpawnError::NonExistentRealScene { .. }) => { self.scenes_to_spawn @@ -450,32 +462,23 @@ impl SceneSpawner { Ok(()) } - pub(crate) fn set_scene_instance_parent_sync(&mut self, world: &mut World) { - let scenes_with_parent = core::mem::take(&mut self.scenes_with_parent); - - for (instance_id, parent) in scenes_with_parent { - if let Some(instance) = self.spawned_instances.get(&instance_id) { - for &entity in instance.entity_map.values() { - // Add the `ChildOf` component to the scene root, and update the `Children` component of - // the scene parent - if !world - .get_entity(entity) - .ok() - // This will filter only the scene root entity, as all other from the - // scene have a parent - // Entities that wouldn't exist anymore are also skipped - // this case shouldn't happen anyway - .is_none_or(|entity| entity.contains::()) - { - world.entity_mut(parent).add_child(entity); - } - } - - // We trigger `SceneInstanceReady` events after processing all scenes - // SceneSpawner may not be available in the observer. - self.instances_ready.push((instance_id, Some(parent))); - } else { - self.scenes_with_parent.push((instance_id, parent)); + fn set_scene_instance_parent_sync(world: &mut World, instance: &InstanceInfo) { + let Some(parent) = instance.parent else { + return; + }; + for &entity in instance.entity_map.values() { + // Add the `ChildOf` component to the scene root, and update the `Children` component of + // the scene parent + if !world + .get_entity(entity) + .ok() + // This will filter only the scene root entity, as all other from the + // scene have a parent + // Entities that wouldn't exist anymore are also skipped + // this case shouldn't happen anyway + .is_none_or(|entity| entity.contains::()) + { + world.entity_mut(parent).add_child(entity); } } } @@ -520,24 +523,17 @@ impl SceneSpawner { pub fn scene_spawner_system(world: &mut World) { world.resource_scope(|world, mut scene_spawner: Mut| { // remove any loading instances where parent is deleted - let mut dead_instances = >::default(); - scene_spawner - .scenes_with_parent - .retain(|(instance, parent)| { - let retain = world.get_entity(*parent).is_ok(); - - if !retain { - dead_instances.insert(*instance); - } - - retain - }); + let is_parent_alive = |parent: &Option| { + parent + .map(|parent| world.get_entity(parent).is_ok()) + .unwrap_or(true) // If we don't have a parent, then consider the parent alive. + }; scene_spawner .dynamic_scenes_to_spawn - .retain(|(_, instance, _)| !dead_instances.contains(instance)); + .retain(|(_, _, parent)| is_parent_alive(parent)); scene_spawner .scenes_to_spawn - .retain(|(_, instance, _)| !dead_instances.contains(instance)); + .retain(|(_, _, parent)| is_parent_alive(parent)); let scene_asset_events = world.resource::>>(); let dynamic_scene_asset_events = world.resource::>>(); @@ -577,7 +573,6 @@ pub fn scene_spawner_system(world: &mut World) { scene_spawner .update_spawned_dynamic_scenes(world, &updated_spawned_dynamic_scenes) .unwrap(); - scene_spawner.set_scene_instance_parent_sync(world); scene_spawner.trigger_scene_ready_events(world); }); } From 1b2ae8f11c7783dfefd8ebb56bf169e3071b3d7c Mon Sep 17 00:00:00 2001 From: andriyDev Date: Tue, 15 Jul 2025 23:32:42 -0700 Subject: [PATCH 8/9] Send `instance_ready` events when reloading scenes as well. --- crates/bevy_scene/src/scene_spawner.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 187339c8c1..5243df357a 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -351,6 +351,10 @@ impl SceneSpawner { Self::despawn_instance_internal(world, instance_info); Self::spawn_sync_internal(world, *id, &mut instance_info.entity_map)?; Self::set_scene_instance_parent_sync(world, instance_info); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready + .push((*instance_id, instance_info.parent)); } } } @@ -377,6 +381,10 @@ impl SceneSpawner { Self::despawn_instance_internal(world, instance_info); Self::spawn_dynamic_internal(world, *id, &mut instance_info.entity_map)?; Self::set_scene_instance_parent_sync(world, instance_info); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready + .push((*instance_id, instance_info.parent)); } } } From 43960f69d9bc2f8e756cb4aa38079194039c9e36 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Wed, 16 Jul 2025 00:23:36 -0700 Subject: [PATCH 9/9] Add a migration guide. --- .../migration-guides/scene_spawner_api.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 release-content/migration-guides/scene_spawner_api.md diff --git a/release-content/migration-guides/scene_spawner_api.md b/release-content/migration-guides/scene_spawner_api.md new file mode 100644 index 0000000000..5303682488 --- /dev/null +++ b/release-content/migration-guides/scene_spawner_api.md @@ -0,0 +1,12 @@ +--- +title: `SceneSpawner` methods have been renamed and replaced. +pull_requests: [18358] +--- + +Some methods on `SceneSpawner` have been renamed: + - `despawn` -> `despawn_dynamic` + - `despawn_sync` -> `despawn_dynamic_sync` + - `update_spawned_scenes` -> `update_spawned_dynamic_scenes` + +In their place, we've added `despawn`, `despawn_sync`, and `update_spawned_scenes` which all act on +`Scene`s (as opposed to `DynamicScene`s).