Change SceneInstanceReady to trigger an observer. (#13859)

# Objective

The `SceneInstanceReady` event would be more ergonomic (and potentially
efficient) if it could be delivered to listeners attached to the scene
entities becoming ready rather than into a World-global queue.

This is an evolution of @Shatur's work in #9313.

## Solution

The scene spawner is changed to trigger observers on the scene entity
when it is ready rather than enqueue an event with `EventWriter`.

This addresses the two outstanding feature requests mentioned on #2218,
that i) the events should be "scoped" in some way and ii) that the
`InstanceId` should be included in the event.

## Testing

Modified the `scene_spawner::tests::event` test to use the new
mechanism.

---

## Changelog

- Changed `SceneInstanceReady` to trigger an entity observer rather than
be written to an event queue.
- Changed `SceneInstanceReady` to carry the `InstanceId` of the scene.

## Migration Guide

If you have a system which read `SceneInstanceReady` events:

> ```fn ready_system(ready_events: EventReader<'_, '_,
SceneInstanceReady>) {```

It must be rewritten as an observer:

> ```commands.observe(|trigger: Trigger<SceneInstanceReady>| {```

Or, if you were expecting the event in relation to a specific entity or
entities, as an entity observer:

> ```commands.entity(entity).observe(|trigger:
Trigger<SceneInstanceReady>| {```
This commit is contained in:
Robin KAY 2024-07-30 22:23:48 +01:00 committed by GitHub
parent ba09f35474
commit 3d1c9ca87f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 88 additions and 116 deletions

View File

@ -56,7 +56,6 @@ impl Plugin for ScenePlugin {
app.init_asset::<DynamicScene>() app.init_asset::<DynamicScene>()
.init_asset::<Scene>() .init_asset::<Scene>()
.init_asset_loader::<SceneLoader>() .init_asset_loader::<SceneLoader>()
.add_event::<SceneInstanceReady>()
.init_resource::<SceneSpawner>() .init_resource::<SceneSpawner>()
.add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain());

View File

