diff --git a/Cargo.toml b/Cargo.toml index 8599622c09..9a39bb09e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2570,6 +2570,17 @@ description = "Demonstrates observers that react to events (both built-in life-c category = "ECS (Entity Component System)" 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]] name = "3d_rotation" path = "examples/transforms/3d_rotation.rs" diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 3df074a75c..d1163b0cd1 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -12,11 +12,13 @@ rand_chacha = "0.3" criterion = { version = "0.3", features = ["html_reports"] } bevy_app = { path = "../crates/bevy_app" } 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_render = { path = "../crates/bevy_render" } bevy_tasks = { path = "../crates/bevy_tasks" } bevy_utils = { path = "../crates/bevy_utils" } -bevy_math = { path = "../crates/bevy_math" } -bevy_render = { path = "../crates/bevy_render" } [profile.release] opt-level = 3 diff --git a/benches/benches/bevy_ecs/benches.rs b/benches/benches/bevy_ecs/benches.rs index 6f1e89fb6d..b3db6abe77 100644 --- a/benches/benches/bevy_ecs/benches.rs +++ b/benches/benches/bevy_ecs/benches.rs @@ -3,6 +3,7 @@ use criterion::criterion_main; mod components; mod events; mod iteration; +mod observers; mod scheduling; mod world; @@ -10,6 +11,7 @@ criterion_main!( components::components_benches, events::event_benches, iteration::iterations_benches, + observers::observer_benches, scheduling::scheduling_benches, world::world_benches, ); diff --git a/benches/benches/bevy_ecs/observers/mod.rs b/benches/benches/bevy_ecs/observers/mod.rs new file mode 100644 index 0000000000..15ac476abc --- /dev/null +++ b/benches/benches/bevy_ecs/observers/mod.rs @@ -0,0 +1,6 @@ +use criterion::criterion_group; + +mod propagation; +use propagation::*; + +criterion_group!(observer_benches, event_propagation); diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs new file mode 100644 index 0000000000..ed22b1b519 --- /dev/null +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -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::, + ), + ) + .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::, + ), + ) + .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::, + add_listeners_to_hierarchy::, + add_listeners_to_hierarchy::, + add_listeners_to_hierarchy::, + ), + ) + .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 {} + +impl Event for TestEvent { + type Traversal = Parent; + const AUTO_PROPAGATE: bool = true; +} + +fn send_events( + mut commands: Commands, + entities: Query>, +) { + let target = entities.iter().choose(&mut rand::thread_rng()).unwrap(); + (0..N_EVENTS).for_each(|_| { + commands.trigger_targets(TestEvent:: {}, 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(_trigger: Trigger>) {} + +fn add_listeners_to_hierarchy( + mut commands: Commands, + roots_and_leaves: Query, Without)>>, + nodes: Query, With)>, +) { + for entity in &roots_and_leaves { + commands.entity(entity).observe(empty_listener::); + } + for entity in &nodes { + maybe_insert_listener::(&mut commands.entity(entity)); + } +} + +fn maybe_insert_listener(commands: &mut EntityCommands) { + if rand::thread_rng().gen_bool(DENSITY as f64 / 100.0) { + commands.observe(empty_listener::); + } +} diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index fc4a7e84a8..a1120c61f9 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -17,6 +17,8 @@ pub fn derive_event(input: TokenStream) -> TokenStream { TokenStream::from(quote! { 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 { diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index f31f9b7c0c..ab56ac58ae 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -1,4 +1,4 @@ -use crate::component::Component; +use crate::{component::Component, traversal::Traversal}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use std::{ @@ -34,7 +34,19 @@ use std::{ label = "invalid `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`]. /// diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index da573baf04..75a214ed68 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -29,6 +29,7 @@ pub mod removal_detection; pub mod schedule; pub mod storage; pub mod system; +pub mod traversal; pub mod world; pub use bevy_ptr as ptr; diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index b3d689d449..82e7268f29 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -15,18 +15,21 @@ use bevy_utils::{EntityHashMap, HashMap}; use std::marker::PhantomData; /// 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 = ()> { event: &'w mut E, + propagate: &'w mut bool, trigger: ObserverTrigger, _marker: PhantomData, } impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// 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 { event, + propagate, trigger, _marker: PhantomData, } @@ -56,6 +59,29 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { pub fn entity(&self) -> 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. @@ -174,6 +200,7 @@ impl Observers { entity: Entity, components: impl Iterator, data: &mut T, + propagate: &mut bool, ) { // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` let (mut world, observers) = unsafe { @@ -197,9 +224,9 @@ impl Observers { entity, }, data.into(), + propagate, ); }; - // Trigger observers listening for any kind of this trigger observers.map.iter().for_each(&mut trigger_observer); @@ -393,6 +420,7 @@ mod tests { use crate as bevy_ecs; use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState}; use crate::prelude::*; + use crate::traversal::Traversal; #[derive(Component)] struct A; @@ -421,6 +449,24 @@ mod tests { } } + #[derive(Component)] + struct Parent(Entity); + + impl Traversal for Parent { + fn traverse(&self) -> Option { + Some(self.0) + } + } + + #[derive(Component)] + struct EventPropagating; + + impl Event for EventPropagating { + type Traversal = Parent; + + const AUTO_PROPAGATE: bool = true; + } + #[test] fn observer_order_spawn_despawn() { let mut world = World::new(); @@ -649,7 +695,7 @@ mod tests { world.spawn(ObserverState { // SAFETY: we registered `event_a` above and it matches the type of TriggerA descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) }, - runner: |mut world, _trigger, _ptr| { + runner: |mut world, _trigger, _ptr, _propagate| { world.resource_mut::().0 += 1; }, ..Default::default() @@ -662,4 +708,233 @@ mod tests { world.flush(); assert_eq!(1, world.resource::().0); } + + #[test] + fn observer_propagating() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| 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::().0); + } + + #[test] + fn observer_propagating_redundant_dispatch_same_entity() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| 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::().0); + } + + #[test] + fn observer_propagating_redundant_dispatch_parent_child() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| 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::().0); + } + + #[test] + fn observer_propagating_halt() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe( + |mut trigger: Trigger, mut res: ResMut| { + 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::().0); + } + + #[test] + fn observer_propagating_join() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child_a = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| { + res.0 += 1; + }) + .id(); + + let child_b = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| { + 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::().0); + } + + #[test] + fn observer_propagating_no_next() { + let mut world = World::new(); + world.init_resource::(); + + let entity = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| 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::().0); + } + + #[test] + fn observer_propagating_parallel_propagation() { + let mut world = World::new(); + world.init_resource::(); + + let parent_a = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child_a = world + .spawn(Parent(parent_a)) + .observe( + |mut trigger: Trigger, mut res: ResMut| { + res.0 += 1; + trigger.propagate(false); + }, + ) + .id(); + + let parent_b = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child_b = world + .spawn(Parent(parent_b)) + .observe(|_: Trigger, mut res: ResMut| 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::().0); + } + + #[test] + fn observer_propagating_world() { + let mut world = World::new(); + world.init_resource::(); + + world.observe(|_: Trigger, mut res: ResMut| 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::().0); + } + + #[test] + fn observer_propagating_world_skipping() { + let mut world = World::new(); + world.init_resource::(); + + world.observe( + |trigger: Trigger, query: Query<&A>, mut res: ResMut| { + 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::().0); + } } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index f68441f270..7ca5dd43ad 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -20,7 +20,7 @@ pub struct ObserverState { impl Default for ObserverState { fn default() -> Self { Self { - runner: |_, _, _| {}, + runner: |_, _, _, _| {}, last_trigger_id: 0, despawned_watched_entities: 0, descriptor: Default::default(), @@ -86,7 +86,7 @@ impl Component for ObserverState { /// 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, /// 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". /// @@ -358,6 +358,7 @@ fn observer_system_runner( mut world: DeferredWorld, observer_trigger: ObserverTrigger, ptr: PtrMut, + propagate: &mut bool, ) { let world = world.as_unsafe_world_cell(); // SAFETY: Observer was triggered so must still exist in world @@ -381,8 +382,12 @@ fn observer_system_runner( } state.last_trigger_id = last_trigger; - // SAFETY: Caller ensures `ptr` is castable to `&mut T` - let trigger: Trigger = Trigger::new(unsafe { ptr.deref_mut() }, observer_trigger); + let trigger: Trigger = Trigger::new( + // SAFETY: Caller ensures `ptr` is castable to `&mut T` + unsafe { ptr.deref_mut() }, + propagate, + observer_trigger, + ); // SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out. // Additionally, IntoObserverSystem is only implemented for functions starting // with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually, diff --git a/crates/bevy_ecs/src/observer/trigger_event.rs b/crates/bevy_ecs/src/observer/trigger_event.rs index ff4000c8ee..fea35c0782 100644 --- a/crates/bevy_ecs/src/observer/trigger_event.rs +++ b/crates/bevy_ecs/src/observer/trigger_event.rs @@ -48,32 +48,34 @@ impl Command for EmitDynamicTrigger( +fn trigger_event( world: &mut World, event_type: ComponentId, event_data: &mut E, targets: Targets, ) { 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` unsafe { - world.trigger_observers_with_data( + world.trigger_observers_with_data::<_, E::Traversal>( event_type, Entity::PLACEHOLDER, targets.components(), event_data, + false, ); }; } else { for target in targets.entities() { // SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new` unsafe { - world.trigger_observers_with_data( + world.trigger_observers_with_data::<_, E::Traversal>( event_type, - target, + *target, targets.components(), event_data, + E::AUTO_PROPAGATE, ); }; } @@ -88,78 +90,78 @@ fn trigger_event( /// [`Observer`]: crate::observer::Observer pub trait TriggerTargets: Send + Sync + 'static { /// The components the trigger should target. - fn components(&self) -> impl ExactSizeIterator; + fn components(&self) -> &[ComponentId]; /// The entities the trigger should target. - fn entities(&self) -> impl ExactSizeIterator; + fn entities(&self) -> &[Entity]; } impl TriggerTargets for () { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + &[] } } impl TriggerTargets for Entity { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - std::iter::once(*self) + fn entities(&self) -> &[Entity] { + std::slice::from_ref(self) } } impl TriggerTargets for Vec { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn entities(&self) -> &[Entity] { + self.as_slice() } } impl TriggerTargets for [Entity; N] { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn entities(&self) -> &[Entity] { + self.as_slice() } } impl TriggerTargets for ComponentId { - fn components(&self) -> impl ExactSizeIterator { - std::iter::once(*self) + fn components(&self) -> &[ComponentId] { + std::slice::from_ref(self) } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + &[] } } impl TriggerTargets for Vec { - fn components(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn components(&self) -> &[ComponentId] { + self.as_slice() } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + &[] } } impl TriggerTargets for [ComponentId; N] { - fn components(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn components(&self) -> &[ComponentId] { + self.as_slice() } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + &[] } } diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs new file mode 100644 index 0000000000..7e4705f1eb --- /dev/null +++ b/crates/bevy_ecs/src/traversal.rs @@ -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; +} + +/// 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 { + None + } +} + +impl Component for TraverseNone { + const STORAGE_TYPE: StorageType = StorageType::Table; +} diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 9d0350abf2..065aba0e9c 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -10,6 +10,7 @@ use crate::{ prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, system::{Commands, Query, Resource}, + traversal::Traversal, }; use super::{ @@ -350,7 +351,14 @@ impl<'w> DeferredWorld<'w> { entity: Entity, components: impl Iterator, ) { - 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. @@ -358,14 +366,34 @@ impl<'w> DeferredWorld<'w> { /// # Safety /// Caller must ensure `E` is accessible as the type represented by `event` #[inline] - pub(crate) unsafe fn trigger_observers_with_data( + pub(crate) unsafe fn trigger_observers_with_data( &mut self, event: ComponentId, - entity: Entity, - components: impl Iterator, + mut entity: Entity, + components: &[ComponentId], data: &mut E, - ) { - Observers::invoke(self.reborrow(), event, entity, components, data); + mut propagate: bool, + ) 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::(entity).and_then(C::traverse) { + entity = traverse_to; + } else { + break; + } + } } /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. diff --git a/crates/bevy_hierarchy/src/components/parent.rs b/crates/bevy_hierarchy/src/components/parent.rs index bb2b6abba0..99d8e45460 100644 --- a/crates/bevy_hierarchy/src/components/parent.rs +++ b/crates/bevy_hierarchy/src/components/parent.rs @@ -3,6 +3,7 @@ use bevy_ecs::reflect::{ReflectComponent, ReflectMapEntities}; use bevy_ecs::{ component::Component, entity::{Entity, EntityMapper, MapEntities}, + traversal::Traversal, world::{FromWorld, World}, }; use std::ops::Deref; @@ -69,3 +70,14 @@ impl Deref for Parent { &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 { + Some(self.0) + } +} diff --git a/examples/README.md b/examples/README.md index 20507cd176..b83ea77123 100644 --- a/examples/README.md +++ b/examples/README.md @@ -278,6 +278,7 @@ Example | Description [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 [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) [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` diff --git a/examples/ecs/observer_propagation.rs b/examples/ecs/observer_propagation.rs new file mode 100644 index 0000000000..f51b6e5f90 --- /dev/null +++ b/examples/ecs/observer_propagation.rs @@ -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>, 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, 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, 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, + mut hp: Query<(&mut HitPoints, &Name)>, + mut commands: Commands, + mut app_exit: EventWriter, +) { + 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"); +}