diff --git a/benches/benches/bevy_ecs/observers/dynamic.rs b/benches/benches/bevy_ecs/observers/dynamic.rs new file mode 100644 index 0000000000000..8b0be622420c3 --- /dev/null +++ b/benches/benches/bevy_ecs/observers/dynamic.rs @@ -0,0 +1,51 @@ +use bevy_ecs::{ + event::Event, + observer::{DynamicEvent, EmitDynamicTrigger, EventSet, Observer, Trigger}, + world::{Command, World}, +}; +use criterion::{black_box, Criterion}; + +pub fn observe_dynamic(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("observe_dynamic"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(4)); + + group.bench_function("1", |bencher| { + let mut world = World::new(); + let event_id_1 = world.init_component::>(); + world.spawn(Observer::new(empty_listener_set::).with_event(event_id_1)); + bencher.iter(|| { + for _ in 0..10000 { + unsafe { + EmitDynamicTrigger::new_with_id(event_id_1, TestEvent::<1>, ()) + .apply(&mut world) + }; + } + }); + }); + group.bench_function("2", |bencher| { + let mut world = World::new(); + let event_id_1 = world.init_component::>(); + let event_id_2 = world.init_component::>(); + world.spawn( + Observer::new(empty_listener_set::) + .with_event(event_id_1) + .with_event(event_id_2), + ); + bencher.iter(|| { + for _ in 0..10000 { + unsafe { + EmitDynamicTrigger::new_with_id(event_id_2, TestEvent::<2>, ()) + .apply(&mut world) + }; + } + }); + }); +} + +#[derive(Event)] +struct TestEvent; + +fn empty_listener_set(trigger: Trigger) { + black_box(trigger); +} diff --git a/benches/benches/bevy_ecs/observers/mod.rs b/benches/benches/bevy_ecs/observers/mod.rs index 0b8c3f24869ce..ceab56c99e1fb 100644 --- a/benches/benches/bevy_ecs/observers/mod.rs +++ b/benches/benches/bevy_ecs/observers/mod.rs @@ -1,8 +1,21 @@ use criterion::criterion_group; +mod dynamic; +mod multievent; mod propagation; +mod semidynamic; mod simple; +use dynamic::*; +use multievent::*; use propagation::*; +use semidynamic::*; use simple::*; -criterion_group!(observer_benches, event_propagation, observe_simple); +criterion_group!( + observer_benches, + event_propagation, + observe_simple, + observe_multievent, + observe_dynamic, + observe_semidynamic +); diff --git a/benches/benches/bevy_ecs/observers/multievent.rs b/benches/benches/bevy_ecs/observers/multievent.rs new file mode 100644 index 0000000000000..4002481a30110 --- /dev/null +++ b/benches/benches/bevy_ecs/observers/multievent.rs @@ -0,0 +1,105 @@ +use bevy_ecs::{ + component::Component, + event::Event, + observer::{EventSet, Observer, Trigger}, + world::World, +}; +use criterion::{black_box, measurement::WallTime, BenchmarkGroup, Criterion}; + +pub fn observe_multievent(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("observe_multievent"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(4)); + + group.bench_function("trigger_single", |bencher| { + let mut world = World::new(); + world.observe(empty_listener_set::>); + bencher.iter(|| { + for _ in 0..10000 { + world.trigger(TestEvent::<1>); + } + }); + }); + + bench_in_set::<1, (TestEvent<1>,)>(&mut group); + bench_in_set::<2, (TestEvent<1>, TestEvent<2>)>(&mut group); + bench_in_set::<4, (TestEvent<1>, TestEvent<2>, TestEvent<3>, TestEvent<4>)>(&mut group); + bench_in_set::< + 8, + ( + TestEvent<1>, + TestEvent<2>, + TestEvent<3>, + TestEvent<4>, + TestEvent<5>, + TestEvent<6>, + TestEvent<7>, + TestEvent<8>, + ), + >(&mut group); + bench_in_set::< + 12, + ( + TestEvent<1>, + TestEvent<2>, + TestEvent<3>, + TestEvent<4>, + TestEvent<5>, + TestEvent<6>, + TestEvent<7>, + TestEvent<8>, + TestEvent<9>, + TestEvent<10>, + TestEvent<11>, + TestEvent<12>, + ), + >(&mut group); + bench_in_set::< + 15, + ( + TestEvent<1>, + TestEvent<2>, + TestEvent<3>, + TestEvent<4>, + TestEvent<5>, + TestEvent<6>, + TestEvent<7>, + TestEvent<8>, + TestEvent<9>, + TestEvent<10>, + TestEvent<11>, + TestEvent<12>, + TestEvent<13>, + TestEvent<14>, + TestEvent<15>, + ), + >(&mut group); +} + +fn bench_in_set(group: &mut BenchmarkGroup) { + group.bench_function(format!("trigger_first/{LAST}"), |bencher| { + let mut world = World::new(); + world.observe(empty_listener_set::); + bencher.iter(|| { + for _ in 0..10000 { + world.trigger(TestEvent::<1>); + } + }); + }); + group.bench_function(format!("trigger_last/{LAST}"), |bencher| { + let mut world = World::new(); + world.observe(empty_listener_set::); + bencher.iter(|| { + for _ in 0..10000 { + world.trigger(TestEvent::); + } + }); + }); +} + +#[derive(Event)] +struct TestEvent; + +fn empty_listener_set(trigger: Trigger) { + black_box(trigger); +} diff --git a/benches/benches/bevy_ecs/observers/semidynamic.rs b/benches/benches/bevy_ecs/observers/semidynamic.rs new file mode 100644 index 0000000000000..a6895cf572e1b --- /dev/null +++ b/benches/benches/bevy_ecs/observers/semidynamic.rs @@ -0,0 +1,183 @@ +use bevy_ecs::{ + event::Event, + observer::{EmitDynamicTrigger, EventSet, Observer, SemiDynamicEvent, Trigger}, + world::{Command, World}, +}; +use criterion::{black_box, Criterion}; + +pub fn observe_semidynamic(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("observe_semidynamic"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(4)); + + group.bench_function("static/1s-1d", |bencher| { + let mut world = World::new(); + let event_id_1 = world.init_component::>(); + world.spawn( + Observer::new(empty_listener_set::>>).with_event(event_id_1), + ); + + bencher.iter(|| { + for _ in 0..10000 { + world.trigger(Static::<1>); + } + }); + }); + group.bench_function("dynamic/1s-1d", |bencher| { + let mut world = World::new(); + let event_id_1 = world.init_component::>(); + world.spawn( + Observer::new(empty_listener_set::>>).with_event(event_id_1), + ); + + bencher.iter(|| { + for _ in 0..10000 { + unsafe { + EmitDynamicTrigger::new_with_id(event_id_1, Dynamic::<1>, ()).apply(&mut world) + }; + } + }); + }); + + group.bench_function("static/15s-15d", |bencher| { + // Aint she perdy? + let mut world = World::new(); + let event_id_1 = world.init_component::>(); + let event_id_2 = world.init_component::>(); + let event_id_3 = world.init_component::>(); + let event_id_4 = world.init_component::>(); + let event_id_5 = world.init_component::>(); + let event_id_6 = world.init_component::>(); + let event_id_7 = world.init_component::>(); + let event_id_8 = world.init_component::>(); + let event_id_9 = world.init_component::>(); + let event_id_10 = world.init_component::>(); + let event_id_11 = world.init_component::>(); + let event_id_12 = world.init_component::>(); + let event_id_13 = world.init_component::>(); + let event_id_14 = world.init_component::>(); + let event_id_15 = world.init_component::>(); + world.spawn( + Observer::new( + empty_listener_set::< + SemiDynamicEvent<( + Static<1>, + Static<2>, + Static<3>, + Static<4>, + Static<5>, + Static<6>, + Static<7>, + Static<8>, + Static<9>, + Static<10>, + Static<11>, + Static<12>, + Static<13>, + Static<14>, + Static<15>, + )>, + >, + ) + .with_event(event_id_1) + .with_event(event_id_2) + .with_event(event_id_3) + .with_event(event_id_4) + .with_event(event_id_5) + .with_event(event_id_6) + .with_event(event_id_7) + .with_event(event_id_8) + .with_event(event_id_9) + .with_event(event_id_10) + .with_event(event_id_11) + .with_event(event_id_12) + .with_event(event_id_13) + .with_event(event_id_14) + .with_event(event_id_15), + ); + + bencher.iter(|| { + for _ in 0..10000 { + world.trigger(Static::<14>); + } + }); + }); + group.bench_function("dynamic/15s-15d", |bencher| { + // Aint she perdy? + let mut world = World::new(); + let event_id_1 = world.init_component::>(); + let event_id_2 = world.init_component::>(); + let event_id_3 = world.init_component::>(); + let event_id_4 = world.init_component::>(); + let event_id_5 = world.init_component::>(); + let event_id_6 = world.init_component::>(); + let event_id_7 = world.init_component::>(); + let event_id_8 = world.init_component::>(); + let event_id_9 = world.init_component::>(); + let event_id_10 = world.init_component::>(); + let event_id_11 = world.init_component::>(); + let event_id_12 = world.init_component::>(); + let event_id_13 = world.init_component::>(); + let event_id_14 = world.init_component::>(); + let event_id_15 = world.init_component::>(); + world.spawn( + Observer::new( + empty_listener_set::< + SemiDynamicEvent<( + Static<1>, + Static<2>, + Static<3>, + Static<4>, + Static<5>, + Static<6>, + Static<7>, + Static<8>, + Static<9>, + Static<10>, + Static<11>, + Static<12>, + Static<13>, + Static<14>, + Static<15>, + )>, + >, + ) + .with_event(event_id_1) + .with_event(event_id_2) + .with_event(event_id_3) + .with_event(event_id_4) + .with_event(event_id_5) + .with_event(event_id_6) + .with_event(event_id_7) + .with_event(event_id_8) + .with_event(event_id_9) + .with_event(event_id_10) + .with_event(event_id_11) + .with_event(event_id_12) + .with_event(event_id_13) + .with_event(event_id_14) + .with_event(event_id_15), + ); + + bencher.iter(|| { + for _ in 0..10000 { + unsafe { + EmitDynamicTrigger::new_with_id(event_id_14, Dynamic::<14>, ()) + .apply(&mut world) + }; + } + }); + }); +} + +/// Static event type +#[derive(Event)] +struct Static; + +/// Dynamic event type +#[derive(Event)] +struct Dynamic; + +fn empty_listener_set(trigger: Trigger) { + black_box(trigger); +} diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 7188102837dea..2d03deff583a9 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -6,6 +6,7 @@ pub use bevy_derive::AppLabel; use bevy_ecs::{ event::{event_update_system, EventCursor}, intern::Interned, + observer::EventSet, prelude::*, schedule::{ScheduleBuildSettings, ScheduleLabel}, system::{IntoObserverSystem, SystemId}, @@ -990,7 +991,7 @@ impl App { } /// Spawns an [`Observer`] entity, which will watch for and respond to the given event. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 600384d478438..c1be95e518b21 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -2,31 +2,32 @@ mod entity_observer; mod runner; +mod set; mod trigger_event; pub use runner::*; +pub use set::*; pub use trigger_event::*; use crate::observer::entity_observer::ObservedBy; use crate::{archetype::ArchetypeFlags, system::IntoObserverSystem, world::*}; use crate::{component::ComponentId, prelude::*, world::DeferredWorld}; -use bevy_ptr::Ptr; 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. It also /// contains event propagation information. See [`Trigger::propagate`] for more information. -pub struct Trigger<'w, E, B: Bundle = ()> { - event: &'w mut E, +pub struct Trigger<'w, E: EventSet, B: Bundle = ()> { + event: E::Item<'w>, propagate: &'w mut bool, trigger: ObserverTrigger, _marker: PhantomData, } -impl<'w, E, B: Bundle> Trigger<'w, E, B> { +impl<'w, E: EventSet, B: Bundle> Trigger<'w, E, B> { /// Creates a new trigger for the given event and observer information. - pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { + pub fn new(event: E::Item<'w>, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { Self { event, propagate, @@ -41,18 +42,13 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { } /// Returns a reference to the triggered event. - pub fn event(&self) -> &E { - self.event + pub fn event(&self) -> E::ReadOnlyItem<'_> { + E::shrink_readonly(&self.event) } /// Returns a mutable reference to the triggered event. - pub fn event_mut(&mut self) -> &mut E { - self.event - } - - /// Returns a pointer to the triggered event. - pub fn event_ptr(&self) -> Ptr { - Ptr::from(&self.event) + pub fn event_mut(&mut self) -> E::Item<'_> { + E::shrink(&mut self.event) } /// Returns the entity that triggered the observer, could be [`Entity::PLACEHOLDER`]. @@ -304,7 +300,7 @@ impl Observers { impl World { /// Spawns a "global" [`Observer`] and returns its [`Entity`]. - pub fn observe( + pub fn observe( &mut self, system: impl IntoObserverSystem, ) -> EntityWorldMut { @@ -433,7 +429,8 @@ mod tests { use crate as bevy_ecs; use crate::observer::{ - EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace, + DynamicEvent, EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace, + Or2, SemiDynamicEvent, }; use crate::prelude::*; use crate::traversal::Traversal; @@ -601,16 +598,60 @@ mod tests { } #[test] - fn observer_multiple_events() { + fn observer_multiple_events_static() { let mut world = World::new(); world.init_resource::(); + #[derive(Event)] + struct Foo(i32); + #[derive(Event)] + struct Bar(bool); + world.observe(|t: Trigger<(Foo, Bar)>, mut res: ResMut| { + res.0 += 1; + match t.event() { + Or2::A(Foo(v)) => assert_eq!(5, *v), + Or2::B(Bar(v)) => assert!(*v), + } + }); + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger(Foo(5)); + world.trigger(Bar(true)); + assert_eq!(2, world.resource::().0); + } + + #[test] + fn observer_multiple_events_dynamic() { + let mut world = World::new(); + world.init_resource::(); + let on_add = world.init_component::(); let on_remove = world.init_component::(); world.spawn( - // SAFETY: OnAdd and OnRemove are both unit types, so this is safe - unsafe { - Observer::new(|_: Trigger, mut res: ResMut| res.0 += 1) - .with_event(on_remove) - }, + Observer::new(|_: Trigger, mut res: ResMut| res.0 += 1) + .with_event(on_add) + .with_event(on_remove), + ); + + let entity = world.spawn(A).id(); + world.despawn(entity); + assert_eq!(2, world.resource::().0); + } + + #[test] + fn observer_multiple_events_semidynamic() { + let mut world = World::new(); + world.init_resource::(); + let on_remove = world.init_component::(); + world.spawn( + Observer::new( + |trigger: Trigger, A>, mut res: ResMut| { + match trigger.event() { + Ok(_onadd) => res.assert_order(0), + Err(_ptr) => res.assert_order(1), + }; + }, + ) + .with_event(on_remove), ); let entity = world.spawn(A).id(); @@ -976,4 +1017,78 @@ mod tests { world.flush(); assert_eq!(2, world.resource::().0); } + + #[test] + fn observer_propagating_multi_event_between() { + let mut world = World::new(); + world.init_resource::(); + #[derive(Event)] + struct Foo; + + let grandparent = world + .spawn_empty() + .observe(|_: Trigger<(EventPropagating,)>, mut res: ResMut| res.0 += 1) + .id(); + let parent = world + .spawn(Parent(grandparent)) + .observe(|_: Trigger<(EventPropagating, Foo)>, mut res: ResMut| res.0 += 1) + .id(); + let child = world + .spawn(Parent(parent)) + .observe(|_: Trigger<(EventPropagating,)>, 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!(3, world.resource::().0); + world.trigger_targets(Foo, parent); + world.flush(); + assert_eq!(4, world.resource::().0); + } + + #[test] + fn observer_propagating_multi_event_all() { + let mut world = World::new(); + world.init_resource::(); + #[derive(Component)] + struct OtherPropagating; + + impl Event for OtherPropagating { + type Traversal = Parent; + + const AUTO_PROPAGATE: bool = true; + } + + let grandparent = world + .spawn_empty() + .observe( + |_: Trigger<(EventPropagating, OtherPropagating)>, mut res: ResMut| res.0 += 1, + ) + .id(); + let parent = world + .spawn(Parent(grandparent)) + .observe( + |_: Trigger<(EventPropagating, OtherPropagating)>, mut res: ResMut| res.0 += 1, + ) + .id(); + let child = world + .spawn(Parent(parent)) + .observe( + |_: Trigger<(EventPropagating, OtherPropagating)>, 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!(3, world.resource::().0); + world.trigger_targets(OtherPropagating, child); + world.flush(); + assert_eq!(6, world.resource::().0); + } } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 05afa4f614f76..246b0529aa3db 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,6 +1,9 @@ use crate::{ component::{ComponentHooks, ComponentId, StorageType}, - observer::{ObserverDescriptor, ObserverTrigger}, + observer::{ + DynamicEvent, EventSet, ObserverDescriptor, ObserverTrigger, SemiDynamicEvent, + StaticEventSet, + }, prelude::*, query::DebugCheckedUnwrap, system::{IntoObserverSystem, ObserverSystem}, @@ -260,12 +263,12 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// serves as the "source of truth" of the observer. /// /// [`SystemParam`]: crate::system::SystemParam -pub struct Observer { - system: BoxedObserverSystem, +pub struct Observer { + system: BoxedObserverSystem, descriptor: ObserverDescriptor, } -impl Observer { +impl Observer { /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered /// for _any_ entity (or no entity). pub fn new(system: impl IntoObserverSystem) -> Self { @@ -301,24 +304,54 @@ impl Observer { /// # Safety /// The type of the `event` [`ComponentId`] _must_ match the actual value /// of the event passed into the observer system. - pub unsafe fn with_event(mut self, event: ComponentId) -> Self { + pub unsafe fn with_event_unchecked(mut self, event: ComponentId) -> Self { self.descriptor.events.push(event); self } } -impl Component for Observer { +impl Observer { + /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] + /// is triggered. + /// + /// # Note + /// As opposed to [`Observer::with_event_unchecked`], this method is safe to use because no pointer casting is performed automatically. + /// That is left to the user to do manually. + pub fn with_event(self, event: ComponentId) -> Self { + // SAFETY: DynamicEvent itself does not perform any unsafe operations (like casting), so this is safe. + unsafe { self.with_event_unchecked(event) } + } +} + +impl Observer, B> { + /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] + /// is triggered. + /// + /// # Note + /// As opposed to [`Observer::with_event_unchecked`], this method is safe to use because no pointer casting is performed automatically + /// on event types outside its statically-known set of. That is left to the user to do manually. + pub fn with_event(self, event: ComponentId) -> Self { + // SAFETY: SemiDynamicEvent itself does not perform any unsafe operations (like casting) + // for event types outside its statically-known set, so this is safe. + unsafe { self.with_event_unchecked(event) } + } +} + +impl Component for Observer { const STORAGE_TYPE: StorageType = StorageType::SparseSet; fn register_component_hooks(hooks: &mut ComponentHooks) { hooks.on_add(|mut world, entity, _| { world.commands().add(move |world: &mut World| { - let event_type = world.init_component::(); + let mut events = Vec::new(); + E::init_components(world, |id| { + events.push(id); + }); let mut components = Vec::new(); B::component_ids(&mut world.components, &mut world.storages, &mut |id| { components.push(id); }); let mut descriptor = ObserverDescriptor { - events: vec![event_type], + events, components, ..Default::default() }; @@ -354,7 +387,7 @@ impl Component for Observer { /// Equivalent to [`BoxedSystem`](crate::system::BoxedSystem) for [`ObserverSystem`]. pub type BoxedObserverSystem = Box>; -fn observer_system_runner( +fn observer_system_runner( mut world: DeferredWorld, observer_trigger: ObserverTrigger, ptr: PtrMut, @@ -381,12 +414,26 @@ fn observer_system_runner( } state.last_trigger_id = last_trigger; - let trigger: Trigger = Trigger::new( - // SAFETY: Caller ensures `ptr` is castable to `&mut T` - unsafe { ptr.deref_mut() }, - propagate, - observer_trigger, - ); + // SAFETY: We have immutable access to the world from the passed in DeferredWorld + let world_ref = unsafe { world.world() }; + // SAFETY: Observer was triggered with an event in the set of events it observes, so it must be convertible to E + // We choose to use unchecked_cast over the safe cast method to avoid the overhead of matching in the single event type case (which is the common case). + let Ok(event) = (unsafe { E::unchecked_cast(world_ref, &observer_trigger, ptr) }) else { + // This branch is only ever hit if the user called Observer::with_event_unchecked with a component ID not matching the event set E, + // EXCEPT when the event set is a singular event type, in which case the cast will always succeed. + // This is a user error and should be logged. + bevy_utils::tracing::error!( + "Observer was triggered with an event that does not match the event set. \ + Did you call Observer::with_event_unchecked with the wrong ID? \ + Observer: {:?} Event: {:?} Set: {:?}", + observer_trigger.observer, + observer_trigger.event_type, + std::any::type_name::() + ); + return; + }; + + let trigger: Trigger = Trigger::new(event, 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/set.rs b/crates/bevy_ecs/src/observer/set.rs new file mode 100644 index 0000000000000..932435d080f14 --- /dev/null +++ b/crates/bevy_ecs/src/observer/set.rs @@ -0,0 +1,387 @@ +use bevy_ptr::{Ptr, PtrMut}; + +use crate::component::ComponentId; +use crate::event::Event; +use crate::observer::ObserverTrigger; +use crate::world::World; + +/// A set of [`Event`]s that can trigger an observer. +/// +/// The provided implementations of this trait are: +/// +/// - All [`Event`]s. +/// - Any tuple of [`Event`]s, up to 15 types. These can be nested. +/// - [`DynamicEvent`], which matches any [`Event`]s dynamically added to the observer with [`Observer::with_event`] and does not reify the event data. +/// - [`SemiDynamicEvent`], which will first try to match a statically-known set of [`Event`]s and reify the event data, +/// and if no match is found, it will fall back to functioning as a [`DynamicEvent`]. +/// +/// # Example +/// +/// TODO +/// +/// # Safety +/// +/// Implementor must ensure that: +/// - [`EventSet::init_components`] must register a [`ComponentId`] for each [`Event`] type in the set. +/// - [`EventSet::matches`] must return `true` if the triggered [`Event`]'s [`ComponentId`] matches a type in the set, +/// or unambiguously always returns `true` or `false`. +/// +/// [`Observer::with_event`]: crate::observer::Observer::with_event +pub unsafe trait EventSet: 'static { + /// The item returned by this [`EventSet`] that will be passed to the observer system function. + /// Most of the time this will be a mutable reference to an [`Event`] type, a tuple of mutable references, or a [`PtrMut`]. + type Item<'trigger>; + /// The read-only variant of the [`Item`](EventSet::Item). + type ReadOnlyItem<'trigger>: Copy; + + /// Safely casts a pointer to the [`Item`](EventSet::Item) type by checking prior + /// whether the triggered [`Event`]'s [`ComponentId`] [`matches`](EventSet::matches) a type in this event set. + fn cast<'trigger>( + world: &World, + observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + if Self::matches(world, observer_trigger) { + // SAFETY: We have checked that the event component id matches the event type + unsafe { Self::unchecked_cast(world, observer_trigger, ptr) } + } else { + Err(ptr) + } + } + + /// Casts a pointer to the [`Item`](EventSet::Item) type + /// without checking if the [`Event`] type matches this event set. + /// + /// # Safety + /// + /// Caller must ensure that the [`Event`]'s [`ComponentId`] [`matches`](EventSet::matches) + /// this event set before calling this function. + unsafe fn unchecked_cast<'trigger>( + world: &World, + observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>>; + + /// Checks if the [`Event`] type matches the observer trigger. + /// + /// # Safety + /// + /// Implementors must ensure that this function returns `true` + /// if the triggered [`Event`]'s [`ComponentId`] matches a type in the set, + /// or unambiguously always returns `true` or `false`. + fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool; + + /// Initialize the components required by this event set. + fn init_components(world: &mut World, ids: impl FnMut(ComponentId)); + + /// Shrink the [`Item`](EventSet::Item) to a shorter lifetime. + fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short>; + + /// Shrink the [`Item`](EventSet::Item) to a shorter lifetime [`ReadOnlyItem`](EventSet::ReadOnlyItem). + fn shrink_readonly<'long: 'short, 'short>( + item: &'short Self::Item<'long>, + ) -> Self::ReadOnlyItem<'short>; +} + +/// An [`EventSet`] that matches a statically pre-defined set of event types. +/// +/// This trait is required in order to prevent a footgun where a user might accidentally specify an `EventSet` similar to +/// `(DynamicEvent, EventA, EventB)`, which would always match `DynamicEvent` and never `EventA` or `EventB`. +/// Therefore, we prevent the introduction of `DynamicEvent` in a static `EventSet`, +/// most notably any `EventSet` tuple made up of normal [`Event`] types. +/// +/// If you need to support both dynamic and static event types in a single observer, +/// you can use [`SemiDynamicEvent`] instead. +/// +/// # Safety +/// +/// Implementors must ensure that [`matches`](EventSet::matches) +/// returns `true` if and only if the event component id matches the event type, +/// and DOES NOT match any other event type. +pub unsafe trait StaticEventSet: EventSet {} + +// SAFETY: The event type has a component id registered in `init_components`, +// and `matches` checks that the event component id matches the event type. +unsafe impl EventSet for E { + type Item<'trigger> = &'trigger mut E; + type ReadOnlyItem<'trigger> = &'trigger E; + + unsafe fn unchecked_cast<'trigger>( + _world: &World, + _observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + // SAFETY: Caller must ensure that the component id matches the event type + Ok(unsafe { ptr.deref_mut() }) + } + + fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool { + world + .component_id::() + .is_some_and(|id| id == observer_trigger.event_type) + } + + fn init_components(world: &mut World, mut ids: impl FnMut(ComponentId)) { + let id = world.init_component::(); + ids(id); + } + + fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> { + item + } + + fn shrink_readonly<'long: 'short, 'short>( + item: &'short Self::Item<'long>, + ) -> Self::ReadOnlyItem<'short> { + item + } +} + +// SAFETY: The event type is a statically known type. +unsafe impl StaticEventSet for E {} + +/// An [`EventSet`] that matches any event type and performs no casting. Instead, it returns the pointer as is. +/// This is useful for observers that do not need to access the event data, or need to do so dynamically. +/// +/// # Example +/// +/// TODO +pub struct DynamicEvent; + +// SAFETY: Performs no unsafe operations, returns the pointer as is. +unsafe impl EventSet for DynamicEvent { + type Item<'trigger> = PtrMut<'trigger>; + type ReadOnlyItem<'trigger> = Ptr<'trigger>; + + fn cast<'trigger>( + _world: &World, + _observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + Ok(ptr) + } + + unsafe fn unchecked_cast<'trigger>( + _world: &World, + _observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + Ok(ptr) + } + + fn matches(_world: &World, _observer_trigger: &ObserverTrigger) -> bool { + // We're treating this as a catch-all event set, so it always matches. + true + } + + fn init_components(_world: &mut World, _ids: impl FnMut(ComponentId)) {} + + fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> { + item.reborrow() + } + + fn shrink_readonly<'long: 'short, 'short>( + item: &'short Self::Item<'long>, + ) -> Self::ReadOnlyItem<'short> { + item.as_ref() + } +} + +/// An [`EventSet`] that either matches a statically pre-defined set of event types and casts the pointer to the event type, +/// or returns the pointer as-is if the event type was not matched. +/// Basically, it allows you to mix static and dynamic event types in a single observer. +/// +/// `SemiDynamicEvent` accepts two type parameters: +/// +/// - **Static** +/// The static event set that will be matched and casted. +/// Generally, this should be a tuple of static event types, like `(FooEvent, BarEvent)`. +/// Must implement [`StaticEventSet`] trait, which means no [`DynamicEvent`] or [`SemiDynamicEvent`] nesting. +/// +/// # Example +/// +/// TODO +pub struct SemiDynamicEvent(std::marker::PhantomData); + +// SAFETY: No unsafe operations are performed. The checked cast variant is used for the static event type. +unsafe impl EventSet for SemiDynamicEvent { + type Item<'trigger> = Result, PtrMut<'trigger>>; + type ReadOnlyItem<'trigger> = Result, Ptr<'trigger>>; + + unsafe fn unchecked_cast<'trigger>( + world: &World, + observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + match Static::cast(world, observer_trigger, ptr) { + Ok(item) => Ok(Ok(item)), + Err(ptr) => Ok(Err(ptr)), + } + } + + fn matches(_world: &World, _observer_trigger: &ObserverTrigger) -> bool { + true + } + + fn init_components(world: &mut World, mut ids: impl FnMut(ComponentId)) { + Static::init_components(world, &mut ids); + } + + fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> { + match item { + Ok(item) => Ok(Static::shrink(item)), + Err(ptr) => Err(ptr.reborrow()), + } + } + + fn shrink_readonly<'long: 'short, 'short>( + item: &'short Self::Item<'long>, + ) -> Self::ReadOnlyItem<'short> { + match item { + Ok(item) => Ok(Static::shrink_readonly(item)), + Err(ptr) => Err(ptr.as_ref()), + } + } +} + +// SAFETY: Forwards to the inner event type, and inherits its safety properties. +unsafe impl EventSet for (A,) { + type Item<'trigger> = A::Item<'trigger>; + type ReadOnlyItem<'trigger> = A::ReadOnlyItem<'trigger>; + + fn cast<'trigger>( + world: &World, + observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + A::cast(world, observer_trigger, ptr) + } + + unsafe fn unchecked_cast<'trigger>( + world: &World, + observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + A::unchecked_cast(world, observer_trigger, ptr) + } + + fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool { + A::matches(world, observer_trigger) + } + + fn init_components(world: &mut World, ids: impl FnMut(ComponentId)) { + A::init_components(world, ids); + } + + fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> { + A::shrink(item) + } + + fn shrink_readonly<'long: 'short, 'short>( + item: &'short Self::Item<'long>, + ) -> Self::ReadOnlyItem<'short> { + A::shrink_readonly(item) + } +} + +// SAFETY: The inner event set is a static event set. +unsafe impl StaticEventSet for (A,) {} + +macro_rules! impl_event_set { + ($Or:ident, $(($P:ident, $p:ident)),*) => { + /// An output type of an observer that observes multiple event types. + #[derive(Copy, Clone)] + pub enum $Or<$($P),*> { + $( + /// A possible event type. + $P($P), + )* + } + + // SAFETY: All event types have a component id registered in `init_components`, + // and `unchecked_cast` calls `matches` before casting to one of the inner event sets. + unsafe impl<$($P: StaticEventSet),*> EventSet for ($($P,)*) { + type Item<'trigger> = $Or<$($P::Item<'trigger>),*>; + type ReadOnlyItem<'trigger> = $Or<$($P::ReadOnlyItem<'trigger>),*>; + + fn cast<'trigger>( + world: &World, + observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + // SAFETY: Each inner event set is checked in order for a match and then casted. + unsafe { Self::unchecked_cast(world, observer_trigger, ptr) } + } + + unsafe fn unchecked_cast<'trigger>( + world: &World, + observer_trigger: &ObserverTrigger, + ptr: PtrMut<'trigger>, + ) -> Result, PtrMut<'trigger>> { + if false { + unreachable!(); + } + $( + else if $P::matches(world, observer_trigger) { + match $P::unchecked_cast(world, observer_trigger, ptr) { + Ok($p) => return Ok($Or::$P($p)), + Err(ptr) => return Err(ptr), + } + } + )* + + Err(ptr) + } + + fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool { + $( + $P::matches(world, observer_trigger) || + )* + false + } + + fn init_components(world: &mut World, mut ids: impl FnMut(ComponentId)) { + $( + $P::init_components(world, &mut ids); + )* + } + + fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> { + match item { + $( + $Or::$P($p) => $Or::$P($P::shrink($p)), + )* + } + } + + fn shrink_readonly<'long: 'short, 'short>( + item: &'short Self::Item<'long>, + ) -> Self::ReadOnlyItem<'short> { + match item { + $( + $Or::$P($p) => $Or::$P($P::shrink_readonly($p)), + )* + } + } + } + + // SAFETY: All inner event types are static event sets. + unsafe impl<$($P: StaticEventSet),*> StaticEventSet for ($($P,)*) {} + }; +} + +// We can't use `all_tuples` here because it doesn't support the extra `OrX` parameter required for each tuple impl. +#[rustfmt::skip] impl_event_set!(Or2, (A, a), (B, b)); +#[rustfmt::skip] impl_event_set!(Or3, (A, a), (B, b), (C, c)); +#[rustfmt::skip] impl_event_set!(Or4, (A, a), (B, b), (C, c), (D, d)); +#[rustfmt::skip] impl_event_set!(Or5, (A, a), (B, b), (C, c), (D, d), (E, e)); +#[rustfmt::skip] impl_event_set!(Or6, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f)); +#[rustfmt::skip] impl_event_set!(Or7, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g)); +#[rustfmt::skip] impl_event_set!(Or8, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h)); +#[rustfmt::skip] impl_event_set!(Or9, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i)); +#[rustfmt::skip] impl_event_set!(Or10, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j)); +#[rustfmt::skip] impl_event_set!(Or11, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k)); +#[rustfmt::skip] impl_event_set!(Or12, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l)); +#[rustfmt::skip] impl_event_set!(Or13, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l), (M, m)); +#[rustfmt::skip] impl_event_set!(Or14, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l), (M, m), (N, n)); +#[rustfmt::skip] impl_event_set!(Or15, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l), (M, m), (N, n), (O, o)); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 875e0ae9de8c6..5a27030426ba0 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -9,7 +9,7 @@ use crate::{ component::{ComponentId, ComponentInfo}, entity::{Entities, Entity}, event::Event, - observer::{Observer, TriggerEvent, TriggerTargets}, + observer::{EventSet, Observer, TriggerEvent, TriggerTargets}, system::{RunSystemWithInput, SystemId}, world::{ command_queue::RawCommandQueue, Command, CommandQueue, EntityWorldMut, FromWorld, @@ -778,7 +778,7 @@ impl<'w, 's> Commands<'w, 's> { } /// Spawn an [`Observer`] and returns the [`EntityCommands`] associated with the entity that stores the observer. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> EntityCommands { @@ -1211,7 +1211,7 @@ impl EntityCommands<'_> { } /// Creates an [`Observer`] listening for a trigger of type `T` that targets this entity. - pub fn observe( + pub fn observe( &mut self, system: impl IntoObserverSystem, ) -> &mut Self { @@ -1443,7 +1443,7 @@ fn log_components(entity: Entity, world: &mut World) { info!("Entity {entity}: {debug_infos:?}"); } -fn observe( +fn observe( observer: impl IntoObserverSystem, ) -> impl EntityCommand { move |entity, world: &mut World| { diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index c5a04f25dd4eb..eae0d663ff2fc 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -1,6 +1,7 @@ use bevy_utils::all_tuples; use crate::{ + observer::EventSet, prelude::{Bundle, Trigger}, system::{System, SystemParam, SystemParamFunction, SystemParamItem}, }; @@ -10,13 +11,13 @@ use super::IntoSystem; /// Implemented for systems that have an [`Observer`] as the first argument. /// /// [`Observer`]: crate::observer::Observer -pub trait ObserverSystem: +pub trait ObserverSystem: System, Out = Out> + Send + 'static { } impl< - E: 'static, + E: EventSet + 'static, B: Bundle, Out, T: System, Out = Out> + Send + 'static, @@ -25,7 +26,9 @@ impl< } /// Implemented for systems that convert into [`ObserverSystem`]. -pub trait IntoObserverSystem: Send + 'static { +pub trait IntoObserverSystem: + Send + 'static +{ /// The type of [`System`] that this instance converts into. type System: ObserverSystem; @@ -37,7 +40,7 @@ impl< S: IntoSystem, Out, M> + Send + 'static, M, Out, - E: 'static, + E: EventSet + 'static, B: Bundle, > IntoObserverSystem for S where @@ -53,7 +56,7 @@ where macro_rules! impl_system_function { ($($param: ident),*) => { #[allow(non_snake_case)] - impl SystemParamFunction, $($param,)*)> for Func + impl SystemParamFunction, $($param,)*)> for Func where for <'a> &'a mut Func: FnMut(Trigger, $($param),*) -> Out + @@ -65,7 +68,7 @@ macro_rules! impl_system_function { #[inline] fn run(&mut self, input: Trigger<'static, E, B>, param_value: SystemParamItem< ($($param,)*)>) -> Out { #[allow(clippy::too_many_arguments)] - fn call_inner( + fn call_inner( mut f: impl FnMut(Trigger<'static, E, B>, $($param,)*) -> Out, input: Trigger<'static, E, B>, $($param: $param,)* diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 34dc767341ba6..bfa23456c6e2e 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -4,8 +4,7 @@ use crate::{ change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{Entities, Entity, EntityLocation}, - event::Event, - observer::{Observer, Observers}, + observer::{EventSet, Observer, Observers}, query::Access, removal_detection::RemovedComponentEvents, storage::Storages, @@ -1445,7 +1444,7 @@ impl<'w> EntityWorldMut<'w> { /// Creates an [`Observer`] listening for events of type `E` targeting this entity. /// In order to trigger the callback the entity must also match the query when the event is fired. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self {