From 4d82820e971320b9d7e1f4a8b60d1dc265ab2fa2 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Tue, 15 Jul 2025 15:28:33 +0200 Subject: [PATCH 01/17] Split new `BroadcastEvent` trait from `Event` --- benches/benches/bevy_ecs/observers/simple.rs | 18 ++- crates/bevy_ecs/README.md | 2 +- crates/bevy_ecs/macros/src/component.rs | 3 +- crates/bevy_ecs/macros/src/lib.rs | 6 +- crates/bevy_ecs/src/archetype.rs | 4 +- crates/bevy_ecs/src/component/tick.rs | 6 +- crates/bevy_ecs/src/event/base.rs | 10 +- crates/bevy_ecs/src/event/mod.rs | 4 +- crates/bevy_ecs/src/lib.rs | 4 +- crates/bevy_ecs/src/observer/mod.rs | 146 +++++++++--------- crates/bevy_ecs/src/observer/runner.rs | 4 +- .../bevy_ecs/src/system/commands/command.rs | 6 +- crates/bevy_ecs/src/system/commands/mod.rs | 8 +- crates/bevy_ecs/src/system/observer_system.rs | 4 +- crates/bevy_ecs/src/system/system_registry.rs | 3 +- crates/bevy_ecs/src/world/deferred_world.rs | 8 +- examples/ecs/observers.rs | 2 +- examples/usage/context_menu.rs | 4 +- 18 files changed, 130 insertions(+), 112 deletions(-) diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 29ade4d2d1032..92b4ca95747b4 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,7 +1,7 @@ use core::hint::black_box; use bevy_ecs::{ - event::EntityEvent, + event::{BroadcastEvent, EntityEvent, Event}, observer::{On, TriggerTargets}, world::World, }; @@ -13,6 +13,9 @@ fn deterministic_rand() -> ChaCha8Rng { ChaCha8Rng::seed_from_u64(42) } +#[derive(Clone, BroadcastEvent)] +struct BroadcastEventBase; + #[derive(Clone, EntityEvent)] struct EventBase; @@ -23,10 +26,10 @@ pub fn observe_simple(criterion: &mut Criterion) { group.bench_function("trigger_simple", |bencher| { let mut world = World::new(); - world.add_observer(empty_listener_base); + world.add_observer(empty_listener::); bencher.iter(|| { for _ in 0..10000 { - world.trigger(EventBase); + world.trigger(BroadcastEventBase); } }); }); @@ -35,7 +38,12 @@ pub fn observe_simple(criterion: &mut Criterion) { let mut world = World::new(); let mut entities = vec![]; for _ in 0..10000 { - entities.push(world.spawn_empty().observe(empty_listener_base).id()); + entities.push( + world + .spawn_empty() + .observe(empty_listener::) + .id(), + ); } entities.shuffle(&mut deterministic_rand()); bencher.iter(|| { @@ -46,7 +54,7 @@ pub fn observe_simple(criterion: &mut Criterion) { group.finish(); } -fn empty_listener_base(trigger: On) { +fn empty_listener(trigger: On) { black_box(trigger); } diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index 302ab44bfe23c..16a6cbcff8eaf 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -306,7 +306,7 @@ Observers are systems that listen for a "trigger" of a specific `Event`: ```rust use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(BroadcastEvent)] struct Speak { message: String } diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index ce084474a2700..ae1427928c747 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -17,7 +17,7 @@ pub const EVENT: &str = "entity_event"; pub const AUTO_PROPAGATE: &str = "auto_propagate"; pub const TRAVERSAL: &str = "traversal"; -pub fn derive_event(input: TokenStream) -> TokenStream { +pub fn derive_broadcast_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); @@ -31,6 +31,7 @@ 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 {} + impl #impl_generics #bevy_ecs_path::event::BroadcastEvent for #struct_name #type_generics #where_clause {} }) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 234a8cf8595dd..d23f439c0ad33 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -547,10 +547,10 @@ pub(crate) fn bevy_ecs_path() -> syn::Path { BevyManifest::shared().get_path("bevy_ecs") } -/// Implement the `Event` trait. -#[proc_macro_derive(Event)] +/// Implement the `BroadcastEvent` trait. +#[proc_macro_derive(BroadcastEvent)] pub fn derive_event(input: TokenStream) -> TokenStream { - component::derive_event(input) + component::derive_broadcast_event(input) } /// Cheat sheet for derive syntax, diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index f682554ce9a51..4a0e57a06e0af 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -23,7 +23,7 @@ use crate::{ bundle::BundleId, component::{ComponentId, Components, RequiredComponentConstructor, StorageType}, entity::{Entity, EntityLocation}, - event::Event, + event::BroadcastEvent, observer::Observers, storage::{ImmutableSparseSet, SparseArray, SparseSet, TableId, TableRow}, }; @@ -35,7 +35,7 @@ use core::{ }; use nonmax::NonMaxU32; -#[derive(Event)] +#[derive(BroadcastEvent)] #[expect(dead_code, reason = "Prepare for the upcoming Query as Entities")] pub(crate) struct ArchetypeCreated(pub ArchetypeId); diff --git a/crates/bevy_ecs/src/component/tick.rs b/crates/bevy_ecs/src/component/tick.rs index 09d82a13d4eb2..683915acbd41d 100644 --- a/crates/bevy_ecs/src/component/tick.rs +++ b/crates/bevy_ecs/src/component/tick.rs @@ -1,4 +1,4 @@ -use bevy_ecs_macros::Event; +use bevy_ecs_macros::BroadcastEvent; use bevy_ptr::UnsafeCellDeref; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -86,7 +86,7 @@ impl Tick { } } -/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make +/// A [`BroadcastEvent`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make /// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus /// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old. /// @@ -111,7 +111,7 @@ impl Tick { /// schedule.0.check_change_ticks(*check); /// }); /// ``` -#[derive(Debug, Clone, Copy, Event)] +#[derive(Debug, Clone, Copy, BroadcastEvent)] pub struct CheckChangeTicks(pub(crate) Tick); impl CheckChangeTicks { diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index a5924d60f4a07..06de097f26af6 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -11,6 +11,7 @@ use core::{ marker::PhantomData, }; +// TODO: Adjust these docs /// Something that "happens" and can be processed by app logic. /// /// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), @@ -89,9 +90,9 @@ use core::{ /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[diagnostic::on_unimplemented( - message = "`{Self}` is not an `Event`", - label = "invalid `Event`", - note = "consider annotating `{Self}` with `#[derive(Event)]`" + message = "`{Self}` is not an `ObserverEvent`", + label = "invalid `ObserverEvent`", + note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` or `#[derive(EntityEvent)]`" )] pub trait Event: Send + Sync + 'static { /// Generates the [`EventKey`] for this event type. @@ -128,6 +129,9 @@ pub trait Event: Send + Sync + 'static { } } +/// A global [`Event`] without an entity target. +pub trait BroadcastEvent: Event {} + /// An [`Event`] that can be targeted at specific entities. /// /// Entity events can be triggered on a [`World`] with specific entity targets using a method diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 020b258557fd4..3a5f1de8d6512 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,8 +11,8 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{BufferedEvent, EntityEvent, Event, EventId, EventKey}; -pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event}; +pub use base::{BroadcastEvent, BufferedEvent, EntityEvent, Event, EventId, EventKey}; +pub use bevy_ecs_macros::{BroadcastEvent, BufferedEvent, EntityEvent}; #[expect(deprecated, reason = "`SendBatchIds` was renamed to `WriteBatchIds`.")] pub use collections::{Events, SendBatchIds, WriteBatchIds}; pub use event_cursor::EventCursor; diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8580df7a3b484..20b741eb9dffd 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -79,8 +79,8 @@ pub mod prelude { entity::{ContainsEntity, Entity, EntityMapper}, error::{BevyError, Result}, event::{ - BufferedEvent, EntityEvent, Event, EventKey, EventMutator, EventReader, EventWriter, - Events, + BroadcastEvent, BufferedEvent, EntityEvent, Event, EventKey, EventMutator, EventReader, + EventWriter, Events, }, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, lifecycle::{ diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 312edc9b497e6..0f4ffe6cb1938 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -145,6 +145,7 @@ pub use trigger_targets::*; use crate::{ change_detection::MaybeLocation, component::ComponentId, + event::BroadcastEvent, prelude::*, system::IntoObserverSystem, world::{DeferredWorld, *}, @@ -186,17 +187,21 @@ impl World { self.spawn(Observer::new(system)) } - /// Triggers the given [`Event`], which will run any [`Observer`]s watching for it. + /// Triggers the given [`BroadcastEvent`], which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. /// If you need to use the event after triggering it, use [`World::trigger_ref`] instead. #[track_caller] - pub fn trigger(&mut self, event: E) { + pub fn trigger(&mut self, event: E) { self.trigger_with_caller(event, MaybeLocation::caller()); } - pub(crate) fn trigger_with_caller(&mut self, mut event: E, caller: MaybeLocation) { + pub(crate) fn trigger_with_caller( + &mut self, + mut event: E, + caller: MaybeLocation, + ) { let event_key = E::register_event_key(self); // SAFETY: We just registered `event_key` with the type of `event` unsafe { @@ -204,18 +209,18 @@ impl World { } } - /// Triggers the given [`Event`] as a mutable reference, which will run any [`Observer`]s watching for it. + /// Triggers the given [`BroadcastEvent`] as a mutable reference, which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. #[track_caller] - pub fn trigger_ref(&mut self, event: &mut E) { + pub fn trigger_ref(&mut self, event: &mut E) { let event_key = E::register_event_key(self); // SAFETY: We just registered `event_key` with the type of `event` unsafe { self.trigger_dynamic_ref_with_caller(event_key, event, MaybeLocation::caller()) }; } - unsafe fn trigger_dynamic_ref_with_caller( + unsafe fn trigger_dynamic_ref_with_caller( &mut self, event_key: EventKey, event_data: &mut E, @@ -497,6 +502,7 @@ impl World { mod tests { use alloc::{vec, vec::Vec}; + use bevy_ecs_macros::BroadcastEvent; use bevy_platform::collections::HashMap; use bevy_ptr::OwningPtr; @@ -522,10 +528,18 @@ mod tests { struct S; #[derive(EntityEvent)] - struct EventA; + struct EntityEventA; + + #[derive(BroadcastEvent)] + struct BroadcastEventA; #[derive(EntityEvent)] - struct EventWithData { + struct EntityEventWithData { + counter: usize, + } + + #[derive(BroadcastEvent)] + struct BroadcastEventWithData { counter: usize, } @@ -681,14 +695,20 @@ mod tests { fn observer_trigger_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: On| trigger.event_mut().counter += 1); - world.add_observer(|mut trigger: On| trigger.event_mut().counter += 2); - world.add_observer(|mut trigger: On| trigger.event_mut().counter += 4); + world.add_observer(|mut trigger: On| { + trigger.event_mut().counter += 1; + }); + world.add_observer(|mut trigger: On| { + trigger.event_mut().counter += 2; + }); + world.add_observer(|mut trigger: On| { + trigger.event_mut().counter += 4; + }); // This flush is required for the last observer to be called when triggering the event, // due to `World::add_observer` returning `WorldEntityMut`. world.flush(); - let mut event = EventWithData { counter: 0 }; + let mut event = BroadcastEventWithData { counter: 0 }; world.trigger_ref(&mut event); assert_eq!(7, event.counter); } @@ -697,20 +717,20 @@ mod tests { fn observer_trigger_targets_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: On| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 1; }); - world.add_observer(|mut trigger: On| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 2; }); - world.add_observer(|mut trigger: On| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 4; }); // This flush is required for the last observer to be called when triggering the event, // due to `World::add_observer` returning `WorldEntityMut`. world.flush(); - let mut event = EventWithData { counter: 0 }; + let mut event = EntityEventWithData { counter: 0 }; let component_a = world.register_component::(); world.trigger_targets_ref(&mut event, component_a); assert_eq!(5, event.counter); @@ -819,43 +839,21 @@ mod tests { assert_eq!(vec!["add_ab"], world.resource::().0); } - #[test] - fn observer_no_target() { - let mut world = World::new(); - world.init_resource::(); - - let system: fn(On) = |_| { - panic!("Trigger routed to non-targeted entity."); - }; - world.spawn_empty().observe(system); - world.add_observer(move |obs: On, mut res: ResMut| { - assert_eq!(obs.target(), Entity::PLACEHOLDER); - res.observed("event_a"); - }); - - // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut - // and therefore does not automatically flush. - world.flush(); - world.trigger(EventA); - world.flush(); - assert_eq!(vec!["event_a"], world.resource::().0); - } - #[test] fn observer_entity_routing() { let mut world = World::new(); world.init_resource::(); - let system: fn(On) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); let entity = world .spawn_empty() - .observe(|_: On, mut res: ResMut| res.observed("a_1")) + .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); - world.add_observer(move |obs: On, mut res: ResMut| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), entity); res.observed("a_2"); }); @@ -863,7 +861,7 @@ mod tests { // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut // and therefore does not automatically flush. world.flush(); - world.trigger_targets(EventA, entity); + world.trigger_targets(EntityEventA, entity); world.flush(); assert_eq!(vec!["a_2", "a_1"], world.resource::().0); } @@ -881,26 +879,27 @@ mod tests { // targets (entity_1, A) let entity_1 = world .spawn_empty() - .observe(|_: On, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); // targets (entity_2, B) let entity_2 = world .spawn_empty() - .observe(|_: On, mut res: ResMut| res.0 += 10) + .observe(|_: On, mut res: ResMut| res.0 += 10) .id(); // targets any entity or component - world.add_observer(|_: On, mut res: ResMut| res.0 += 100); + world.add_observer(|_: On, mut res: ResMut| res.0 += 100); // targets any entity, and components A or B - world.add_observer(|_: On, mut res: ResMut| res.0 += 1000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 1000); // test all tuples - world.add_observer(|_: On, mut res: ResMut| res.0 += 10000); + world + .add_observer(|_: On, mut res: ResMut| res.0 += 10000); world.add_observer( - |_: On, mut res: ResMut| { + |_: On, mut res: ResMut| { res.0 += 100000; }, ); world.add_observer( - |_: On, + |_: On, mut res: ResMut| res.0 += 1000000, ); @@ -908,21 +907,21 @@ mod tests { world.flush(); // trigger for an entity and a component - world.trigger_targets(EventA, (entity_1, component_a)); + world.trigger_targets(EntityEventA, (entity_1, component_a)); world.flush(); // only observer that doesn't trigger is the one only watching entity_2 assert_eq!(1111101, world.resource::().0); world.resource_mut::().0 = 0; // trigger for both entities, but no components: trigger once per entity target - world.trigger_targets(EventA, (entity_1, entity_2)); + world.trigger_targets(EntityEventA, (entity_1, entity_2)); world.flush(); // only the observer that doesn't require components triggers - once per entity assert_eq!(200, world.resource::().0); world.resource_mut::().0 = 0; // trigger for both components, but no entities: trigger once - world.trigger_targets(EventA, (component_a, component_b)); + world.trigger_targets(EntityEventA, (component_a, component_b)); world.flush(); // all component observers trigger, entities are not observed assert_eq!(1111100, world.resource::().0); @@ -930,14 +929,17 @@ mod tests { // trigger for both entities and both components: trigger once per entity target // we only get 2222211 because a given observer can trigger only once per entity target - world.trigger_targets(EventA, ((component_a, component_b), (entity_1, entity_2))); + world.trigger_targets( + EntityEventA, + ((component_a, component_b), (entity_1, entity_2)), + ); world.flush(); assert_eq!(2222211, world.resource::().0); world.resource_mut::().0 = 0; // trigger to test complex tuples: (A, B, (A, B)) world.trigger_targets( - EventA, + EntityEventA, (component_a, component_b, (component_a, component_b)), ); world.flush(); @@ -947,7 +949,7 @@ mod tests { // trigger to test complex tuples: (A, B, (A, B), ((A, B), (A, B))) world.trigger_targets( - EventA, + EntityEventA, ( component_a, component_b, @@ -962,7 +964,7 @@ mod tests { // trigger to test the most complex tuple: (A, B, (A, B), (B, A), (A, B, ((A, B), (B, A)))) world.trigger_targets( - EventA, + EntityEventA, ( component_a, component_b, @@ -999,7 +1001,7 @@ mod tests { }); let entity = entity.flush(); - world.trigger_targets(EventA, entity); + world.trigger_targets(EntityEventA, entity); world.flush(); assert_eq!(vec!["event_a"], world.resource::().0); } @@ -1021,7 +1023,7 @@ mod tests { world.commands().queue(move |world: &mut World| { // SAFETY: we registered `event_a` above and it matches the type of EventA - unsafe { world.trigger_targets_dynamic(event_a, EventA, ()) }; + unsafe { world.trigger_targets_dynamic(event_a, EntityEventA, ()) }; }); world.flush(); assert_eq!(vec!["event_a"], world.resource::().0); @@ -1350,10 +1352,12 @@ mod tests { let mut world = World::new(); // This fails because `ResA` is not present in the world - world.add_observer(|_: On, _: Res, mut commands: Commands| { - commands.insert_resource(ResB); - }); - world.trigger(EventA); + world.add_observer( + |_: On, _: Res, mut commands: Commands| { + commands.insert_resource(ResB); + }, + ); + world.trigger(BroadcastEventA); } #[test] @@ -1363,14 +1367,14 @@ mod tests { let mut world = World::new(); world.add_observer( - |_: On, mut params: ParamSet<(Query, Commands)>| { + |_: On, mut params: ParamSet<(Query, Commands)>| { params.p1().insert_resource(ResA); }, ); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut // and therefore does not automatically flush. world.flush(); - world.trigger(EventA); + world.trigger(BroadcastEventA); world.flush(); assert!(world.get_resource::().is_some()); @@ -1379,7 +1383,7 @@ mod tests { #[test] #[track_caller] fn observer_caller_location_event() { - #[derive(Event)] + #[derive(BroadcastEvent)] struct EventA; let caller = MaybeLocation::caller(); @@ -1419,7 +1423,7 @@ mod tests { let b_id = world.register_component::(); world.add_observer( - |trigger: On, mut counter: ResMut| { + |trigger: On, mut counter: ResMut| { for &component in trigger.components() { *counter.0.entry(component).or_default() += 1; } @@ -1427,11 +1431,11 @@ mod tests { ); world.flush(); - world.trigger_targets(EventA, [a_id, b_id]); - world.trigger_targets(EventA, a_id); - world.trigger_targets(EventA, b_id); - world.trigger_targets(EventA, [a_id, b_id]); - world.trigger_targets(EventA, a_id); + world.trigger_targets(EntityEventA, [a_id, b_id]); + world.trigger_targets(EntityEventA, a_id); + world.trigger_targets(EntityEventA, b_id); + world.trigger_targets(EntityEventA, [a_id, b_id]); + world.trigger_targets(EntityEventA, a_id); world.flush(); let counter = world.resource::(); diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index f25e742eed300..249f4b43a9723 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -93,11 +93,11 @@ mod tests { use super::*; use crate::{ error::{ignore, DefaultErrorHandler}, - event::Event, + event::BroadcastEvent, observer::On, }; - #[derive(Event)] + #[derive(BroadcastEvent)] struct TriggerEvent; #[test] diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index 83dad342a803c..47898d51c3502 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -9,7 +9,7 @@ use crate::{ change_detection::MaybeLocation, entity::Entity, error::Result, - event::{BufferedEvent, EntityEvent, Event, Events}, + event::{BroadcastEvent, BufferedEvent, EntityEvent, Events}, observer::TriggerTargets, resource::Resource, schedule::ScheduleLabel, @@ -208,9 +208,9 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { } } -/// A [`Command`] that sends a global [`Event`] without any targets. +/// A [`Command`] that sends a [`BroadcastEvent`]. #[track_caller] -pub fn trigger(event: impl Event) -> impl Command { +pub fn trigger(event: impl BroadcastEvent) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { world.trigger_with_caller(event, caller); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index f7e19423797d3..1efbd78bd1a5a 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -20,7 +20,7 @@ use crate::{ component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut}, error::{warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, - event::{BufferedEvent, EntityEvent, Event}, + event::{BroadcastEvent, BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, resource::Resource, schedule::ScheduleLabel, @@ -1083,11 +1083,11 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::run_system_cached_with(system, input).handle_error_with(warn)); } - /// Sends a global [`Event`] without any targets. + /// Sends a [`BroadcastEvent`]. /// - /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. + /// This will run any [`Observer`] of the given [`BroadcastEvent`] that isn't scoped to specific targets. #[track_caller] - pub fn trigger(&mut self, event: impl Event) { + pub fn trigger(&mut self, event: impl BroadcastEvent) { self.queue(command::trigger(event)); } diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 862ebf71c7eb4..af5652128d68c 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -53,13 +53,13 @@ where #[cfg(test)] mod tests { use crate::{ - event::Event, + event::BroadcastEvent, observer::On, system::{In, IntoSystem}, world::World, }; - #[derive(Event)] + #[derive(BroadcastEvent)] struct TriggerEvent; #[test] diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index bc87cd4feae50..fb5c0a4b6d753 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -526,6 +526,7 @@ impl core::fmt::Debug for RegisteredSystemError { mod tests { use core::cell::Cell; + use bevy_ecs_macros::BroadcastEvent; use bevy_utils::default; use crate::{prelude::*, system::SystemId}; @@ -898,7 +899,7 @@ mod tests { #[test] fn system_with_input_mut() { - #[derive(Event)] + #[derive(BroadcastEvent)] struct MyEvent { cancelled: bool, } diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 64f1fa409f9aa..ac18b89da9891 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -7,7 +7,7 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::{ComponentId, Mutable}, entity::Entity, - event::{BufferedEvent, EntityEvent, Event, EventId, EventKey, Events, WriteBatchIds}, + event::{BroadcastEvent, BufferedEvent, EntityEvent, EventId, EventKey, Events, WriteBatchIds}, lifecycle::{HookContext, INSERT, REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, @@ -860,12 +860,12 @@ impl<'w> DeferredWorld<'w> { } } - /// Sends a global [`Event`] without any targets. + /// Sends a [`BroadcastEvent`]. /// - /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. + /// This will run any [`Observer`] of the given [`BroadcastEvent`] that isn't scoped to specific targets. /// /// [`Observer`]: crate::observer::Observer - pub fn trigger(&mut self, trigger: impl Event) { + pub fn trigger(&mut self, trigger: impl BroadcastEvent) { self.commands().trigger(trigger); } diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs index 925537c39ef2d..e151ad04a007e 100644 --- a/examples/ecs/observers.rs +++ b/examples/ecs/observers.rs @@ -60,7 +60,7 @@ impl Mine { } } -#[derive(Event)] +#[derive(BroadcastEvent)] struct ExplodeMines { pos: Vec2, radius: f32, diff --git a/examples/usage/context_menu.rs b/examples/usage/context_menu.rs index a50e5cdabbae7..14db9cc9c2d1c 100644 --- a/examples/usage/context_menu.rs +++ b/examples/usage/context_menu.rs @@ -8,13 +8,13 @@ use bevy::{ use std::fmt::Debug; /// event opening a new context menu at position `pos` -#[derive(Event)] +#[derive(BroadcastEvent)] struct OpenContextMenu { pos: Vec2, } /// event will be sent to close currently open context menus -#[derive(Event)] +#[derive(BroadcastEvent)] struct CloseContextMenus; /// marker component identifying root of a context menu From 9154556d971c1195bfd5de7316bdfd6c56b4b363 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Tue, 15 Jul 2025 20:16:43 +0200 Subject: [PATCH 02/17] docs fixes --- crates/bevy_app/src/app.rs | 2 +- crates/bevy_ecs/src/observer/distributed_storage.rs | 12 ++++++------ src/lib.rs | 1 - 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 5756286cf4f03..1b49497e265a8 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1320,7 +1320,7 @@ impl App { /// # /// # let mut app = App::new(); /// # - /// # #[derive(Event)] + /// # #[derive(BroadcastEvent)] /// # struct Party { /// # friends_allowed: bool, /// # }; diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs index 10961d4de7c46..30e81c84c2ed1 100644 --- a/crates/bevy_ecs/src/observer/distributed_storage.rs +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -44,7 +44,7 @@ use crate::prelude::ReflectComponent; /// ``` /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); -/// #[derive(Event)] +/// #[derive(BroadcastEvent)] /// struct Speak { /// message: String, /// } @@ -67,7 +67,7 @@ use crate::prelude::ReflectComponent; /// ``` /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); -/// # #[derive(Event)] +/// # #[derive(BroadcastEvent)] /// # struct Speak; /// // These are functionally the same: /// world.add_observer(|trigger: On| {}); @@ -79,7 +79,7 @@ use crate::prelude::ReflectComponent; /// ``` /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); -/// # #[derive(Event)] +/// # #[derive(BroadcastEvent)] /// # struct PrintNames; /// # #[derive(Component, Debug)] /// # struct Name; @@ -97,7 +97,7 @@ use crate::prelude::ReflectComponent; /// ``` /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); -/// # #[derive(Event)] +/// # #[derive(BroadcastEvent)] /// # struct SpawnThing; /// # #[derive(Component, Debug)] /// # struct Thing; @@ -111,9 +111,9 @@ use crate::prelude::ReflectComponent; /// ``` /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); -/// # #[derive(Event)] +/// # #[derive(BroadcastEvent)] /// # struct A; -/// # #[derive(Event)] +/// # #[derive(BroadcastEvent)] /// # struct B; /// world.add_observer(|trigger: On, mut commands: Commands| { /// commands.trigger(B); diff --git a/src/lib.rs b/src/lib.rs index a1b1767bddbf7..7a0a9994f3f34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] - //! [![Bevy Logo](https://bevy.org/assets/bevy_logo_docs.svg)](https://bevy.org) //! //! Bevy is an open-source modular game engine built in Rust, with a focus on developer productivity From bd812cd9e07e87310333af4eafe6e1413bcb9536 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Tue, 15 Jul 2025 20:18:01 +0200 Subject: [PATCH 03/17] Make `BroadcastEvent` and `EntityEvent` mutually exclusive --- crates/bevy_ecs/macros/src/component.rs | 11 ++-- crates/bevy_ecs/src/event/base.rs | 75 ++++++++++++++++++++++--- crates/bevy_ecs/src/event/mod.rs | 5 +- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index ae1427928c747..f5d3a306a25ce 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -30,8 +30,9 @@ pub fn derive_broadcast_event(input: TokenStream) -> TokenStream { let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { - impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {} - impl #impl_generics #bevy_ecs_path::event::BroadcastEvent for #struct_name #type_generics #where_clause {} + impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { + type Kind = #bevy_ecs_path::event::BroadcastEventKind; + } }) } @@ -74,10 +75,8 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { - impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {} - impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { - type Traversal = #traversal; - const AUTO_PROPAGATE: bool = #auto_propagate; + impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { + type Kind = #bevy_ecs_path::event::EntityEventKind; } }) } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 06de097f26af6..5089ae351171d 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -11,7 +11,7 @@ use core::{ marker::PhantomData, }; -// TODO: Adjust these docs +// TODO: These docs need to be moved around and adjusted /// Something that "happens" and can be processed by app logic. /// /// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), @@ -36,7 +36,7 @@ use core::{ /// ``` /// # use bevy_ecs::prelude::*; /// # -/// #[derive(Event)] +/// #[derive(BroadcastEvent)] /// struct Speak { /// message: String, /// } @@ -47,7 +47,7 @@ use core::{ /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event)] +/// # #[derive(BroadcastEvent)] /// # struct Speak { /// # message: String, /// # } @@ -64,7 +64,7 @@ use core::{ /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event)] +/// # #[derive(BroadcastEvent)] /// # struct Speak { /// # message: String, /// # } @@ -90,11 +90,14 @@ use core::{ /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[diagnostic::on_unimplemented( - message = "`{Self}` is not an `ObserverEvent`", - label = "invalid `ObserverEvent`", + message = "`{Self}` is not an `Event`", + label = "invalid `Event`", note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` or `#[derive(EntityEvent)]`" )] pub trait Event: Send + Sync + 'static { + /// + type Kind; + /// Generates the [`EventKey`] for this event type. /// /// If this type has already been registered, @@ -130,7 +133,32 @@ pub trait Event: Send + Sync + 'static { } /// A global [`Event`] without an entity target. -pub trait BroadcastEvent: Event {} +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `BroadcastEvent`", + label = "invalid `BroadcastEvent`", + note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]`" +)] +pub trait BroadcastEvent: Event + sealed_a::SealedA {} + +#[doc(hidden)] +pub struct BroadcastEventKind; + +#[diagnostic::do_not_recommend] +impl BroadcastEvent for T where T: Event {} + +pub(crate) mod sealed_a { + use super::*; + + /// Seal for the [`BroadcastEvent`] trait. + #[diagnostic::on_unimplemented( + message = "manual implementations of `BroadcastEvent` are disallowed", + note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` instead" + )] + pub trait SealedA {} + + #[diagnostic::do_not_recommend] + impl SealedA for T where T: Event {} +} /// An [`Event`] that can be targeted at specific entities. /// @@ -248,7 +276,7 @@ pub trait BroadcastEvent: Event {} label = "invalid `EntityEvent`", note = "consider annotating `{Self}` with `#[derive(EntityEvent)]`" )] -pub trait EntityEvent: Event { +pub trait EntityEvent: Event + sealed_b::SealedB { /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. /// /// [`Entity`]: crate::entity::Entity @@ -263,6 +291,37 @@ pub trait EntityEvent: Event { const AUTO_PROPAGATE: bool = false; } +#[doc(hidden)] +pub struct EntityEventKind, const A: bool> { + _t: PhantomData, + _r: PhantomData, +} + +// Blanket impl for EntityEvent +#[diagnostic::do_not_recommend] +impl, const A: bool> EntityEvent for T +where + T: Event>, +{ + type Traversal = R; + const AUTO_PROPAGATE: bool = A; +} + +pub(crate) mod sealed_b { + use super::*; + + /// Seal for the [`EntityEvent`] trait. + #[diagnostic::on_unimplemented( + message = "manual implementations of `EntityEvent` are disallowed", + note = "consider annotating `{Self}` with `#[derive(EntityEvent)]` instead" + )] + pub trait SealedB {} + + #[diagnostic::do_not_recommend] + impl, const A: bool> SealedB for T where T: Event> + {} +} + /// A buffered event for pull-based event handling. /// /// Buffered events can be written with [`EventWriter`] and read using the [`EventReader`] system parameter. diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 3a5f1de8d6512..6acde7003da11 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,7 +11,10 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{BroadcastEvent, BufferedEvent, EntityEvent, Event, EventId, EventKey}; +pub use base::{ + BroadcastEvent, BroadcastEventKind, BufferedEvent, EntityEvent, EntityEventKind, Event, + EventId, EventKey, +}; pub use bevy_ecs_macros::{BroadcastEvent, BufferedEvent, EntityEvent}; #[expect(deprecated, reason = "`SendBatchIds` was renamed to `WriteBatchIds`.")] pub use collections::{Events, SendBatchIds, WriteBatchIds}; From d80e61b8a49f23ae94f750f5eca6bc99058d1882 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Tue, 15 Jul 2025 20:18:58 +0200 Subject: [PATCH 04/17] Adjust docs and target getters of `On` and `ObserverTrigger` --- crates/bevy_ecs/src/observer/system_param.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index 0b711c4309ee5..71687a55ac60e 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -106,23 +106,19 @@ impl<'w, E, B: Bundle> On<'w, E, B> { } impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { - /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. + /// Returns the [`Entity`] that was targeted by the [`EntityEvent`] that triggered this observer. /// /// Note that if event propagation is enabled, this may not be the same as the original target of the event, /// which can be accessed via [`On::original_target`]. - /// - /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. pub fn target(&self) -> Entity { - self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) + self.trigger.current_target.unwrap() } - /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. + /// Returns the original [`Entity`] that the [`EntityEvent`] was targeted at when it was first triggered. /// /// If event propagation is not enabled, this will always return the same value as [`On::target`]. - /// - /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. pub fn original_target(&self) -> Entity { - self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) + self.trigger.original_target.unwrap() } /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. @@ -185,11 +181,11 @@ pub struct ObserverTrigger { pub event_key: EventKey, /// The [`ComponentId`]s the trigger targeted. pub components: SmallVec<[ComponentId; 2]>, - /// The entity that the entity-event targeted, if any. + /// For [`EntityEvent`]s this is the entity that the event targeted. Always `None` for [`BroadcastEvent`]. /// /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. pub current_target: Option, - /// The entity that the entity-event was originally targeted at, if any. + /// For [`EntityEvent`]s this is the entity that the event was originally targeted at. Always `None` for [`BroadcastEvent`]. /// /// If event propagation is enabled, this will be the first entity that the event was targeted at, /// even if the event was propagated to other entities. From 26235156bc8b1af22013e50bbd33d748a8ad4bff Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Tue, 15 Jul 2025 20:19:47 +0200 Subject: [PATCH 05/17] Adjust release note --- release-content/release-notes/event_split.md | 33 +++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md index b650c29e73303..5220359dc72cc 100644 --- a/release-content/release-notes/event_split.md +++ b/release-content/release-notes/event_split.md @@ -1,7 +1,7 @@ --- title: Event Split -authors: ["@Jondolf"] -pull_requests: [19647, 20101] +authors: ["@Jondolf", "@tim-blackbird"] +pull_requests: [19647, 20101, 20104] --- In past releases, all event types were defined by simply deriving the `Event` trait: @@ -23,21 +23,25 @@ The first two are observer APIs, while the third is a fully separate "buffered" All three patterns are fundamentally different in both the interface and usage. Despite the same event type being used everywhere, APIs are typically built to support only one of them. -This has led to a lot of confusion and frustration for users. A common footgun was using a "buffered event" with an observer, -or an observer event with `EventReader`, leaving the user wondering why the event is not being detected. +This has led to a lot of confusion and frustration for users. Common footguns include: +- Using a "buffered event" with an observer, or an observer event with `EventReader`, leaving the user wondering why the event is not being detected. +- `On`(formerly `Trigger`) has a `target` getter which would return `Entity::PLACEHOLDER` for events sent via `trigger` rather than `trigger_targets`. -**Bevy 0.17** aims to solve this ambiguity by splitting the event traits into `Event`, `EntityEvent`, and `BufferedEvent`. +**Bevy 0.17** aims to solve this ambiguity by splitting the different kinds of events into multiple traits: -- `Event`: A shared trait for observer events. -- `EntityEvent`: An `Event` that additionally supports targeting specific entities and propagating the event from one entity to another. -- `BufferedEvent`: An event that supports usage with `EventReader` and `EventWriter` for pull-based event handling. +- `Event`: A supertrait for observer events. + - `BroadcastEvent`: An observer event without an entity target. + - `EntityEvent`: An observer event that targets specific entities and can propagate the event from one entity to another across relationships. +- `BufferedEvent`: An event used with `EventReader` and `EventWriter` for pull-based event handling. + +Note: To fully prevent the footgun of `On::target` returning `Entity::PLACEHOLDER` the `BroadcastEvent` and `EntityEvent` traits were made mutually exclusive. ## Using Events -A basic `Event` can be defined like before, by deriving the `Event` trait. +Events without an entity target can be defined, by deriving the `BroadcastEvent` trait. ```rust -#[derive(Event)] +#[derive(BroadcastEvent)] struct Speak { message: String, } @@ -57,8 +61,8 @@ commands.trigger(Speak { }); ``` -To allow an event to be targeted at entities and even propagated further, you can instead derive `EntityEvent`. -It supports optionally specifying some options for propagation using the `event` attribute: +To make an event target entities and even be propagated further, you can instead derive `EntityEvent`. +It supports optionally specifying some options for propagation using the `entity_event` attribute: ```rust // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. @@ -69,8 +73,7 @@ struct Damage { } ``` -Every `EntityEvent` is also an `Event`, so you can still use `trigger` to trigger them globally. -However, entity events also support targeted observer APIs such as `trigger_targets` and `observe`: +`EntityEvent`s can be used with targeted observer APIs such as `trigger_targets` and `observe`: ```rust // Spawn an enemy entity. @@ -116,6 +119,6 @@ fn read_messages(mut reader: EventReader) { In summary: -- Need a basic event you can trigger and observe? Derive `Event`! +- Need an event you can trigger and observe? Derive `BroadcastEvent`! - Need the observer event to be targeted at an entity? Derive `EntityEvent`! - Need the event to be buffered and support the `EventReader`/`EventWriter` API? Derive `BufferedEvent`! From 8035f60e91a5ec9fc824e68dc6e489c01161b99c Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Tue, 15 Jul 2025 20:21:04 +0200 Subject: [PATCH 06/17] Temporarily disable targetless `SceneInstanceReady` event --- crates/bevy_scene/src/scene_spawner.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 13713fe64ce7f..01bbf19123119 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -421,7 +421,8 @@ impl SceneSpawner { .trigger_targets(SceneInstanceReady { instance_id }, parent); } else { // Defer via commands otherwise SceneSpawner is not available in the observer. - world.commands().trigger(SceneInstanceReady { instance_id }); + // TODO: Thinkies + // world.commands().trigger(SceneInstanceReady { instance_id }); } } } From 060c183fe3066d922df0065303ac1df53759d4ed Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Tue, 15 Jul 2025 20:45:46 +0200 Subject: [PATCH 07/17] Hide associated type `Kind` as implementation detail --- crates/bevy_ecs/src/event/base.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 5089ae351171d..583bf99f1f54c 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -95,7 +95,7 @@ use core::{ note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` or `#[derive(EntityEvent)]`" )] pub trait Event: Send + Sync + 'static { - /// + #[doc(hidden)] type Kind; /// Generates the [`EventKey`] for this event type. From 779e4535d9c88263d454d30a97261c1858e5960a Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 15 Jul 2025 20:51:04 +0200 Subject: [PATCH 08/17] Update event_split.md --- release-content/release-notes/event_split.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md index 5220359dc72cc..65099bfdce193 100644 --- a/release-content/release-notes/event_split.md +++ b/release-content/release-notes/event_split.md @@ -1,7 +1,7 @@ --- title: Event Split authors: ["@Jondolf", "@tim-blackbird"] -pull_requests: [19647, 20101, 20104] +pull_requests: [19647, 20101, 20104, 20151] --- In past releases, all event types were defined by simply deriving the `Event` trait: From 1161216938dacc72757e1913d32d54460bc2cba7 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 14:02:26 +0200 Subject: [PATCH 09/17] Revert changes related mutual exclusivity + minor docs and release note changes --- crates/bevy_ecs/macros/src/component.rs | 11 ++-- crates/bevy_ecs/src/event/base.rs | 63 +------------------- crates/bevy_ecs/src/event/mod.rs | 5 +- crates/bevy_ecs/src/observer/system_param.rs | 14 +++-- crates/bevy_scene/src/scene_spawner.rs | 7 ++- release-content/release-notes/event_split.md | 4 +- 6 files changed, 24 insertions(+), 80 deletions(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index f5d3a306a25ce..ae1427928c747 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -30,9 +30,8 @@ pub fn derive_broadcast_event(input: TokenStream) -> TokenStream { let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { - impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { - type Kind = #bevy_ecs_path::event::BroadcastEventKind; - } + impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {} + impl #impl_generics #bevy_ecs_path::event::BroadcastEvent for #struct_name #type_generics #where_clause {} }) } @@ -75,8 +74,10 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { - impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { - type Kind = #bevy_ecs_path::event::EntityEventKind; + impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {} + impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { + type Traversal = #traversal; + const AUTO_PROPAGATE: bool = #auto_propagate; } }) } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 583bf99f1f54c..041b127129624 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -95,9 +95,6 @@ use core::{ note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` or `#[derive(EntityEvent)]`" )] pub trait Event: Send + Sync + 'static { - #[doc(hidden)] - type Kind; - /// Generates the [`EventKey`] for this event type. /// /// If this type has already been registered, @@ -133,32 +130,7 @@ pub trait Event: Send + Sync + 'static { } /// A global [`Event`] without an entity target. -#[diagnostic::on_unimplemented( - message = "`{Self}` is not an `BroadcastEvent`", - label = "invalid `BroadcastEvent`", - note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]`" -)] -pub trait BroadcastEvent: Event + sealed_a::SealedA {} - -#[doc(hidden)] -pub struct BroadcastEventKind; - -#[diagnostic::do_not_recommend] -impl BroadcastEvent for T where T: Event {} - -pub(crate) mod sealed_a { - use super::*; - - /// Seal for the [`BroadcastEvent`] trait. - #[diagnostic::on_unimplemented( - message = "manual implementations of `BroadcastEvent` are disallowed", - note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` instead" - )] - pub trait SealedA {} - - #[diagnostic::do_not_recommend] - impl SealedA for T where T: Event {} -} +pub trait BroadcastEvent: Event {} /// An [`Event`] that can be targeted at specific entities. /// @@ -276,7 +248,7 @@ pub(crate) mod sealed_a { label = "invalid `EntityEvent`", note = "consider annotating `{Self}` with `#[derive(EntityEvent)]`" )] -pub trait EntityEvent: Event + sealed_b::SealedB { +pub trait EntityEvent: Event { /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. /// /// [`Entity`]: crate::entity::Entity @@ -291,37 +263,6 @@ pub trait EntityEvent: Event + sealed_b::SealedB { const AUTO_PROPAGATE: bool = false; } -#[doc(hidden)] -pub struct EntityEventKind, const A: bool> { - _t: PhantomData, - _r: PhantomData, -} - -// Blanket impl for EntityEvent -#[diagnostic::do_not_recommend] -impl, const A: bool> EntityEvent for T -where - T: Event>, -{ - type Traversal = R; - const AUTO_PROPAGATE: bool = A; -} - -pub(crate) mod sealed_b { - use super::*; - - /// Seal for the [`EntityEvent`] trait. - #[diagnostic::on_unimplemented( - message = "manual implementations of `EntityEvent` are disallowed", - note = "consider annotating `{Self}` with `#[derive(EntityEvent)]` instead" - )] - pub trait SealedB {} - - #[diagnostic::do_not_recommend] - impl, const A: bool> SealedB for T where T: Event> - {} -} - /// A buffered event for pull-based event handling. /// /// Buffered events can be written with [`EventWriter`] and read using the [`EventReader`] system parameter. diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 6acde7003da11..3a5f1de8d6512 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,10 +11,7 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{ - BroadcastEvent, BroadcastEventKind, BufferedEvent, EntityEvent, EntityEventKind, Event, - EventId, EventKey, -}; +pub use base::{BroadcastEvent, BufferedEvent, EntityEvent, Event, EventId, EventKey}; pub use bevy_ecs_macros::{BroadcastEvent, BufferedEvent, EntityEvent}; #[expect(deprecated, reason = "`SendBatchIds` was renamed to `WriteBatchIds`.")] pub use collections::{Events, SendBatchIds, WriteBatchIds}; diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index 71687a55ac60e..edac6f602103b 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -110,15 +110,19 @@ impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { /// /// Note that if event propagation is enabled, this may not be the same as the original target of the event, /// which can be accessed via [`On::original_target`]. + /// + /// If the event is also a [`BroadcastEvent`] sent with [`trigger`](World::trigger), this will return [`Entity::PLACEHOLDER`]. pub fn target(&self) -> Entity { - self.trigger.current_target.unwrap() + self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) } /// Returns the original [`Entity`] that the [`EntityEvent`] was targeted at when it was first triggered. /// /// If event propagation is not enabled, this will always return the same value as [`On::target`]. + /// + /// If the event is also a [`BroadcastEvent`] sent with [`trigger`](World::trigger), this will return [`Entity::PLACEHOLDER`]. pub fn original_target(&self) -> Entity { - self.trigger.original_target.unwrap() + self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) } /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. @@ -181,11 +185,13 @@ pub struct ObserverTrigger { pub event_key: EventKey, /// The [`ComponentId`]s the trigger targeted. pub components: SmallVec<[ComponentId; 2]>, - /// For [`EntityEvent`]s this is the entity that the event targeted. Always `None` for [`BroadcastEvent`]. + /// For [`EntityEvent`]s used with `trigger_targets` this is the entity that the event targeted. + /// Can only be `None` for [`BroadcastEvent`]s used with `trigger`. /// /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. pub current_target: Option, - /// For [`EntityEvent`]s this is the entity that the event was originally targeted at. Always `None` for [`BroadcastEvent`]. + /// For [`EntityEvent`]s used with `trigger_targets` this is the entity that the event was originally targeted at. + /// Can only be `None` for [`BroadcastEvent`]s used with `trigger`. /// /// If event propagation is enabled, this will be the first entity that the event was targeted at, /// even if the event was propagated to other entities. diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 01bbf19123119..75caff003f6fa 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -2,7 +2,7 @@ use crate::{DynamicScene, Scene}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::{ entity::{Entity, EntityHashMap}, - event::{EntityEvent, EventCursor, Events}, + event::{BroadcastEvent, EntityEvent, EventCursor, Events}, hierarchy::ChildOf, reflect::AppTypeRegistry, resource::Resource, @@ -34,6 +34,8 @@ pub struct SceneInstanceReady { pub instance_id: InstanceId, } +impl BroadcastEvent for SceneInstanceReady {} + /// Information about a scene instance. #[derive(Debug)] pub struct InstanceInfo { @@ -421,8 +423,7 @@ impl SceneSpawner { .trigger_targets(SceneInstanceReady { instance_id }, parent); } else { // Defer via commands otherwise SceneSpawner is not available in the observer. - // TODO: Thinkies - // world.commands().trigger(SceneInstanceReady { instance_id }); + world.commands().trigger(SceneInstanceReady { instance_id }); } } } diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md index 65099bfdce193..d2d906262e18b 100644 --- a/release-content/release-notes/event_split.md +++ b/release-content/release-notes/event_split.md @@ -25,7 +25,7 @@ APIs are typically built to support only one of them. This has led to a lot of confusion and frustration for users. Common footguns include: - Using a "buffered event" with an observer, or an observer event with `EventReader`, leaving the user wondering why the event is not being detected. -- `On`(formerly `Trigger`) has a `target` getter which would return `Entity::PLACEHOLDER` for events sent via `trigger` rather than `trigger_targets`. +- `On`(formerly `Trigger`) has a `target` getter which would cause confusion for events only mean to be used with `trigger` where it returns `Entity::PLACEHOLDER`. **Bevy 0.17** aims to solve this ambiguity by splitting the different kinds of events into multiple traits: @@ -34,8 +34,6 @@ This has led to a lot of confusion and frustration for users. Common footguns in - `EntityEvent`: An observer event that targets specific entities and can propagate the event from one entity to another across relationships. - `BufferedEvent`: An event used with `EventReader` and `EventWriter` for pull-based event handling. -Note: To fully prevent the footgun of `On::target` returning `Entity::PLACEHOLDER` the `BroadcastEvent` and `EntityEvent` traits were made mutually exclusive. - ## Using Events Events without an entity target can be defined, by deriving the `BroadcastEvent` trait. From 15ffea3884e115be31525f082e6a8a96db7108e2 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 14:56:28 +0200 Subject: [PATCH 10/17] docs! --- crates/bevy_ecs/src/event/base.rs | 118 ++++++++----------- crates/bevy_ecs/src/event/mod.rs | 22 +++- crates/bevy_scene/src/scene_spawner.rs | 4 +- release-content/release-notes/event_split.md | 1 + 4 files changed, 72 insertions(+), 73 deletions(-) diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 041b127129624..fd1144bc2ad75 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -11,27 +11,57 @@ use core::{ marker::PhantomData, }; -// TODO: These docs need to be moved around and adjusted -/// Something that "happens" and can be processed by app logic. -/// -/// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), -/// causing any global [`Observer`] watching that event to run. This allows for push-based -/// event handling where observers are immediately notified of events as they happen. -/// -/// Additional event handling behavior can be enabled by implementing the [`EntityEvent`] -/// and [`BufferedEvent`] traits: -/// -/// - [`EntityEvent`]s support targeting specific entities, triggering any observers watching those targets. -/// They are useful for entity-specific event handlers and can even be propagated from one entity to another. -/// - [`BufferedEvent`]s support a pull-based event handling system where events are written using an [`EventWriter`] -/// and read later using an [`EventReader`]. This is an alternative to observers that allows efficient batch processing -/// of events at fixed points in a schedule. +/// Supertrait for the observer based [`BroadcastEvent`] and [`EntityEvent`]. /// /// Events must be thread-safe. +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `Event`", + label = "invalid `Event`", + note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` or `#[derive(EntityEvent)]`" +)] +pub trait Event: Send + Sync + 'static { + /// Generates the [`EventKey`] for this event type. + /// + /// If this type has already been registered, + /// this will return the existing [`EventKey`]. + /// + /// This is used by various dynamically typed observer APIs, + /// such as [`World::trigger_targets_dynamic`]. + /// + /// # Warning + /// + /// This method should not be overridden by implementers, + /// and should always correspond to the implementation of [`event_key`](Event::event_key). + fn register_event_key(world: &mut World) -> EventKey { + EventKey(world.register_component::>()) + } + + /// Fetches the [`EventKey`] for this event type, + /// if it has already been generated. + /// + /// This is used by various dynamically typed observer APIs, + /// such as [`World::trigger_targets_dynamic`]. + /// + /// # Warning + /// + /// This method should not be overridden by implementers, + /// and should always correspond to the implementation of + /// [`register_event_key`](Event::register_event_key). + fn event_key(world: &World) -> Option { + world + .component_id::>() + .map(EventKey) + } +} + +/// An [`Event`] without an entity target. +/// +/// [`BroadcastEvent`]s can be triggered on a [`World`] with the method [`trigger`](World::trigger), +/// causing any [`Observer`] watching the event for those entities to run. /// /// # Usage /// -/// The [`Event`] trait can be derived: +/// The [`BroadcastEvent`] trait can be derived: /// /// ``` /// # use bevy_ecs::prelude::*; @@ -71,12 +101,6 @@ use core::{ /// # /// # let mut world = World::new(); /// # -/// # world.add_observer(|trigger: On| { -/// # println!("{}", trigger.message); -/// # }); -/// # -/// # world.flush(); -/// # /// world.trigger(Speak { /// message: "Hello!".to_string(), /// }); @@ -85,51 +109,7 @@ use core::{ /// For events that additionally need entity targeting or buffering, consider also deriving /// [`EntityEvent`] or [`BufferedEvent`], respectively. /// -/// [`World`]: crate::world::World /// [`Observer`]: crate::observer::Observer -/// [`EventReader`]: super::EventReader -/// [`EventWriter`]: super::EventWriter -#[diagnostic::on_unimplemented( - message = "`{Self}` is not an `Event`", - label = "invalid `Event`", - note = "consider annotating `{Self}` with `#[derive(BroadcastEvent)]` or `#[derive(EntityEvent)]`" -)] -pub trait Event: Send + Sync + 'static { - /// Generates the [`EventKey`] for this event type. - /// - /// If this type has already been registered, - /// this will return the existing [`EventKey`]. - /// - /// This is used by various dynamically typed observer APIs, - /// such as [`World::trigger_targets_dynamic`]. - /// - /// # Warning - /// - /// This method should not be overridden by implementers, - /// and should always correspond to the implementation of [`event_key`](Event::event_key). - fn register_event_key(world: &mut World) -> EventKey { - EventKey(world.register_component::>()) - } - - /// Fetches the [`EventKey`] for this event type, - /// if it has already been generated. - /// - /// This is used by various dynamically typed observer APIs, - /// such as [`World::trigger_targets_dynamic`]. - /// - /// # Warning - /// - /// This method should not be overridden by implementers, - /// and should always correspond to the implementation of - /// [`register_event_key`](Event::register_event_key). - fn event_key(world: &World) -> Option { - world - .component_id::>() - .map(EventKey) - } -} - -/// A global [`Event`] without an entity target. pub trait BroadcastEvent: Event {} /// An [`Event`] that can be targeted at specific entities. @@ -138,7 +118,7 @@ pub trait BroadcastEvent: Event {} /// like [`trigger_targets`](World::trigger_targets), causing any [`Observer`] watching the event /// for those entities to run. /// -/// Unlike basic [`Event`]s, entity events can optionally be propagated from one entity target to another +/// Unlike [`BroadcastEvent`]s, entity events can optionally be propagated from one entity target to another /// based on the [`EntityEvent::Traversal`] type associated with the event. This enables use cases /// such as bubbling events to parent entities for UI purposes. /// @@ -237,12 +217,8 @@ pub trait BroadcastEvent: Event {} /// world.trigger_targets(Damage { amount: 10.0 }, armor_piece); /// ``` /// -/// [`World`]: crate::world::World /// [`TriggerTargets`]: crate::observer::TriggerTargets /// [`Observer`]: crate::observer::Observer -/// [`Events`]: super::Events -/// [`EventReader`]: super::EventReader -/// [`EventWriter`]: super::EventWriter #[diagnostic::on_unimplemented( message = "`{Self}` is not an `EntityEvent`", label = "invalid `EntityEvent`", diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 3a5f1de8d6512..8cb73523b96bd 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -1,4 +1,24 @@ -//! Event handling types. +//! Events are that "happens" and can be processed by app logic. +//! +//! [Event]s can be triggered on a [`World`](bevy_ecs::world::World) using a method like [`trigger`], +//! causing any global [`Observer`] watching that event to run. This allows for push-based +//! event handling where observers are immediately notified of events as they happen. +//! +//! Event handling behavior can be enabled by implementing the [`EntityEvent`] +//! and [`BufferedEvent`] traits: +//! +//! - [`Event`]: A supertrait for observer events. +//! - [`BroadcastEvent`]: An observer event without an entity target. +//! - [`EntityEvent`]s support targeting specific entities, triggering any observers watching those targets. +//! They are useful for entity-specific event handlers and can even be propagated from one entity to another. +//! - [`BufferedEvent`]s support a pull-based event handling system where events are written using an [`EventWriter`] +//! and read later using an [`EventReader`]. This is an alternative to observers that allows efficient batch processing +//! of events at fixed points in a schedule. +//! +//! [`World`]: crate::world::World +//! [`trigger`]: crate::world::World::trigger +//! [`Observer`]: crate::observer::Observer + mod base; mod collections; mod event_cursor; diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 75caff003f6fa..c415e8e1cbeee 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -22,11 +22,13 @@ use bevy_ecs::{ system::{Commands, Query}, }; -/// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use. +/// This [`Event`] is triggered when the [`SceneInstance`] becomes ready to use. +/// If the scene has a parent the event will be triggered on that entity, otherwise the event has no target. /// /// See also [`On`], [`SceneSpawner::instance_is_ready`]. /// /// [`On`]: bevy_ecs::observer::On +/// [`Event`]: bevy_ecs::event::Event #[derive(Clone, Copy, Debug, Eq, PartialEq, EntityEvent, Reflect)] #[reflect(Debug, PartialEq, Clone)] pub struct SceneInstanceReady { diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md index d2d906262e18b..b3bf36ab37c71 100644 --- a/release-content/release-notes/event_split.md +++ b/release-content/release-notes/event_split.md @@ -24,6 +24,7 @@ All three patterns are fundamentally different in both the interface and usage. APIs are typically built to support only one of them. This has led to a lot of confusion and frustration for users. Common footguns include: + - Using a "buffered event" with an observer, or an observer event with `EventReader`, leaving the user wondering why the event is not being detected. - `On`(formerly `Trigger`) has a `target` getter which would cause confusion for events only mean to be used with `trigger` where it returns `Entity::PLACEHOLDER`. From d7b76314d539908be566d6c4c8307ae30155bcdc Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 15:00:43 +0200 Subject: [PATCH 11/17] Fix markdown errors --- release-content/release-notes/event_split.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md index b3bf36ab37c71..0796406bce0dc 100644 --- a/release-content/release-notes/event_split.md +++ b/release-content/release-notes/event_split.md @@ -26,13 +26,13 @@ APIs are typically built to support only one of them. This has led to a lot of confusion and frustration for users. Common footguns include: - Using a "buffered event" with an observer, or an observer event with `EventReader`, leaving the user wondering why the event is not being detected. -- `On`(formerly `Trigger`) has a `target` getter which would cause confusion for events only mean to be used with `trigger` where it returns `Entity::PLACEHOLDER`. +- `On`(formerly `Trigger`) has a `target` getter which would cause confusion for events only meant to be used with `trigger` where it returns `Entity::PLACEHOLDER`. **Bevy 0.17** aims to solve this ambiguity by splitting the different kinds of events into multiple traits: - `Event`: A supertrait for observer events. - - `BroadcastEvent`: An observer event without an entity target. - - `EntityEvent`: An observer event that targets specific entities and can propagate the event from one entity to another across relationships. + - `BroadcastEvent`: An observer event without an entity target. + - `EntityEvent`: An observer event that targets specific entities and can propagate the event from one entity to another across relationships. - `BufferedEvent`: An event used with `EventReader` and `EventWriter` for pull-based event handling. ## Using Events From e50a4d7ac40d50c682e0c99aca8e68e89b43c1d1 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 16:24:01 +0200 Subject: [PATCH 12/17] More documentation work --- crates/bevy_ecs/src/event/mod.rs | 18 +++++++++--------- .../src/observer/distributed_storage.rs | 2 +- crates/bevy_ecs/src/observer/mod.rs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 8cb73523b96bd..88e5fef8643ed 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -1,22 +1,22 @@ -//! Events are that "happens" and can be processed by app logic. +//! Events are things that "happen" and can be processed by app logic. //! //! [Event]s can be triggered on a [`World`](bevy_ecs::world::World) using a method like [`trigger`], //! causing any global [`Observer`] watching that event to run. This allows for push-based //! event handling where observers are immediately notified of events as they happen. //! -//! Event handling behavior can be enabled by implementing the [`EntityEvent`] -//! and [`BufferedEvent`] traits: -//! -//! - [`Event`]: A supertrait for observer events. -//! - [`BroadcastEvent`]: An observer event without an entity target. -//! - [`EntityEvent`]s support targeting specific entities, triggering any observers watching those targets. -//! They are useful for entity-specific event handlers and can even be propagated from one entity to another. -//! - [`BufferedEvent`]s support a pull-based event handling system where events are written using an [`EventWriter`] +//! - [`Event`]: A supertrait for push-based events that trigger global observers added via [`add_observer`]. +//! - [`BroadcastEvent`]: An event without an entity target. Can be used via [`trigger`] +//! - [`EntityEvent`]: An event targeting specific entities, triggering any observers watching those targets. Can be used via [`trigger_targets`]. +//! They can trigger entity-specific observers added via [`observe`] and can be propagated from one entity to another. +//! - [`BufferedEvent`]: Support a pull-based event handling system where events are written using an [`EventWriter`] //! and read later using an [`EventReader`]. This is an alternative to observers that allows efficient batch processing //! of events at fixed points in a schedule. //! //! [`World`]: crate::world::World +//! [`add_observer`]: crate::world::World::add_observer +//! [`observe`]: crate::world::EntityWorldMut::observe //! [`trigger`]: crate::world::World::trigger +//! [`trigger_targets`]: crate::world::World::trigger_targets //! [`Observer`]: crate::observer::Observer mod base; diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs index 30e81c84c2ed1..0a3ecbc510ac3 100644 --- a/crates/bevy_ecs/src/observer/distributed_storage.rs +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -211,7 +211,7 @@ pub struct Observer { } impl Observer { - /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered + /// 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). /// /// # Panics diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 0f4ffe6cb1938..70e405882e237 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -152,7 +152,7 @@ use crate::{ }; impl World { - /// Spawns a "global" [`Observer`] which will watch for the given event. + /// Spawns a global [`Observer`] which will watch for the given event. /// Returns its [`Entity`] as a [`EntityWorldMut`]. /// /// `system` can be any system whose first parameter is [`On`]. From 42232dd087351258aaf65c818786775441b98e49 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 16:24:26 +0200 Subject: [PATCH 13/17] missed rename --- crates/bevy_ecs/macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index d23f439c0ad33..d05b62c04914c 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -549,7 +549,7 @@ pub(crate) fn bevy_ecs_path() -> syn::Path { /// Implement the `BroadcastEvent` trait. #[proc_macro_derive(BroadcastEvent)] -pub fn derive_event(input: TokenStream) -> TokenStream { +pub fn derive_broadcast_event(input: TokenStream) -> TokenStream { component::derive_broadcast_event(input) } From 1b1283a6f87e4f590751ce81caece1aca19ea8e6 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 16:24:45 +0200 Subject: [PATCH 14/17] Revert some unnecessary changes --- benches/benches/bevy_ecs/observers/simple.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 92b4ca95747b4..09e8b2e473023 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,7 +1,7 @@ use core::hint::black_box; use bevy_ecs::{ - event::{BroadcastEvent, EntityEvent, Event}, + event::{BroadcastEvent, EntityEvent}, observer::{On, TriggerTargets}, world::World, }; @@ -13,12 +13,11 @@ fn deterministic_rand() -> ChaCha8Rng { ChaCha8Rng::seed_from_u64(42) } -#[derive(Clone, BroadcastEvent)] -struct BroadcastEventBase; - #[derive(Clone, EntityEvent)] struct EventBase; +impl BroadcastEvent for EventBase {} + pub fn observe_simple(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("observe"); group.warm_up_time(core::time::Duration::from_millis(500)); @@ -26,10 +25,10 @@ pub fn observe_simple(criterion: &mut Criterion) { group.bench_function("trigger_simple", |bencher| { let mut world = World::new(); - world.add_observer(empty_listener::); + world.add_observer(empty_listener_base); bencher.iter(|| { for _ in 0..10000 { - world.trigger(BroadcastEventBase); + world.trigger(EventBase); } }); }); @@ -38,12 +37,7 @@ pub fn observe_simple(criterion: &mut Criterion) { let mut world = World::new(); let mut entities = vec![]; for _ in 0..10000 { - entities.push( - world - .spawn_empty() - .observe(empty_listener::) - .id(), - ); + entities.push(world.spawn_empty().observe(empty_listener_base).id()); } entities.shuffle(&mut deterministic_rand()); bencher.iter(|| { @@ -54,7 +48,7 @@ pub fn observe_simple(criterion: &mut Criterion) { group.finish(); } -fn empty_listener(trigger: On) { +fn empty_listener_base(trigger: On) { black_box(trigger); } From 21602eab7fdbe9f1a85995148ded25cc6e3707c5 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 16:51:27 +0200 Subject: [PATCH 15/17] Moar doc fixes --- crates/bevy_ecs/src/event/base.rs | 7 ++----- crates/bevy_ecs/src/event/mod.rs | 6 +----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index fd1144bc2ad75..21874458ed1a6 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -57,7 +57,7 @@ pub trait Event: Send + Sync + 'static { /// An [`Event`] without an entity target. /// /// [`BroadcastEvent`]s can be triggered on a [`World`] with the method [`trigger`](World::trigger), -/// causing any [`Observer`] watching the event for those entities to run. +/// causing any global [`Observer`]s for that event to run. /// /// # Usage /// @@ -106,9 +106,6 @@ pub trait Event: Send + Sync + 'static { /// }); /// ``` /// -/// For events that additionally need entity targeting or buffering, consider also deriving -/// [`EntityEvent`] or [`BufferedEvent`], respectively. -/// /// [`Observer`]: crate::observer::Observer pub trait BroadcastEvent: Event {} @@ -126,7 +123,7 @@ pub trait BroadcastEvent: Event {} /// /// # Usage /// -/// The [`EntityEvent`] trait can be derived. The `event` attribute can be used to further configure +/// The [`EntityEvent`] trait can be derived. The `entity_event` attribute can be used to further configure /// the propagation behavior: adding `auto_propagate` sets [`EntityEvent::AUTO_PROPAGATE`] to `true`, /// while adding `traversal = X` sets [`EntityEvent::Traversal`] to be of type `X`. /// diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 88e5fef8643ed..049c9f5450fdf 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -1,11 +1,7 @@ //! Events are things that "happen" and can be processed by app logic. //! -//! [Event]s can be triggered on a [`World`](bevy_ecs::world::World) using a method like [`trigger`], -//! causing any global [`Observer`] watching that event to run. This allows for push-based -//! event handling where observers are immediately notified of events as they happen. -//! //! - [`Event`]: A supertrait for push-based events that trigger global observers added via [`add_observer`]. -//! - [`BroadcastEvent`]: An event without an entity target. Can be used via [`trigger`] +//! - [`BroadcastEvent`]: An event without an entity target. Can be used via [`trigger`]. //! - [`EntityEvent`]: An event targeting specific entities, triggering any observers watching those targets. Can be used via [`trigger_targets`]. //! They can trigger entity-specific observers added via [`observe`] and can be propagated from one entity to another. //! - [`BufferedEvent`]: Support a pull-based event handling system where events are written using an [`EventWriter`] From f17ecc8d89ff28e51cd24ef334ae99140c3b5ae4 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Thu, 17 Jul 2025 16:51:34 +0200 Subject: [PATCH 16/17] Add `observer_no_target` test back --- crates/bevy_ecs/src/observer/mod.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 70e405882e237..ae8ab9b09aaba 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -527,6 +527,11 @@ mod tests { #[component(storage = "SparseSet")] struct S; + #[derive(EntityEvent)] + struct EventA; + + impl BroadcastEvent for EventA {} + #[derive(EntityEvent)] struct EntityEventA; @@ -839,6 +844,28 @@ mod tests { assert_eq!(vec!["add_ab"], world.resource::().0); } + #[test] + fn observer_no_target() { + let mut world = World::new(); + world.init_resource::(); + + let system: fn(On) = |_| { + panic!("Trigger routed to non-targeted entity."); + }; + world.spawn_empty().observe(system); + world.add_observer(move |obs: On, mut res: ResMut| { + assert_eq!(obs.target(), Entity::PLACEHOLDER); + res.observed("event_a"); + }); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger(EventA); + world.flush(); + assert_eq!(vec!["event_a"], world.resource::().0); + } + #[test] fn observer_entity_routing() { let mut world = World::new(); From 1a0191ff7531e779928043e4818aaac997fea3eb Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 17 Jul 2025 17:09:40 +0200 Subject: [PATCH 17/17] Undo newline in lib.rs --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 7a0a9994f3f34..a1b1767bddbf7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] + //! [![Bevy Logo](https://bevy.org/assets/bevy_logo_docs.svg)](https://bevy.org) //! //! Bevy is an open-source modular game engine built in Rust, with a focus on developer productivity