-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# Objective Add basic bubbling to observers, modeled off `bevy_eventlistener`. ## Solution - Introduce a new `Traversal` trait for components which point to other entities. - Provide a default `TraverseNone: Traversal` component which cannot be constructed. - Implement `Traversal` for `Parent`. - The `Event` trait now has an associated `Traversal` which defaults to `TraverseNone`. - Added a field `bubbling: &mut bool` to `Trigger` which can be used to instruct the runner to bubble the event to the entity specified by the event's traversal type. - Added an associated constant `SHOULD_BUBBLE` to `Event` which configures the default bubbling state. - Added logic to wire this all up correctly. Introducing the new associated information directly on `Event` (instead of a new `BubblingEvent` trait) lets us dispatch both bubbling and non-bubbling events through the same api. ## Testing I have added several unit tests to cover the common bugs I identified during development. Running the unit tests should be enough to validate correctness. The changes effect unsafe portions of the code, but should not change any of the safety assertions. ## Changelog Observers can now bubble up the entity hierarchy! To create a bubbling event, change your `Derive(Event)` to something like the following: ```rust #[derive(Component)] struct MyEvent; impl Event for MyEvent { type Traverse = Parent; // This event will propagate up from child to parent. const AUTO_PROPAGATE: bool = true; // This event will propagate by default. } ``` You can dispatch a bubbling event using the normal `world.trigger_targets(MyEvent, entity)`. Halting an event mid-bubble can be done using `trigger.propagate(false)`. Events with `AUTO_PROPAGATE = false` will not propagate by default, but you can enable it using `trigger.propagate(true)`. If there are multiple observers attached to a target, they will all be triggered by bubbling. They all share a bubbling state, which can be accessed mutably using `trigger.propagation_mut()` (`trigger.propagate` is just sugar for this). You can choose to implement `Traversal` for your own types, if you want to bubble along a different structure than provided by `bevy_hierarchy`. Implementers must be careful never to produce loops, because this will cause bevy to hang. ## Migration Guide + Manual implementations of `Event` should add associated type `Traverse = TraverseNone` and associated constant `AUTO_PROPAGATE = false`; + `Trigger::new` has new field `propagation: &mut Propagation` which provides the bubbling state. + `ObserverRunner` now takes the same `&mut Propagation` as a final parameter. --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Torstein Grindvik <[email protected]> Co-authored-by: Carter Anderson <[email protected]>
- Loading branch information
1 parent
d57531a
commit ed2b8e0
Showing
16 changed files
with
731 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
use criterion::criterion_group; | ||
|
||
mod propagation; | ||
use propagation::*; | ||
|
||
criterion_group!(observer_benches, event_propagation); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
use bevy_app::{App, First, Startup}; | ||
use bevy_ecs::{ | ||
component::Component, | ||
entity::Entity, | ||
event::{Event, EventWriter}, | ||
observer::Trigger, | ||
query::{Or, With, Without}, | ||
system::{Commands, EntityCommands, Query}, | ||
}; | ||
use bevy_hierarchy::{BuildChildren, Children, Parent}; | ||
use bevy_internal::MinimalPlugins; | ||
|
||
use criterion::{black_box, criterion_group, criterion_main, Criterion}; | ||
use rand::{seq::IteratorRandom, Rng}; | ||
|
||
const DENSITY: usize = 20; // percent of nodes with listeners | ||
const ENTITY_DEPTH: usize = 64; | ||
const ENTITY_WIDTH: usize = 200; | ||
const N_EVENTS: usize = 500; | ||
|
||
pub fn event_propagation(criterion: &mut Criterion) { | ||
let mut group = criterion.benchmark_group("event_propagation"); | ||
group.warm_up_time(std::time::Duration::from_millis(500)); | ||
group.measurement_time(std::time::Duration::from_secs(4)); | ||
|
||
group.bench_function("baseline", |bencher| { | ||
let mut app = App::new(); | ||
app.add_plugins(MinimalPlugins) | ||
.add_systems(Startup, spawn_listener_hierarchy); | ||
app.update(); | ||
|
||
bencher.iter(|| { | ||
black_box(app.update()); | ||
}); | ||
}); | ||
|
||
group.bench_function("single_event_type", |bencher| { | ||
let mut app = App::new(); | ||
app.add_plugins(MinimalPlugins) | ||
.add_systems( | ||
Startup, | ||
( | ||
spawn_listener_hierarchy, | ||
add_listeners_to_hierarchy::<DENSITY, 1>, | ||
), | ||
) | ||
.add_systems(First, send_events::<1, N_EVENTS>); | ||
app.update(); | ||
|
||
bencher.iter(|| { | ||
black_box(app.update()); | ||
}); | ||
}); | ||
|
||
group.bench_function("single_event_type_no_listeners", |bencher| { | ||
let mut app = App::new(); | ||
app.add_plugins(MinimalPlugins) | ||
.add_systems( | ||
Startup, | ||
( | ||
spawn_listener_hierarchy, | ||
add_listeners_to_hierarchy::<DENSITY, 1>, | ||
), | ||
) | ||
.add_systems(First, send_events::<9, N_EVENTS>); | ||
app.update(); | ||
|
||
bencher.iter(|| { | ||
black_box(app.update()); | ||
}); | ||
}); | ||
|
||
group.bench_function("four_event_types", |bencher| { | ||
let mut app = App::new(); | ||
const FRAC_N_EVENTS_4: usize = N_EVENTS / 4; | ||
const FRAC_DENSITY_4: usize = DENSITY / 4; | ||
|
||
app.add_plugins(MinimalPlugins) | ||
.add_systems( | ||
Startup, | ||
( | ||
spawn_listener_hierarchy, | ||
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 1>, | ||
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 2>, | ||
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 3>, | ||
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 4>, | ||
), | ||
) | ||
.add_systems(First, send_events::<1, FRAC_N_EVENTS_4>) | ||
.add_systems(First, send_events::<2, FRAC_N_EVENTS_4>) | ||
.add_systems(First, send_events::<3, FRAC_N_EVENTS_4>) | ||
.add_systems(First, send_events::<4, FRAC_N_EVENTS_4>); | ||
app.update(); | ||
|
||
bencher.iter(|| { | ||
black_box(app.update()); | ||
}); | ||
}); | ||
|
||
group.finish(); | ||
} | ||
|
||
#[derive(Clone, Component)] | ||
struct TestEvent<const N: usize> {} | ||
|
||
impl<const N: usize> Event for TestEvent<N> { | ||
type Traversal = Parent; | ||
const AUTO_PROPAGATE: bool = true; | ||
} | ||
|
||
fn send_events<const N: usize, const N_EVENTS: usize>( | ||
mut commands: Commands, | ||
entities: Query<Entity, Without<Children>>, | ||
) { | ||
let target = entities.iter().choose(&mut rand::thread_rng()).unwrap(); | ||
(0..N_EVENTS).for_each(|_| { | ||
commands.trigger_targets(TestEvent::<N> {}, target); | ||
}); | ||
} | ||
|
||
fn spawn_listener_hierarchy(mut commands: Commands) { | ||
for _ in 0..ENTITY_WIDTH { | ||
let mut parent = commands.spawn_empty().id(); | ||
for _ in 0..ENTITY_DEPTH { | ||
let child = commands.spawn_empty().id(); | ||
commands.entity(parent).add_child(child); | ||
parent = child; | ||
} | ||
} | ||
} | ||
|
||
fn empty_listener<const N: usize>(_trigger: Trigger<TestEvent<N>>) {} | ||
|
||
fn add_listeners_to_hierarchy<const DENSITY: usize, const N: usize>( | ||
mut commands: Commands, | ||
roots_and_leaves: Query<Entity, Or<(Without<Parent>, Without<Children>)>>, | ||
nodes: Query<Entity, (With<Parent>, With<Children>)>, | ||
) { | ||
for entity in &roots_and_leaves { | ||
commands.entity(entity).observe(empty_listener::<N>); | ||
} | ||
for entity in &nodes { | ||
maybe_insert_listener::<DENSITY, N>(&mut commands.entity(entity)); | ||
} | ||
} | ||
|
||
fn maybe_insert_listener<const DENSITY: usize, const N: usize>(commands: &mut EntityCommands) { | ||
if rand::thread_rng().gen_bool(DENSITY as f64 / 100.0) { | ||
commands.observe(empty_listener::<N>); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.