@ -13,15 +13,15 @@ use bevy_utils::{tracing::error, HashMap, HashSet};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
/// Emitted when [`crate::SceneInstance`] becomes ready to use. /// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use.
/// ///
/// See also [`SceneSpawner::instance_is_ready`]. /// See also [`Trigger`], [`SceneSpawner::instance_is_ready`].
///
/// [`Trigger`]: bevy_ecs::observer::Trigger
#[derive(Clone, Copy, Debug, Eq, PartialEq, Event)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Event)]
pub struct SceneInstanceReady { pub struct SceneInstanceReady {
/// ID of the spawned instance. /// Instance which has been spawned.
pub id: InstanceId, pub instance_id: InstanceId,
/// Entity to which the scene was spawned as a child.
pub parent: Option<Entity>,
} }
/// Information about a scene instance. /// Information about a scene instance.
@ -323,10 +323,8 @@ impl SceneSpawner {
// Scenes with parents need more setup before they are ready. // Scenes with parents need more setup before they are ready.
// See `set_scene_instance_parent_sync()`. // See `set_scene_instance_parent_sync()`.
if parent.is_none() { if parent.is_none() {
world.send_event(SceneInstanceReady { // Defer via commands otherwise SceneSpawner is not available in the observer.
id: instance_id, world.commands().trigger(SceneInstanceReady { instance_id });
parent: None,
});
} }
} }
Err(SceneSpawnError::NonExistentScene { .. }) => { Err(SceneSpawnError::NonExistentScene { .. }) => {
@ -350,10 +348,8 @@ impl SceneSpawner {
// Scenes with parents need more setup before they are ready. // Scenes with parents need more setup before they are ready.
// See `set_scene_instance_parent_sync()`. // See `set_scene_instance_parent_sync()`.
if parent.is_none() { if parent.is_none() {
world.send_event(SceneInstanceReady { // Defer via commands otherwise SceneSpawner is not available in the observer.
id: instance_id, world.commands().trigger(SceneInstanceReady { instance_id });
parent: None,
});
} }
} }
Err(SceneSpawnError::NonExistentRealScene { .. }) => { Err(SceneSpawnError::NonExistentRealScene { .. }) => {
@ -392,10 +388,10 @@ impl SceneSpawner {
} }
} }
world.send_event(SceneInstanceReady { // Defer via commands otherwise SceneSpawner is not available in the observer.
id: instance_id, world
parent: Some(parent), .commands()
}); .trigger_targets(SceneInstanceReady { instance_id }, parent);
} else { } else {
self.scenes_with_parent.push((instance_id, parent)); self.scenes_with_parent.push((instance_id, parent));
} }
@ -479,7 +475,7 @@ mod tests {
use bevy_app::App; use bevy_app::App;
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_asset::{AssetPlugin, AssetServer}; use bevy_asset::{AssetPlugin, AssetServer};
use bevy_ecs::event::EventReader; use bevy_ecs::observer::Trigger;
use bevy_ecs::prelude::ReflectComponent; use bevy_ecs::prelude::ReflectComponent;
use bevy_ecs::query::With; use bevy_ecs::query::With;
use bevy_ecs::system::{Commands, Res, ResMut, RunSystemOnce}; use bevy_ecs::system::{Commands, Res, ResMut, RunSystemOnce};
@ -545,9 +541,13 @@ mod tests {
#[reflect(Component)] #[reflect(Component)]
struct ComponentA; struct ComponentA;
#[derive(Resource, Default)]
struct TriggerCount(u32);
fn setup() -> App { fn setup() -> App {
let mut app = App::new(); let mut app = App::new();
app.add_plugins((AssetPlugin::default(), ScenePlugin)); app.add_plugins((AssetPlugin::default(), ScenePlugin));
app.init_resource::<TriggerCount>();
app.register_type::<ComponentA>(); app.register_type::<ComponentA>();
app.world_mut().spawn(ComponentA); app.world_mut().spawn(ComponentA);
@ -569,8 +569,50 @@ mod tests {
) )
} }
fn build_dynamic_scene(app: &mut App) -> Handle<DynamicScene> {
app.world_mut()
.run_system_once(|world: &World, asset_server: Res<'_, AssetServer>| {
asset_server.add(DynamicScene::from_world(world))
})
}
fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Entity) {
// Add observer
app.world_mut().observe(
move |trigger: Trigger<SceneInstanceReady>,
scene_spawner: Res<SceneSpawner>,
mut trigger_count: ResMut<TriggerCount>| {
assert_eq!(
trigger.event().instance_id,
scene_id,
"`SceneInstanceReady` contains the wrong `InstanceId`"
);
assert_eq!(
trigger.entity(),
scene_entity,
"`SceneInstanceReady` triggered on the wrong parent entity"
);
assert!(
scene_spawner.instance_is_ready(trigger.event().instance_id),
"`InstanceId` is not ready"
);
trigger_count.0 += 1;
},
);
// Check observer is triggered once.
app.update();
app.world_mut()
.run_system_once(|trigger_count: Res<TriggerCount>| {
assert_eq!(
trigger_count.0, 1,
"wrong number of `SceneInstanceReady` triggers"
);
});
}
#[test] #[test]
fn event_scene() { fn observe_scene() {
let mut app = setup(); let mut app = setup();
// Build scene. // Build scene.
@ -583,25 +625,30 @@ mod tests {
scene_spawner.spawn(scene.clone()) scene_spawner.spawn(scene.clone())
}); });
// Check for event arrival. // Check trigger.
app.update(); observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER);
app.world_mut().run_system_once(
move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| {
let mut events = ev_scene.read();
let event = events.next().expect("found no `SceneInstanceReady` event");
assert_eq!(
event.id, scene_id,
"`SceneInstanceReady` contains the wrong `InstanceId`"
);
assert!(events.next().is_none(), "found more than one event");
},
);
} }
#[test] #[test]
fn event_scene_as_child() { fn observe_dynamic_scene() {
let mut app = setup();
// Build scene.
let scene = build_dynamic_scene(&mut app);
// Spawn scene.
let scene_id =
app.world_mut()
.run_system_once(move |mut scene_spawner: ResMut<'_, SceneSpawner>| {
scene_spawner.spawn_dynamic(scene.clone())
});
// Check trigger.
observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER);
}
#[test]
fn observe_scene_as_child() {
let mut app = setup(); let mut app = setup();
// Build scene. // Build scene.
@ -616,68 +663,12 @@ mod tests {
}, },
); );
// Check for event arrival. // Check trigger.
app.update(); observe_trigger(&mut app, scene_id, scene_entity);
app.world_mut().run_system_once(
move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| {
let mut events = ev_scene.read();
let event = events.next().expect("found no `SceneInstanceReady` event");
assert_eq!(
event.id, scene_id,
"`SceneInstanceReady` contains the wrong `InstanceId`"
);
assert_eq!(
event.parent,
Some(scene_entity),
"`SceneInstanceReady` contains the wrong parent entity"
);
assert!(events.next().is_none(), "found more than one event");
},
);
}
fn build_dynamic_scene(app: &mut App) -> Handle<DynamicScene> {
app.world_mut()
.run_system_once(|world: &World, asset_server: Res<'_, AssetServer>| {
asset_server.add(DynamicScene::from_world(world))
})
} }
#[test] #[test]
fn event_dynamic_scene() { fn observe_dynamic_scene_as_child() {
let mut app = setup();
// Build scene.
let scene = build_dynamic_scene(&mut app);
// Spawn scene.
let scene_id =
app.world_mut()
.run_system_once(move |mut scene_spawner: ResMut<'_, SceneSpawner>| {
scene_spawner.spawn_dynamic(scene.clone())
});
// Check for event arrival.
app.update();
app.world_mut().run_system_once(
move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| {
let mut events = ev_scene.read();
let event = events.next().expect("found no `SceneInstanceReady` event");
assert_eq!(
event.id, scene_id,
"`SceneInstanceReady` contains the wrong `InstanceId`"
);
assert!(events.next().is_none(), "found more than one event");
},
);
}
#[test]
fn event_dynamic_scene_as_child() {
let mut app = setup(); let mut app = setup();
// Build scene. // Build scene.
@ -692,26 +683,8 @@ mod tests {
}, },
); );
// Check for event arrival. // Check trigger.
app.update(); observe_trigger(&mut app, scene_id, scene_entity);
app.world_mut().run_system_once(
move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| {
let mut events = ev_scene.read();
let event = events.next().expect("found no `SceneInstanceReady` event");
assert_eq!(
event.id, scene_id,
"`SceneInstanceReady` contains the wrong `InstanceId`"
);
assert_eq!(
event.parent,
Some(scene_entity),
"`SceneInstanceReady` contains the wrong parent entity"
);
assert!(events.next().is_none(), "found more than one event");
},
);
} }
#[test] #[test]