Minimal Bubbling Observers (#13991)
# 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 <alice.i.cecile@gmail.com> Co-authored-by: Torstein Grindvik <52322338+torsteingrindvik@users.noreply.github.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
d57531a900
commit
ed2b8e0f35
11
Cargo.toml
11
Cargo.toml
@ -2570,6 +2570,17 @@ description = "Demonstrates observers that react to events (both built-in life-c
|
|||||||
category = "ECS (Entity Component System)"
|
category = "ECS (Entity Component System)"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "observer_propagation"
|
||||||
|
path = "examples/ecs/observer_propagation.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.observer_propagation]
|
||||||
|
name = "Observer Propagation"
|
||||||
|
description = "Demonstrates event propagation with observers"
|
||||||
|
category = "ECS (Entity Component System)"
|
||||||
|
wasm = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "3d_rotation"
|
name = "3d_rotation"
|
||||||
path = "examples/transforms/3d_rotation.rs"
|
path = "examples/transforms/3d_rotation.rs"
|
||||||
|
@ -12,11 +12,13 @@ rand_chacha = "0.3"
|
|||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
bevy_app = { path = "../crates/bevy_app" }
|
bevy_app = { path = "../crates/bevy_app" }
|
||||||
bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] }
|
bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] }
|
||||||
|
bevy_hierarchy = { path = "../crates/bevy_hierarchy" }
|
||||||
|
bevy_internal = { path = "../crates/bevy_internal" }
|
||||||
|
bevy_math = { path = "../crates/bevy_math" }
|
||||||
bevy_reflect = { path = "../crates/bevy_reflect" }
|
bevy_reflect = { path = "../crates/bevy_reflect" }
|
||||||
|
bevy_render = { path = "../crates/bevy_render" }
|
||||||
bevy_tasks = { path = "../crates/bevy_tasks" }
|
bevy_tasks = { path = "../crates/bevy_tasks" }
|
||||||
bevy_utils = { path = "../crates/bevy_utils" }
|
bevy_utils = { path = "../crates/bevy_utils" }
|
||||||
bevy_math = { path = "../crates/bevy_math" }
|
|
||||||
bevy_render = { path = "../crates/bevy_render" }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
@ -3,6 +3,7 @@ use criterion::criterion_main;
|
|||||||
mod components;
|
mod components;
|
||||||
mod events;
|
mod events;
|
||||||
mod iteration;
|
mod iteration;
|
||||||
|
mod observers;
|
||||||
mod scheduling;
|
mod scheduling;
|
||||||
mod world;
|
mod world;
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ criterion_main!(
|
|||||||
components::components_benches,
|
components::components_benches,
|
||||||
events::event_benches,
|
events::event_benches,
|
||||||
iteration::iterations_benches,
|
iteration::iterations_benches,
|
||||||
|
observers::observer_benches,
|
||||||
scheduling::scheduling_benches,
|
scheduling::scheduling_benches,
|
||||||
world::world_benches,
|
world::world_benches,
|
||||||
);
|
);
|
||||||
|
6
benches/benches/bevy_ecs/observers/mod.rs
Normal file
6
benches/benches/bevy_ecs/observers/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use criterion::criterion_group;
|
||||||
|
|
||||||
|
mod propagation;
|
||||||
|
use propagation::*;
|
||||||
|
|
||||||
|
criterion_group!(observer_benches, event_propagation);
|
151
benches/benches/bevy_ecs/observers/propagation.rs
Normal file
151
benches/benches/bevy_ecs/observers/propagation.rs
Normal file
@ -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>);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,8 @@ pub fn derive_event(input: TokenStream) -> TokenStream {
|
|||||||
|
|
||||||
TokenStream::from(quote! {
|
TokenStream::from(quote! {
|
||||||
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
|
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
|
||||||
|
type Traversal = #bevy_ecs_path::traversal::TraverseNone;
|
||||||
|
const AUTO_PROPAGATE: bool = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
|
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::component::Component;
|
use crate::{component::Component, traversal::Traversal};
|
||||||
#[cfg(feature = "bevy_reflect")]
|
#[cfg(feature = "bevy_reflect")]
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
use std::{
|
use std::{
|
||||||
@ -34,7 +34,19 @@ use std::{
|
|||||||
label = "invalid `Event`",
|
label = "invalid `Event`",
|
||||||
note = "consider annotating `{Self}` with `#[derive(Event)]`"
|
note = "consider annotating `{Self}` with `#[derive(Event)]`"
|
||||||
)]
|
)]
|
||||||
pub trait Event: Component {}
|
pub trait Event: Component {
|
||||||
|
/// The component that describes which Entity to propagate this event to next, when [propagation] is enabled.
|
||||||
|
///
|
||||||
|
/// [propagation]: crate::observer::Trigger::propagate
|
||||||
|
type Traversal: Traversal;
|
||||||
|
|
||||||
|
/// When true, this event will always attempt to propagate when [triggered], without requiring a call
|
||||||
|
/// to [`Trigger::propagate`].
|
||||||
|
///
|
||||||
|
/// [triggered]: crate::system::Commands::trigger_targets
|
||||||
|
/// [`Trigger::propagate`]: crate::observer::Trigger::propagate
|
||||||
|
const AUTO_PROPAGATE: bool = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// An `EventId` uniquely identifies an event stored in a specific [`World`].
|
/// An `EventId` uniquely identifies an event stored in a specific [`World`].
|
||||||
///
|
///
|
||||||
|
@ -29,6 +29,7 @@ pub mod removal_detection;
|
|||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
pub mod traversal;
|
||||||
pub mod world;
|
pub mod world;
|
||||||
|
|
||||||
pub use bevy_ptr as ptr;
|
pub use bevy_ptr as ptr;
|
||||||
|
@ -15,18 +15,21 @@ use bevy_utils::{EntityHashMap, HashMap};
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the
|
/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the
|
||||||
/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well.
|
/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also
|
||||||
|
/// contains event propagation information. See [`Trigger::propagate`] for more information.
|
||||||
pub struct Trigger<'w, E, B: Bundle = ()> {
|
pub struct Trigger<'w, E, B: Bundle = ()> {
|
||||||
event: &'w mut E,
|
event: &'w mut E,
|
||||||
|
propagate: &'w mut bool,
|
||||||
trigger: ObserverTrigger,
|
trigger: ObserverTrigger,
|
||||||
_marker: PhantomData<B>,
|
_marker: PhantomData<B>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'w, E, B: Bundle> Trigger<'w, E, B> {
|
impl<'w, E, B: Bundle> Trigger<'w, E, B> {
|
||||||
/// Creates a new trigger for the given event and observer information.
|
/// Creates a new trigger for the given event and observer information.
|
||||||
pub fn new(event: &'w mut E, trigger: ObserverTrigger) -> Self {
|
pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self {
|
||||||
Self {
|
Self {
|
||||||
event,
|
event,
|
||||||
|
propagate,
|
||||||
trigger,
|
trigger,
|
||||||
_marker: PhantomData,
|
_marker: PhantomData,
|
||||||
}
|
}
|
||||||
@ -56,6 +59,29 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> {
|
|||||||
pub fn entity(&self) -> Entity {
|
pub fn entity(&self) -> Entity {
|
||||||
self.trigger.entity
|
self.trigger.entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities.
|
||||||
|
///
|
||||||
|
/// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events
|
||||||
|
/// use `TraverseNone` which ends the path immediately and prevents propagation.
|
||||||
|
///
|
||||||
|
/// To enable propagation, you must:
|
||||||
|
/// + Set [`Event::Traversal`] to the component you want to propagate along.
|
||||||
|
/// + Either call `propagate(true)` in the first observer or set [`Event::AUTO_PROPAGATE`] to `true`.
|
||||||
|
///
|
||||||
|
/// You can prevent an event from propagating further using `propagate(false)`.
|
||||||
|
///
|
||||||
|
/// [`Traversal`]: crate::traversal::Traversal
|
||||||
|
pub fn propagate(&mut self, should_propagate: bool) {
|
||||||
|
*self.propagate = should_propagate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value of the flag that controls event propagation. See [`propagate`] for more information.
|
||||||
|
///
|
||||||
|
/// [`propagate`]: Trigger::propagate
|
||||||
|
pub fn get_propagate(&self) -> bool {
|
||||||
|
*self.propagate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A description of what an [`Observer`] observes.
|
/// A description of what an [`Observer`] observes.
|
||||||
@ -174,6 +200,7 @@ impl Observers {
|
|||||||
entity: Entity,
|
entity: Entity,
|
||||||
components: impl Iterator<Item = ComponentId>,
|
components: impl Iterator<Item = ComponentId>,
|
||||||
data: &mut T,
|
data: &mut T,
|
||||||
|
propagate: &mut bool,
|
||||||
) {
|
) {
|
||||||
// SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld`
|
// SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld`
|
||||||
let (mut world, observers) = unsafe {
|
let (mut world, observers) = unsafe {
|
||||||
@ -197,9 +224,9 @@ impl Observers {
|
|||||||
entity,
|
entity,
|
||||||
},
|
},
|
||||||
data.into(),
|
data.into(),
|
||||||
|
propagate,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trigger observers listening for any kind of this trigger
|
// Trigger observers listening for any kind of this trigger
|
||||||
observers.map.iter().for_each(&mut trigger_observer);
|
observers.map.iter().for_each(&mut trigger_observer);
|
||||||
|
|
||||||
@ -393,6 +420,7 @@ mod tests {
|
|||||||
use crate as bevy_ecs;
|
use crate as bevy_ecs;
|
||||||
use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState};
|
use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::traversal::Traversal;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct A;
|
struct A;
|
||||||
@ -421,6 +449,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct Parent(Entity);
|
||||||
|
|
||||||
|
impl Traversal for Parent {
|
||||||
|
fn traverse(&self) -> Option<Entity> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct EventPropagating;
|
||||||
|
|
||||||
|
impl Event for EventPropagating {
|
||||||
|
type Traversal = Parent;
|
||||||
|
|
||||||
|
const AUTO_PROPAGATE: bool = true;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn observer_order_spawn_despawn() {
|
fn observer_order_spawn_despawn() {
|
||||||
let mut world = World::new();
|
let mut world = World::new();
|
||||||
@ -649,7 +695,7 @@ mod tests {
|
|||||||
world.spawn(ObserverState {
|
world.spawn(ObserverState {
|
||||||
// SAFETY: we registered `event_a` above and it matches the type of TriggerA
|
// SAFETY: we registered `event_a` above and it matches the type of TriggerA
|
||||||
descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) },
|
descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) },
|
||||||
runner: |mut world, _trigger, _ptr| {
|
runner: |mut world, _trigger, _ptr, _propagate| {
|
||||||
world.resource_mut::<R>().0 += 1;
|
world.resource_mut::<R>().0 += 1;
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@ -662,4 +708,233 @@ mod tests {
|
|||||||
world.flush();
|
world.flush();
|
||||||
assert_eq!(1, world.resource::<R>().0);
|
assert_eq!(1, world.resource::<R>().0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
let parent = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child = world
|
||||||
|
.spawn(Parent(parent))
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, child);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(2, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_redundant_dispatch_same_entity() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
let parent = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child = world
|
||||||
|
.spawn(Parent(parent))
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, [child, child]);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(4, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_redundant_dispatch_parent_child() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
let parent = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child = world
|
||||||
|
.spawn(Parent(parent))
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, [child, parent]);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(3, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_halt() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
let parent = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child = world
|
||||||
|
.spawn(Parent(parent))
|
||||||
|
.observe(
|
||||||
|
|mut trigger: Trigger<EventPropagating>, mut res: ResMut<R>| {
|
||||||
|
res.0 += 1;
|
||||||
|
trigger.propagate(false);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, child);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(1, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_join() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
let parent = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child_a = world
|
||||||
|
.spawn(Parent(parent))
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| {
|
||||||
|
res.0 += 1;
|
||||||
|
})
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child_b = world
|
||||||
|
.spawn(Parent(parent))
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| {
|
||||||
|
res.0 += 1;
|
||||||
|
})
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, [child_a, child_b]);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(4, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_no_next() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
let entity = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, entity);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(1, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_parallel_propagation() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
let parent_a = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child_a = world
|
||||||
|
.spawn(Parent(parent_a))
|
||||||
|
.observe(
|
||||||
|
|mut trigger: Trigger<EventPropagating>, mut res: ResMut<R>| {
|
||||||
|
res.0 += 1;
|
||||||
|
trigger.propagate(false);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let parent_b = world
|
||||||
|
.spawn_empty()
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let child_b = world
|
||||||
|
.spawn(Parent(parent_b))
|
||||||
|
.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, [child_a, child_b]);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(3, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_world() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
world.observe(|_: Trigger<EventPropagating>, mut res: ResMut<R>| res.0 += 1);
|
||||||
|
|
||||||
|
let grandparent = world.spawn_empty().id();
|
||||||
|
let parent = world.spawn(Parent(grandparent)).id();
|
||||||
|
let child = world.spawn(Parent(parent)).id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, child);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(3, world.resource::<R>().0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observer_propagating_world_skipping() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<R>();
|
||||||
|
|
||||||
|
world.observe(
|
||||||
|
|trigger: Trigger<EventPropagating>, query: Query<&A>, mut res: ResMut<R>| {
|
||||||
|
if query.get(trigger.entity()).is_ok() {
|
||||||
|
res.0 += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let grandparent = world.spawn(A).id();
|
||||||
|
let parent = world.spawn(Parent(grandparent)).id();
|
||||||
|
let child = world.spawn((A, Parent(parent))).id();
|
||||||
|
|
||||||
|
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
|
||||||
|
// and therefore does not automatically flush.
|
||||||
|
world.flush();
|
||||||
|
world.trigger_targets(EventPropagating, child);
|
||||||
|
world.flush();
|
||||||
|
assert_eq!(2, world.resource::<R>().0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ pub struct ObserverState {
|
|||||||
impl Default for ObserverState {
|
impl Default for ObserverState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
runner: |_, _, _| {},
|
runner: |_, _, _, _| {},
|
||||||
last_trigger_id: 0,
|
last_trigger_id: 0,
|
||||||
despawned_watched_entities: 0,
|
despawned_watched_entities: 0,
|
||||||
descriptor: Default::default(),
|
descriptor: Default::default(),
|
||||||
@ -86,7 +86,7 @@ impl Component for ObserverState {
|
|||||||
/// Type for function that is run when an observer is triggered.
|
/// Type for function that is run when an observer is triggered.
|
||||||
/// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component,
|
/// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component,
|
||||||
/// but can be overridden for custom behaviour.
|
/// but can be overridden for custom behaviour.
|
||||||
pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut);
|
pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: &mut bool);
|
||||||
|
|
||||||
/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer".
|
/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer".
|
||||||
///
|
///
|
||||||
@ -358,6 +358,7 @@ fn observer_system_runner<E: Event, B: Bundle>(
|
|||||||
mut world: DeferredWorld,
|
mut world: DeferredWorld,
|
||||||
observer_trigger: ObserverTrigger,
|
observer_trigger: ObserverTrigger,
|
||||||
ptr: PtrMut,
|
ptr: PtrMut,
|
||||||
|
propagate: &mut bool,
|
||||||
) {
|
) {
|
||||||
let world = world.as_unsafe_world_cell();
|
let world = world.as_unsafe_world_cell();
|
||||||
// SAFETY: Observer was triggered so must still exist in world
|
// SAFETY: Observer was triggered so must still exist in world
|
||||||
@ -381,8 +382,12 @@ fn observer_system_runner<E: Event, B: Bundle>(
|
|||||||
}
|
}
|
||||||
state.last_trigger_id = last_trigger;
|
state.last_trigger_id = last_trigger;
|
||||||
|
|
||||||
|
let trigger: Trigger<E, B> = Trigger::new(
|
||||||
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
|
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
|
||||||
let trigger: Trigger<E, B> = Trigger::new(unsafe { ptr.deref_mut() }, observer_trigger);
|
unsafe { ptr.deref_mut() },
|
||||||
|
propagate,
|
||||||
|
observer_trigger,
|
||||||
|
);
|
||||||
// SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out.
|
// SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out.
|
||||||
// Additionally, IntoObserverSystem is only implemented for functions starting
|
// Additionally, IntoObserverSystem is only implemented for functions starting
|
||||||
// with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually,
|
// with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually,
|
||||||
|
@ -48,32 +48,34 @@ impl<E: Event, Targets: TriggerTargets> Command for EmitDynamicTrigger<E, Target
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn trigger_event<E, Targets: TriggerTargets>(
|
fn trigger_event<E: Event, Targets: TriggerTargets>(
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
event_type: ComponentId,
|
event_type: ComponentId,
|
||||||
event_data: &mut E,
|
event_data: &mut E,
|
||||||
targets: Targets,
|
targets: Targets,
|
||||||
) {
|
) {
|
||||||
let mut world = DeferredWorld::from(world);
|
let mut world = DeferredWorld::from(world);
|
||||||
if targets.entities().len() == 0 {
|
if targets.entities().is_empty() {
|
||||||
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
|
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
|
||||||
unsafe {
|
unsafe {
|
||||||
world.trigger_observers_with_data(
|
world.trigger_observers_with_data::<_, E::Traversal>(
|
||||||
event_type,
|
event_type,
|
||||||
Entity::PLACEHOLDER,
|
Entity::PLACEHOLDER,
|
||||||
targets.components(),
|
targets.components(),
|
||||||
event_data,
|
event_data,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
for target in targets.entities() {
|
for target in targets.entities() {
|
||||||
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
|
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
|
||||||
unsafe {
|
unsafe {
|
||||||
world.trigger_observers_with_data(
|
world.trigger_observers_with_data::<_, E::Traversal>(
|
||||||
event_type,
|
event_type,
|
||||||
target,
|
*target,
|
||||||
targets.components(),
|
targets.components(),
|
||||||
event_data,
|
event_data,
|
||||||
|
E::AUTO_PROPAGATE,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -88,78 +90,78 @@ fn trigger_event<E, Targets: TriggerTargets>(
|
|||||||
/// [`Observer`]: crate::observer::Observer
|
/// [`Observer`]: crate::observer::Observer
|
||||||
pub trait TriggerTargets: Send + Sync + 'static {
|
pub trait TriggerTargets: Send + Sync + 'static {
|
||||||
/// The components the trigger should target.
|
/// The components the trigger should target.
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId>;
|
fn components(&self) -> &[ComponentId];
|
||||||
|
|
||||||
/// The entities the trigger should target.
|
/// The entities the trigger should target.
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity>;
|
fn entities(&self) -> &[Entity];
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TriggerTargets for () {
|
impl TriggerTargets for () {
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
fn components(&self) -> &[ComponentId] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
fn entities(&self) -> &[Entity] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TriggerTargets for Entity {
|
impl TriggerTargets for Entity {
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
fn components(&self) -> &[ComponentId] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
fn entities(&self) -> &[Entity] {
|
||||||
std::iter::once(*self)
|
std::slice::from_ref(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TriggerTargets for Vec<Entity> {
|
impl TriggerTargets for Vec<Entity> {
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
fn components(&self) -> &[ComponentId] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
fn entities(&self) -> &[Entity] {
|
||||||
self.iter().copied()
|
self.as_slice()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> TriggerTargets for [Entity; N] {
|
impl<const N: usize> TriggerTargets for [Entity; N] {
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
fn components(&self) -> &[ComponentId] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
fn entities(&self) -> &[Entity] {
|
||||||
self.iter().copied()
|
self.as_slice()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TriggerTargets for ComponentId {
|
impl TriggerTargets for ComponentId {
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
fn components(&self) -> &[ComponentId] {
|
||||||
std::iter::once(*self)
|
std::slice::from_ref(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
fn entities(&self) -> &[Entity] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TriggerTargets for Vec<ComponentId> {
|
impl TriggerTargets for Vec<ComponentId> {
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
fn components(&self) -> &[ComponentId] {
|
||||||
self.iter().copied()
|
self.as_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
fn entities(&self) -> &[Entity] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> TriggerTargets for [ComponentId; N] {
|
impl<const N: usize> TriggerTargets for [ComponentId; N] {
|
||||||
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
|
fn components(&self) -> &[ComponentId] {
|
||||||
self.iter().copied()
|
self.as_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
|
fn entities(&self) -> &[Entity] {
|
||||||
[].into_iter()
|
&[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
43
crates/bevy_ecs/src/traversal.rs
Normal file
43
crates/bevy_ecs/src/traversal.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//! A trait for components that let you traverse the ECS.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
component::{Component, StorageType},
|
||||||
|
entity::Entity,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A component that can point to another entity, and which can be used to define a path through the ECS.
|
||||||
|
///
|
||||||
|
/// Traversals are used to [specify the direction] of [event propagation] in [observers]. By default,
|
||||||
|
/// events use the [`TraverseNone`] placeholder component, which cannot actually be created or added to
|
||||||
|
/// an entity and so never causes traversal.
|
||||||
|
///
|
||||||
|
/// Infinite loops are possible, and are not checked for. While looping can be desirable in some contexts
|
||||||
|
/// (for example, an observer that triggers itself multiple times before stopping), following an infinite
|
||||||
|
/// traversal loop without an eventual exit will can your application to hang. Each implementer of `Traversal`
|
||||||
|
/// for documenting possible looping behavior, and consumers of those implementations are responsible for
|
||||||
|
/// avoiding infinite loops in their code.
|
||||||
|
///
|
||||||
|
/// [specify the direction]: crate::event::Event::Traversal
|
||||||
|
/// [event propagation]: crate::observer::Trigger::propagate
|
||||||
|
/// [observers]: crate::observer::Observer
|
||||||
|
pub trait Traversal: Component {
|
||||||
|
/// Returns the next entity to visit.
|
||||||
|
fn traverse(&self) -> Option<Entity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A traversal component that doesn't traverse anything. Used to provide a default traversal
|
||||||
|
/// implementation for events.
|
||||||
|
///
|
||||||
|
/// It is not possible to actually construct an instance of this component.
|
||||||
|
pub enum TraverseNone {}
|
||||||
|
|
||||||
|
impl Traversal for TraverseNone {
|
||||||
|
#[inline(always)]
|
||||||
|
fn traverse(&self) -> Option<Entity> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for TraverseNone {
|
||||||
|
const STORAGE_TYPE: StorageType = StorageType::Table;
|
||||||
|
}
|
@ -10,6 +10,7 @@ use crate::{
|
|||||||
prelude::{Component, QueryState},
|
prelude::{Component, QueryState},
|
||||||
query::{QueryData, QueryFilter},
|
query::{QueryData, QueryFilter},
|
||||||
system::{Commands, Query, Resource},
|
system::{Commands, Query, Resource},
|
||||||
|
traversal::Traversal,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -350,7 +351,14 @@ impl<'w> DeferredWorld<'w> {
|
|||||||
entity: Entity,
|
entity: Entity,
|
||||||
components: impl Iterator<Item = ComponentId>,
|
components: impl Iterator<Item = ComponentId>,
|
||||||
) {
|
) {
|
||||||
Observers::invoke(self.reborrow(), event, entity, components, &mut ());
|
Observers::invoke::<_>(
|
||||||
|
self.reborrow(),
|
||||||
|
event,
|
||||||
|
entity,
|
||||||
|
components,
|
||||||
|
&mut (),
|
||||||
|
&mut false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Triggers all event observers for [`ComponentId`] in target.
|
/// Triggers all event observers for [`ComponentId`] in target.
|
||||||
@ -358,14 +366,34 @@ impl<'w> DeferredWorld<'w> {
|
|||||||
/// # Safety
|
/// # Safety
|
||||||
/// Caller must ensure `E` is accessible as the type represented by `event`
|
/// Caller must ensure `E` is accessible as the type represented by `event`
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) unsafe fn trigger_observers_with_data<E>(
|
pub(crate) unsafe fn trigger_observers_with_data<E, C>(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: ComponentId,
|
event: ComponentId,
|
||||||
entity: Entity,
|
mut entity: Entity,
|
||||||
components: impl Iterator<Item = ComponentId>,
|
components: &[ComponentId],
|
||||||
data: &mut E,
|
data: &mut E,
|
||||||
) {
|
mut propagate: bool,
|
||||||
Observers::invoke(self.reborrow(), event, entity, components, data);
|
) where
|
||||||
|
C: Traversal,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
Observers::invoke::<_>(
|
||||||
|
self.reborrow(),
|
||||||
|
event,
|
||||||
|
entity,
|
||||||
|
components.iter().copied(),
|
||||||
|
data,
|
||||||
|
&mut propagate,
|
||||||
|
);
|
||||||
|
if !propagate {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(traverse_to) = self.get::<C>(entity).and_then(C::traverse) {
|
||||||
|
entity = traverse_to;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets.
|
/// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets.
|
||||||
|
@ -3,6 +3,7 @@ use bevy_ecs::reflect::{ReflectComponent, ReflectMapEntities};
|
|||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
component::Component,
|
component::Component,
|
||||||
entity::{Entity, EntityMapper, MapEntities},
|
entity::{Entity, EntityMapper, MapEntities},
|
||||||
|
traversal::Traversal,
|
||||||
world::{FromWorld, World},
|
world::{FromWorld, World},
|
||||||
};
|
};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
@ -69,3 +70,14 @@ impl Deref for Parent {
|
|||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This provides generalized hierarchy traversal for use in [event propagation].
|
||||||
|
///
|
||||||
|
/// `Parent::traverse` will never form loops in properly-constructed hierarchies.
|
||||||
|
///
|
||||||
|
/// [event propagation]: bevy_ecs::observer::Trigger::propagate
|
||||||
|
impl Traversal for Parent {
|
||||||
|
fn traverse(&self) -> Option<Entity> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -278,6 +278,7 @@ Example | Description
|
|||||||
[Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities
|
[Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities
|
||||||
[Iter Combinations](../examples/ecs/iter_combinations.rs) | Shows how to iterate over combinations of query results
|
[Iter Combinations](../examples/ecs/iter_combinations.rs) | Shows how to iterate over combinations of query results
|
||||||
[Nondeterministic System Order](../examples/ecs/nondeterministic_system_order.rs) | Systems run in parallel, but their order isn't always deterministic. Here's how to detect and fix this.
|
[Nondeterministic System Order](../examples/ecs/nondeterministic_system_order.rs) | Systems run in parallel, but their order isn't always deterministic. Here's how to detect and fix this.
|
||||||
|
[Observer Propagation](../examples/ecs/observer_propagation.rs) | Demonstrates event propagation with observers
|
||||||
[Observers](../examples/ecs/observers.rs) | Demonstrates observers that react to events (both built-in life-cycle events and custom events)
|
[Observers](../examples/ecs/observers.rs) | Demonstrates observers that react to events (both built-in life-cycle events and custom events)
|
||||||
[One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them
|
[One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them
|
||||||
[Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator`
|
[Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator`
|
||||||
|
125
examples/ecs/observer_propagation.rs
Normal file
125
examples/ecs/observer_propagation.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
//! Demonstrates how to propagate events through the hierarchy with observers.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer};
|
||||||
|
use rand::{seq::IteratorRandom, thread_rng, Rng};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins((MinimalPlugins, LogPlugin::default()))
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
attack_armor.run_if(on_timer(Duration::from_millis(200))),
|
||||||
|
)
|
||||||
|
// Add a global observer that will emit a line whenever an attack hits an entity.
|
||||||
|
.observe(attack_hits)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// In this example, we spawn a goblin wearing different pieces of armor. Each piece of armor
|
||||||
|
// is represented as a child entity, with an `Armor` component.
|
||||||
|
//
|
||||||
|
// We're going to model how attack damage can be partially blocked by the goblin's armor using
|
||||||
|
// event bubbling. Our events will target the armor, and if the armor isn't strong enough to block
|
||||||
|
// the attack it will continue up and hit the goblin.
|
||||||
|
fn setup(mut commands: Commands) {
|
||||||
|
commands
|
||||||
|
.spawn((Name::new("Goblin"), HitPoints(50)))
|
||||||
|
.observe(take_damage)
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent
|
||||||
|
.spawn((Name::new("Helmet"), Armor(5)))
|
||||||
|
.observe(block_attack);
|
||||||
|
parent
|
||||||
|
.spawn((Name::new("Socks"), Armor(10)))
|
||||||
|
.observe(block_attack);
|
||||||
|
parent
|
||||||
|
.spawn((Name::new("Shirt"), Armor(15)))
|
||||||
|
.observe(block_attack);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This event represents an attack we want to "bubble" up from the armor to the goblin.
|
||||||
|
#[derive(Clone, Component)]
|
||||||
|
struct Attack {
|
||||||
|
damage: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We enable propagation by implementing `Event` manually (rather than using a derive) and specifying
|
||||||
|
// two important pieces of information:
|
||||||
|
impl Event for Attack {
|
||||||
|
// 1. Which component we want to propagate along. In this case, we want to "bubble" (meaning propagate
|
||||||
|
// from child to parent) so we use the `Parent` component for propagation. The component supplied
|
||||||
|
// must implement the `Traversal` trait.
|
||||||
|
type Traversal = Parent;
|
||||||
|
// 2. We can also choose whether or not this event will propagate by default when triggered. If this is
|
||||||
|
// false, it will only propagate following a call to `Trigger::propagate(true)`.
|
||||||
|
const AUTO_PROPAGATE: bool = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An entity that can take damage.
|
||||||
|
#[derive(Component, Deref, DerefMut)]
|
||||||
|
struct HitPoints(u16);
|
||||||
|
|
||||||
|
/// For damage to reach the wearer, it must exceed the armor.
|
||||||
|
#[derive(Component, Deref)]
|
||||||
|
struct Armor(u16);
|
||||||
|
|
||||||
|
/// A normal bevy system that attacks a piece of the goblin's armor on a timer.
|
||||||
|
fn attack_armor(entities: Query<Entity, With<Armor>>, mut commands: Commands) {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
if let Some(target) = entities.iter().choose(&mut rng) {
|
||||||
|
let damage = thread_rng().gen_range(1..20);
|
||||||
|
commands.trigger_targets(Attack { damage }, target);
|
||||||
|
info!("⚔️ Attack for {} damage", damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attack_hits(trigger: Trigger<Attack>, name: Query<&Name>) {
|
||||||
|
if let Ok(name) = name.get(trigger.entity()) {
|
||||||
|
info!("Attack hit {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage.
|
||||||
|
fn block_attack(mut trigger: Trigger<Attack>, armor: Query<(&Armor, &Name)>) {
|
||||||
|
let (armor, name) = armor.get(trigger.entity()).unwrap();
|
||||||
|
let attack = trigger.event_mut();
|
||||||
|
let damage = attack.damage.saturating_sub(**armor);
|
||||||
|
if damage > 0 {
|
||||||
|
info!("🩸 {} damage passed through {}", damage, name);
|
||||||
|
// The attack isn't stopped by the armor. We reduce the damage of the attack, and allow
|
||||||
|
// it to continue on to the goblin.
|
||||||
|
attack.damage = damage;
|
||||||
|
} else {
|
||||||
|
info!("🛡️ {} damage blocked by {}", attack.damage, name);
|
||||||
|
// Armor stopped the attack, the event stops here.
|
||||||
|
trigger.propagate(false);
|
||||||
|
info!("(propagation halted early)\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack,
|
||||||
|
/// or the wearer is attacked directly.
|
||||||
|
fn take_damage(
|
||||||
|
trigger: Trigger<Attack>,
|
||||||
|
mut hp: Query<(&mut HitPoints, &Name)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut app_exit: EventWriter<bevy::app::AppExit>,
|
||||||
|
) {
|
||||||
|
let attack = trigger.event();
|
||||||
|
let (mut hp, name) = hp.get_mut(trigger.entity()).unwrap();
|
||||||
|
**hp = hp.saturating_sub(attack.damage);
|
||||||
|
|
||||||
|
if **hp > 0 {
|
||||||
|
info!("{} has {:.1} HP", name, hp.0);
|
||||||
|
} else {
|
||||||
|
warn!("💀 {} has died a gruesome death", name);
|
||||||
|
commands.entity(trigger.entity()).despawn_recursive();
|
||||||
|
app_exit.send(bevy::app::AppExit::Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("(propagation reached root)\n");
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user