mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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
16 changed files with 731 additions and 53 deletions
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)"
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
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! {
|
||||
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 {
|
||||
|
|
|
@ -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`].
|
||||
///
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
|
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},
|
||||
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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
|
|
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 a new issue