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:
Miles Silberling-Cook 2024-07-15 08:39:41 -05:00 committed by GitHub
parent d57531a900
commit ed2b8e0f35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 731 additions and 53 deletions

View file

@ -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"

View file

@ -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

View file

@ -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,
);

View file

@ -0,0 +1,6 @@
use criterion::criterion_group;
mod propagation;
use propagation::*;
criterion_group!(observer_benches, event_propagation);

View 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>);
}
}

View file

@ -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 {

View file

@ -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`].
///

View file

@ -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;

View file

@ -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<B>,
}
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<Item = ComponentId>,
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<Entity> {
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::<R>().0 += 1;
},
..Default::default()
@ -662,4 +708,233 @@ mod tests {
world.flush();
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);
}
}

View file

@ -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<E: Event, B: Bundle>(
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<E: Event, B: Bundle>(
}
state.last_trigger_id = last_trigger;
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
let trigger: Trigger<E, B> = Trigger::new(unsafe { ptr.deref_mut() }, observer_trigger);
let trigger: Trigger<E, B> = 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,

View file

@ -48,32 +48,34 @@ impl<E: Event, Targets: TriggerTargets> Command for EmitDynamicTrigger<E, Target
}
#[inline]
fn trigger_event<E, Targets: TriggerTargets>(
fn trigger_event<E: Event, Targets: TriggerTargets>(
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<E, Targets: TriggerTargets>(
/// [`Observer`]: crate::observer::Observer
pub trait TriggerTargets: Send + Sync + 'static {
/// The components the trigger should target.
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId>;
fn components(&self) -> &[ComponentId];
/// The entities the trigger should target.
fn entities(&self) -> impl ExactSizeIterator<Item = Entity>;
fn entities(&self) -> &[Entity];
}
impl TriggerTargets for () {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
fn components(&self) -> &[ComponentId] {
&[]
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
fn entities(&self) -> &[Entity] {
&[]
}
}
impl TriggerTargets for Entity {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
fn components(&self) -> &[ComponentId] {
&[]
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
std::iter::once(*self)
fn entities(&self) -> &[Entity] {
std::slice::from_ref(self)
}
}
impl TriggerTargets for Vec<Entity> {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
fn components(&self) -> &[ComponentId] {
&[]
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
self.iter().copied()
fn entities(&self) -> &[Entity] {
self.as_slice()
}
}
impl<const N: usize> TriggerTargets for [Entity; N] {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
fn components(&self) -> &[ComponentId] {
&[]
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
self.iter().copied()
fn entities(&self) -> &[Entity] {
self.as_slice()
}
}
impl TriggerTargets for ComponentId {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
std::iter::once(*self)
fn components(&self) -> &[ComponentId] {
std::slice::from_ref(self)
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
fn entities(&self) -> &[Entity] {
&[]
}
}
impl TriggerTargets for Vec<ComponentId> {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
self.iter().copied()
fn components(&self) -> &[ComponentId] {
self.as_slice()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
fn entities(&self) -> &[Entity] {
&[]
}
}
impl<const N: usize> TriggerTargets for [ComponentId; N] {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
self.iter().copied()
fn components(&self) -> &[ComponentId] {
self.as_slice()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
fn entities(&self) -> &[Entity] {
&[]
}
}

View 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;
}

View file

@ -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<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.
@ -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<E>(
pub(crate) unsafe fn trigger_observers_with_data<E, C>(
&mut self,
event: ComponentId,
entity: Entity,
components: impl Iterator<Item = ComponentId>,
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::<C>(entity).and_then(C::traverse) {
entity = traverse_to;
} else {
break;
}
}
}
/// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets.

View file

@ -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<Entity> {
Some(self.0)
}
}

View file

@ -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`

View 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");
}