From eee4d85ae66acf54397c7e7dc5f561077eac6602 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 21 Aug 2025 01:27:39 -0700 Subject: [PATCH 01/44] Static Events / Observers --- .../benches/bevy_ecs/observers/propagation.rs | 2 +- benches/benches/bevy_ecs/observers/simple.rs | 26 +- crates/bevy_app/src/app.rs | 2 +- crates/bevy_ecs/macros/src/component.rs | 46 ++- crates/bevy_ecs/macros/src/lib.rs | 6 +- crates/bevy_ecs/src/archetype.rs | 12 +- crates/bevy_ecs/src/bundle/insert.rs | 43 +- crates/bevy_ecs/src/bundle/remove.rs | 23 +- crates/bevy_ecs/src/bundle/spawner.rs | 21 +- crates/bevy_ecs/src/entity/clone_entities.rs | 4 +- crates/bevy_ecs/src/event/base.rs | 271 ++++++------ crates/bevy_ecs/src/event/mod.rs | 27 +- crates/bevy_ecs/src/event/trigger.rs | 270 ++++++++++++ crates/bevy_ecs/src/lifecycle.rs | 39 +- .../src/observer/centralized_storage.rs | 89 +--- .../src/observer/distributed_storage.rs | 1 + crates/bevy_ecs/src/observer/mod.rs | 391 +++++------------- crates/bevy_ecs/src/observer/runner.rs | 42 +- crates/bevy_ecs/src/observer/system_param.rs | 116 +++--- .../bevy_ecs/src/observer/trigger_targets.rs | 130 +++--- crates/bevy_ecs/src/storage/sparse_set.rs | 4 +- .../bevy_ecs/src/system/commands/command.rs | 16 +- .../src/system/commands/entity_command.rs | 4 +- crates/bevy_ecs/src/system/commands/mod.rs | 10 +- crates/bevy_ecs/src/system/input.rs | 4 +- crates/bevy_ecs/src/system/mod.rs | 2 +- crates/bevy_ecs/src/system/observer_system.rs | 9 +- crates/bevy_ecs/src/world/deferred_world.rs | 126 ++---- crates/bevy_ecs/src/world/entity_ref.rs | 60 +-- crates/bevy_ecs/src/world/mod.rs | 16 +- crates/bevy_input_focus/src/lib.rs | 4 +- crates/bevy_picking/src/events.rs | 2 +- .../bevy_scene/src/dynamic_scene_builder.rs | 2 +- crates/bevy_scene/src/scene.rs | 2 +- crates/bevy_scene/src/scene_spawner.rs | 6 +- examples/ecs/observer_propagation.rs | 8 +- examples/ui/scroll.rs | 2 +- release-content/release-notes/event_split.md | 2 +- 38 files changed, 948 insertions(+), 892 deletions(-) create mode 100644 crates/bevy_ecs/src/event/trigger.rs diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 3b862cc1a631f..742751d2a0d7a 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -62,7 +62,7 @@ pub fn event_propagation(criterion: &mut Criterion) { } #[derive(EntityEvent, Clone, Component)] -#[entity_event(traversal = &'static ChildOf, auto_propagate)] +#[entity_event(propagate, auto_propagate)] struct TestEvent {} fn send_events(world: &mut World, leaves: &[Entity]) { diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index a4915e6afa807..68be7cbffe5c6 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,8 +1,9 @@ use core::hint::black_box; use bevy_ecs::{ - event::EntityEvent, - observer::{On, TriggerTargets}, + entity::Entity, + event::{EntityEvent, Event}, + observer::{EventTargets, On}, world::World, }; @@ -13,8 +14,11 @@ fn deterministic_rand() -> ChaCha8Rng { ChaCha8Rng::seed_from_u64(42) } +#[derive(Clone, Event)] +struct SimpleEvent; + #[derive(Clone, EntityEvent)] -struct EventBase; +struct SimpleEntityEvent; pub fn observe_simple(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("observe"); @@ -23,10 +27,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(on_simple_event); bencher.iter(|| { for _ in 0..10000 { - world.trigger(EventBase); + world.trigger(SimpleEvent); } }); }); @@ -35,7 +39,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_base).id()); + entities.push(world.spawn_empty().observe(on_simple_entity_event).id()); } entities.shuffle(&mut deterministic_rand()); bencher.iter(|| { @@ -46,10 +50,14 @@ pub fn observe_simple(criterion: &mut Criterion) { group.finish(); } -fn empty_listener_base(event: On) { +fn on_simple_event(event: On) { + black_box(event); +} + +fn on_simple_entity_event(event: On) { black_box(event); } -fn send_base_event(world: &mut World, entities: impl TriggerTargets) { - world.trigger_targets(EventBase, entities); +fn send_base_event(world: &mut World, entities: impl EventTargets) { + world.trigger_targets(SimpleEntityEvent, entities); } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 26a89d9f3ea3e..37c1ac2913790 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -11,7 +11,7 @@ pub use bevy_derive::AppLabel; use bevy_ecs::{ component::RequiredComponentsError, error::{DefaultErrorHandler, ErrorHandler}, - event::{event_update_system, EventCursor}, + event::{event_update_system, Event, EventCursor}, intern::Interned, prelude::*, schedule::{InternedSystemSet, ScheduleBuildSettings, ScheduleLabel}, diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index fe4fe4fa5b00c..6ad5b870f9823 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -13,9 +13,11 @@ use syn::{ LitStr, Member, Path, Result, Token, Type, Visibility, }; -pub const EVENT: &str = "entity_event"; -pub const AUTO_PROPAGATE: &str = "auto_propagate"; +pub const ENTITY_EVENT: &str = "entity_event"; +pub const PROPAGATE: &str = "propagate"; +// TODO: `traversal` is deprecated. Remove this (and related code) after the next release. pub const TRAVERSAL: &str = "traversal"; +pub const AUTO_PROPAGATE: &str = "auto_propagate"; pub fn derive_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); @@ -30,14 +32,18 @@ pub fn derive_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::Event for #struct_name #type_generics #where_clause { + type Target<'a> = (); + type Trigger = #bevy_ecs_path::event::GlobalTrigger; + } }) } pub fn derive_entity_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let mut auto_propagate = false; - let mut traversal: Type = parse_quote!(()); + let mut propagate = true; + let mut traversal: Option = None; let bevy_ecs_path: Path = crate::bevy_ecs_path(); let mut processed_attrs = Vec::new(); @@ -47,7 +53,11 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { .predicates .push(parse_quote! { Self: Send + Sync + 'static }); - for attr in ast.attrs.iter().filter(|attr| attr.path().is_ident(EVENT)) { + for attr in ast + .attrs + .iter() + .filter(|attr| attr.path().is_ident(ENTITY_EVENT)) + { if let Err(e) = attr.parse_nested_meta(|meta| match meta.path.get_ident() { Some(ident) if processed_attrs.iter().any(|i| ident == i) => { Err(meta.error(format!("duplicate attribute: {ident}"))) @@ -58,8 +68,16 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { Ok(()) } Some(ident) if ident == TRAVERSAL => { - traversal = meta.value()?.parse()?; - processed_attrs.push(TRAVERSAL); + Err(meta.error( + "`traversal` has been renamed to `propagate`, use that instead. If you were writing `traversal = &'static ChildOf`, you can now just write `propagate`, which defaults to the ChildOf traversal." + )) + } + Some(ident) if ident == PROPAGATE => { + propagate = true; + if meta.input.peek(Token![=]) { + traversal = Some(meta.value()?.parse()?); + } + processed_attrs.push(PROPAGATE); Ok(()) } Some(ident) => Err(meta.error(format!("unsupported attribute: {ident}"))), @@ -72,11 +90,17 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { let struct_name = &ast.ident; let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + let trigger = if propagate { + let traversal = traversal + .unwrap_or_else(|| parse_quote! { &'static #bevy_ecs_path::hierarchy::ChildOf}); + quote! {#bevy_ecs_path::event::PropagateEntityTrigger<#auto_propagate, Self, #traversal>} + } else { + quote! {#bevy_ecs_path::event::EntityTrigger} + }; 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 Target<'a> = #bevy_ecs_path::entity::Entity; + type Trigger = #trigger; } }) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 964bc56adbbb2..84f07d53e2f91 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -558,8 +558,10 @@ pub fn derive_event(input: TokenStream) -> TokenStream { /// /// ```ignore /// #[derive(EntityEvent)] -/// /// Traversal component -/// #[entity_event(traversal = &'static ChildOf)] +/// /// Enable propagation, which defaults to using the ChildOf component +/// #[entity_event(propagate)] +/// /// Enable propagation using the given Traversal implementation +/// #[entity_event(propagate = &'static ChildOf)] /// /// Always propagate /// #[entity_event(auto_propagate)] /// struct MyEvent; diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index bbb22b59b8f2d..1ed58a2b74063 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -524,14 +524,22 @@ impl Archetype { .map(|(id, _)| *id) } - /// Gets an iterator of all of the components in the archetype. + /// Returns a slice of all of the components in the archetype. /// /// All of the IDs are unique. #[inline] - pub fn components(&self) -> impl Iterator + Clone + '_ { + pub fn components(&self) -> &[ComponentId] { self.components.indices() } + /// Gets an iterator of all of the components in the archetype. + /// + /// All of the IDs are unique. + #[inline] + pub fn iter_components(&self) -> impl Iterator + Clone { + self.components.indices().iter().copied() + } + /// Returns the total number of components in the archetype #[inline] pub fn component_count(&self) -> usize { diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index 0388b5e6fd87c..5035113557c48 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -11,7 +11,8 @@ use crate::{ change_detection::MaybeLocation, component::{Components, ComponentsRegistrator, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, - lifecycle::{ADD, INSERT, REPLACE}, + event::EntityComponents, + lifecycle::{Add, Insert, Replace, ADD, INSERT, REPLACE}, observer::Observers, query::DebugCheckedUnwrap as _, relationship::RelationshipHookMode, @@ -165,10 +166,13 @@ impl<'w> BundleInserter<'w> { if insert_mode == InsertMode::Replace { if archetype.has_replace_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( REPLACE, - Some(entity), - archetype_after_insert.iter_existing(), + &mut Replace, + EntityComponents { + entity, + components: &archetype_after_insert.existing, + }, caller, ); } @@ -350,10 +354,13 @@ impl<'w> BundleInserter<'w> { caller, ); if new_archetype.has_add_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( ADD, - Some(entity), - archetype_after_insert.iter_added(), + &mut Add, + EntityComponents { + entity, + components: &archetype_after_insert.added, + }, caller, ); } @@ -368,10 +375,17 @@ impl<'w> BundleInserter<'w> { relationship_hook_mode, ); if new_archetype.has_insert_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( INSERT, - Some(entity), - archetype_after_insert.iter_inserted(), + &mut Insert, + EntityComponents { + entity, + // PERF: this is not a regression from what we were doing before, but ideally we don't + // need to collect here + components: &archetype_after_insert + .iter_inserted() + .collect::>(), + }, caller, ); } @@ -387,10 +401,13 @@ impl<'w> BundleInserter<'w> { relationship_hook_mode, ); if new_archetype.has_insert_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( INSERT, - Some(entity), - archetype_after_insert.iter_added(), + &mut Insert, + EntityComponents { + entity, + components: &archetype_after_insert.added, + }, caller, ); } diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index c0d2bd765a11a..17fa0d05e68cf 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -8,7 +8,8 @@ use crate::{ change_detection::MaybeLocation, component::{ComponentId, Components, ComponentsRegistrator, StorageType}, entity::{Entity, EntityLocation}, - lifecycle::{REMOVE, REPLACE}, + event::EntityComponents, + lifecycle::{Remove, Replace, REMOVE, REPLACE}, observer::Observers, relationship::RelationshipHookMode, storage::{SparseSets, Storages, Table}, @@ -150,10 +151,14 @@ impl<'w> BundleRemover<'w> { .filter(|component_id| self.old_archetype.as_ref().contains(*component_id)) }; if self.old_archetype.as_ref().has_replace_observer() { - deferred_world.trigger_observers( + let components = bundle_components_in_archetype().collect::>(); + deferred_world.trigger_raw( REPLACE, - Some(entity), - bundle_components_in_archetype(), + &mut Replace, + EntityComponents { + entity, + components: &components, + }, caller, ); } @@ -165,10 +170,14 @@ impl<'w> BundleRemover<'w> { self.relationship_hook_mode, ); if self.old_archetype.as_ref().has_remove_observer() { - deferred_world.trigger_observers( + let components = bundle_components_in_archetype().collect::>(); + deferred_world.trigger_raw( REMOVE, - Some(entity), - bundle_components_in_archetype(), + &mut Remove, + EntityComponents { + entity, + components: &components, + }, caller, ); } diff --git a/crates/bevy_ecs/src/bundle/spawner.rs b/crates/bevy_ecs/src/bundle/spawner.rs index 407bfda8facc4..0d1fe226eb37b 100644 --- a/crates/bevy_ecs/src/bundle/spawner.rs +++ b/crates/bevy_ecs/src/bundle/spawner.rs @@ -8,7 +8,8 @@ use crate::{ change_detection::MaybeLocation, component::{ComponentsRegistrator, Tick}, entity::{Entities, Entity, EntityLocation}, - lifecycle::{ADD, INSERT}, + event::EntityComponents, + lifecycle::{Add, Insert, ADD, INSERT}, relationship::RelationshipHookMode, storage::Table, world::{unsafe_world_cell::UnsafeWorldCell, World}, @@ -135,10 +136,13 @@ impl<'w> BundleSpawner<'w> { caller, ); if archetype.has_add_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( ADD, - Some(entity), - bundle_info.iter_contributed_components(), + &mut Add, + EntityComponents { + entity, + components: bundle_info.contributed_components(), + }, caller, ); } @@ -150,10 +154,13 @@ impl<'w> BundleSpawner<'w> { RelationshipHookMode::Run, ); if archetype.has_insert_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( INSERT, - Some(entity), - bundle_info.iter_contributed_components(), + &mut Insert, + EntityComponents { + entity, + components: bundle_info.contributed_components(), + }, caller, ); } diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 2f7000357f08f..4c110d0057c9a 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -1116,14 +1116,14 @@ impl CloneByFilter for OptOut { ) { match self.insert_mode { InsertMode::Replace => { - for component in source_archetype.components() { + for component in source_archetype.iter_components() { if !self.deny.contains(&component) { clone_component(component); } } } InsertMode::Keep => { - for component in source_archetype.components() { + for component in source_archetype.iter_components() { if !target_archetype.contains(component) && !self.deny.contains(&component) { clone_component(component); } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 2ba0de493eb7a..afd2d33d83201 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -1,7 +1,10 @@ -use crate::change_detection::MaybeLocation; -use crate::component::ComponentId; -use crate::world::World; -use crate::{component::Component, traversal::Traversal}; +use crate::{ + change_detection::MaybeLocation, + component::{Component, ComponentId}, + entity::Entity, + event::Trigger, + world::World, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use core::{ @@ -11,123 +14,6 @@ use core::{ marker::PhantomData, }; -/// 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. -/// -/// Events must be thread-safe. -/// -/// # Usage -/// -/// The [`Event`] trait can be derived: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # -/// #[derive(Event)] -/// struct Speak { -/// message: String, -/// } -/// ``` -/// -/// An [`Observer`] can then be added to listen for this event type: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # -/// # #[derive(Event)] -/// # struct Speak { -/// # message: String, -/// # } -/// # -/// # let mut world = World::new(); -/// # -/// world.add_observer(|event: On| { -/// println!("{}", event.message); -/// }); -/// ``` -/// -/// The event can be triggered on the [`World`] using the [`trigger`](World::trigger) method: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # -/// # #[derive(Event)] -/// # struct Speak { -/// # message: String, -/// # } -/// # -/// # let mut world = World::new(); -/// # -/// # world.add_observer(|event: On| { -/// # println!("{}", event.message); -/// # }); -/// # -/// # world.flush(); -/// # -/// world.trigger(Speak { -/// message: "Hello!".to_string(), -/// }); -/// ``` -/// -/// 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(Event)]`" -)] -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`] that can be targeted at specific entities. /// /// Entity events can be triggered on a [`World`] with specific entity targets using a method @@ -144,14 +30,14 @@ pub trait Event: Send + Sync + 'static { /// /// The [`EntityEvent`] trait can be derived. The `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`. +/// while adding `propagate = X` sets [`EntityEvent::Traversal`] to be of type `X`. /// /// ``` /// # use bevy_ecs::prelude::*; /// # /// // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. /// #[derive(EntityEvent)] -/// #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// #[entity_event(propagate, auto_propagate)] /// struct Damage { /// amount: f32, /// } @@ -163,7 +49,7 @@ pub trait Event: Send + Sync + 'static { /// # use bevy_ecs::prelude::*; /// # /// # #[derive(EntityEvent)] -/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # #[entity_event(propagate, auto_propagate)] /// # struct Damage { /// # amount: f32, /// # } @@ -202,7 +88,7 @@ pub trait Event: Send + Sync + 'static { /// # use bevy_ecs::prelude::*; /// # /// # #[derive(EntityEvent)] -/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # #[entity_event(propagate, auto_propagate)] /// # struct Damage { /// # amount: f32, /// # } @@ -244,19 +130,134 @@ pub trait Event: Send + Sync + 'static { label = "invalid `EntityEvent`", note = "consider annotating `{Self}` with `#[derive(EntityEvent)]`" )] -pub trait EntityEvent: Event { - /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. +pub trait EntityEvent: + for<'a> Event = Entity, Trigger: Trigger = Entity>> +{ +} + +impl Event = Entity, Trigger: Trigger = Entity>>> EntityEvent + for E +{ +} + +/// 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. +/// +/// Events must be thread-safe. +/// +/// # Usage +/// +/// The [`Event`] trait can be derived: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// #[derive(Event)] +/// struct Speak { +/// message: String, +/// } +/// ``` +/// +/// An [`Observer`] can then be added to listen for this event type: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// world.add_observer(|event: On| { +/// println!("{}", event.message); +/// }); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger`](World::trigger) method: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// # world.add_observer(|event: On| { +/// # println!("{}", event.message); +/// # }); +/// # +/// # world.flush(); +/// # +/// world.trigger(Speak { +/// message: "Hello!".to_string(), +/// }); +/// ``` +/// +/// 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(Event)]`" +)] +pub trait Event: Send + Sync + 'static { + type Target<'a>; + type Trigger: for<'a> Trigger = Self::Target<'a>>; + + /// Generates the [`EventKey`] for this event type. + /// + /// If this type has already been registered, + /// this will return the existing [`EventKey`]. /// - /// [`Entity`]: crate::entity::Entity - /// [propagation]: crate::observer::On::propagate - type Traversal: Traversal; + /// 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::>()) + } - /// When true, this event will always attempt to propagate when [triggered], without requiring a call - /// to [`On::propagate`]. + /// 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`]. /// - /// [triggered]: crate::system::Commands::trigger_targets - /// [`On::propagate`]: crate::observer::On::propagate - const AUTO_PROPAGATE: bool = false; + /// # 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 buffered event for pull-based event handling. diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 020b258557fd4..6afcc07a47f14 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -7,24 +7,21 @@ mod mut_iterators; mod mutator; mod reader; mod registry; +mod trigger; mod update; mod writer; -pub(crate) use base::EventInstance; -pub use base::{BufferedEvent, EntityEvent, Event, EventId, EventKey}; +pub use base::*; pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event}; -#[expect(deprecated, reason = "`SendBatchIds` was renamed to `WriteBatchIds`.")] -pub use collections::{Events, SendBatchIds, WriteBatchIds}; -pub use event_cursor::EventCursor; -#[cfg(feature = "multi_threaded")] -pub use iterators::EventParIter; -pub use iterators::{EventIterator, EventIteratorWithId}; -#[cfg(feature = "multi_threaded")] -pub use mut_iterators::EventMutParIter; -pub use mut_iterators::{EventMutIterator, EventMutIteratorWithId}; -pub use mutator::EventMutator; -pub use reader::EventReader; -pub use registry::{EventRegistry, ShouldUpdateEvents}; +pub use collections::*; +pub use event_cursor::*; +pub use iterators::*; +pub use mut_iterators::*; +pub use mutator::*; +pub use reader::*; +pub use registry::*; +pub use trigger::*; + #[expect( deprecated, reason = "`EventUpdates` was renamed to `EventUpdateSystems`." @@ -35,6 +32,8 @@ pub use update::{ }; pub use writer::EventWriter; +pub(crate) use base::EventInstance; + #[cfg(test)] mod tests { use alloc::{vec, vec::Vec}; diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs new file mode 100644 index 0000000000000..9439f395e556b --- /dev/null +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -0,0 +1,270 @@ +use crate::{ + component::ComponentId, + entity::Entity, + event::Event, + observer::{CachedObservers, TriggerContext}, + traversal::Traversal, + world::DeferredWorld, +}; +use bevy_ptr::{Ptr, PtrMut}; +use core::marker::PhantomData; + +pub trait Trigger: Default { + type Target<'a>; + fn trigger( + &mut self, + world: DeferredWorld, + observers: &CachedObservers, + event: PtrMut, + target: &Self::Target<'_>, + trigger_context: &TriggerContext, + ); +} + +#[derive(Default)] +pub struct GlobalTrigger; + +impl Trigger for GlobalTrigger { + type Target<'a> = (); + + fn trigger( + &mut self, + mut world: DeferredWorld, + observers: &CachedObservers, + mut event: PtrMut, + target: &Self::Target<'_>, + trigger_context: &TriggerContext, + ) { + // SAFETY: there are no outstanding world references + unsafe { + world.as_unsafe_world_cell().increment_trigger_id(); + } + for (observer, runner) in observers.global_observers() { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + target.into(), + self.into(), + ); + } + } +} + +#[derive(Default)] +pub struct EntityTrigger; + +impl Trigger for EntityTrigger { + type Target<'a> = Entity; + fn trigger( + &mut self, + world: DeferredWorld, + observers: &CachedObservers, + event: PtrMut, + target: &Self::Target<'_>, + trigger_context: &TriggerContext, + ) { + trigger_entity_raw( + world, + observers, + event, + target.into(), + target, + self.into(), + trigger_context, + ); + } +} + +fn trigger_entity_raw( + mut world: DeferredWorld, + observers: &CachedObservers, + mut event: PtrMut, + target: Ptr, + target_entity: &Entity, + mut trigger: PtrMut, + trigger_context: &TriggerContext, +) { + // SAFETY: there are no outstanding world references + unsafe { + world.as_unsafe_world_cell().increment_trigger_id(); + } + for (observer, runner) in observers.global_observers() { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + target, + trigger.reborrow(), + ); + } + + if let Some(map) = observers.entity_observers().get(target_entity) { + for (observer, runner) in map { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + target, + trigger.reborrow(), + ); + } + } +} + +pub struct PropagateEntityTrigger< + const AUTO_PROPAGATE: bool, + E: for<'t> Event = Entity>, + T: Traversal, +> { + pub original_entity: Entity, + pub propagate: bool, + _marker: PhantomData<(E, T)>, +} + +impl Event = Entity>, T: Traversal> Default + for PropagateEntityTrigger +{ + fn default() -> Self { + Self { + original_entity: Entity::PLACEHOLDER, + propagate: AUTO_PROPAGATE, + _marker: Default::default(), + } + } +} + +impl Event = Entity>, T: Traversal> Trigger + for PropagateEntityTrigger +{ + type Target<'a> = Entity; + + fn trigger( + &mut self, + mut world: DeferredWorld, + observers: &CachedObservers, + mut event: PtrMut, + target: &Self::Target<'_>, + trigger_context: &TriggerContext, + ) { + self.original_entity = *target; + trigger_entity_raw( + world.reborrow(), + observers, + event.reborrow(), + target.into(), + target, + self.into(), + trigger_context, + ); + + let mut current_target = *target; + loop { + if !self.propagate { + return; + } + if let Ok(entity) = world.get_entity(current_target) + && let Some(item) = entity.get_components::() + && let Some(traverse_to) = + // TODO: Sort out the safety of this + T::traverse(item, unsafe { event.reborrow().deref_mut() }) + { + current_target = traverse_to; + } else { + break; + } + + trigger_entity_raw( + world.reborrow(), + observers, + event.reborrow(), + (¤t_target).into(), + ¤t_target, + self.into(), + trigger_context, + ); + } + } +} + +#[derive(Default)] +pub struct EntityComponentsTrigger; + +pub struct EntityComponents<'a> { + pub entity: Entity, + pub components: &'a [ComponentId], +} + +impl Trigger for EntityComponentsTrigger { + type Target<'a> = EntityComponents<'a>; + + fn trigger( + &mut self, + mut world: DeferredWorld, + observers: &CachedObservers, + mut event: PtrMut, + target: &Self::Target<'_>, + trigger_context: &TriggerContext, + ) { + trigger_entity_raw( + world.reborrow(), + observers, + event.reborrow(), + target.into(), + &target.entity, + self.into(), + trigger_context, + ); + + // Trigger observers listening to this trigger targeting a specific component + for id in target.components { + if let Some(component_observers) = observers.component_observers().get(id) { + for (observer, runner) in component_observers.global_observers() { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + target.into(), + self.into(), + ); + } + + if let Some(map) = component_observers + .entity_component_observers() + .get(&target.entity) + { + for (observer, runner) in map { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + target.into(), + self.into(), + ); + } + } + } + } + } +} + +pub trait EntityTarget { + fn entity(&self) -> Entity; +} + +impl EntityTarget for Entity { + fn entity(&self) -> Entity { + *self + } +} + +impl<'a> EntityTarget for EntityComponents<'a> { + fn entity(&self) -> Entity { + self.entity + } +} diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index e45f9eefa0113..2ad3208cc6aa2 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -54,8 +54,8 @@ use crate::{ component::{Component, ComponentId, ComponentIdFor, Tick}, entity::Entity, event::{ - BufferedEvent, EntityEvent, EventCursor, EventId, EventIterator, EventIteratorWithId, - EventKey, Events, + BufferedEvent, EntityComponents, EntityComponentsTrigger, Event, EventCursor, EventId, + EventIterator, EventIteratorWithId, EventKey, Events, }, query::FilteredAccessSet, relationship::RelationshipHookMode, @@ -328,49 +328,74 @@ pub const DESPAWN: EventKey = EventKey(ComponentId::new(4)); /// Trigger emitted when a component is inserted onto an entity that does not already have that /// component. Runs before `Insert`. /// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. -#[derive(EntityEvent, Debug, Clone)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnAdd")] pub struct Add; +impl Event for Add { + type Target<'a> = EntityComponents<'a>; + type Trigger = EntityComponentsTrigger; +} + /// Trigger emitted when a component is inserted, regardless of whether or not the entity already /// had that component. Runs after `Add`, if it ran. /// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. -#[derive(EntityEvent, Debug, Clone)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnInsert")] pub struct Insert; +impl Event for Insert { + type Target<'a> = EntityComponents<'a>; + type Trigger = EntityComponentsTrigger; +} + /// Trigger emitted when a component is removed from an entity, regardless /// of whether or not it is later replaced. /// /// Runs before the value is replaced, so you can still access the original component data. /// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. -#[derive(EntityEvent, Debug, Clone)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnReplace")] pub struct Replace; +impl Event for Replace { + type Target<'a> = EntityComponents<'a>; + type Trigger = EntityComponentsTrigger; +} + /// Trigger emitted when a component is removed from an entity, and runs before the component is /// removed, so you can still access the component data. /// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. -#[derive(EntityEvent, Debug, Clone)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnRemove")] pub struct Remove; +impl Event for Remove { + type Target<'a> = EntityComponents<'a>; + type Trigger = EntityComponentsTrigger; +} + /// Trigger emitted for each component on an entity when it is despawned. /// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. -#[derive(EntityEvent, Debug, Clone)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnDespawn")] pub struct Despawn; +impl Event for Despawn { + type Target<'a> = EntityComponents<'a>; + type Trigger = EntityComponentsTrigger; +} + /// Deprecated in favor of [`Add`]. #[deprecated(since = "0.17.0", note = "Renamed to `Add`.")] pub type OnAdd = Add; diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs index 2e3b78a8daf83..40a98e8e18afc 100644 --- a/crates/bevy_ecs/src/observer/centralized_storage.rs +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -12,13 +12,8 @@ use bevy_platform::collections::HashMap; use crate::{ - archetype::ArchetypeFlags, - change_detection::MaybeLocation, - component::ComponentId, - entity::EntityHashMap, - observer::{ObserverRunner, ObserverTrigger}, - prelude::*, - world::DeferredWorld, + archetype::ArchetypeFlags, component::ComponentId, entity::EntityHashMap, + observer::ObserverRunner, prelude::*, }; /// An internal lookup table tracking all of the observers in the world. @@ -71,80 +66,6 @@ impl Observers { } } - /// This will run the observers of the given `event_key`, targeting the given `entity` and `components`. - pub(crate) fn invoke( - mut world: DeferredWorld, - event_key: EventKey, - current_target: Option, - original_entity: Option, - components: impl Iterator + Clone, - data: &mut T, - propagate: &mut bool, - caller: MaybeLocation, - ) { - // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` - let (mut world, observers) = unsafe { - let world = world.as_unsafe_world_cell(); - // SAFETY: There are no outstanding world references - world.increment_trigger_id(); - let observers = world.observers(); - let Some(observers) = observers.try_get_observers(event_key) else { - return; - }; - // SAFETY: The only outstanding reference to world is `observers` - (world.into_deferred(), observers) - }; - - let trigger_for_components = components.clone(); - - let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { - (runner)( - world.reborrow(), - ObserverTrigger { - observer, - event_key, - components: components.clone().collect(), - entity: current_target, - original_entity, - caller, - }, - data.into(), - propagate, - ); - }; - // Trigger observers listening for any kind of this trigger - observers - .global_observers - .iter() - .for_each(&mut trigger_observer); - - // Trigger entity observers listening for this kind of trigger - if let Some(target_entity) = current_target { - if let Some(map) = observers.entity_observers.get(&target_entity) { - map.iter().for_each(&mut trigger_observer); - } - } - - // Trigger observers listening to this trigger targeting a specific component - trigger_for_components.for_each(|id| { - if let Some(component_observers) = observers.component_observers.get(&id) { - component_observers - .global_observers - .iter() - .for_each(&mut trigger_observer); - - if let Some(target_entity) = current_target { - if let Some(map) = component_observers - .entity_component_observers - .get(&target_entity) - { - map.iter().for_each(&mut trigger_observer); - } - } - } - }); - } - pub(crate) fn is_archetype_cached(event_key: EventKey) -> Option { use crate::lifecycle::*; @@ -207,13 +128,13 @@ impl CachedObservers { } /// Returns the observers listening for this trigger targeting components. - pub fn get_component_observers(&self) -> &HashMap { + pub fn component_observers(&self) -> &HashMap { &self.component_observers } /// Returns the observers listening for this trigger targeting entities. - pub fn entity_observers(&self) -> &HashMap { - &self.component_observers + pub fn entity_observers(&self) -> &EntityHashMap { + &self.entity_observers } } diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs index 4610e26047b8b..02cc74b4ba47d 100644 --- a/crates/bevy_ecs/src/observer/distributed_storage.rs +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -18,6 +18,7 @@ use crate::{ entity::Entity, entity_disabling::Internal, error::{ErrorContext, ErrorHandler}, + event::Event, lifecycle::{ComponentHook, HookContext}, observer::{observer_system_runner, ObserverRunner}, prelude::*, diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index fe15a67e17c87..625ab3ac959f3 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -144,7 +144,7 @@ pub use trigger_targets::*; use crate::{ change_detection::MaybeLocation, - component::ComponentId, + event::Event, prelude::*, system::IntoObserverSystem, world::{DeferredWorld, *}, @@ -192,16 +192,8 @@ impl World { /// 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) { - self.trigger_with_caller(event, MaybeLocation::caller()); - } - - 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 { - self.trigger_dynamic_ref_with_caller(event_key, &mut event, caller); - } + pub fn trigger<'a, E: Event = ()>>(&mut self, mut event: E) { + self.trigger_with_caller(&mut event, (), MaybeLocation::caller()); } /// Triggers the given [`Event`] as a mutable reference, which will run any [`Observer`]s watching for it. @@ -209,31 +201,8 @@ impl World { /// 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) { - 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( - &mut self, - event_key: EventKey, - event_data: &mut E, - caller: MaybeLocation, - ) { - let mut world = DeferredWorld::from(self); - // SAFETY: `event_data` is accessible as the type represented by `event_key` - unsafe { - world.trigger_observers_with_data::<_, ()>( - event_key, - None, - None, - core::iter::empty::(), - event_data, - false, - caller, - ); - }; + pub fn trigger_ref<'a, E: Event = ()>>(&mut self, event: &mut E) { + self.trigger_with_caller(event, (), MaybeLocation::caller()); } /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. @@ -242,125 +211,38 @@ impl World { /// 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_targets_ref`] instead. #[track_caller] - pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { - self.trigger_targets_with_caller(event, targets, MaybeLocation::caller()); - } - - pub(crate) fn trigger_targets_with_caller( + pub fn trigger_targets<'a, E: Event>( &mut self, mut event: E, - targets: impl TriggerTargets, - caller: MaybeLocation, + targets: impl EventTargets>, ) { - let event_key = E::register_event_key(self); - // SAFETY: We just registered `event_key` with the type of `event` - unsafe { - self.trigger_targets_dynamic_ref_with_caller(event_key, &mut event, targets, caller); - } + self.trigger_with_caller(&mut event, targets, MaybeLocation::caller()); } - /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, + /// Triggers the given [`Event`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets`], 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_targets_ref( + pub fn trigger_targets_ref<'a, E: Event>( &mut self, event: &mut E, - targets: impl TriggerTargets, - ) { - let event_key = E::register_event_key(self); - // SAFETY: We just registered `event_key` with the type of `event` - unsafe { self.trigger_targets_dynamic_ref(event_key, event, targets) }; - } - - /// Triggers the given [`EntityEvent`] for the given `targets`, 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_targets_dynamic_ref`] instead. - /// - /// # Safety - /// - /// Caller must ensure that `event_data` is accessible as the type represented by `event_key`. - #[track_caller] - pub unsafe fn trigger_targets_dynamic( - &mut self, - event_key: EventKey, - mut event_data: E, - targets: Targets, + targets: impl EventTargets>, ) { - // SAFETY: `event_data` is accessible as the type represented by `event_key` - unsafe { - self.trigger_targets_dynamic_ref(event_key, &mut event_data, targets); - }; - } - - /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, - /// which will run any [`Observer`]s watching for it. - /// - /// Compared to [`World::trigger_targets_dynamic`], this method is most useful when it's necessary to check - /// or use the event after it has been modified by observers. - /// - /// # Safety - /// - /// Caller must ensure that `event_data` is accessible as the type represented by `event_key`. - #[track_caller] - pub unsafe fn trigger_targets_dynamic_ref( - &mut self, - event_key: EventKey, - event_data: &mut E, - targets: Targets, - ) { - self.trigger_targets_dynamic_ref_with_caller( - event_key, - event_data, - targets, - MaybeLocation::caller(), - ); + self.trigger_with_caller(event, targets, MaybeLocation::caller()); } - /// # Safety - /// - /// See `trigger_targets_dynamic_ref` - unsafe fn trigger_targets_dynamic_ref_with_caller( + pub(crate) fn trigger_with_caller<'a, E: Event>( &mut self, - event_key: EventKey, - event_data: &mut E, - targets: Targets, + event: &mut E, + targets: impl EventTargets>, caller: MaybeLocation, ) { - let mut world = DeferredWorld::from(self); - let mut entity_targets = targets.entities().peekable(); - if entity_targets.peek().is_none() { - // SAFETY: `event_data` is accessible as the type represented by `event_key` - unsafe { - world.trigger_observers_with_data::<_, E::Traversal>( - event_key, - None, - None, - targets.components(), - event_data, - false, - caller, - ); - }; - } else { - for target_entity in entity_targets { - // SAFETY: `event_data` is accessible as the type represented by `event_key` - unsafe { - world.trigger_observers_with_data::<_, E::Traversal>( - event_key, - Some(target_entity), - Some(target_entity), - targets.components(), - event_data, - E::AUTO_PROPAGATE, - caller, - ); - }; - } + let event_key = E::register_event_key(self); + // SAFETY: event_key was just registered and matches `event` + unsafe { + DeferredWorld::from(self).trigger_raw(event_key, event, targets, caller); } } @@ -476,7 +358,7 @@ impl World { let archetype = &mut archetypes.archetypes[archetype.index()]; if archetype.contains(*component) { let no_longer_observed = archetype - .components() + .iter_components() .all(|id| !cache.component_observers.contains_key(&id)); if no_longer_observed { @@ -497,16 +379,16 @@ impl World { mod tests { use alloc::{vec, vec::Vec}; - use bevy_platform::collections::HashMap; use bevy_ptr::OwningPtr; use crate::{ change_detection::MaybeLocation, - component::ComponentId, entity_disabling::Internal, + event::{EntityComponents, EntityComponentsTrigger, Event}, + hierarchy::ChildOf, observer::{Observer, Replace}, prelude::*, - traversal::Traversal, + world::DeferredWorld, }; #[derive(Component)] @@ -519,14 +401,29 @@ mod tests { #[component(storage = "SparseSet")] struct S; - #[derive(EntityEvent)] + #[derive(Event)] struct EventA; #[derive(EntityEvent)] + struct EntityEventA; + + struct EntityComponentsEvent; + + impl Event for EntityComponentsEvent { + type Target<'a> = EntityComponents<'a>; + type Trigger = EntityComponentsTrigger; + } + + #[derive(Event)] struct EventWithData { counter: usize, } + #[derive(EntityEvent)] + struct EntityEventWithData { + counter: usize, + } + #[derive(Resource, Default)] struct Order(Vec<&'static str>); @@ -537,17 +434,8 @@ mod tests { } } - #[derive(Component)] - struct ChildOf(Entity); - - impl Traversal for &'_ ChildOf { - fn traverse(item: Self::Item<'_, '_>, _: &D) -> Option { - Some(item.0) - } - } - #[derive(Component, EntityEvent)] - #[entity_event(traversal = &'static ChildOf, auto_propagate)] + #[entity_event(propagate, auto_propagate)] struct EventPropagating; #[test] @@ -684,26 +572,6 @@ mod tests { assert_eq!(7, event.counter); } - #[test] - fn observer_trigger_targets_ref() { - let mut world = World::new(); - - world.add_observer(|mut event: On| { - event.counter += 1; - }); - world.add_observer(|mut event: On| { - event.counter += 2; - }); - world.add_observer(|mut event: On| { - event.counter += 4; - }); - - let mut event = EventWithData { counter: 0 }; - let component_a = world.register_component::(); - world.trigger_targets_ref(&mut event, component_a); - assert_eq!(5, event.counter); - } - #[test] fn observer_multiple_listeners() { let mut world = World::new(); @@ -813,44 +681,26 @@ 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.entity(), Entity::PLACEHOLDER); - res.observed("event_a"); - }); - - world.trigger(EventA); - 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.entity(), entity); res.observed("a_2"); }); - world.trigger_targets(EventA, entity); + world.trigger_targets(EntityEventA, entity); assert_eq!(vec!["a_2", "a_1"], world.resource::().0); } @@ -867,93 +717,78 @@ 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| { + |_: On, mut res: ResMut| res.0 += 10000, + ); + world.add_observer( + |_: On, mut res: ResMut| { res.0 += 100000; }, ); world.add_observer( - |_: On, + |_: On, mut res: ResMut| res.0 += 1000000, ); // trigger for an entity and a component - world.trigger_targets(EventA, (entity_1, component_a)); + world.trigger_targets( + EntityComponentsEvent, + EntityComponents { + entity: entity_1, + components: &[component_a], + }, + ); // 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( + EntityComponentsEvent, + [ + EntityComponents { + entity: entity_1, + components: &[], + }, + EntityComponents { + entity: entity_2, + components: &[], + }, + ], + ); // 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)); - // all component observers trigger, entities are not observed - assert_eq!(1111100, world.resource::().0); - world.resource_mut::().0 = 0; - // 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))); - assert_eq!(2222211, world.resource::().0); - world.resource_mut::().0 = 0; - - // trigger to test complex tuples: (A, B, (A, B)) - world.trigger_targets( - EventA, - (component_a, component_b, (component_a, component_b)), - ); - // the duplicate components in the tuple don't cause multiple triggers - assert_eq!(1111100, world.resource::().0); - world.resource_mut::().0 = 0; - - // trigger to test complex tuples: (A, B, (A, B), ((A, B), (A, B))) - world.trigger_targets( - EventA, - ( - component_a, - component_b, - (component_a, component_b), - ((component_a, component_b), (component_a, component_b)), - ), - ); - // the duplicate components in the tuple don't cause multiple triggers - assert_eq!(1111100, world.resource::().0); - world.resource_mut::().0 = 0; - - // trigger to test the most complex tuple: (A, B, (A, B), (B, A), (A, B, ((A, B), (B, A)))) world.trigger_targets( - EventA, - ( - component_a, - component_b, - (component_a, component_b), - (component_b, component_a), - ( - component_a, - component_b, - ((component_a, component_b), (component_b, component_a)), - ), - ), + EntityComponentsEvent, + [ + EntityComponents { + entity: entity_1, + components: &[component_a, component_b], + }, + EntityComponents { + entity: entity_2, + components: &[component_a, component_b], + }, + ], ); - // the duplicate components in the tuple don't cause multiple triggers - assert_eq!(1111100, world.resource::().0); + assert_eq!(2222211, world.resource::().0); world.resource_mut::().0 = 0; } @@ -973,9 +808,7 @@ mod tests { // SAFETY: we registered `component_id` above. unsafe { entity.insert_by_id(component_id, ptr) }; }); - let entity = entity.flush(); - world.trigger_targets(EventA, entity); assert_eq!(vec!["event_a"], world.resource::().0); } @@ -983,20 +816,29 @@ mod tests { fn observer_dynamic_trigger() { let mut world = World::new(); world.init_resource::(); - let event_a = Remove::register_event_key(&mut world); + let event_a = EventA::register_event_key(&mut world); // SAFETY: we registered `event_a` above and it matches the type of EventA let observe = unsafe { - Observer::with_dynamic_runner(|mut world, _trigger, _ptr, _propagate| { - world.resource_mut::().observed("event_a"); - }) + Observer::with_dynamic_runner( + |mut world, _observer, _trigger_context, _event, _target, _trigger| { + world.resource_mut::().observed("event_a"); + }, + ) .with_event_key(event_a) }; world.spawn(observe); 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 { + DeferredWorld::from(world).trigger_raw( + event_a, + &mut EventA, + (), + MaybeLocation::caller(), + ) + }; }); world.flush(); assert_eq!(vec!["event_a"], world.resource::().0); @@ -1346,33 +1188,6 @@ mod tests { world.commands().spawn(Component).clear(); } - #[test] - fn observer_triggered_components() { - #[derive(Resource, Default)] - struct Counter(HashMap); - - let mut world = World::new(); - world.init_resource::(); - let a_id = world.register_component::(); - let b_id = world.register_component::(); - - world.add_observer(|event: On, mut counter: ResMut| { - for &component in event.components() { - *counter.0.entry(component).or_default() += 1; - } - }); - - 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); - - let counter = world.resource::(); - assert_eq!(4, *counter.0.get(&a_id).unwrap()); - assert_eq!(3, *counter.0.get(&b_id).unwrap()); - } - #[test] fn observer_watch_entities() { let mut world = World::new(); @@ -1380,14 +1195,14 @@ mod tests { let entities = world .spawn_batch(core::iter::repeat_n((), 4)) .collect::>(); - let observer = Observer::new(|_: On, mut order: ResMut| { + let observer = Observer::new(|_: On, mut order: ResMut| { order.observed("a"); }); world.spawn(observer.with_entities(entities.iter().copied().take(2))); - world.trigger_targets(EventA, [entities[0], entities[1]]); + world.trigger_targets(EntityEventA, [entities[0], entities[1]]); assert_eq!(vec!["a", "a"], world.resource::().0); - world.trigger_targets(EventA, [entities[2], entities[3]]); + world.trigger_targets(EntityEventA, [entities[2], entities[3]]); assert_eq!(vec!["a", "a"], world.resource::().0); } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 7f2536f5f8318..71963b0158ea1 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -4,33 +4,40 @@ use core::any::Any; use crate::{ error::ErrorContext, - observer::ObserverTrigger, + event::Event, + observer::TriggerContext, prelude::*, query::DebugCheckedUnwrap, system::{ObserverSystem, RunSystemError}, world::DeferredWorld, }; -use bevy_ptr::PtrMut; +use bevy_ptr::{Ptr, PtrMut}; /// Type for function that is run when an observer is triggered. /// /// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component, /// but can be overridden for custom behavior. -pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: &mut bool); +pub type ObserverRunner = fn( + DeferredWorld, + observer: Entity, + &TriggerContext, + event: PtrMut, + target: Ptr, + trigger: PtrMut, +); pub(super) fn observer_system_runner>( mut world: DeferredWorld, - observer_trigger: ObserverTrigger, - ptr: PtrMut, - propagate: &mut bool, + observer: Entity, + trigger_context: &TriggerContext, + event_ptr: PtrMut, + target_ptr: Ptr, + trigger_ptr: PtrMut, ) { let world = world.as_unsafe_world_cell(); + // SAFETY: Observer was triggered so must still exist in world - let observer_cell = unsafe { - world - .get_entity(observer_trigger.observer) - .debug_checked_unwrap() - }; + let observer_cell = unsafe { world.get_entity(observer).debug_checked_unwrap() }; // SAFETY: Observer was triggered so must have an `Observer` let mut state = unsafe { observer_cell.get_mut::().debug_checked_unwrap() }; @@ -41,11 +48,18 @@ pub(super) fn observer_system_runner = unsafe { target_ptr.deref() }; + let on: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` - unsafe { ptr.deref_mut() }, - propagate, - observer_trigger, + unsafe { event_ptr.deref_mut() }, + observer, + target, + trigger, + trigger_context, ); // SAFETY: diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index a09cda0676c8b..f42e9f16d4c63 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -1,15 +1,19 @@ //! System parameters for working with observers. -use core::marker::PhantomData; -use core::ops::DerefMut; -use core::{fmt::Debug, ops::Deref}; +use core::{ + fmt::Debug, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; use bevy_ptr::Ptr; -use smallvec::SmallVec; use crate::{ - bundle::Bundle, change_detection::MaybeLocation, component::ComponentId, event::EntityEvent, + bundle::Bundle, + change_detection::MaybeLocation, + event::{EntityTarget, Event, PropagateEntityTrigger}, prelude::*, + traversal::Traversal, }; /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the @@ -26,10 +30,12 @@ use crate::{ /// Providing multiple components in this bundle will cause this event to be triggered by any /// matching component in the bundle, /// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). -pub struct On<'w, E, B: Bundle = ()> { +pub struct On<'w, E: Event, B: Bundle = ()> { event: &'w mut E, - propagate: &'w mut bool, - trigger: ObserverTrigger, + observer: Entity, + target: &'w E::Target<'w>, + trigger: &'w mut E::Trigger, + trigger_context: &'w TriggerContext, _marker: PhantomData, } @@ -37,20 +43,28 @@ pub struct On<'w, E, B: Bundle = ()> { #[deprecated(since = "0.17.0", note = "Renamed to `On`.")] pub type Trigger<'w, E, B = ()> = On<'w, E, B>; -impl<'w, E, B: Bundle> On<'w, E, B> { +impl<'w, E: Event, B: Bundle> On<'w, E, B> { /// Creates a new instance of [`On`] 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: &'w mut E, + observer: Entity, + target: &'w E::Target<'w>, + trigger: &'w mut E::Trigger, + trigger_context: &'w TriggerContext, + ) -> Self { Self { event, - propagate, + observer, + target, trigger, + trigger_context, _marker: PhantomData, } } /// Returns the event type of this [`On`] instance. pub fn event_key(&self) -> EventKey { - self.trigger.event_key + self.trigger_context.event_key } /// Returns a reference to the triggered event. @@ -68,11 +82,14 @@ impl<'w, E, B: Bundle> On<'w, E, B> { Ptr::from(&self.event) } - /// Returns the components that triggered the observer, out of the - /// components defined in `B`. Does not necessarily include all of them as - /// `B` acts like an `OR` filter rather than an `AND` filter. - pub fn components(&self) -> &[ComponentId] { - &self.trigger.components + /// Returns the trigger context for this event. + pub fn trigger(&self) -> &E::Trigger { + self.trigger + } + + /// Returns the target for this event. For entity events, consider using [`On::entity`]. + pub fn target(&self) -> &E::Target<'w> { + self.target } /// Returns the [`Entity`] that observed the triggered event. @@ -96,16 +113,16 @@ impl<'w, E, B: Bundle> On<'w, E, B> { /// world.trigger_targets(AssertEvent, observer); /// ``` pub fn observer(&self) -> Entity { - self.trigger.observer + self.observer } /// Returns the source code location that triggered this observer. pub fn caller(&self) -> MaybeLocation { - self.trigger.caller + self.trigger_context.caller } } -impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { +impl<'w, E: for<'t> Event: EntityTarget>, B: Bundle> On<'w, E, B> { /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. /// /// Note that if event propagation is enabled, this may not be the same as the original target of the event, @@ -113,16 +130,23 @@ impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { /// /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. pub fn entity(&self) -> Entity { - self.trigger.entity.unwrap_or(Entity::PLACEHOLDER) + self.target.entity() } +} +impl< + 'w, + const AUTO_PROPAGATE: bool, + E: for<'t> Event = Entity, Trigger = PropagateEntityTrigger>, + B: Bundle, + T: Traversal, + > On<'w, E, B> +{ /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. /// /// If event propagation is not enabled, this will always return the same value as [`On::entity`]. - /// - /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. pub fn original_entity(&self) -> Entity { - self.trigger.original_entity.unwrap_or(Entity::PLACEHOLDER) + self.trigger.original_entity } /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. @@ -138,29 +162,31 @@ impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { /// /// [`Traversal`]: crate::traversal::Traversal pub fn propagate(&mut self, should_propagate: bool) { - *self.propagate = should_propagate; + self.trigger.propagate = should_propagate; } /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. /// /// [`propagate`]: On::propagate pub fn get_propagate(&self) -> bool { - *self.propagate + self.trigger.propagate } } -impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { +impl<'w, E: for<'t> Event: Debug> + Debug, B: Bundle> Debug + for On<'w, E, B> +{ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("On") .field("event", &self.event) - .field("propagate", &self.propagate) .field("trigger", &self.trigger) + .field("target", &self.target) .field("_marker", &self._marker) .finish() } } -impl<'w, E, B: Bundle> Deref for On<'w, E, B> { +impl<'w, E: Event, B: Bundle> Deref for On<'w, E, B> { type Target = E; fn deref(&self) -> &Self::Target { @@ -168,7 +194,7 @@ impl<'w, E, B: Bundle> Deref for On<'w, E, B> { } } -impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { +impl<'w, E: Event, B: Bundle> DerefMut for On<'w, E, B> { fn deref_mut(&mut self) -> &mut Self::Target { self.event } @@ -177,30 +203,18 @@ impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { /// Metadata about a specific [`Event`] that triggered an observer. /// /// This information is exposed via methods on [`On`]. -#[derive(Debug)] -pub struct ObserverTrigger { - /// The [`Entity`] of the observer handling the trigger. - pub observer: Entity, +pub struct TriggerContext { /// The [`EventKey`] the trigger targeted. - pub event_key: EventKey, - /// The [`ComponentId`]s the trigger targeted. - pub components: SmallVec<[ComponentId; 2]>, - /// The entity that the entity-event targeted, if any. - /// - /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_entity`]. - pub entity: Option, - /// The entity that the entity-event was originally targeted at, if any. - /// - /// 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. - pub original_entity: Option, + pub(crate) event_key: EventKey, /// The location of the source code that triggered the observer. - pub caller: MaybeLocation, + pub(crate) caller: MaybeLocation, } -impl ObserverTrigger { - /// Returns the components that the trigger targeted. - pub fn components(&self) -> &[ComponentId] { - &self.components +impl TriggerContext { + pub fn new(world: &mut World, caller: MaybeLocation) -> Self { + Self { + event_key: E::register_event_key(world), + caller, + } } } diff --git a/crates/bevy_ecs/src/observer/trigger_targets.rs b/crates/bevy_ecs/src/observer/trigger_targets.rs index 77728e4acdd10..890f242e66465 100644 --- a/crates/bevy_ecs/src/observer/trigger_targets.rs +++ b/crates/bevy_ecs/src/observer/trigger_targets.rs @@ -1,8 +1,7 @@ //! Stores the [`TriggerTargets`] trait. -use crate::{component::ComponentId, prelude::*}; +use crate::{entity::Entity, event::EntityComponents}; use alloc::vec::Vec; -use variadics_please::all_tuples; /// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. /// @@ -12,106 +11,71 @@ use variadics_please::all_tuples; /// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. /// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, /// allowing you to trigger events for multiple targets at once. -pub trait TriggerTargets { - /// The components the trigger should target. - fn components(&self) -> impl Iterator + Clone + '_; - - /// The entities the trigger should target. - fn entities(&self) -> impl Iterator + Clone + '_; +pub trait EventTargets: Send + Sync { + fn targets<'a>(&'a self) -> impl Iterator + 'a + where + T: 'a; } -impl TriggerTargets for &T { - fn components(&self) -> impl Iterator + Clone + '_ { - (**self).components() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - (**self).entities() +impl + ?Sized, T: 'static> EventTargets for &L { + fn targets<'a>(&'a self) -> impl Iterator + 'a + where + T: 'a, + { + (**self).targets() } } -impl TriggerTargets for Entity { - fn components(&self) -> impl Iterator + Clone + '_ { - [].into_iter() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) +impl EventTargets for Entity { + fn targets<'a>(&'a self) -> impl Iterator + 'a + where + Entity: 'a, + { + core::iter::once(self) } } -impl TriggerTargets for ComponentId { - fn components(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - [].into_iter() +impl<'a> EventTargets> for EntityComponents<'a> { + fn targets<'b>(&'b self) -> impl Iterator> + 'b + where + EntityComponents<'a>: 'b, + { + core::iter::once(self) } } -impl TriggerTargets for Vec { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) +impl EventTargets<()> for () { + fn targets<'a>(&'a self) -> impl Iterator + 'a + where + (): 'a, + { + core::iter::once(&()) } } -impl TriggerTargets for [T; N] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) +impl EventTargets for Vec { + fn targets<'a>(&'a self) -> impl Iterator + 'a + where + T: 'a, + { + self.iter() } } -impl TriggerTargets for [T] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) +impl EventTargets for [T; N] { + fn targets<'a>(&'a self) -> impl Iterator + 'a + where + T: 'a, + { + self.iter() } } -macro_rules! impl_trigger_targets_tuples { - ($(#[$meta:meta])* $($trigger_targets: ident),*) => { - #[expect(clippy::allow_attributes, reason = "can't guarantee violation of non_snake_case")] - #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] - $(#[$meta])* - impl<$($trigger_targets: TriggerTargets),*> TriggerTargets for ($($trigger_targets,)*) - { - fn components(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.components()); - )* - iter - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.entities()); - )* - iter - } - } +impl EventTargets for [T] { + fn targets<'a>(&'a self) -> impl Iterator + 'a + where + T: 'a, + { + self.iter() } } - -all_tuples!( - #[doc(fake_variadic)] - impl_trigger_targets_tuples, - 0, - 15, - T -); diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index bb28f967af377..f4c47207dd20a 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -421,8 +421,8 @@ macro_rules! impl_sparse_set { } /// Returns an iterator visiting all keys (indices) in arbitrary order. - pub fn indices(&self) -> impl Iterator + Clone + '_ { - self.indices.iter().cloned() + pub fn indices(&self) -> &[I] { + &self.indices } /// Returns an iterator visiting all values in arbitrary order. diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index 83dad342a803c..674d136d60389 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -9,8 +9,8 @@ use crate::{ change_detection::MaybeLocation, entity::Entity, error::Result, - event::{BufferedEvent, EntityEvent, Event, Events}, - observer::TriggerTargets, + event::{BufferedEvent, Event, Events}, + observer::EventTargets, resource::Resource, schedule::ScheduleLabel, system::{IntoSystem, SystemId, SystemInput}, @@ -210,22 +210,22 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { /// A [`Command`] that sends a global [`Event`] without any targets. #[track_caller] -pub fn trigger(event: impl Event) -> impl Command { +pub fn trigger<'a>(mut event: impl Event = ()>) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { - world.trigger_with_caller(event, caller); + world.trigger_with_caller(&mut event, (), caller); } } /// A [`Command`] that sends an [`EntityEvent`] for the given targets. #[track_caller] -pub fn trigger_targets( - event: impl EntityEvent, - targets: impl TriggerTargets + Send + Sync + 'static, +pub fn trigger_targets<'a, E: Event>( + mut event: E, + targets: impl EventTargets> + 'static, ) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { - world.trigger_targets_with_caller(event, targets, caller); + world.trigger_with_caller(&mut event, targets, caller); } } diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 74f206b348c0f..7499a5a11493b 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -257,12 +257,12 @@ pub fn observe( /// /// This will run any [`Observer`](crate::observer::Observer) of the given [`EntityEvent`] watching the entity. #[track_caller] -pub fn trigger(event: impl EntityEvent) -> impl EntityCommand { +pub fn trigger(mut event: impl EntityEvent) -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { let id = entity.id(); entity.world_scope(|world| { - world.trigger_targets_with_caller(event, id, caller); + world.trigger_with_caller(&mut event, id, caller); }); } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index bf520223af5b8..c55a813290cb0 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -21,7 +21,7 @@ use crate::{ entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut}, error::{warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, event::{BufferedEvent, EntityEvent, Event}, - observer::{Observer, TriggerTargets}, + observer::{EventTargets, Observer}, resource::Resource, schedule::ScheduleLabel, system::{ @@ -1094,7 +1094,7 @@ impl<'w, 's> Commands<'w, 's> { /// with [`entity_command::trigger(event)`](entity_command::trigger). /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] - pub fn trigger(&mut self, event: impl Event) { + pub fn trigger<'a>(&mut self, event: impl Event = ()>) { self.queue(command::trigger(event)); } @@ -1109,10 +1109,10 @@ impl<'w, 's> Commands<'w, 's> { /// with [`entity_command::trigger(event)`](entity_command::trigger). /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] - pub fn trigger_targets( + pub fn trigger_targets<'a, E: Event>( &mut self, - event: impl EntityEvent, - targets: impl TriggerTargets + Send + Sync + 'static, + event: E, + targets: impl EventTargets> + 'static, ) { self.queue(command::trigger_targets(event, targets)); } diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index c8d799b05d544..50f2a85ccde94 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -2,7 +2,7 @@ use core::ops::{Deref, DerefMut}; use variadics_please::all_tuples; -use crate::{bundle::Bundle, prelude::On, system::System}; +use crate::{bundle::Bundle, event::Event, prelude::On, system::System}; /// Trait for types that can be used as input to [`System`]s. /// @@ -222,7 +222,7 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// Used for [`ObserverSystem`]s. /// /// [`ObserverSystem`]: crate::system::ObserverSystem -impl SystemInput for On<'_, E, B> { +impl SystemInput for On<'_, E, B> { type Param<'i> = On<'i, E, B>; type Inner<'i> = On<'i, E, B>; diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index d971e312a7034..08375f6309743 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -1127,7 +1127,7 @@ mod tests { for entity in &query { let location = entities.get(entity).unwrap(); let archetype = archetypes.get(location.archetype_id).unwrap(); - let archetype_components = archetype.components().collect::>(); + let archetype_components = archetype.components(); let bundle_id = bundles .get_id(TypeId::of::<(W, W)>()) .expect("Bundle used to spawn entity should exist"); diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 862ebf71c7eb4..6f4abddd84379 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -1,4 +1,5 @@ use crate::{ + event::Event, prelude::{Bundle, On}, system::System, }; @@ -6,12 +7,12 @@ use crate::{ use super::IntoSystem; /// Implemented for [`System`]s that have [`On`] as the first argument. -pub trait ObserverSystem: +pub trait ObserverSystem: System, Out = Out> + Send + 'static { } -impl ObserverSystem for T where +impl ObserverSystem for T where T: System, Out = Out> + Send + 'static { } @@ -28,7 +29,7 @@ impl ObserverSystem for T where label = "the trait `IntoObserverSystem` is not implemented", note = "for function `ObserverSystem`s, ensure the first argument is `On` and any subsequent ones are `SystemParam`" )] -pub trait IntoObserverSystem: Send + 'static { +pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. type System: ObserverSystem; @@ -36,7 +37,7 @@ pub trait IntoObserverSystem: Send + 'static fn into_system(this: Self) -> Self::System; } -impl IntoObserverSystem for S +impl IntoObserverSystem for S where S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 9c247bcab2638..f855a470e4a89 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -7,15 +7,16 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::{ComponentId, Mutable}, entity::Entity, - event::{BufferedEvent, EntityEvent, Event, EventId, EventKey, Events, WriteBatchIds}, - lifecycle::{HookContext, INSERT, REPLACE}, - observer::{Observers, TriggerTargets}, + event::{ + BufferedEvent, EntityComponents, Event, EventId, EventKey, Events, Trigger, WriteBatchIds, + }, + lifecycle::{HookContext, Insert, Replace, INSERT, REPLACE}, + observer::{EventTargets, TriggerContext}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, relationship::RelationshipHookMode, resource::Resource, system::{Commands, Query}, - traversal::Traversal, world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; @@ -167,10 +168,13 @@ impl<'w> DeferredWorld<'w> { relationship_hook_mode, ); if archetype.has_replace_observer() { - self.trigger_observers( + self.trigger_raw( REPLACE, - Some(entity), - [component_id].into_iter(), + &mut Replace, + EntityComponents { + entity, + components: &[component_id], + }, MaybeLocation::caller(), ); } @@ -207,10 +211,13 @@ impl<'w> DeferredWorld<'w> { relationship_hook_mode, ); if archetype.has_insert_observer() { - self.trigger_observers( + self.trigger_raw( INSERT, - Some(entity), - [component_id].into_iter(), + &mut Insert, + EntityComponents { + entity, + components: &[component_id], + }, MaybeLocation::caller(), ); } @@ -778,85 +785,32 @@ impl<'w> DeferredWorld<'w> { } } - /// Triggers all event observers for [`ComponentId`] in target. - /// - /// # Safety - /// Caller must ensure observers listening for `event_key` can accept ZST pointers - #[inline] - pub(crate) unsafe fn trigger_observers( - &mut self, - event_key: EventKey, - target: Option, - components: impl Iterator + Clone, - caller: MaybeLocation, - ) { - Observers::invoke::<_>( - self.reborrow(), - event_key, - target, - target, - components, - &mut (), - &mut false, - caller, - ); - } - - /// Triggers all event observers for [`ComponentId`] in target. + /// Triggers all `event` observers for the given `targets` /// /// # Safety /// Caller must ensure `E` is accessible as the type represented by `event_key` #[inline] - pub(crate) unsafe fn trigger_observers_with_data( + pub(crate) unsafe fn trigger_raw<'a, E: Event>( &mut self, event_key: EventKey, - current_target: Option, - original_entity: Option, - components: impl Iterator + Clone, - data: &mut E, - mut propagate: bool, + event: &mut E, + targets: impl EventTargets>, caller: MaybeLocation, - ) where - T: Traversal, - { - Observers::invoke::<_>( - self.reborrow(), - event_key, - current_target, - original_entity, - components.clone(), - data, - &mut propagate, - caller, - ); - let Some(mut current_target) = current_target else { - return; - }; - - loop { - if !propagate { + ) { + // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` + let (mut world, observers) = unsafe { + let world = self.as_unsafe_world_cell(); + let observers = world.observers(); + let Some(observers) = observers.try_get_observers(event_key) else { return; - } - if let Some(traverse_to) = self - .get_entity(current_target) - .ok() - .and_then(|entity| entity.get_components::()) - .and_then(|item| T::traverse(item, data)) - { - current_target = traverse_to; - } else { - break; - } - Observers::invoke::<_>( - self.reborrow(), - event_key, - Some(current_target), - original_entity, - components.clone(), - data, - &mut propagate, - caller, - ); + }; + // SAFETY: The only outstanding reference to world is `observers` + (world.into_deferred(), observers) + }; + let context = TriggerContext { event_key, caller }; + for target in targets.targets() { + let mut trigger = ::default(); + trigger.trigger(world.reborrow(), observers, event.into(), target, &context); } } @@ -865,7 +819,8 @@ impl<'w> DeferredWorld<'w> { /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. /// /// [`Observer`]: crate::observer::Observer - pub fn trigger(&mut self, trigger: impl Event) { + pub fn trigger<'a>(&mut self, trigger: impl Event = ()>) { + // TODO: can we just use trigger_raw here? self.commands().trigger(trigger); } @@ -874,11 +829,12 @@ impl<'w> DeferredWorld<'w> { /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. /// /// [`Observer`]: crate::observer::Observer - pub fn trigger_targets( + pub fn trigger_targets<'a, E: Event>( &mut self, - trigger: impl EntityEvent, - targets: impl TriggerTargets + Send + Sync + 'static, + trigger: E, + targets: impl EventTargets> + 'static, ) { + // TODO: can we just use trigger_raw here? self.commands().trigger_targets(trigger, targets); } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 5bdd86282efe7..661c50d6f8a6e 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -13,8 +13,8 @@ use crate::{ ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityIdLocation, EntityLocation, OptIn, OptOut, }, - event::EntityEvent, - lifecycle::{DESPAWN, REMOVE, REPLACE}, + event::{EntityComponents, EntityEvent, EntityTarget, Event}, + lifecycle::{Despawn, Remove, Replace, DESPAWN, REMOVE, REPLACE}, observer::Observer, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, relationship::RelationshipHookMode, @@ -2361,7 +2361,7 @@ impl<'w> EntityWorldMut<'w> { // PERF: this could be stored in an Archetype Edge let to_remove = &old_archetype - .components() + .iter_components() .filter(|c| !retained_bundle_info.contributed_components().contains(c)) .collect::>(); let remove_bundle = @@ -2512,7 +2512,8 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn clear_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { let location = self.location(); - let component_ids: Vec = self.archetype().components().collect(); + // PERF: this should not be necessary + let component_ids: Vec = self.archetype().components().to_vec(); let components = &mut self.world.components; let bundle_id = self.world.bundles.init_dynamic_info( @@ -2576,51 +2577,60 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: All components in the archetype exist in world unsafe { if archetype.has_despawn_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( DESPAWN, - Some(self.entity), - archetype.components(), + &mut Despawn, + EntityComponents { + entity: self.entity, + components: archetype.components(), + }, caller, ); } deferred_world.trigger_on_despawn( archetype, self.entity, - archetype.components(), + archetype.iter_components(), caller, ); if archetype.has_replace_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( REPLACE, - Some(self.entity), - archetype.components(), + &mut Replace, + EntityComponents { + entity: self.entity, + components: archetype.components(), + }, caller, ); } deferred_world.trigger_on_replace( archetype, self.entity, - archetype.components(), + archetype.iter_components(), caller, RelationshipHookMode::Run, ); if archetype.has_remove_observer() { - deferred_world.trigger_observers( + deferred_world.trigger_raw( REMOVE, - Some(self.entity), - archetype.components(), + &mut Remove, + EntityComponents { + entity: self.entity, + components: archetype.components(), + }, caller, ); } deferred_world.trigger_on_remove( archetype, self.entity, - archetype.components(), + archetype.iter_components(), caller, ); } - for component_id in archetype.components() { + for component_id in archetype.iter_components() { world.removed_components.write(component_id, self.entity); } @@ -2852,14 +2862,14 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the given system is an exclusive system. #[track_caller] - pub fn observe( + pub fn observe<'a, E: Event: EntityTarget>, B: Bundle, M>( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { self.observe_with_caller(observer, MaybeLocation::caller()) } - pub(crate) fn observe_with_caller( + pub(crate) fn observe_with_caller<'a, E: Event: EntityTarget>, B: Bundle, M>( &mut self, observer: impl IntoObserverSystem, caller: MaybeLocation, @@ -5239,7 +5249,7 @@ mod tests { let ent = world.spawn((Marker::<1>, Marker::<2>, Marker::<3>)).id(); world.entity_mut(ent).retain::<()>(); - assert_eq!(world.entity(ent).archetype().components().next(), None); + assert_eq!(world.entity(ent).archetype().components().len(), 0); } // Test removing some components with `retain`, including components not on the entity. @@ -5255,15 +5265,7 @@ mod tests { // Check that marker 2 was retained. assert!(world.entity(ent).get::>().is_some()); // Check that only marker 2 was retained. - assert_eq!( - world - .entity(ent) - .archetype() - .components() - .collect::>() - .len(), - 1 - ); + assert_eq!(world.entity(ent).archetype().components().len(), 1); } // regression test for https://github.com/bevyengine/bevy/pull/7805 diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 9e36484cc5141..33782fffc3dba 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -17,14 +17,7 @@ pub use crate::{ change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, world::command_queue::CommandQueue, }; -use crate::{ - error::{DefaultErrorHandler, ErrorHandler}, - event::BufferedEvent, - lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, - prelude::{Add, Despawn, Insert, Remove, Replace}, -}; pub use bevy_ecs_macros::FromWorld; -use bevy_utils::prelude::DebugName; pub use deferred_world::DeferredWorld; pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_ref::{ @@ -50,9 +43,11 @@ use crate::{ }, entity::{Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, - event::{Event, EventId, Events, WriteBatchIds}, - lifecycle::RemovedComponentEvents, + error::{DefaultErrorHandler, ErrorHandler}, + event::{BufferedEvent, Event, EventId, Events, WriteBatchIds}, + lifecycle::{ComponentHooks, RemovedComponentEvents, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, observer::Observers, + prelude::{Add, Despawn, Insert, Remove, Replace}, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, resource::Resource, @@ -69,6 +64,7 @@ use crate::{ use alloc::{boxed::Box, vec::Vec}; use bevy_platform::sync::atomic::{AtomicU32, Ordering}; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{any::TypeId, fmt}; use log::warn; use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; @@ -888,7 +884,7 @@ impl World { .expect("ArchetypeId was retrieved from an EntityLocation and should correspond to an Archetype"); Ok(archetype - .components() + .iter_components() .filter_map(|id| self.components().get_info(id))) } diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 9809a7627bd79..2101564b208f5 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -140,7 +140,7 @@ pub struct InputFocusVisible(pub bool); /// To set up your own bubbling input event, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, /// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`]. #[derive(EntityEvent, Clone, Debug, Component)] -#[entity_event(traversal = WindowTraversal, auto_propagate)] +#[entity_event(propagate = WindowTraversal, auto_propagate)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] pub struct FocusedInput { /// The underlying input event. @@ -152,7 +152,7 @@ pub struct FocusedInput { /// An event which is used to set input focus. Trigger this on an entity, and it will bubble /// until it finds a focusable entity, and then set focus to it. #[derive(Clone, EntityEvent)] -#[entity_event(traversal = WindowTraversal, auto_propagate)] +#[entity_event(propagate = WindowTraversal, auto_propagate)] pub struct AcquireFocus { /// The primary window entity. window: Entity, diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 97e6bfad3b56d..5373b5ca7d031 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -60,7 +60,7 @@ use crate::{ /// The documentation for the [`pointer_events`] explains the events this module exposes and /// the order in which they fire. #[derive(BufferedEvent, EntityEvent, Clone, PartialEq, Debug, Reflect, Component)] -#[entity_event(traversal = PointerTraversal, auto_propagate)] +#[entity_event(propagate = PointerTraversal, auto_propagate)] #[reflect(Component, Debug, Clone)] pub struct Pointer { /// The pointer that triggered this event diff --git a/crates/bevy_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index ee0b15847a315..feddacfdfb091 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -283,7 +283,7 @@ impl<'w> DynamicSceneBuilder<'w> { }; let original_entity = self.original_world.entity(entity); - for component_id in original_entity.archetype().components() { + for component_id in original_entity.archetype().iter_components() { let mut extract_and_push = || { let type_id = self .original_world diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 255f688673c86..db92bdc468431 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -119,7 +119,7 @@ impl Scene { .get(&scene_entity.id()) .expect("should have previously spawned an entity"); - for component_id in archetype.components() { + for component_id in archetype.iter_components() { let component_info = self .world .components() diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index f3a9ab7e149de..71d56a18102a1 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -511,7 +511,11 @@ 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: triggering this for PLACEHOLDER is suboptimal, but this scene system is on + // its way out, so lets avoid breaking people by making a second event. + world + .commands() + .trigger_targets(SceneInstanceReady { instance_id }, Entity::PLACEHOLDER); } } } diff --git a/examples/ecs/observer_propagation.rs b/examples/ecs/observer_propagation.rs index d995f24d794f5..3e862b7cc9fe8 100644 --- a/examples/ecs/observer_propagation.rs +++ b/examples/ecs/observer_propagation.rs @@ -45,16 +45,14 @@ fn setup(mut commands: Commands) { // // We enable propagation by adding the event attribute and specifying two important pieces of information. // -// - **traversal:** -// Which component we want to propagate along. In this case, we want to "bubble" (meaning propagate -// from child to parent) so we use the `ChildOf` component for propagation. The component supplied -// must implement the `Traversal` trait. +// - **propagate:** +// Enables the default propagation behavior ("bubbling" up from child to parent using the ChildOf component). // // - **auto_propagate:** // We can also choose whether or not this event will propagate by default when triggered. If this is // false, it will only propagate following a call to `On::propagate(true)`. #[derive(Clone, Component, EntityEvent)] -#[entity_event(traversal = &'static ChildOf, auto_propagate)] +#[entity_event(propagate, auto_propagate)] struct Attack { damage: u16, } diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 46f5ca362005b..9aa638768de4b 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -51,7 +51,7 @@ fn send_scroll_events( /// UI scrolling event. #[derive(EntityEvent, Debug)] -#[entity_event(auto_propagate, traversal = &'static ChildOf)] +#[entity_event(propagate, auto_propagate)] struct Scroll { /// Scroll delta in logical coordinates. delta: Vec2, diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md index a1b90f4edc514..8cfb3b3096242 100644 --- a/release-content/release-notes/event_split.md +++ b/release-content/release-notes/event_split.md @@ -63,7 +63,7 @@ It supports optionally specifying some options for propagation using the `event` ```rust // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. #[derive(EntityEvent)] -#[entity_event(traversal = &'static ChildOf, auto_propagate)] +#[entity_event(propagate, auto_propagate)] struct Damage { amount: f32, } From 5a9a82084415b66f0fb35243decc13e408242d4c Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 23 Aug 2025 16:01:12 -0700 Subject: [PATCH 02/44] Single Event API: remove Event::Target --- Cargo.toml | 1 + .../benches/bevy_ecs/observers/propagation.rs | 8 +- benches/benches/bevy_ecs/observers/simple.rs | 12 +- crates/bevy_animation/Cargo.toml | 1 + crates/bevy_animation/macros/Cargo.toml | 22 +++ crates/bevy_animation/macros/LICENSE-APACHE | 176 +++++++++++++++++ crates/bevy_animation/macros/LICENSE-MIT | 19 ++ .../macros/src/animation_event.rs | 26 +++ crates/bevy_animation/macros/src/lib.rs | 16 ++ crates/bevy_animation/src/animation_event.rs | 34 ++++ crates/bevy_animation/src/lib.rs | 33 +++- crates/bevy_core_widgets/src/core_button.rs | 37 ++-- crates/bevy_core_widgets/src/core_checkbox.rs | 42 +++-- crates/bevy_core_widgets/src/core_radio.rs | 16 +- .../bevy_core_widgets/src/core_scrollbar.rs | 12 +- crates/bevy_core_widgets/src/core_slider.rs | 92 ++++----- crates/bevy_ecs/macros/src/component.rs | 92 ++++++++- crates/bevy_ecs/macros/src/lib.rs | 2 +- crates/bevy_ecs/src/bundle/insert.rs | 38 ++-- crates/bevy_ecs/src/bundle/remove.rs | 16 +- crates/bevy_ecs/src/bundle/spawner.rs | 16 +- crates/bevy_ecs/src/event/base.rs | 143 ++------------ crates/bevy_ecs/src/event/trigger.rs | 149 +++++++-------- crates/bevy_ecs/src/lifecycle.rs | 54 +++--- .../bevy_ecs/src/observer/entity_cloning.rs | 13 +- crates/bevy_ecs/src/observer/mod.rs | 178 ++++++++---------- crates/bevy_ecs/src/observer/runner.rs | 18 +- crates/bevy_ecs/src/observer/system_param.rs | 37 +--- .../bevy_ecs/src/observer/trigger_targets.rs | 81 -------- .../src/relationship/related_methods.rs | 5 +- .../bevy_ecs/src/system/commands/command.rs | 17 +- .../src/system/commands/entity_command.rs | 14 -- crates/bevy_ecs/src/system/commands/mod.rs | 44 +---- crates/bevy_ecs/src/world/deferred_world.rs | 43 +---- crates/bevy_ecs/src/world/entity_ref.rs | 68 ++++--- crates/bevy_ecs/src/world/error.rs | 14 +- crates/bevy_feathers/src/alpha_pattern.rs | 2 +- crates/bevy_feathers/src/font_styles.rs | 4 +- crates/bevy_feathers/src/theme.rs | 8 +- crates/bevy_input_focus/src/lib.rs | 36 ++-- crates/bevy_input_focus/src/tab_navigation.rs | 20 +- crates/bevy_pbr/src/render/light.rs | 12 +- crates/bevy_picking/src/events.rs | 67 +++++-- crates/bevy_render/src/gpu_readback.rs | 12 +- crates/bevy_render/src/sync_world.rs | 23 +-- .../bevy_render/src/view/window/screenshot.rs | 12 +- crates/bevy_scene/src/scene_spawner.rs | 15 +- crates/bevy_ui/src/interaction_states.rs | 28 +-- crates/bevy_winit/src/cursor/mod.rs | 4 +- examples/3d/edit_material_on_gltf.rs | 6 +- examples/3d/solari.rs | 4 +- examples/animation/animated_mesh.rs | 6 +- examples/animation/animated_mesh_events.rs | 10 +- examples/animation/animation_events.rs | 3 +- examples/animation/morph_targets.rs | 6 +- examples/ecs/entity_disabling.rs | 7 +- examples/ecs/error_handling.rs | 4 +- examples/ecs/observer_propagation.rs | 23 ++- examples/ecs/observers.rs | 37 ++-- examples/ecs/removal_detection.rs | 5 +- examples/no_std/library/src/lib.rs | 6 +- examples/picking/debug_picking.rs | 6 +- examples/picking/mesh_picking.rs | 6 +- examples/picking/simple_picking.rs | 6 +- examples/stress_tests/many_foxes.rs | 6 +- examples/testbed/3d.rs | 8 +- examples/ui/core_widgets_observers.rs | 60 +++--- examples/ui/directional_navigation.rs | 50 +++-- examples/ui/drag_to_scroll.rs | 69 ++++--- examples/ui/render_ui_to_texture.rs | 16 +- examples/ui/scroll.rs | 21 +-- examples/ui/tab_navigation.rs | 6 +- examples/ui/viewport_node.rs | 4 +- 73 files changed, 1142 insertions(+), 1065 deletions(-) create mode 100644 crates/bevy_animation/macros/Cargo.toml create mode 100644 crates/bevy_animation/macros/LICENSE-APACHE create mode 100644 crates/bevy_animation/macros/LICENSE-MIT create mode 100644 crates/bevy_animation/macros/src/animation_event.rs create mode 100644 crates/bevy_animation/macros/src/lib.rs create mode 100644 crates/bevy_animation/src/animation_event.rs delete mode 100644 crates/bevy_ecs/src/observer/trigger_targets.rs diff --git a/Cargo.toml b/Cargo.toml index 28612f1c0a516..7a46ca46e3981 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -632,6 +632,7 @@ serde_json = "1.0.140" bytemuck = "1" bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. +bevy_animation = { path = "crates/bevy_animation", version = "0.17.0-dev", default-features = false } bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } bevy_state = { path = "crates/bevy_state", version = "0.17.0-dev", default-features = false } bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false } diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 742751d2a0d7a..e7a55e0a5e1cf 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -63,13 +63,15 @@ pub fn event_propagation(criterion: &mut Criterion) { #[derive(EntityEvent, Clone, Component)] #[entity_event(propagate, auto_propagate)] -struct TestEvent {} +struct TestEvent { + entity: Entity, +} fn send_events(world: &mut World, leaves: &[Entity]) { - let target = leaves.iter().choose(&mut rand::rng()).unwrap(); + let target = *leaves.iter().choose(&mut rand::rng()).unwrap(); (0..N_EVENTS).for_each(|_| { - world.trigger_targets(TestEvent:: {}, *target); + world.trigger(TestEvent:: { entity }); }); } diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 68be7cbffe5c6..8114e041cf3b4 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -18,7 +18,9 @@ fn deterministic_rand() -> ChaCha8Rng { struct SimpleEvent; #[derive(Clone, EntityEvent)] -struct SimpleEntityEvent; +struct SimpleEntityEvent { + entity: Entity, +} pub fn observe_simple(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("observe"); @@ -43,7 +45,9 @@ pub fn observe_simple(criterion: &mut Criterion) { } entities.shuffle(&mut deterministic_rand()); bencher.iter(|| { - send_base_event(&mut world, &entities); + for entity in entities { + world.trigger(SimpleEntityEvent { entity }); + } }); }); @@ -57,7 +61,3 @@ fn on_simple_event(event: On) { fn on_simple_entity_event(event: On) { black_box(event); } - -fn send_base_event(world: &mut World, entities: impl EventTargets) { - world.trigger_targets(SimpleEntityEvent, entities); -} diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index ec3d91956ce38..a6013626ac645 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [dependencies] # bevy +bevy_animation_macros = { path = "macros", version = "0.17.0-dev" } bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } diff --git a/crates/bevy_animation/macros/Cargo.toml b/crates/bevy_animation/macros/Cargo.toml new file mode 100644 index 0000000000000..a56d11c387801 --- /dev/null +++ b/crates/bevy_animation/macros/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bevy_animation_macros" +version = "0.17.0-dev" +description = "Macros for bevy_animation" +edition = "2024" +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } + +syn = { version = "2.0", features = ["full"] } +quote = "1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options"] +all-features = true diff --git a/crates/bevy_animation/macros/LICENSE-APACHE b/crates/bevy_animation/macros/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_animation/macros/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_animation/macros/LICENSE-MIT b/crates/bevy_animation/macros/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_animation/macros/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_animation/macros/src/animation_event.rs b/crates/bevy_animation/macros/src/animation_event.rs new file mode 100644 index 0000000000000..b4abf9fc85f07 --- /dev/null +++ b/crates/bevy_animation/macros/src/animation_event.rs @@ -0,0 +1,26 @@ +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +pub fn derive_animation_event(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let manifest = BevyManifest::shared(); + let bevy_ecs = manifest.get_path("bevy_ecs"); + let bevy_animation = manifest.get_path("bevy_animation"); + + let generics = ast.generics; + let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); + + let struct_name = &ast.ident; + + quote! { + impl #impl_generics #bevy_ecs::event::Event for #struct_name #type_generics #where_clause { + type Trigger<'a> = #bevy_animation::AnimationEventTrigger; + } + + impl #impl_generics #bevy_animation::AnimationEvent for #struct_name #type_generics #where_clause { + } + } + .into() +} diff --git a/crates/bevy_animation/macros/src/lib.rs b/crates/bevy_animation/macros/src/lib.rs new file mode 100644 index 0000000000000..2954b412b4c5a --- /dev/null +++ b/crates/bevy_animation/macros/src/lib.rs @@ -0,0 +1,16 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +//! Macros for deriving animation behaviors. + +extern crate proc_macro; + +mod animation_event; + +use proc_macro::TokenStream; + +/// Implements the `AnimationEvent` trait for a type - see the trait +/// docs for an example usage. +#[proc_macro_derive(AnimationEvent)] +pub fn derive_animation_event(input: TokenStream) -> TokenStream { + animation_event::derive_animation_event(input) +} diff --git a/crates/bevy_animation/src/animation_event.rs b/crates/bevy_animation/src/animation_event.rs new file mode 100644 index 0000000000000..eec243b90b317 --- /dev/null +++ b/crates/bevy_animation/src/animation_event.rs @@ -0,0 +1,34 @@ +pub use bevy_animation_macros::AnimationEvent; + +use bevy_ecs::{ + entity::Entity, + event::{trigger_entity_internal, Event, Trigger}, + observer::{CachedObservers, TriggerContext}, + world::DeferredWorld, +}; + +pub trait AnimationEvent: Clone + for<'a> Event = AnimationEventTrigger> {} + +pub struct AnimationEventTrigger { + pub animation_player: Entity, +} + +impl Trigger for AnimationEventTrigger { + fn trigger( + &mut self, + world: DeferredWorld, + observers: &CachedObservers, + trigger_context: &TriggerContext, + event: &mut E, + ) { + let animation_player = self.animation_player; + trigger_entity_internal( + world, + observers, + event.into(), + self.into(), + animation_player, + trigger_context, + ); + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 386eb614be3a3..2ddff477b8f5a 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -14,8 +14,12 @@ pub mod animation_curves; pub mod gltf_curves; pub mod graph; pub mod transition; + +mod animation_event; mod util; +pub use animation_event::*; + use core::{ any::TypeId, cell::RefCell, @@ -108,17 +112,17 @@ pub struct AnimationClip { #[reflect(Clone)] struct TimedAnimationEvent { time: f32, - event: AnimationEvent, + event: AnimationEventData, } #[derive(Reflect, Debug, Clone)] #[reflect(Clone)] -struct AnimationEvent { +struct AnimationEventData { #[reflect(ignore, clone)] trigger: AnimationEventFn, } -impl AnimationEvent { +impl AnimationEventData { fn trigger(&self, commands: &mut Commands, entity: Entity, time: f32, weight: f32) { (self.trigger.0)(commands, entity, time, weight); } @@ -329,11 +333,16 @@ impl AnimationClip { /// is reached in the animation. /// /// See also [`add_event_to_target`](Self::add_event_to_target). - pub fn add_event(&mut self, time: f32, event: impl EntityEvent + Clone) { + pub fn add_event(&mut self, time: f32, event: impl AnimationEvent) { self.add_event_fn( time, move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { - commands.entity(entity).trigger(event.clone()); + commands.trigger_with( + event.clone(), + AnimationEventTrigger { + animation_player: entity, + }, + ); }, ); } @@ -348,13 +357,18 @@ impl AnimationClip { &mut self, target_id: AnimationTargetId, time: f32, - event: impl EntityEvent + Clone, + event: impl AnimationEvent, ) { self.add_event_fn_to_target( target_id, time, move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { - commands.entity(entity).trigger(event.clone()); + commands.trigger_with( + event.clone(), + AnimationEventTrigger { + animation_player: entity, + }, + ); }, ); } @@ -419,7 +433,7 @@ impl AnimationClip { index, TimedAnimationEvent { time, - event: AnimationEvent { + event: AnimationEventData { trigger: AnimationEventFn(Arc::new(trigger_fn)), }, }, @@ -1521,11 +1535,12 @@ impl<'a> Iterator for TriggeredEventsIter<'a> { #[cfg(test)] mod tests { + use crate as bevy_animation; use bevy_reflect::{DynamicMap, Map}; use super::*; - #[derive(EntityEvent, Reflect, Clone)] + #[derive(AnimationEvent, Reflect, Clone)] struct A; #[track_caller] diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 059a2751c863d..bfd6db6385c46 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -6,6 +6,7 @@ use bevy_ecs::system::In; use bevy_ecs::{ component::Component, entity::Entity, + event::EntityEvent, observer::On, query::With, system::{Commands, Query}, @@ -34,7 +35,7 @@ fn button_on_key_event( q_state: Query<(&CoreButton, Has)>, mut commands: Commands, ) { - if let Ok((bstate, disabled)) = q_state.get(event.entity()) + if let Ok((bstate, disabled)) = q_state.get(event.focused_entity) && !disabled { let input_event = &event.input; @@ -43,31 +44,31 @@ fn button_on_key_event( && (input_event.key_code == KeyCode::Enter || input_event.key_code == KeyCode::Space) { event.propagate(false); - commands.notify_with(&bstate.on_activate, Activate(event.entity())); + commands.notify_with(&bstate.on_activate, Activate(event.focused_entity)); } } } fn button_on_pointer_click( - mut event: On>, + mut click: On>, mut q_state: Query<(&CoreButton, Has, Has)>, mut commands: Commands, ) { - if let Ok((bstate, pressed, disabled)) = q_state.get_mut(event.entity()) { - event.propagate(false); + if let Ok((bstate, pressed, disabled)) = q_state.get_mut(click.entity) { + click.propagate(false); if pressed && !disabled { - commands.notify_with(&bstate.on_activate, Activate(event.entity())); + commands.notify_with(&bstate.on_activate, Activate(click.entity)); } } } fn button_on_pointer_down( - mut event: On>, + mut press: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { - event.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(press.entity) { + press.propagate(false); if !disabled && !pressed { commands.entity(button).insert(Pressed); } @@ -75,12 +76,12 @@ fn button_on_pointer_down( } fn button_on_pointer_up( - mut event: On>, + mut release: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { - event.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(release.entity) { + release.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); } @@ -88,12 +89,12 @@ fn button_on_pointer_up( } fn button_on_pointer_drag_end( - mut event: On>, + mut drag_end: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { - event.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(drag_end.entity) { + drag_end.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); } @@ -101,12 +102,12 @@ fn button_on_pointer_drag_end( } fn button_on_pointer_cancel( - mut event: On>, + mut cancel: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { - event.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(cancel.entity) { + cancel.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); } diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs index ed84efd44d259..4ca9551d3ddc4 100644 --- a/crates/bevy_core_widgets/src/core_checkbox.rs +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -16,6 +16,7 @@ use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; use crate::{Callback, Notify as _, ValueChange}; +use bevy_ecs::entity::Entity; /// Headless widget implementation for checkboxes. The [`Checked`] component represents the current /// state of the checkbox. The `on_change` field is an optional system id that will be run when the @@ -42,38 +43,38 @@ fn checkbox_on_key_input( q_checkbox: Query<(&CoreCheckbox, Has), Without>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.entity()) { + if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.focused_entity) { let event = &ev.event().input; if event.state == ButtonState::Pressed && !event.repeat && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) { ev.propagate(false); - set_checkbox_state(&mut commands, ev.entity(), checkbox, !is_checked); + set_checkbox_state(&mut commands, ev.focused_entity, checkbox, !is_checked); } } } fn checkbox_on_pointer_click( - mut ev: On>, + mut click: On>, q_checkbox: Query<(&CoreCheckbox, Has, Has)>, focus: Option>, focus_visible: Option>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.entity()) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(click.entity) { // Clicking on a button makes it the focused input, // and hides the focus ring if it was visible. if let Some(mut focus) = focus { - focus.0 = Some(ev.entity()); + focus.0 = Some(click.entity); } if let Some(mut focus_visible) = focus_visible { focus_visible.0 = false; } - ev.propagate(false); + click.propagate(false); if !disabled { - set_checkbox_state(&mut commands, ev.entity(), checkbox, !is_checked); + set_checkbox_state(&mut commands, click.entity, checkbox, !is_checked); } } } @@ -98,7 +99,10 @@ fn checkbox_on_pointer_click( /// } /// ``` #[derive(EntityEvent)] -pub struct SetChecked(pub bool); +pub struct SetChecked { + pub entity: Entity, + pub checked: bool, +} /// Event which can be triggered on a checkbox to toggle the checked state. This can be used to /// control the checkbox via gamepad buttons or other inputs. @@ -120,44 +124,44 @@ pub struct SetChecked(pub bool); /// } /// ``` #[derive(EntityEvent)] -pub struct ToggleChecked; +pub struct ToggleChecked { + pub entity: Entity, +} fn checkbox_on_set_checked( - mut ev: On, + set_checked: On, q_checkbox: Query<(&CoreCheckbox, Has, Has)>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.entity()) { - ev.propagate(false); + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(set_checked.entity) { if disabled { return; } - let will_be_checked = ev.event().0; + let will_be_checked = set_checked.checked; if will_be_checked != is_checked { - set_checkbox_state(&mut commands, ev.entity(), checkbox, will_be_checked); + set_checkbox_state(&mut commands, set_checked.entity, checkbox, will_be_checked); } } } fn checkbox_on_toggle_checked( - mut ev: On, + toggle_checked: On, q_checkbox: Query<(&CoreCheckbox, Has, Has)>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.entity()) { - ev.propagate(false); + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(toggle_checked.entity) { if disabled { return; } - set_checkbox_state(&mut commands, ev.entity(), checkbox, !is_checked); + set_checkbox_state(&mut commands, toggle_checked.entity, checkbox, !is_checked); } } fn set_checkbox_state( commands: &mut Commands, - entity: impl Into, + entity: impl Into, checkbox: &CoreCheckbox, new_state: bool, ) { diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs index e5ba867a3410f..95afe0863556b 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -1,15 +1,13 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin}; -use bevy_ecs::hierarchy::{ChildOf, Children}; -use bevy_ecs::query::Has; -use bevy_ecs::system::In; use bevy_ecs::{ component::Component, + hierarchy::{ChildOf, Children}, observer::On, - query::With, + query::{Has, With}, reflect::ReflectComponent, - system::{Commands, Query}, + system::{Commands, In, Query}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; @@ -61,7 +59,7 @@ fn radio_group_on_key_input( q_children: Query<&Children>, mut commands: Commands, ) { - if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.entity()) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.focused_entity) { let event = &ev.event().input; if event.state == ButtonState::Pressed && !event.repeat @@ -80,7 +78,7 @@ fn radio_group_on_key_input( // Find all radio descendants that are not disabled let radio_buttons = q_children - .iter_descendants(ev.entity()) + .iter_descendants(ev.focused_entity) .filter_map(|child_id| match q_radio.get(child_id) { Ok((checked, false)) => Some((child_id, checked)), Ok((_, true)) | Err(_) => None, @@ -149,7 +147,7 @@ fn radio_group_on_button_click( q_children: Query<&Children>, mut commands: Commands, ) { - if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.entity()) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.entity) { // Starting with the original target, search upward for a radio button. let radio_id = if q_radio.contains(ev.original_entity()) { ev.original_entity() @@ -180,7 +178,7 @@ fn radio_group_on_button_click( // Gather all the enabled radio group descendants for exclusion. let radio_buttons = q_children - .iter_descendants(ev.entity()) + .iter_descendants(ev.entity) .filter_map(|child_id| match q_radio.get(child_id) { Ok((checked, false)) => Some((child_id, checked)), Ok((_, true)) | Err(_) => None, diff --git a/crates/bevy_core_widgets/src/core_scrollbar.rs b/crates/bevy_core_widgets/src/core_scrollbar.rs index 00569243ec593..ad23ea813751c 100644 --- a/crates/bevy_core_widgets/src/core_scrollbar.rs +++ b/crates/bevy_core_widgets/src/core_scrollbar.rs @@ -110,10 +110,10 @@ fn scrollbar_on_pointer_down( mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, ui_scale: Res, ) { - if q_thumb.contains(ev.entity()) { + if q_thumb.contains(ev.entity) { // If they click on the thumb, do nothing. This will be handled by the drag event. ev.propagate(false); - } else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.entity()) { + } else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.entity) { // If they click on the scrollbar track, page up or down. ev.propagate(false); @@ -162,7 +162,7 @@ fn scrollbar_on_drag_start( q_scrollbar: Query<&CoreScrollbar>, q_scroll_area: Query<&ScrollPosition>, ) { - if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.entity()) { + if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.entity) { ev.propagate(false); if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent) && let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) @@ -183,7 +183,7 @@ fn scrollbar_on_drag( mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, ui_scale: Res, ) { - if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.entity()) + if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.entity) && let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent) { ev.propagate(false); @@ -219,7 +219,7 @@ fn scrollbar_on_drag_end( mut ev: On>, mut q_thumb: Query<&mut CoreScrollbarDragState, With>, ) { - if let Ok(mut drag) = q_thumb.get_mut(ev.entity()) { + if let Ok(mut drag) = q_thumb.get_mut(ev.entity) { ev.propagate(false); if drag.dragging { drag.dragging = false; @@ -231,7 +231,7 @@ fn scrollbar_on_drag_cancel( mut ev: On>, mut q_thumb: Query<&mut CoreScrollbarDragState, With>, ) { - if let Ok(mut drag) = q_thumb.get_mut(ev.entity()) { + if let Ok(mut drag) = q_thumb.get_mut(ev.entity) { ev.propagate(false); if drag.dragging { drag.dragging = false; diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index e680b849eda63..1df0d592d6573 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -28,6 +28,7 @@ use bevy_ui::{ }; use crate::{Callback, Notify, ValueChange}; +use bevy_ecs::entity::Entity; /// Defines how the slider should behave when you click on the track (not the thumb). #[derive(Debug, Default, PartialEq, Clone, Copy, Reflect)] @@ -228,7 +229,7 @@ pub struct CoreSliderDragState { } pub(crate) fn slider_on_pointer_down( - mut event: On>, + mut press: On>, q_slider: Query<( &CoreSlider, &SliderValue, @@ -245,9 +246,9 @@ pub(crate) fn slider_on_pointer_down( mut commands: Commands, ui_scale: Res, ) { - if q_thumb.contains(event.entity()) { + if q_thumb.contains(press.entity) { // Thumb click, stop propagation to prevent track click. - event.propagate(false); + press.propagate(false); } else if let Ok(( slider, value, @@ -258,10 +259,10 @@ pub(crate) fn slider_on_pointer_down( node_target, transform, disabled, - )) = q_slider.get(event.entity()) + )) = q_slider.get(press.entity) { // Track click - event.propagate(false); + press.propagate(false); if disabled { return; @@ -269,13 +270,13 @@ pub(crate) fn slider_on_pointer_down( // Find thumb size by searching descendants for the first entity with CoreSliderThumb let thumb_size = q_children - .iter_descendants(event.entity()) + .iter_descendants(press.entity) .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) .unwrap_or(0.0); // Detect track click. let local_pos = transform.try_inverse().unwrap().transform_point2( - event.pointer_location.position * node_target.scale_factor() / ui_scale.0, + press.pointer_location.position * node_target.scale_factor() / ui_scale.0, ); let track_width = node.size().x - thumb_size; // Avoid division by zero @@ -303,14 +304,12 @@ pub(crate) fn slider_on_pointer_down( }); if matches!(slider.on_change, Callback::Ignore) { - commands - .entity(event.entity()) - .insert(SliderValue(new_value)); + commands.entity(press.entity).insert(SliderValue(new_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: event.entity(), + source: press.entity, value: new_value, }, ); @@ -319,7 +318,7 @@ pub(crate) fn slider_on_pointer_down( } pub(crate) fn slider_on_drag_start( - mut event: On>, + mut drag_start: On>, mut q_slider: Query< ( &SliderValue, @@ -329,8 +328,8 @@ pub(crate) fn slider_on_drag_start( With, >, ) { - if let Ok((value, mut drag, disabled)) = q_slider.get_mut(event.entity()) { - event.propagate(false); + if let Ok((value, mut drag, disabled)) = q_slider.get_mut(drag_start.entity) { + drag_start.propagate(false); if !disabled { drag.dragging = true; drag.offset = value.0; @@ -355,7 +354,7 @@ pub(crate) fn slider_on_drag( ui_scale: Res, ) { if let Ok((node, slider, range, precision, transform, drag, disabled)) = - q_slider.get_mut(event.entity()) + q_slider.get_mut(event.entity) { event.propagate(false); if drag.dragging && !disabled { @@ -364,7 +363,7 @@ pub(crate) fn slider_on_drag( let distance = transform.transform_vector2(distance); // Find thumb size by searching descendants for the first entity with CoreSliderThumb let thumb_size = q_children - .iter_descendants(event.entity()) + .iter_descendants(event.entity) .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) .unwrap_or(0.0); let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0); @@ -382,13 +381,13 @@ pub(crate) fn slider_on_drag( if matches!(slider.on_change, Callback::Ignore) { commands - .entity(event.entity()) + .entity(event.entity) .insert(SliderValue(rounded_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: event.entity(), + source: event.entity, value: rounded_value, }, ); @@ -398,11 +397,11 @@ pub(crate) fn slider_on_drag( } pub(crate) fn slider_on_drag_end( - mut event: On>, + mut drag_end: On>, mut q_slider: Query<(&CoreSlider, &mut CoreSliderDragState)>, ) { - if let Ok((_slider, mut drag)) = q_slider.get_mut(event.entity()) { - event.propagate(false); + if let Ok((_slider, mut drag)) = q_slider.get_mut(drag_end.entity) { + drag_end.propagate(false); if drag.dragging { drag.dragging = false; } @@ -420,7 +419,7 @@ fn slider_on_key_input( )>, mut commands: Commands, ) { - if let Ok((slider, value, range, step, disabled)) = q_slider.get(event.entity()) { + if let Ok((slider, value, range, step, disabled)) = q_slider.get(event.focused_entity) { let input_event = &event.input; if !disabled && input_event.state == ButtonState::Pressed { let new_value = match input_event.key_code { @@ -435,13 +434,13 @@ fn slider_on_key_input( event.propagate(false); if matches!(slider.on_change, Callback::Ignore) { commands - .entity(event.entity()) + .entity(event.focused_entity) .insert(SliderValue(new_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: event.entity(), + source: event.focused_entity, value: new_value, }, ); @@ -450,23 +449,23 @@ fn slider_on_key_input( } } -pub(crate) fn slider_on_insert(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn slider_on_insert(insert: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(insert.entity); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_orientation(Orientation::Horizontal); } } -pub(crate) fn slider_on_insert_value(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn slider_on_insert_value(insert: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(insert.entity); let value = entity.get::().unwrap().0; if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_numeric_value(value.into()); } } -pub(crate) fn slider_on_insert_range(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn slider_on_insert_range(insert: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(insert.entity); let range = *entity.get::().unwrap(); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_min_numeric_value(range.start().into()); @@ -474,8 +473,8 @@ pub(crate) fn slider_on_insert_range(event: On, mut world: } } -pub(crate) fn slider_on_insert_step(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn slider_on_insert_step(insert: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(insert.entity); let step = entity.get::().unwrap().0; if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_numeric_value_step(step.into()); @@ -508,7 +507,13 @@ pub(crate) fn slider_on_insert_step(event: On, mut world: De /// } /// ``` #[derive(EntityEvent, Clone)] -pub enum SetSliderValue { +pub struct SetSliderValue { + pub entity: Entity, + pub change: SliderValueChange, +} + +#[derive(Clone)] +pub enum SliderValueChange { /// Set the slider value to a specific value. Absolute(f32), /// Add a delta to the slider value. @@ -518,28 +523,25 @@ pub enum SetSliderValue { } fn slider_on_set_value( - mut event: On, + event: On, q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>, mut commands: Commands, ) { - if let Ok((slider, value, range, step)) = q_slider.get(event.entity()) { - event.propagate(false); - let new_value = match event.event() { - SetSliderValue::Absolute(new_value) => range.clamp(*new_value), - SetSliderValue::Relative(delta) => range.clamp(value.0 + *delta), - SetSliderValue::RelativeStep(delta) => { - range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default()) + if let Ok((slider, value, range, step)) = q_slider.get(event.entity) { + let new_value = match event.change { + SliderValueChange::Absolute(new_value) => range.clamp(new_value), + SliderValueChange::Relative(delta) => range.clamp(value.0 + delta), + SliderValueChange::RelativeStep(delta) => { + range.clamp(value.0 + delta * step.map(|s| s.0).unwrap_or_default()) } }; if matches!(slider.on_change, Callback::Ignore) { - commands - .entity(event.entity()) - .insert(SliderValue(new_value)); + commands.entity(event.entity).insert(SliderValue(new_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: event.entity(), + source: event.entity, value: new_value, }, ); diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 6ad5b870f9823..a334b859d17fc 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -9,7 +9,7 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::{Brace, Comma, Paren}, - Data, DataEnum, DataStruct, DeriveInput, Expr, ExprCall, ExprPath, Field, Fields, Ident, + Data, DataEnum, DataStruct, DeriveInput, Expr, ExprCall, ExprPath, Field, Fields, Ident, Index, LitStr, Member, Path, Result, Token, Type, Visibility, }; @@ -18,6 +18,7 @@ pub const PROPAGATE: &str = "propagate"; // TODO: `traversal` is deprecated. Remove this (and related code) after the next release. pub const TRAVERSAL: &str = "traversal"; pub const AUTO_PROPAGATE: &str = "auto_propagate"; +pub const TRIGGER: &str = "trigger"; pub fn derive_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); @@ -33,8 +34,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 { - type Target<'a> = (); - type Trigger = #bevy_ecs_path::event::GlobalTrigger; + type Trigger<'a> = #bevy_ecs_path::event::GlobalTrigger; } }) } @@ -42,8 +42,9 @@ pub fn derive_event(input: TokenStream) -> TokenStream { pub fn derive_entity_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let mut auto_propagate = false; - let mut propagate = true; + let mut propagate = false; let mut traversal: Option = None; + let mut trigger: Option = None; let bevy_ecs_path: Path = crate::bevy_ecs_path(); let mut processed_attrs = Vec::new(); @@ -63,6 +64,7 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { Err(meta.error(format!("duplicate attribute: {ident}"))) } Some(ident) if ident == AUTO_PROPAGATE => { + propagate = true; auto_propagate = true; processed_attrs.push(AUTO_PROPAGATE); Ok(()) @@ -80,6 +82,10 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { processed_attrs.push(PROPAGATE); Ok(()) } + Some(ident) if ident == TRIGGER => { + trigger = Some(meta.value()?.parse()?); + Ok(()) + } Some(ident) => Err(meta.error(format!("unsupported attribute: {ident}"))), None => Err(meta.error("expected identifier")), }) { @@ -87,10 +93,26 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { } } + if trigger.is_some() && propagate { + return syn::Error::new( + ast.span(), + "Cannot define both #[entity_event(trigger)] and #[entity_event(propagate)]", + ) + .into_compile_error() + .into(); + } + + let entity_field = match get_entity_event_field(&ast) { + Ok(value) => value, + Err(err) => return err.into_compile_error().into(), + }; + let struct_name = &ast.ident; let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); - let trigger = if propagate { + let trigger = if let Some(trigger) = trigger { + quote! {#trigger} + } else if propagate { let traversal = traversal .unwrap_or_else(|| parse_quote! { &'static #bevy_ecs_path::hierarchy::ChildOf}); quote! {#bevy_ecs_path::event::PropagateEntityTrigger<#auto_propagate, Self, #traversal>} @@ -99,12 +121,67 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { }; TokenStream::from(quote! { impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { - type Target<'a> = #bevy_ecs_path::entity::Entity; - type Trigger = #trigger; + type Trigger<'a> = #trigger; } + + impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { + fn entity(&self) -> #bevy_ecs_path::entity::Entity { + self.#entity_field + } + + fn entity_mut(&mut self) -> &mut #bevy_ecs_path::entity::Entity { + &mut self.#entity_field + } + } + }) } +/// Returns the field with the `#[entity]` attribute, the only field if unnamed, +/// or the field with the name "entity". +fn get_entity_event_field(ast: &DeriveInput) -> Result { + let Data::Struct(DataStruct { fields, .. }) = &ast.data else { + return Err(syn::Error::new( + ast.span(), + "EntityEvent can only be derived for structs.", + )); + }; + match fields { + Fields::Named(fields) => fields.named.iter().find_map(|field| { + if field.ident.as_ref().is_some_and(|i| i == "entity") || field + .attrs + .iter() + .any(|attr| attr.path().is_ident(EVENT_ENTITY)) { + Some(Member::Named(field.ident.clone()?)) + } else { + None + } + }).ok_or(syn::Error::new( + fields.span(), + "EntityEvent derive expected a field name 'entity' or a field annotated with #[event_entity]." + )), + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => Ok(Member::Unnamed(Index::from(0))), + Fields::Unnamed(fields) => fields.unnamed.iter().enumerate().find_map(|(index, field)| { + if field + .attrs + .iter() + .any(|attr| attr.path().is_ident(EVENT_ENTITY)) { + Some(Member::Unnamed(Index::from(index))) + } else { + None + } + }) + .ok_or(syn::Error::new( + fields.span(), + "EntityEvent derive expected unnamed structs with one field or with a field annotated with #[event_entity].", + )), + Fields::Unit => Err(syn::Error::new( + fields.span(), + "EntityEvent derive expected named or unnamed struct, found unit struct.", + )), + } +} + pub fn derive_buffered_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); @@ -441,6 +518,7 @@ pub const STORAGE: &str = "storage"; pub const REQUIRE: &str = "require"; pub const RELATIONSHIP: &str = "relationship"; pub const RELATIONSHIP_TARGET: &str = "relationship_target"; +pub const EVENT_ENTITY: &str = "event_entity"; pub const ON_ADD: &str = "on_add"; pub const ON_INSERT: &str = "on_insert"; diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 84f07d53e2f91..2ab1ca2e3886d 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -566,7 +566,7 @@ pub fn derive_event(input: TokenStream) -> TokenStream { /// #[entity_event(auto_propagate)] /// struct MyEvent; /// ``` -#[proc_macro_derive(EntityEvent, attributes(entity_event))] +#[proc_macro_derive(EntityEvent, attributes(entity_event, event_entity))] pub fn derive_entity_event(input: TokenStream) -> TokenStream { component::derive_entity_event(input) } diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index 5035113557c48..3080cc35ceefb 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -11,7 +11,7 @@ use crate::{ change_detection::MaybeLocation, component::{Components, ComponentsRegistrator, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, - event::EntityComponents, + event::EntityComponentsTrigger, lifecycle::{Add, Insert, Replace, ADD, INSERT, REPLACE}, observer::Observers, query::DebugCheckedUnwrap as _, @@ -168,11 +168,8 @@ impl<'w> BundleInserter<'w> { if archetype.has_replace_observer() { deferred_world.trigger_raw( REPLACE, - &mut Replace, - EntityComponents { - entity, - components: &archetype_after_insert.existing, - }, + &mut Replace { entity }, + &mut EntityComponentsTrigger(&archetype_after_insert.existing), caller, ); } @@ -356,11 +353,8 @@ impl<'w> BundleInserter<'w> { if new_archetype.has_add_observer() { deferred_world.trigger_raw( ADD, - &mut Add, - EntityComponents { - entity, - components: &archetype_after_insert.added, - }, + &mut Add { entity }, + &mut EntityComponentsTrigger(&archetype_after_insert.added), caller, ); } @@ -377,15 +371,12 @@ impl<'w> BundleInserter<'w> { if new_archetype.has_insert_observer() { deferred_world.trigger_raw( INSERT, - &mut Insert, - EntityComponents { - entity, - // PERF: this is not a regression from what we were doing before, but ideally we don't - // need to collect here - components: &archetype_after_insert - .iter_inserted() - .collect::>(), - }, + &mut Insert { entity }, + // PERF: this is not a regression from what we were doing before, but ideally we don't + // need to collect here + &mut EntityComponentsTrigger( + &archetype_after_insert.iter_inserted().collect::>(), + ), caller, ); } @@ -403,11 +394,8 @@ impl<'w> BundleInserter<'w> { if new_archetype.has_insert_observer() { deferred_world.trigger_raw( INSERT, - &mut Insert, - EntityComponents { - entity, - components: &archetype_after_insert.added, - }, + &mut Insert { entity }, + &mut EntityComponentsTrigger(&archetype_after_insert.added), caller, ); } diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index 17fa0d05e68cf..ca0ea3ac3b83d 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -8,7 +8,7 @@ use crate::{ change_detection::MaybeLocation, component::{ComponentId, Components, ComponentsRegistrator, StorageType}, entity::{Entity, EntityLocation}, - event::EntityComponents, + event::EntityComponentsTrigger, lifecycle::{Remove, Replace, REMOVE, REPLACE}, observer::Observers, relationship::RelationshipHookMode, @@ -154,11 +154,8 @@ impl<'w> BundleRemover<'w> { let components = bundle_components_in_archetype().collect::>(); deferred_world.trigger_raw( REPLACE, - &mut Replace, - EntityComponents { - entity, - components: &components, - }, + &mut Replace { entity }, + &mut EntityComponentsTrigger(&components), caller, ); } @@ -173,11 +170,8 @@ impl<'w> BundleRemover<'w> { let components = bundle_components_in_archetype().collect::>(); deferred_world.trigger_raw( REMOVE, - &mut Remove, - EntityComponents { - entity, - components: &components, - }, + &mut Remove { entity }, + &mut EntityComponentsTrigger(&components), caller, ); } diff --git a/crates/bevy_ecs/src/bundle/spawner.rs b/crates/bevy_ecs/src/bundle/spawner.rs index 0d1fe226eb37b..90b2ddefe0a37 100644 --- a/crates/bevy_ecs/src/bundle/spawner.rs +++ b/crates/bevy_ecs/src/bundle/spawner.rs @@ -8,7 +8,7 @@ use crate::{ change_detection::MaybeLocation, component::{ComponentsRegistrator, Tick}, entity::{Entities, Entity, EntityLocation}, - event::EntityComponents, + event::EntityComponentsTrigger, lifecycle::{Add, Insert, ADD, INSERT}, relationship::RelationshipHookMode, storage::Table, @@ -138,11 +138,8 @@ impl<'w> BundleSpawner<'w> { if archetype.has_add_observer() { deferred_world.trigger_raw( ADD, - &mut Add, - EntityComponents { - entity, - components: bundle_info.contributed_components(), - }, + &mut Add { entity }, + &mut EntityComponentsTrigger(bundle_info.contributed_components()), caller, ); } @@ -156,11 +153,8 @@ impl<'w> BundleSpawner<'w> { if archetype.has_insert_observer() { deferred_world.trigger_raw( INSERT, - &mut Insert, - EntityComponents { - entity, - components: bundle_info.contributed_components(), - }, + &mut Insert { entity }, + &mut EntityComponentsTrigger(bundle_info.contributed_components()), caller, ); } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index afd2d33d83201..f7b3e44c99cb6 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -14,132 +14,6 @@ use core::{ marker::PhantomData, }; -/// An [`Event`] that can be targeted at specific entities. -/// -/// Entity events can be triggered on a [`World`] with specific entity targets using a method -/// 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 -/// based on the [`EntityEvent::Traversal`] type associated with the event. This enables use cases -/// such as bubbling events to parent entities for UI purposes. -/// -/// Entity events must be thread-safe. -/// -/// # Usage -/// -/// The [`EntityEvent`] trait can be derived. The `event` attribute can be used to further configure -/// the propagation behavior: adding `auto_propagate` sets [`EntityEvent::AUTO_PROPAGATE`] to `true`, -/// while adding `propagate = X` sets [`EntityEvent::Traversal`] to be of type `X`. -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # -/// // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. -/// #[derive(EntityEvent)] -/// #[entity_event(propagate, auto_propagate)] -/// struct Damage { -/// amount: f32, -/// } -/// ``` -/// -/// An [`Observer`] can then be added to listen for this event type for the desired entity: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # -/// # #[derive(EntityEvent)] -/// # #[entity_event(propagate, auto_propagate)] -/// # struct Damage { -/// # amount: f32, -/// # } -/// # -/// # #[derive(Component)] -/// # struct Health(f32); -/// # -/// # #[derive(Component)] -/// # struct Enemy; -/// # -/// # #[derive(Component)] -/// # struct ArmorPiece; -/// # -/// # let mut world = World::new(); -/// # -/// // Spawn an enemy entity. -/// let enemy = world.spawn((Enemy, Health(100.0))).id(); -/// -/// // Spawn some armor as a child of the enemy entity. -/// // When the armor takes damage, it will bubble the event up to the enemy, -/// // which can then handle the event with its own observer. -/// let armor_piece = world -/// .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) -/// .observe(|event: On, mut query: Query<&mut Health>| { -/// // Note: `On::entity` only exists because this is an `EntityEvent`. -/// let mut health = query.get_mut(event.entity()).unwrap(); -/// health.0 -= event.amount; -/// }) -/// .id(); -/// ``` -/// -/// The event can be triggered on the [`World`] using the [`trigger_targets`](World::trigger_targets) method, -/// providing the desired entity target(s): -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # -/// # #[derive(EntityEvent)] -/// # #[entity_event(propagate, auto_propagate)] -/// # struct Damage { -/// # amount: f32, -/// # } -/// # -/// # #[derive(Component)] -/// # struct Health(f32); -/// # -/// # #[derive(Component)] -/// # struct Enemy; -/// # -/// # #[derive(Component)] -/// # struct ArmorPiece; -/// # -/// # let mut world = World::new(); -/// # -/// # let enemy = world.spawn((Enemy, Health(100.0))).id(); -/// # let armor_piece = world -/// # .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) -/// # .observe(|event: On, mut query: Query<&mut Health>| { -/// # // Note: `On::entity` only exists because this is an `EntityEvent`. -/// # let mut health = query.get_mut(event.entity()).unwrap(); -/// # health.0 -= event.amount; -/// # }) -/// # .id(); -/// # -/// # world.flush(); -/// # -/// 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`", - note = "consider annotating `{Self}` with `#[derive(EntityEvent)]`" -)] -pub trait EntityEvent: - for<'a> Event = Entity, Trigger: Trigger = Entity>> -{ -} - -impl Event = Entity, Trigger: Trigger = Entity>>> EntityEvent - for E -{ -} - /// Something that "happens" and can be processed by app logic. /// /// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), @@ -222,9 +96,8 @@ impl Event = Entity, Trigger: Trigger = Entity> label = "invalid `Event`", note = "consider annotating `{Self}` with `#[derive(Event)]`" )] -pub trait Event: Send + Sync + 'static { - type Target<'a>; - type Trigger: for<'a> Trigger = Self::Target<'a>>; +pub trait Event: Send + Sync + Sized + 'static { + type Trigger<'a>: Trigger; /// Generates the [`EventKey`] for this event type. /// @@ -238,7 +111,10 @@ pub trait Event: Send + Sync + 'static { /// /// 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 { + fn register_event_key(world: &mut World) -> EventKey + where + Self: 'static, + { EventKey(world.register_component::>()) } @@ -345,7 +221,12 @@ pub trait BufferedEvent: Send + Sync + 'static {} /// This type is an implementation detail and should never be made public. // TODO: refactor events to store their metadata on distinct entities, rather than using `ComponentId` #[derive(Component)] -struct EventWrapperComponent(PhantomData); +struct EventWrapperComponent(PhantomData); + +pub trait EntityEvent: Event { + fn entity(&self) -> Entity; + fn entity_mut(&mut self) -> &mut Entity; +} /// An `EventId` uniquely identifies an event stored in a specific [`World`]. /// diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 9439f395e556b..98d72988c0d3a 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -1,39 +1,46 @@ use crate::{ component::ComponentId, entity::Entity, - event::Event, + event::{EntityEvent, Event}, observer::{CachedObservers, TriggerContext}, traversal::Traversal, world::DeferredWorld, }; -use bevy_ptr::{Ptr, PtrMut}; +use bevy_ptr::PtrMut; use core::marker::PhantomData; -pub trait Trigger: Default { - type Target<'a>; +pub trait Trigger { fn trigger( &mut self, world: DeferredWorld, observers: &CachedObservers, - event: PtrMut, - target: &Self::Target<'_>, trigger_context: &TriggerContext, + event: &mut E, ); } #[derive(Default)] pub struct GlobalTrigger; -impl Trigger for GlobalTrigger { - type Target<'a> = (); - +impl Trigger for GlobalTrigger { fn trigger( + &mut self, + world: DeferredWorld, + observers: &CachedObservers, + trigger_context: &TriggerContext, + event: &mut E, + ) { + self.trigger_internal(world, observers, trigger_context, event.into()); + } +} + +impl GlobalTrigger { + fn trigger_internal( &mut self, mut world: DeferredWorld, observers: &CachedObservers, - mut event: PtrMut, - target: &Self::Target<'_>, trigger_context: &TriggerContext, + mut event: PtrMut, ) { // SAFETY: there are no outstanding world references unsafe { @@ -45,7 +52,6 @@ impl Trigger for GlobalTrigger { *observer, trigger_context, event.reborrow(), - target.into(), self.into(), ); } @@ -55,35 +61,36 @@ impl Trigger for GlobalTrigger { #[derive(Default)] pub struct EntityTrigger; -impl Trigger for EntityTrigger { - type Target<'a> = Entity; +impl Trigger for EntityTrigger { fn trigger( &mut self, world: DeferredWorld, observers: &CachedObservers, - event: PtrMut, - target: &Self::Target<'_>, trigger_context: &TriggerContext, + event: &mut E, ) { - trigger_entity_raw( + let entity = event.entity(); + trigger_entity_internal( world, observers, - event, - target.into(), - target, + event.into(), self.into(), + entity, trigger_context, ); } } -fn trigger_entity_raw( +/// Trigger observers listening for the given entity event. +/// The `target_entity` should match the `EntityEvent::entity` on `event` for logical correctness. +// Note: this is not an EntityTrigger method because we want to reuse this logic for the entity propagation trigger +#[inline(never)] +pub fn trigger_entity_internal( mut world: DeferredWorld, observers: &CachedObservers, mut event: PtrMut, - target: Ptr, - target_entity: &Entity, mut trigger: PtrMut, + target_entity: Entity, trigger_context: &TriggerContext, ) { // SAFETY: there are no outstanding world references @@ -96,36 +103,30 @@ fn trigger_entity_raw( *observer, trigger_context, event.reborrow(), - target, trigger.reborrow(), ); } - if let Some(map) = observers.entity_observers().get(target_entity) { + if let Some(map) = observers.entity_observers().get(&target_entity) { for (observer, runner) in map { (runner)( world.reborrow(), *observer, trigger_context, event.reborrow(), - target, trigger.reborrow(), ); } } } -pub struct PropagateEntityTrigger< - const AUTO_PROPAGATE: bool, - E: for<'t> Event = Entity>, - T: Traversal, -> { +pub struct PropagateEntityTrigger> { pub original_entity: Entity, pub propagate: bool, _marker: PhantomData<(E, T)>, } -impl Event = Entity>, T: Traversal> Default +impl> Default for PropagateEntityTrigger { fn default() -> Self { @@ -137,53 +138,49 @@ impl Event = Entity>, T: Trave } } -impl Event = Entity>, T: Traversal> Trigger +impl> Trigger for PropagateEntityTrigger { - type Target<'a> = Entity; - fn trigger( &mut self, mut world: DeferredWorld, observers: &CachedObservers, - mut event: PtrMut, - target: &Self::Target<'_>, trigger_context: &TriggerContext, + event: &mut E, ) { - self.original_entity = *target; - trigger_entity_raw( + let mut current_entity = event.entity(); + self.original_entity = current_entity; + trigger_entity_internal( world.reborrow(), observers, - event.reborrow(), - target.into(), - target, + event.into(), self.into(), + current_entity, trigger_context, ); - let mut current_target = *target; loop { if !self.propagate { return; } - if let Ok(entity) = world.get_entity(current_target) + if let Ok(entity) = world.get_entity(current_entity) && let Some(item) = entity.get_components::() && let Some(traverse_to) = // TODO: Sort out the safety of this - T::traverse(item, unsafe { event.reborrow().deref_mut() }) + T::traverse(item, event) { - current_target = traverse_to; + current_entity = traverse_to; } else { break; } - trigger_entity_raw( + *event.entity_mut() = current_entity; + trigger_entity_internal( world.reborrow(), observers, - event.reborrow(), - (¤t_target).into(), - ¤t_target, + event.into(), self.into(), + current_entity, trigger_context, ); } @@ -191,36 +188,42 @@ impl Event = Entity>, T: Trave } #[derive(Default)] -pub struct EntityComponentsTrigger; +pub struct EntityComponentsTrigger<'a>(pub &'a [ComponentId]); -pub struct EntityComponents<'a> { - pub entity: Entity, - pub components: &'a [ComponentId], +impl<'a, E: EntityEvent> Trigger for EntityComponentsTrigger<'a> { + fn trigger( + &mut self, + world: DeferredWorld, + observers: &CachedObservers, + trigger_context: &TriggerContext, + event: &mut E, + ) { + let entity = event.entity(); + self.trigger_internal(world, observers, event.into(), entity, trigger_context); + } } -impl Trigger for EntityComponentsTrigger { - type Target<'a> = EntityComponents<'a>; - - fn trigger( +impl<'a> EntityComponentsTrigger<'a> { + #[inline(never)] + fn trigger_internal( &mut self, mut world: DeferredWorld, observers: &CachedObservers, mut event: PtrMut, - target: &Self::Target<'_>, + entity: Entity, trigger_context: &TriggerContext, ) { - trigger_entity_raw( + trigger_entity_internal( world.reborrow(), observers, event.reborrow(), - target.into(), - &target.entity, self.into(), + entity, trigger_context, ); // Trigger observers listening to this trigger targeting a specific component - for id in target.components { + for id in self.0 { if let Some(component_observers) = observers.component_observers().get(id) { for (observer, runner) in component_observers.global_observers() { (runner)( @@ -228,14 +231,13 @@ impl Trigger for EntityComponentsTrigger { *observer, trigger_context, event.reborrow(), - target.into(), self.into(), ); } if let Some(map) = component_observers .entity_component_observers() - .get(&target.entity) + .get(&entity) { for (observer, runner) in map { (runner)( @@ -243,7 +245,6 @@ impl Trigger for EntityComponentsTrigger { *observer, trigger_context, event.reborrow(), - target.into(), self.into(), ); } @@ -252,19 +253,3 @@ impl Trigger for EntityComponentsTrigger { } } } - -pub trait EntityTarget { - fn entity(&self) -> Entity; -} - -impl EntityTarget for Entity { - fn entity(&self) -> Entity { - *self - } -} - -impl<'a> EntityTarget for EntityComponents<'a> { - fn entity(&self) -> Entity { - self.entity - } -} diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index 2ad3208cc6aa2..c48deecae5ad0 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -54,8 +54,8 @@ use crate::{ component::{Component, ComponentId, ComponentIdFor, Tick}, entity::Entity, event::{ - BufferedEvent, EntityComponents, EntityComponentsTrigger, Event, EventCursor, EventId, - EventIterator, EventIteratorWithId, EventKey, Events, + BufferedEvent, EntityComponentsTrigger, EntityEvent, EventCursor, EventId, EventIterator, + EventIteratorWithId, EventKey, Events, }, query::FilteredAccessSet, relationship::RelationshipHookMode, @@ -328,29 +328,25 @@ pub const DESPAWN: EventKey = EventKey(ComponentId::new(4)); /// Trigger emitted when a component is inserted onto an entity that does not already have that /// component. Runs before `Insert`. /// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, EntityEvent)] +#[entity_event(trigger = EntityComponentsTrigger<'a>)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnAdd")] -pub struct Add; - -impl Event for Add { - type Target<'a> = EntityComponents<'a>; - type Trigger = EntityComponentsTrigger; +pub struct Add { + pub entity: Entity, } /// Trigger emitted when a component is inserted, regardless of whether or not the entity already /// had that component. Runs after `Add`, if it ran. /// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, EntityEvent)] +#[entity_event(trigger = EntityComponentsTrigger<'a>)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnInsert")] -pub struct Insert; - -impl Event for Insert { - type Target<'a> = EntityComponents<'a>; - type Trigger = EntityComponentsTrigger; +pub struct Insert { + pub entity: Entity, } /// Trigger emitted when a component is removed from an entity, regardless @@ -358,42 +354,36 @@ impl Event for Insert { /// /// Runs before the value is replaced, so you can still access the original component data. /// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, EntityEvent)] +#[entity_event(trigger = EntityComponentsTrigger<'a>)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnReplace")] -pub struct Replace; - -impl Event for Replace { - type Target<'a> = EntityComponents<'a>; - type Trigger = EntityComponentsTrigger; +pub struct Replace { + pub entity: Entity, } /// Trigger emitted when a component is removed from an entity, and runs before the component is /// removed, so you can still access the component data. /// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, EntityEvent)] +#[entity_event(trigger = EntityComponentsTrigger<'a>)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnRemove")] -pub struct Remove; - -impl Event for Remove { - type Target<'a> = EntityComponents<'a>; - type Trigger = EntityComponentsTrigger; +pub struct Remove { + pub entity: Entity, } /// Trigger emitted for each component on an entity when it is despawned. /// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, EntityEvent)] +#[entity_event(trigger = EntityComponentsTrigger<'a>)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnDespawn")] -pub struct Despawn; - -impl Event for Despawn { - type Target<'a> = EntityComponents<'a>; - type Trigger = EntityComponentsTrigger; +pub struct Despawn { + pub entity: Entity, } /// Deprecated in favor of [`Add`]. diff --git a/crates/bevy_ecs/src/observer/entity_cloning.rs b/crates/bevy_ecs/src/observer/entity_cloning.rs index 6c75bbd8a4320..7af2d2129c594 100644 --- a/crates/bevy_ecs/src/observer/entity_cloning.rs +++ b/crates/bevy_ecs/src/observer/entity_cloning.rs @@ -72,7 +72,11 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo #[cfg(test)] mod tests { use crate::{ - entity::EntityCloner, event::EntityEvent, observer::On, resource::Resource, system::ResMut, + entity::{Entity, EntityCloner}, + event::EntityEvent, + observer::On, + resource::Resource, + system::ResMut, world::World, }; @@ -80,7 +84,7 @@ mod tests { struct Num(usize); #[derive(EntityEvent)] - struct E; + struct E(Entity); #[test] fn clone_entity_with_observer() { @@ -93,14 +97,15 @@ mod tests { .id(); world.flush(); - world.trigger_targets(E, e); + world.trigger(E(e)); let e_clone = world.spawn_empty().id(); EntityCloner::build_opt_out(&mut world) .add_observers(true) .clone_entity(e, e_clone); - world.trigger_targets(E, [e, e_clone]); + world.trigger(E(e)); + world.trigger(E(e_clone)); assert_eq!(world.resource::().0, 3); } diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 625ab3ac959f3..3779b0832d3d5 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -134,13 +134,11 @@ mod distributed_storage; mod entity_cloning; mod runner; mod system_param; -mod trigger_targets; pub use centralized_storage::*; pub use distributed_storage::*; pub use runner::*; pub use system_param::*; -pub use trigger_targets::*; use crate::{ change_detection::MaybeLocation, @@ -192,57 +190,42 @@ impl World { /// 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<'a, E: Event = ()>>(&mut self, mut event: E) { - self.trigger_with_caller(&mut event, (), MaybeLocation::caller()); - } - - /// Triggers the given [`Event`] 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<'a, E: Event = ()>>(&mut self, event: &mut E) { - self.trigger_with_caller(event, (), MaybeLocation::caller()); + pub fn trigger<'a, E: Event: Default>>(&mut self, mut event: E) { + self.trigger_with_ref_caller( + &mut event, + &mut as Default>::default(), + MaybeLocation::caller(), + ); } - /// Triggers the given [`EntityEvent`] for the given `targets`, 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_targets_ref`] instead. #[track_caller] - pub fn trigger_targets<'a, E: Event>( - &mut self, - mut event: E, - targets: impl EventTargets>, - ) { - self.trigger_with_caller(&mut event, targets, MaybeLocation::caller()); + pub fn trigger_with<'a, E: Event>(&mut self, mut event: E, mut trigger: E::Trigger<'a>) { + self.trigger_with_ref_caller(&mut event, &mut trigger, MaybeLocation::caller()); } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, - /// which will run any [`Observer`]s watching for it. + /// Triggers the given [`Event`] as a mutable reference, which will run any [`Observer`]s watching for it. /// - /// Compared to [`World::trigger_targets`], this method is most useful when it's necessary to check + /// 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_targets_ref<'a, E: Event>( - &mut self, - event: &mut E, - targets: impl EventTargets>, - ) { - self.trigger_with_caller(event, targets, MaybeLocation::caller()); + pub fn trigger_ref<'a, E: Event: Default>>(&mut self, event: &mut E) { + self.trigger_with_ref_caller( + event, + &mut as Default>::default(), + MaybeLocation::caller(), + ); } - pub(crate) fn trigger_with_caller<'a, E: Event>( + pub(crate) fn trigger_with_ref_caller<'a, E: Event>( &mut self, event: &mut E, - targets: impl EventTargets>, + trigger: &mut E::Trigger<'a>, caller: MaybeLocation, ) { let event_key = E::register_event_key(self); // SAFETY: event_key was just registered and matches `event` unsafe { - DeferredWorld::from(self).trigger_raw(event_key, event, targets, caller); + DeferredWorld::from(self).trigger_raw(event_key, event, trigger, caller); } } @@ -384,7 +367,7 @@ mod tests { use crate::{ change_detection::MaybeLocation, entity_disabling::Internal, - event::{EntityComponents, EntityComponentsTrigger, Event}, + event::{EntityComponentsTrigger, Event, GlobalTrigger}, hierarchy::ChildOf, observer::{Observer, Replace}, prelude::*, @@ -405,14 +388,11 @@ mod tests { struct EventA; #[derive(EntityEvent)] - struct EntityEventA; - - struct EntityComponentsEvent; + struct EntityEventA(Entity); - impl Event for EntityComponentsEvent { - type Target<'a> = EntityComponents<'a>; - type Trigger = EntityComponentsTrigger; - } + #[derive(EntityEvent)] + #[entity_event(trigger = EntityComponentsTrigger<'a>)] + struct EntityComponentsEvent(Entity); #[derive(Event)] struct EventWithData { @@ -421,6 +401,7 @@ mod tests { #[derive(EntityEvent)] struct EntityEventWithData { + entity: Entity, counter: usize, } @@ -436,7 +417,7 @@ mod tests { #[derive(Component, EntityEvent)] #[entity_event(propagate, auto_propagate)] - struct EventPropagating; + struct EventPropagating(Entity); #[test] fn observer_order_spawn_despawn() { @@ -527,22 +508,22 @@ mod tests { let mut world = World::new(); world.init_resource::(); world.add_observer( - |obs: On, mut res: ResMut, mut commands: Commands| { + |add: On, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); - commands.entity(obs.entity()).insert(B); + commands.entity(add.entity).insert(B); }, ); world.add_observer( - |obs: On, mut res: ResMut, mut commands: Commands| { + |remove: On, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); - commands.entity(obs.entity()).remove::(); + commands.entity(remove.entity).remove::(); }, ); world.add_observer( - |obs: On, mut res: ResMut, mut commands: Commands| { + |add: On, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); - commands.entity(obs.entity()).remove::(); + commands.entity(add.entity).remove::(); }, ); world.add_observer(|_: On, mut res: ResMut| { @@ -695,12 +676,12 @@ mod tests { .spawn_empty() .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); - world.add_observer(move |obs: On, mut res: ResMut| { - assert_eq!(obs.entity(), entity); + world.add_observer(move |event: On, mut res: ResMut| { + assert_eq!(event.entity(), entity); res.observed("a_2"); }); - world.trigger_targets(EntityEventA, entity); + world.trigger(EntityEventA(entity)); assert_eq!(vec!["a_2", "a_1"], world.resource::().0); } @@ -744,49 +725,37 @@ mod tests { ); // trigger for an entity and a component - world.trigger_targets( - EntityComponentsEvent, - EntityComponents { - entity: entity_1, - components: &[component_a], - }, + world.trigger_with( + EntityComponentsEvent(entity_1), + EntityComponentsTrigger(&[component_a]), ); // 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( - EntityComponentsEvent, - [ - EntityComponents { - entity: entity_1, - components: &[], - }, - EntityComponents { - entity: entity_2, - components: &[], - }, - ], + world.trigger_with( + EntityComponentsEvent(entity_1), + EntityComponentsTrigger(&[]), + ); + world.trigger_with( + EntityComponentsEvent(entity_2), + EntityComponentsTrigger(&[]), ); + // 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 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( - EntityComponentsEvent, - [ - EntityComponents { - entity: entity_1, - components: &[component_a, component_b], - }, - EntityComponents { - entity: entity_2, - components: &[component_a, component_b], - }, - ], + world.trigger_with( + EntityComponentsEvent(entity_1), + EntityComponentsTrigger(&[component_a, component_b]), + ); + world.trigger_with( + EntityComponentsEvent(entity_2), + EntityComponentsTrigger(&[component_a, component_b]), ); assert_eq!(2222211, world.resource::().0); world.resource_mut::().0 = 0; @@ -821,7 +790,7 @@ mod tests { // SAFETY: we registered `event_a` above and it matches the type of EventA let observe = unsafe { Observer::with_dynamic_runner( - |mut world, _observer, _trigger_context, _event, _target, _trigger| { + |mut world, _observer, _trigger_context, _event, _trigger| { world.resource_mut::().observed("event_a"); }, ) @@ -835,10 +804,10 @@ mod tests { DeferredWorld::from(world).trigger_raw( event_a, &mut EventA, - (), + &mut GlobalTrigger, MaybeLocation::caller(), - ) - }; + ); + } }); world.flush(); assert_eq!(vec!["event_a"], world.resource::().0); @@ -869,7 +838,7 @@ mod tests { }, ); - world.trigger_targets(EventPropagating, child); + world.trigger(EventPropagating(child)); assert_eq!(vec!["child", "parent"], world.resource::().0); } @@ -893,7 +862,8 @@ mod tests { }) .id(); - world.trigger_targets(EventPropagating, [child, child]); + world.trigger(EventPropagating(child)); + world.trigger(EventPropagating(child)); assert_eq!( vec!["child", "parent", "child", "parent"], @@ -920,7 +890,9 @@ mod tests { }) .id(); - world.trigger_targets(EventPropagating, [child, parent]); + world.trigger(EventPropagating(child)); + world.trigger(EventPropagating(parent)); + assert_eq!( vec!["child", "parent", "parent"], world.resource::().0 @@ -947,7 +919,7 @@ mod tests { }) .id(); - world.trigger_targets(EventPropagating, child); + world.trigger(EventPropagating(child)); assert_eq!(vec!["child"], world.resource::().0); } @@ -978,7 +950,8 @@ mod tests { }) .id(); - world.trigger_targets(EventPropagating, [child_a, child_b]); + world.trigger(EventPropagating(child_a)); + world.trigger(EventPropagating(child_b)); assert_eq!( vec!["child_a", "parent", "child_b", "parent"], @@ -998,7 +971,7 @@ mod tests { }) .id(); - world.trigger_targets(EventPropagating, entity); + world.trigger(EventPropagating(entity)); assert_eq!(vec!["event"], world.resource::().0); } @@ -1036,7 +1009,8 @@ mod tests { }) .id(); - world.trigger_targets(EventPropagating, [child_a, child_b]); + world.trigger(EventPropagating(child_a)); + world.trigger(EventPropagating(child_b)); assert_eq!( vec!["child_a", "child_b", "parent_b"], @@ -1057,7 +1031,7 @@ mod tests { let parent = world.spawn(ChildOf(grandparent)).id(); let child = world.spawn(ChildOf(parent)).id(); - world.trigger_targets(EventPropagating, child); + world.trigger(EventPropagating(child)); assert_eq!(vec!["event", "event", "event"], world.resource::().0); } @@ -1079,7 +1053,7 @@ mod tests { let parent = world.spawn(ChildOf(grandparent)).id(); let child = world.spawn((A, ChildOf(parent))).id(); - world.trigger_targets(EventPropagating, child); + world.trigger(EventPropagating(child)); assert_eq!(vec!["event", "event"], world.resource::().0); } @@ -1087,9 +1061,9 @@ mod tests { // Originally for https://github.com/bevyengine/bevy/issues/18452 #[test] fn observer_modifies_relationship() { - fn on_add(event: On, mut commands: Commands) { + fn on_add(add: On, mut commands: Commands) { commands - .entity(event.entity()) + .entity(add.entity) .with_related_entities::(|rsc| { rsc.spawn_empty(); }); @@ -1200,9 +1174,11 @@ mod tests { }); world.spawn(observer.with_entities(entities.iter().copied().take(2))); - world.trigger_targets(EntityEventA, [entities[0], entities[1]]); + world.trigger(EntityEventA(entities[0])); + world.trigger(EntityEventA(entities[1])); assert_eq!(vec!["a", "a"], world.resource::().0); - world.trigger_targets(EntityEventA, [entities[2], entities[3]]); + world.trigger(EntityEventA(entities[2])); + world.trigger(EntityEventA(entities[3])); assert_eq!(vec!["a", "a"], world.resource::().0); } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 71963b0158ea1..dba3fb9150092 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -11,27 +11,20 @@ use crate::{ system::{ObserverSystem, RunSystemError}, world::DeferredWorld, }; -use bevy_ptr::{Ptr, PtrMut}; +use bevy_ptr::PtrMut; /// Type for function that is run when an observer is triggered. /// /// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component, /// but can be overridden for custom behavior. -pub type ObserverRunner = fn( - DeferredWorld, - observer: Entity, - &TriggerContext, - event: PtrMut, - target: Ptr, - trigger: PtrMut, -); +pub type ObserverRunner = + fn(DeferredWorld, observer: Entity, &TriggerContext, event: PtrMut, trigger: PtrMut); pub(super) fn observer_system_runner>( mut world: DeferredWorld, observer: Entity, trigger_context: &TriggerContext, event_ptr: PtrMut, - target_ptr: Ptr, trigger_ptr: PtrMut, ) { let world = world.as_unsafe_world_cell(); @@ -49,15 +42,12 @@ pub(super) fn observer_system_runner = unsafe { target_ptr.deref() }; + let trigger: &mut E::Trigger<'_> = unsafe { trigger_ptr.deref_mut() }; let on: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { event_ptr.deref_mut() }, observer, - target, trigger, trigger_context, ); diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index f42e9f16d4c63..d06ee34b2c711 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -11,7 +11,7 @@ use bevy_ptr::Ptr; use crate::{ bundle::Bundle, change_detection::MaybeLocation, - event::{EntityTarget, Event, PropagateEntityTrigger}, + event::{Event, PropagateEntityTrigger}, prelude::*, traversal::Traversal, }; @@ -31,10 +31,9 @@ use crate::{ /// matching component in the bundle, /// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). pub struct On<'w, E: Event, B: Bundle = ()> { - event: &'w mut E, observer: Entity, - target: &'w E::Target<'w>, - trigger: &'w mut E::Trigger, + event: &'w mut E, + trigger: &'w mut E::Trigger<'w>, trigger_context: &'w TriggerContext, _marker: PhantomData, } @@ -48,14 +47,12 @@ impl<'w, E: Event, B: Bundle> On<'w, E, B> { pub fn new( event: &'w mut E, observer: Entity, - target: &'w E::Target<'w>, - trigger: &'w mut E::Trigger, + trigger: &'w mut E::Trigger<'w>, trigger_context: &'w TriggerContext, ) -> Self { Self { event, observer, - target, trigger, trigger_context, _marker: PhantomData, @@ -83,15 +80,10 @@ impl<'w, E: Event, B: Bundle> On<'w, E, B> { } /// Returns the trigger context for this event. - pub fn trigger(&self) -> &E::Trigger { + pub fn trigger(&self) -> &E::Trigger<'w> { self.trigger } - /// Returns the target for this event. For entity events, consider using [`On::entity`]. - pub fn target(&self) -> &E::Target<'w> { - self.target - } - /// Returns the [`Entity`] that observed the triggered event. /// This allows you to despawn the observer, ceasing observation. /// @@ -122,22 +114,10 @@ impl<'w, E: Event, B: Bundle> On<'w, E, B> { } } -impl<'w, E: for<'t> Event: EntityTarget>, B: Bundle> On<'w, E, B> { - /// Returns the [`Entity`] that was targeted by the `event` 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_entity`]. - /// - /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. - pub fn entity(&self) -> Entity { - self.target.entity() - } -} - impl< 'w, const AUTO_PROPAGATE: bool, - E: for<'t> Event = Entity, Trigger = PropagateEntityTrigger>, + E: EntityEvent + for<'t> Event = PropagateEntityTrigger>, B: Bundle, T: Traversal, > On<'w, E, B> @@ -173,14 +153,11 @@ impl< } } -impl<'w, E: for<'t> Event: Debug> + Debug, B: Bundle> Debug - for On<'w, E, B> -{ +impl<'w, E: for<'t> Event: Debug> + Debug, B: Bundle> Debug for On<'w, E, B> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("On") .field("event", &self.event) .field("trigger", &self.trigger) - .field("target", &self.target) .field("_marker", &self._marker) .finish() } diff --git a/crates/bevy_ecs/src/observer/trigger_targets.rs b/crates/bevy_ecs/src/observer/trigger_targets.rs deleted file mode 100644 index 890f242e66465..0000000000000 --- a/crates/bevy_ecs/src/observer/trigger_targets.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Stores the [`TriggerTargets`] trait. - -use crate::{entity::Entity, event::EntityComponents}; -use alloc::vec::Vec; - -/// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. -/// -/// When an event is triggered with [`TriggerTargets`], any [`Observer`] that watches for that specific -/// event-target combination will run. -/// -/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. -/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, -/// allowing you to trigger events for multiple targets at once. -pub trait EventTargets: Send + Sync { - fn targets<'a>(&'a self) -> impl Iterator + 'a - where - T: 'a; -} - -impl + ?Sized, T: 'static> EventTargets for &L { - fn targets<'a>(&'a self) -> impl Iterator + 'a - where - T: 'a, - { - (**self).targets() - } -} - -impl EventTargets for Entity { - fn targets<'a>(&'a self) -> impl Iterator + 'a - where - Entity: 'a, - { - core::iter::once(self) - } -} - -impl<'a> EventTargets> for EntityComponents<'a> { - fn targets<'b>(&'b self) -> impl Iterator> + 'b - where - EntityComponents<'a>: 'b, - { - core::iter::once(self) - } -} - -impl EventTargets<()> for () { - fn targets<'a>(&'a self) -> impl Iterator + 'a - where - (): 'a, - { - core::iter::once(&()) - } -} - -impl EventTargets for Vec { - fn targets<'a>(&'a self) -> impl Iterator + 'a - where - T: 'a, - { - self.iter() - } -} - -impl EventTargets for [T; N] { - fn targets<'a>(&'a self) -> impl Iterator + 'a - where - T: 'a, - { - self.iter() - } -} - -impl EventTargets for [T] { - fn targets<'a>(&'a self) -> impl Iterator + 'a - where - T: 'a, - { - self.iter() - } -} diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 7cd539c7312e1..e9b7849441ece 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -905,11 +905,10 @@ mod tests { let result_entity = world.spawn(ObserverResult::default()).id(); world.add_observer( - move |event: On, + move |replace: On, has_relationship: Query>, mut results: Query<&mut ObserverResult>| { - let entity = event.entity(); - if has_relationship.get(entity).unwrap_or(false) { + if has_relationship.get(replace.entity).unwrap_or(false) { results.get_mut(result_entity).unwrap().success = true; } }, diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index 674d136d60389..78d4d77775111 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -10,7 +10,6 @@ use crate::{ entity::Entity, error::Result, event::{BufferedEvent, Event, Events}, - observer::EventTargets, resource::Resource, schedule::ScheduleLabel, system::{IntoSystem, SystemId, SystemInput}, @@ -210,22 +209,26 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { /// A [`Command`] that sends a global [`Event`] without any targets. #[track_caller] -pub fn trigger<'a>(mut event: impl Event = ()>) -> impl Command { +pub fn trigger<'a, E: Event: Default>>(mut event: E) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { - world.trigger_with_caller(&mut event, (), caller); + world.trigger_with_ref_caller( + &mut event, + &mut as Default>::default(), + caller, + ); } } -/// A [`Command`] that sends an [`EntityEvent`] for the given targets. +/// A [`Command`] that sends a global [`Event`] without any targets. #[track_caller] -pub fn trigger_targets<'a, E: Event>( +pub fn trigger_with: Send + Sync>>( mut event: E, - targets: impl EventTargets> + 'static, + mut trigger: E::Trigger<'static>, ) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { - world.trigger_with_caller(&mut event, targets, caller); + world.trigger_with_ref_caller(&mut event, &mut trigger, caller); } } diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 7499a5a11493b..f670d14b91abb 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -253,20 +253,6 @@ pub fn observe( } } -/// An [`EntityCommand`] that sends an [`EntityEvent`] targeting an entity. -/// -/// This will run any [`Observer`](crate::observer::Observer) of the given [`EntityEvent`] watching the entity. -#[track_caller] -pub fn trigger(mut event: impl EntityEvent) -> impl EntityCommand { - let caller = MaybeLocation::caller(); - move |mut entity: EntityWorldMut| { - let id = entity.id(); - entity.world_scope(|world| { - world.trigger_with_caller(&mut event, id, caller); - }); - } -} - /// An [`EntityCommand`] that clones parts of an entity onto another entity, /// configured through [`EntityClonerBuilder`]. /// diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index c55a813290cb0..6fccb6522087f 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -21,7 +21,7 @@ use crate::{ entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut}, error::{warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, event::{BufferedEvent, EntityEvent, Event}, - observer::{EventTargets, Observer}, + observer::Observer, resource::Resource, schedule::ScheduleLabel, system::{ @@ -1083,38 +1083,18 @@ 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. - /// - /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. - /// - /// If the entity that this command targets does not exist when the command is applied, - /// the command will fail, possibly causing it to panic based on the default [error handler](crate::error) set. - /// - /// To queue this command with a specific handler, use [`EntityCommands::queue_handled`] - /// with [`entity_command::trigger(event)`](entity_command::trigger). - /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] - pub fn trigger<'a>(&mut self, event: impl Event = ()>) { + pub fn trigger<'a>(&mut self, event: impl Event: Default>) { self.queue(command::trigger(event)); } - /// Sends an [`EntityEvent`] for the given targets. - /// - /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. - /// - /// If the entity that this command targets does not exist when the command is applied, - /// the command will fail, possibly causing it to panic based on the default [error handler](crate::error) set. - /// - /// To queue this command with a specific handler, use [`EntityCommands::queue_handled`] - /// with [`entity_command::trigger(event)`](entity_command::trigger). - /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] - pub fn trigger_targets<'a, E: Event>( + pub fn trigger_with: Send + Sync>>( &mut self, event: E, - targets: impl EventTargets> + 'static, + trigger: E::Trigger<'static>, ) { - self.queue(command::trigger_targets(event, targets)); + self.queue(command::trigger_with(event, trigger)); } /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated @@ -2005,18 +1985,10 @@ impl<'a> EntityCommands<'a> { &mut self.commands } - /// Sends an [`EntityEvent`] targeting the entity. - /// - /// This will run any [`Observer`] of the given [`EntityEvent`] watching this entity. - /// - /// If the entity that this command targets does not exist when the command is applied, - /// the command will fail, possibly causing it to panic based on the default error handler set. - /// To queue this command with a handler, use [`EntityCommands::queue_handled`] - /// with [`entity_command::trigger(event)`](entity_command::trigger). - /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] - pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { - self.queue(entity_command::trigger(event)) + pub fn trigger<'t>(&mut self, event: impl Event: Default>) -> &mut Self { + self.commands_mut().queue(command::trigger(event)); + self } /// Creates an [`Observer`] listening for events of type `E` targeting this entity. diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index f855a470e4a89..7c89a944a3a54 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -8,10 +8,11 @@ use crate::{ component::{ComponentId, Mutable}, entity::Entity, event::{ - BufferedEvent, EntityComponents, Event, EventId, EventKey, Events, Trigger, WriteBatchIds, + BufferedEvent, EntityComponentsTrigger, Event, EventId, EventKey, Events, Trigger, + WriteBatchIds, }, lifecycle::{HookContext, Insert, Replace, INSERT, REPLACE}, - observer::{EventTargets, TriggerContext}, + observer::TriggerContext, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, relationship::RelationshipHookMode, @@ -170,11 +171,8 @@ impl<'w> DeferredWorld<'w> { if archetype.has_replace_observer() { self.trigger_raw( REPLACE, - &mut Replace, - EntityComponents { - entity, - components: &[component_id], - }, + &mut Replace { entity }, + &mut EntityComponentsTrigger(&[component_id]), MaybeLocation::caller(), ); } @@ -213,11 +211,8 @@ impl<'w> DeferredWorld<'w> { if archetype.has_insert_observer() { self.trigger_raw( INSERT, - &mut Insert, - EntityComponents { - entity, - components: &[component_id], - }, + &mut Insert { entity }, + &mut EntityComponentsTrigger(&[component_id]), MaybeLocation::caller(), ); } @@ -794,7 +789,7 @@ impl<'w> DeferredWorld<'w> { &mut self, event_key: EventKey, event: &mut E, - targets: impl EventTargets>, + trigger: &mut E::Trigger<'a>, caller: MaybeLocation, ) { // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` @@ -808,10 +803,7 @@ impl<'w> DeferredWorld<'w> { (world.into_deferred(), observers) }; let context = TriggerContext { event_key, caller }; - for target in targets.targets() { - let mut trigger = ::default(); - trigger.trigger(world.reborrow(), observers, event.into(), target, &context); - } + trigger.trigger(world.reborrow(), observers, &context, event); } /// Sends a global [`Event`] without any targets. @@ -819,25 +811,10 @@ impl<'w> DeferredWorld<'w> { /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. /// /// [`Observer`]: crate::observer::Observer - pub fn trigger<'a>(&mut self, trigger: impl Event = ()>) { - // TODO: can we just use trigger_raw here? + pub fn trigger<'a>(&mut self, trigger: impl Event: Default>) { self.commands().trigger(trigger); } - /// Sends an [`EntityEvent`] with the given `targets` - /// - /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. - /// - /// [`Observer`]: crate::observer::Observer - pub fn trigger_targets<'a, E: Event>( - &mut self, - trigger: E, - targets: impl EventTargets> + 'static, - ) { - // TODO: can we just use trigger_raw here? - self.commands().trigger_targets(trigger, targets); - } - /// Gets an [`UnsafeWorldCell`] containing the underlying world. /// /// # Safety diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 661c50d6f8a6e..257f5bcb49540 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -13,7 +13,7 @@ use crate::{ ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityIdLocation, EntityLocation, OptIn, OptOut, }, - event::{EntityComponents, EntityEvent, EntityTarget, Event}, + event::{EntityComponentsTrigger, EntityEvent, Event}, lifecycle::{Despawn, Remove, Replace, DESPAWN, REMOVE, REPLACE}, observer::Observer, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, @@ -2025,6 +2025,28 @@ impl<'w> EntityWorldMut<'w> { ) } + #[track_caller] + pub fn trigger<'a, E: Event: Default>>(&mut self, mut event: E) { + self.world_scope(|world| { + world.trigger_with_ref_caller( + &mut event, + &mut as Default>::default(), + MaybeLocation::caller(), + ); + }); + } + + #[track_caller] + pub fn trigger_with<'a, E: Event: Send + Sync>>( + &mut self, + mut event: E, + mut trigger: E::Trigger<'a>, + ) { + self.world_scope(|world| { + world.trigger_with_ref_caller(&mut event, &mut trigger, MaybeLocation::caller()); + }); + } + /// Split into a new function so we can pass the calling location into the function when using /// as a command. #[inline] @@ -2579,11 +2601,10 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_despawn_observer() { deferred_world.trigger_raw( DESPAWN, - &mut Despawn, - EntityComponents { + &mut Despawn { entity: self.entity, - components: archetype.components(), }, + &mut EntityComponentsTrigger(archetype.components()), caller, ); } @@ -2596,11 +2617,10 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_replace_observer() { deferred_world.trigger_raw( REPLACE, - &mut Replace, - EntityComponents { + &mut Replace { entity: self.entity, - components: archetype.components(), }, + &mut EntityComponentsTrigger(archetype.components()), caller, ); } @@ -2614,11 +2634,10 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_remove_observer() { deferred_world.trigger_raw( REMOVE, - &mut Remove, - EntityComponents { + &mut Remove { entity: self.entity, - components: archetype.components(), }, + &mut EntityComponentsTrigger(archetype.components()), caller, ); } @@ -2840,19 +2859,6 @@ impl<'w> EntityWorldMut<'w> { } } - /// Triggers the given `event` for this entity, which will run any observers watching for it. - /// - /// # Panics - /// - /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { - self.assert_not_despawned(); - self.world.trigger_targets(event, self.entity); - self.world.flush(); - self.update_location(); - self - } - /// 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. /// @@ -2862,14 +2868,14 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the given system is an exclusive system. #[track_caller] - pub fn observe<'a, E: Event: EntityTarget>, B: Bundle, M>( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { self.observe_with_caller(observer, MaybeLocation::caller()) } - pub(crate) fn observe_with_caller<'a, E: Event: EntityTarget>, B: Bundle, M>( + pub(crate) fn observe_with_caller( &mut self, observer: impl IntoObserverSystem, caller: MaybeLocation, @@ -6084,7 +6090,7 @@ mod tests { } #[derive(EntityEvent)] - struct TestEvent; + struct TestEvent(Entity); #[test] fn adding_observer_updates_location() { @@ -6100,7 +6106,9 @@ mod tests { world.flush(); let mut a = world.entity_mut(entity); - a.trigger(TestEvent); // this adds command to change entity archetype + // SAFETY: this _intentionally_ doesn't update the location, to ensure that we're actually testing + // that observe() updates location + unsafe { a.world_mut().trigger(TestEvent(entity)) } a.observe(|_: On| {}); // this flushes commands implicitly by spawning let location = a.location(); assert_eq!(world.entities().get(entity), Some(location)); @@ -6110,8 +6118,8 @@ mod tests { #[should_panic] fn location_on_despawned_entity_panics() { let mut world = World::new(); - world.add_observer(|event: On, mut commands: Commands| { - commands.entity(event.entity()).despawn(); + world.add_observer(|add: On, mut commands: Commands| { + commands.entity(add.entity).despawn(); }); let entity = world.spawn_empty().id(); let mut a = world.entity_mut(entity); @@ -6140,8 +6148,8 @@ mod tests { let entity = world.spawn_empty().id(); assert_eq!(world.resource::().0, 1); world.commands().queue(count_flush); + world.flush_commands(); let mut a = world.entity_mut(entity); - a.trigger(TestEvent); assert_eq!(a.world().resource::().0, 2); a.insert(TestComponent(0)); assert_eq!(a.world().resource::().0, 3); diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index d04c28a5dfe10..d2581f8279cb6 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -79,28 +79,26 @@ pub enum ResourceFetchError { mod tests { use crate::{ prelude::*, - system::{entity_command::trigger, RunSystemOnce}, + system::{command::trigger, RunSystemOnce}, }; // Inspired by https://github.com/bevyengine/bevy/issues/19623 #[test] fn fixing_panicking_entity_commands() { #[derive(EntityEvent)] - struct Kill; + struct Kill(Entity); #[derive(EntityEvent)] - struct FollowupEvent; + struct FollowupEvent(Entity); fn despawn(event: On, mut commands: Commands) { commands.entity(event.entity()).despawn(); } - fn followup(on: On, mut commands: Commands) { + fn followup(event: On, mut commands: Commands) { // When using a simple .trigger() here, this panics because the entity has already been despawned. // Instead, we need to use `.queue_handled` or `.queue_silenced` to avoid the panic. - commands - .entity(on.entity()) - .queue_silenced(trigger(FollowupEvent)); + commands.queue_silenced(trigger(FollowupEvent(event.entity()))); } let mut world = World::new(); @@ -115,7 +113,7 @@ mod tests { // Trigger a kill event on the entity fn kill_everything(mut commands: Commands, query: Query) { for id in query.iter() { - commands.entity(id).trigger(Kill); + commands.trigger(Kill(id)); } } world.run_system_once(kill_everything).unwrap(); diff --git a/crates/bevy_feathers/src/alpha_pattern.rs b/crates/bevy_feathers/src/alpha_pattern.rs index 1a2732a2e57e8..754a063456483 100644 --- a/crates/bevy_feathers/src/alpha_pattern.rs +++ b/crates/bevy_feathers/src/alpha_pattern.rs @@ -47,7 +47,7 @@ fn on_add_alpha_pattern( mut q_material_node: Query<&mut MaterialNode>, r_material: Res, ) { - if let Ok(mut material) = q_material_node.get_mut(ev.entity()) { + if let Ok(mut material) = q_material_node.get_mut(ev.entity) { material.0 = r_material.0.clone(); } } diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs index 96e76c5787ca3..da90abd9ea911 100644 --- a/crates/bevy_feathers/src/font_styles.rs +++ b/crates/bevy_feathers/src/font_styles.rs @@ -50,13 +50,13 @@ pub(crate) fn on_changed_font( assets: Res, mut commands: Commands, ) { - if let Ok(style) = font_style.get(ev.entity()) + if let Ok(style) = font_style.get(ev.entity) && let Some(font) = match style.font { HandleOrPath::Handle(ref h) => Some(h.clone()), HandleOrPath::Path(ref p) => Some(assets.load::(p)), } { - commands.entity(ev.entity()).insert(Propagate(TextFont { + commands.entity(ev.entity).insert(Propagate(TextFont { font, font_size: style.font_size, ..Default::default() diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs index 1fd095b764c8e..81fc652dc8379 100644 --- a/crates/bevy_feathers/src/theme.rs +++ b/crates/bevy_feathers/src/theme.rs @@ -109,7 +109,7 @@ pub(crate) fn on_changed_background( theme: Res, ) { // Update background colors where the design token has changed. - if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.entity()) { + if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.entity) { bg.0 = theme.color(theme_bg.0); } } @@ -120,7 +120,7 @@ pub(crate) fn on_changed_border( theme: Res, ) { // Update background colors where the design token has changed. - if let Ok((mut border, theme_border)) = q_border.get_mut(ev.entity()) { + if let Ok((mut border, theme_border)) = q_border.get_mut(ev.entity) { border.set_all(theme.color(theme_border.0)); } } @@ -133,10 +133,10 @@ pub(crate) fn on_changed_font_color( theme: Res, mut commands: Commands, ) { - if let Ok(token) = font_color.get(ev.entity()) { + if let Ok(token) = font_color.get(ev.entity) { let color = theme.color(token.0); commands - .entity(ev.entity()) + .entity(ev.entity) .insert(Propagate(TextColor(color))); } } diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 2101564b208f5..7ceb61827fffc 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -143,6 +143,8 @@ pub struct InputFocusVisible(pub bool); #[entity_event(propagate = WindowTraversal, auto_propagate)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] pub struct FocusedInput { + #[event_entity] + pub focused_entity: Entity, /// The underlying input event. pub input: E, /// The primary window entity. @@ -154,6 +156,8 @@ pub struct FocusedInput { #[derive(Clone, EntityEvent)] #[entity_event(propagate = WindowTraversal, auto_propagate)] pub struct AcquireFocus { + #[event_entity] + pub focused_entity: Entity, /// The primary window entity. window: Entity, } @@ -265,38 +269,32 @@ pub fn dispatch_focused_input( // Check if the focused entity is still alive if entities.contains(focused_entity) { for ev in key_events.read() { - commands.trigger_targets( - FocusedInput { - input: ev.clone(), - window, - }, + commands.trigger(FocusedInput { focused_entity, - ); + input: ev.clone(), + window, + }); } } else { // If the focused entity no longer exists, clear focus and dispatch to window focus.0 = None; for ev in key_events.read() { - commands.trigger_targets( - FocusedInput { - input: ev.clone(), - window, - }, + commands.trigger(FocusedInput { + focused_entity: window, + input: ev.clone(), window, - ); + }); } } } else { // If no element has input focus, then dispatch the input event to the primary window. // There should be only one primary window. for ev in key_events.read() { - commands.trigger_targets( - FocusedInput { - input: ev.clone(), - window, - }, + commands.trigger(FocusedInput { + focused_entity: window, + input: ev.clone(), window, - ); + }); } } } @@ -422,7 +420,7 @@ mod tests { event: On>, mut query: Query<&mut GatherKeyboardEvents>, ) { - if let Ok(mut gather) = query.get_mut(event.entity()) { + if let Ok(mut gather) = query.get_mut(event.focused_entity) { if let Key::Character(c) = &event.input.logical_key { gather.0.push_str(c.as_str()); } diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 2599054313b65..86ae48b973d55 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -29,6 +29,7 @@ use bevy_app::{App, Plugin, Startup}; use bevy_ecs::{ component::Component, entity::Entity, + event::EntityEvent, hierarchy::{ChildOf, Children}, observer::On, query::{With, Without}, @@ -322,14 +323,14 @@ pub(crate) fn acquire_focus( mut focus: ResMut, ) { // If the entity has a TabIndex - if focusable.contains(ev.entity()) { + if focusable.contains(ev.focused_entity) { // Stop and focus it ev.propagate(false); // Don't mutate unless we need to, for change detection - if focus.0 != Some(ev.entity()) { - focus.0 = Some(ev.entity()); + if focus.0 != Some(ev.focused_entity) { + focus.0 = Some(ev.focused_entity); } - } else if windows.contains(ev.entity()) { + } else if windows.contains(ev.focused_entity) { // Stop and clear focus ev.propagate(false); // Don't mutate unless we need to, for change detection @@ -357,7 +358,7 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + press: On>, mut focus_visible: ResMut, windows: Query>, mut commands: Commands, @@ -366,16 +367,17 @@ fn click_to_focus( // for every ancestor, but only for the original entity. Also, users may want to stop // propagation on the pointer event at some point along the bubbling chain, so we need our // own dedicated event whose propagation we can control. - if ev.entity() == ev.original_entity() { + if press.entity == press.original_entity() { // Clicking hides focus if focus_visible.0 { focus_visible.0 = false; } // Search for a focusable parent entity, defaulting to window if none. if let Ok(window) = windows.single() { - commands - .entity(ev.entity()) - .trigger(AcquireFocus { window }); + commands.trigger(AcquireFocus { + focused_entity: press.entity, + window, + }); } } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 155f2b2f2b9e9..8bce0f4556bb6 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -614,30 +614,30 @@ pub struct LightViewEntities(EntityHashMap>); // TODO: using required component pub(crate) fn add_light_view_entities( - event: On, + add: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(event.entity()) { + if let Ok(mut v) = commands.get_entity(add.entity) { v.insert(LightViewEntities::default()); } } /// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. pub(crate) fn extracted_light_removed( - event: On, + remove: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(event.entity()) { + if let Ok(mut v) = commands.get_entity(remove.entity) { v.try_remove::(); } } pub(crate) fn remove_light_view_entities( - event: On, + remove: On, query: Query<&LightViewEntities>, mut commands: Commands, ) { - if let Ok(entities) = query.get(event.entity()) { + if let Ok(entities) = query.get(remove.entity) { for v in entities.0.values() { for e in v.iter().copied() { if let Ok(mut v) = commands.get_entity(e) { diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 5373b5ca7d031..229d606131b15 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -63,6 +63,8 @@ use crate::{ #[entity_event(propagate = PointerTraversal, auto_propagate)] #[reflect(Component, Debug, Clone)] pub struct Pointer { + /// The entity this pointer event happened for. + pub entity: Entity, /// The pointer that triggered this event pub pointer_id: PointerId, /// The location of the pointer during this event @@ -124,11 +126,12 @@ impl core::ops::Deref for Pointer { impl Pointer { /// Construct a new `Pointer` event. - pub fn new(id: PointerId, location: Location, event: E) -> Self { + pub fn new(id: PointerId, location: Location, event: E, entity: Entity) -> Self { Self { pointer_id: id, pointer_location: location, event, + entity, } } } @@ -494,8 +497,13 @@ pub fn pointer_events( }; // Always send Out events - let out_event = Pointer::new(pointer_id, location.clone(), Out { hit: hit.clone() }); - commands.trigger_targets(out_event.clone(), hovered_entity); + let out_event = Pointer::new( + pointer_id, + location.clone(), + Out { hit: hit.clone() }, + hovered_entity, + ); + commands.trigger(out_event.clone()); event_writers.out_events.write(out_event); // Possibly send DragLeave events @@ -511,8 +519,9 @@ pub fn pointer_events( dragged: *drag_target, hit: hit.clone(), }, + hovered_entity, ); - commands.trigger_targets(drag_leave_event.clone(), hovered_entity); + commands.trigger(drag_leave_event.clone()); event_writers.drag_leave_events.write(drag_leave_event); } } @@ -552,15 +561,21 @@ pub fn pointer_events( dragged: *drag_target, hit: hit.clone(), }, + hovered_entity, ); - commands.trigger_targets(drag_enter_event.clone(), hovered_entity); + commands.trigger(drag_enter_event.clone()); event_writers.drag_enter_events.write(drag_enter_event); } } // Always send Over events - let over_event = Pointer::new(pointer_id, location.clone(), Over { hit: hit.clone() }); - commands.trigger_targets(over_event.clone(), hovered_entity); + let over_event = Pointer::new( + pointer_id, + location.clone(), + Over { hit: hit.clone() }, + hovered_entity, + ); + commands.trigger(over_event.clone()); event_writers.over_events.write(over_event); } } @@ -589,8 +604,9 @@ pub fn pointer_events( button, hit: hit.clone(), }, + hovered_entity, ); - commands.trigger_targets(pressed_event.clone(), hovered_entity); + commands.trigger(pressed_event.clone()); event_writers.pressed_events.write(pressed_event); // Also insert the press into the state state @@ -617,8 +633,9 @@ pub fn pointer_events( hit: hit.clone(), duration: now - *press_instant, }, + hovered_entity, ); - commands.trigger_targets(click_event.clone(), hovered_entity); + commands.trigger(click_event.clone()); event_writers.click_events.write(click_event); } // Always send the Release event @@ -629,8 +646,9 @@ pub fn pointer_events( button, hit: hit.clone(), }, + hovered_entity, ); - commands.trigger_targets(released_event.clone(), hovered_entity); + commands.trigger(released_event.clone()); event_writers.released_events.write(released_event); } @@ -646,8 +664,9 @@ pub fn pointer_events( dropped: drag_target, hit: hit.clone(), }, + *dragged_over, ); - commands.trigger_targets(drag_drop_event.clone(), *dragged_over); + commands.trigger(drag_drop_event.clone()); event_writers.drag_drop_events.write(drag_drop_event); } // Emit DragEnd @@ -658,8 +677,9 @@ pub fn pointer_events( button, distance: drag.latest_pos - drag.start_pos, }, + drag_target, ); - commands.trigger_targets(drag_end_event.clone(), drag_target); + commands.trigger(drag_end_event.clone()); event_writers.drag_end_events.write(drag_end_event); // Emit DragLeave for (dragged_over, hit) in state.dragging_over.iter() { @@ -671,8 +691,9 @@ pub fn pointer_events( dragged: drag_target, hit: hit.clone(), }, + *dragged_over, ); - commands.trigger_targets(drag_leave_event.clone(), *dragged_over); + commands.trigger(drag_leave_event.clone()); event_writers.drag_leave_events.write(drag_leave_event); } } @@ -710,8 +731,9 @@ pub fn pointer_events( button, hit: hit.clone(), }, + *press_target, ); - commands.trigger_targets(drag_start_event.clone(), *press_target); + commands.trigger(drag_start_event.clone()); event_writers.drag_start_events.write(drag_start_event); } @@ -729,8 +751,9 @@ pub fn pointer_events( distance: location.position - drag.start_pos, delta, }, + *drag_target, ); - commands.trigger_targets(drag_event.clone(), *drag_target); + commands.trigger(drag_event.clone()); event_writers.drag_events.write(drag_event); // Update drag position @@ -751,8 +774,9 @@ pub fn pointer_events( dragged: *drag_target, hit: hit.clone(), }, + hovered_entity, ); - commands.trigger_targets(drag_over_event.clone(), hovered_entity); + commands.trigger(drag_over_event.clone()); event_writers.drag_over_events.write(drag_over_event); } } @@ -771,8 +795,9 @@ pub fn pointer_events( hit: hit.clone(), delta, }, + hovered_entity, ); - commands.trigger_targets(move_event.clone(), hovered_entity); + commands.trigger(move_event.clone()); event_writers.move_events.write(move_event); } } @@ -792,8 +817,9 @@ pub fn pointer_events( y, hit: hit.clone(), }, + hovered_entity, ); - commands.trigger_targets(scroll_event.clone(), hovered_entity); + commands.trigger(scroll_event.clone()); event_writers.scroll_events.write(scroll_event); } } @@ -805,8 +831,9 @@ pub fn pointer_events( .iter() .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) { - let cancel_event = Pointer::new(pointer_id, location.clone(), Cancel { hit }); - commands.trigger_targets(cancel_event.clone(), hovered_entity); + let cancel_event = + Pointer::new(pointer_id, location.clone(), Cancel { hit }, hovered_entity); + commands.trigger(cancel_event.clone()); event_writers.cancel_events.write(cancel_event); } // Clear the state for the canceled pointer diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index 38934e274eabd..a83ff654a31f2 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -113,13 +113,17 @@ impl Readback { /// requested buffer or texture. #[derive(EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] -pub struct ReadbackComplete(pub Vec); +pub struct ReadbackComplete { + pub entity: Entity, + #[deref] + pub data: Vec, +} impl ReadbackComplete { /// Convert the raw bytes of the event to a shader type. pub fn to_shader_type(&self) -> T { let mut val = T::default(); - let mut reader = Reader::new::(&self.0, 0).expect("Failed to create Reader"); + let mut reader = Reader::new::(&self.data, 0).expect("Failed to create Reader"); T::read_from(&mut val, &mut reader); val } @@ -234,8 +238,8 @@ fn sync_readbacks( max_unused_frames: Res, ) { readbacks.mapped.retain(|readback| { - if let Ok((entity, buffer, result)) = readback.rx.try_recv() { - main_world.trigger_targets(ReadbackComplete(result), entity); + if let Ok((entity, buffer, data)) = readback.rx.try_recv() { + main_world.trigger(ReadbackComplete { data, entity }); buffer_pool.return_buffer(&buffer); false } else { diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index f5cdff8fba38b..8c45666378003 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,10 +1,10 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::entity::EntityHash; -use bevy_ecs::lifecycle::{Add, Remove}; use bevy_ecs::{ component::Component, - entity::{ContainsEntity, Entity, EntityEquivalent}, + entity::{ContainsEntity, Entity, EntityEquivalent, EntityHash}, + event::EntityEvent, + lifecycle::{Add, Remove}, observer::On, query::With, reflect::ReflectComponent, @@ -94,15 +94,15 @@ impl Plugin for SyncWorldPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); app.add_observer( - |event: On, mut pending: ResMut| { - pending.push(EntityRecord::Added(event.entity())); + |add: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(add.entity)); }, ); app.add_observer( - |event: On, + |remove: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(event.entity()) { + if let Ok(e) = query.get(remove.entity) { pending.push(EntityRecord::Removed(*e)); }; }, @@ -504,6 +504,7 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, + event::EntityEvent, lifecycle::{Add, Remove}, observer::On, query::With, @@ -526,15 +527,15 @@ mod tests { main_world.init_resource::(); main_world.add_observer( - |event: On, mut pending: ResMut| { - pending.push(EntityRecord::Added(event.entity())); + |add: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(add.entity)); }, ); main_world.add_observer( - |event: On, + |remove: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(event.entity()) { + if let Ok(e) = query.get(remove.entity) { pending.push(EntityRecord::Removed(*e)); }; }, diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index b613063a9532c..646fd01bb7523 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -39,9 +39,13 @@ use std::{ use tracing::{error, info, warn}; use wgpu::{CommandEncoder, Extent3d, TextureFormat}; -#[derive(EntityEvent, Deref, DerefMut, Reflect, Debug)] +#[derive(EntityEvent, Reflect, Deref, DerefMut, Debug)] #[reflect(Debug)] -pub struct ScreenshotCaptured(pub Image); +pub struct ScreenshotCaptured { + pub entity: Entity, + #[deref] + pub image: Image, +} /// A component that signals to the renderer to capture a screenshot this frame. /// @@ -125,7 +129,7 @@ struct RenderScreenshotsSender(Sender<(Entity, Image)>); pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On) { let path = path.as_ref().to_owned(); move |event| { - let img = event.0.clone(); + let img = event.image.clone(); match img.try_into_dynamic() { Ok(dyn_img) => match image::ImageFormat::from_path(&path) { Ok(format) => { @@ -196,7 +200,7 @@ pub fn trigger_screenshots( let captured_screenshots = captured_screenshots.lock().unwrap(); while let Ok((entity, image)) = captured_screenshots.try_recv() { commands.entity(entity).insert(Captured); - commands.trigger_targets(ScreenshotCaptured(image), entity); + commands.trigger(ScreenshotCaptured { image, entity }); } } diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 71d56a18102a1..d3f4e96ff55b1 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -30,6 +30,7 @@ use bevy_ecs::{ #[derive(Clone, Copy, Debug, Eq, PartialEq, EntityEvent, Reflect)] #[reflect(Debug, PartialEq, Clone)] pub struct SceneInstanceReady { + pub entity: Entity, /// Instance which has been spawned. pub instance_id: InstanceId, } @@ -506,16 +507,18 @@ impl SceneSpawner { for (instance_id, parent) in self.instances_ready.drain(..) { if let Some(parent) = parent { // Defer via commands otherwise SceneSpawner is not available in the observer. - world - .commands() - .trigger_targets(SceneInstanceReady { instance_id }, parent); + world.commands().trigger(SceneInstanceReady { + instance_id, + entity: parent, + }); } else { // Defer via commands otherwise SceneSpawner is not available in the observer. // TODO: triggering this for PLACEHOLDER is suboptimal, but this scene system is on // its way out, so lets avoid breaking people by making a second event. - world - .commands() - .trigger_targets(SceneInstanceReady { instance_id }, Entity::PLACEHOLDER); + world.commands().trigger(SceneInstanceReady { + instance_id, + entity: Entity::PLACEHOLDER, + }); } } } diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index 88df5eebb9681..693fa3eef225c 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -2,6 +2,7 @@ use bevy_a11y::AccessibilityNode; use bevy_ecs::{ component::Component, + event::EntityEvent, lifecycle::{Add, Remove}, observer::On, world::DeferredWorld, @@ -18,15 +19,18 @@ use bevy_ecs::{ #[derive(Component, Debug, Clone, Copy, Default)] pub struct InteractionDisabled; -pub(crate) fn on_add_disabled(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn on_add_disabled(add: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(add.entity); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_disabled(); } } -pub(crate) fn on_remove_disabled(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn on_remove_disabled( + remove: On, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(remove.entity); if let Some(mut accessibility) = entity.get_mut::() { accessibility.clear_disabled(); } @@ -45,8 +49,8 @@ pub struct Checkable; #[derive(Component, Default, Debug)] pub struct Checked; -pub(crate) fn on_add_checkable(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn on_add_checkable(add: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(add.entity); let checked = entity.get::().is_some(); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(match checked { @@ -56,23 +60,23 @@ pub(crate) fn on_add_checkable(event: On, mut world: DeferredWorld } } -pub(crate) fn on_remove_checkable(event: On, mut world: DeferredWorld) { +pub(crate) fn on_remove_checkable(add: On, mut world: DeferredWorld) { // Remove the 'toggled' attribute entirely. - let mut entity = world.entity_mut(event.entity()); + let mut entity = world.entity_mut(add.entity); if let Some(mut accessibility) = entity.get_mut::() { accessibility.clear_toggled(); } } -pub(crate) fn on_add_checked(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn on_add_checked(add: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(add.entity); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(accesskit::Toggled::True); } } -pub(crate) fn on_remove_checked(event: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(event.entity()); +pub(crate) fn on_remove_checked(remove: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(remove.entity); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(accesskit::Toggled::False); } diff --git a/crates/bevy_winit/src/cursor/mod.rs b/crates/bevy_winit/src/cursor/mod.rs index c946a5160fefb..d857111ba43b7 100644 --- a/crates/bevy_winit/src/cursor/mod.rs +++ b/crates/bevy_winit/src/cursor/mod.rs @@ -222,10 +222,10 @@ fn update_cursors( } /// Resets the cursor to the default icon when `CursorIcon` is removed. -fn on_remove_cursor_icon(event: On, mut commands: Commands) { +fn on_remove_cursor_icon(remove: On, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands - .entity(event.entity()) + .entity(remove.entity) .try_insert(PendingCursor(Some(CursorSource::System( convert_system_cursor_icon(SystemCursorIcon::Default), )))); diff --git a/examples/3d/edit_material_on_gltf.rs b/examples/3d/edit_material_on_gltf.rs index de540f19f745a..4148100d45e16 100644 --- a/examples/3d/edit_material_on_gltf.rs +++ b/examples/3d/edit_material_on_gltf.rs @@ -58,7 +58,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { } fn change_material( - event: On, + scene_ready: On, mut commands: Commands, children: Query<&Children>, color_override: Query<&ColorOverride>, @@ -66,12 +66,12 @@ fn change_material( mut asset_materials: ResMut>, ) { // Get the `ColorOverride` of the entity, if it does not have a color override, skip - let Ok(color_override) = color_override.get(event.entity()) else { + let Ok(color_override) = color_override.get(scene_ready.entity) else { return; }; // Iterate over all children recursively - for descendants in children.iter_descendants(event.entity()) { + for descendants in children.iter_descendants(scene_ready.entity) { // Get the material of the descendant if let Some(material) = mesh_materials .get(descendants) diff --git a/examples/3d/solari.rs b/examples/3d/solari.rs index 3e3e8d4c07487..c177a3a9ace7b 100644 --- a/examples/3d/solari.rs +++ b/examples/3d/solari.rs @@ -110,7 +110,7 @@ fn setup( } fn add_raytracing_meshes_on_scene_load( - event: On, + scene_ready: On, children: Query<&Children>, mesh: Query<&Mesh3d>, mut meshes: ResMut>, @@ -132,7 +132,7 @@ fn add_raytracing_meshes_on_scene_load( } // Add raytracing mesh handles - for descendant in children.iter_descendants(event.entity()) { + for descendant in children.iter_descendants(scene_ready.entity) { if let Ok(mesh) = mesh.get(descendant) { commands .entity(descendant) diff --git a/examples/animation/animated_mesh.rs b/examples/animation/animated_mesh.rs index 7ae660c30d224..466d53be2ff22 100644 --- a/examples/animation/animated_mesh.rs +++ b/examples/animation/animated_mesh.rs @@ -62,7 +62,7 @@ fn setup_mesh_and_animation( } fn play_animation_when_ready( - event: On, + scene_ready: On, mut commands: Commands, children: Query<&Children>, animations_to_play: Query<&AnimationToPlay>, @@ -70,12 +70,12 @@ fn play_animation_when_ready( ) { // The entity we spawned in `setup_mesh_and_animation` is the trigger's target. // Start by finding the AnimationToPlay component we added to that entity. - if let Ok(animation_to_play) = animations_to_play.get(event.entity()) { + if let Ok(animation_to_play) = animations_to_play.get(scene_ready.entity) { // The SceneRoot component will have spawned the scene as a hierarchy // of entities parented to our entity. Since the asset contained a skinned // mesh and animations, it will also have spawned an animation player // component. Search our entity's descendants to find the animation player. - for child in children.iter_descendants(event.entity()) { + for child in children.iter_descendants(scene_ready.entity) { if let Ok(mut player) = players.get_mut(child) { // Tell the animation player to start the animation and keep // repeating it. diff --git a/examples/animation/animated_mesh_events.rs b/examples/animation/animated_mesh_events.rs index 776b8a0e21503..d451ca8efbbba 100644 --- a/examples/animation/animated_mesh_events.rs +++ b/examples/animation/animated_mesh_events.rs @@ -6,6 +6,7 @@ use bevy::{ animation::AnimationTargetId, color::palettes::css::WHITE, light::CascadeShadowConfigBuilder, prelude::*, }; +use bevy_animation::AnimationEvent; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -37,17 +38,20 @@ struct Animations { graph_handle: Handle, } -#[derive(EntityEvent, Reflect, Clone)] +#[derive(AnimationEvent, Reflect, Clone)] struct OnStep; fn observe_on_step( - event: On, + on_step: On, particle: Res, mut commands: Commands, transforms: Query<&GlobalTransform>, mut seeded_rng: ResMut, ) { - let translation = transforms.get(event.entity()).unwrap().translation(); + let translation = transforms + .get(on_step.trigger().animation_player) + .unwrap() + .translation(); // Spawn a bunch of particles. for _ in 0..14 { let horizontal = seeded_rng.0.random::() * seeded_rng.0.random_range(8.0..12.0); diff --git a/examples/animation/animation_events.rs b/examples/animation/animation_events.rs index b6e86a7f1b64a..e80e0e0de3b53 100644 --- a/examples/animation/animation_events.rs +++ b/examples/animation/animation_events.rs @@ -1,6 +1,7 @@ //! Demonstrate how to use animation events. use bevy::{ + animation::AnimationEvent, color::palettes::css::{ALICE_BLUE, BLACK, CRIMSON}, core_pipeline::bloom::Bloom, prelude::*, @@ -18,7 +19,7 @@ fn main() { #[derive(Component)] struct MessageText; -#[derive(EntityEvent, Clone)] +#[derive(AnimationEvent, Clone)] struct MessageEvent { value: String, color: Color, diff --git a/examples/animation/morph_targets.rs b/examples/animation/morph_targets.rs index 63e374ed7b1b4..8a9ab855bcf3b 100644 --- a/examples/animation/morph_targets.rs +++ b/examples/animation/morph_targets.rs @@ -56,14 +56,14 @@ fn setup( } fn play_animation_when_ready( - event: On, + scene_ready: On, mut commands: Commands, children: Query<&Children>, animations_to_play: Query<&AnimationToPlay>, mut players: Query<&mut AnimationPlayer>, ) { - if let Ok(animation_to_play) = animations_to_play.get(event.entity()) { - for child in children.iter_descendants(event.entity()) { + if let Ok(animation_to_play) = animations_to_play.get(scene_ready.entity) { + for child in children.iter_descendants(scene_ready.entity) { if let Ok(mut player) = players.get_mut(child) { player.play(animation_to_play.index).repeat(); diff --git a/examples/ecs/entity_disabling.rs b/examples/ecs/entity_disabling.rs index e5f1b5b1572a2..ef370d6c5f606 100644 --- a/examples/ecs/entity_disabling.rs +++ b/examples/ecs/entity_disabling.rs @@ -36,19 +36,18 @@ fn main() { struct DisableOnClick; fn disable_entities_on_click( - event: On>, + click: On>, valid_query: Query<&DisableOnClick>, mut commands: Commands, ) { - let clicked_entity = event.entity(); // Windows and text are entities and can be clicked! // We definitely don't want to disable the window itself, // because that would cause the app to close! - if valid_query.contains(clicked_entity) { + if valid_query.contains(click.entity) { // Just add the `Disabled` component to the entity to disable it. // Note that the `Disabled` component is *only* added to the entity, // its children are not affected. - commands.entity(clicked_entity).insert(Disabled); + commands.entity(click.entity).insert(Disabled); } } diff --git a/examples/ecs/error_handling.rs b/examples/ecs/error_handling.rs index e7ec1b54a1a0a..a84cbcd20870a 100644 --- a/examples/ecs/error_handling.rs +++ b/examples/ecs/error_handling.rs @@ -123,12 +123,12 @@ fn setup( // Observer systems can also return a `Result`. fn fallible_observer( - event: On>, + pointer_move: On>, mut world: DeferredWorld, mut step: Local, ) -> Result { let mut transform = world - .get_mut::(event.entity()) + .get_mut::(pointer_move.entity) .ok_or("No transform found.")?; *step = if transform.translation.x > 3. { diff --git a/examples/ecs/observer_propagation.rs b/examples/ecs/observer_propagation.rs index 3e862b7cc9fe8..2e89a5bb43aef 100644 --- a/examples/ecs/observer_propagation.rs +++ b/examples/ecs/observer_propagation.rs @@ -54,6 +54,7 @@ fn setup(mut commands: Commands) { #[derive(Clone, Component, EntityEvent)] #[entity_event(propagate, auto_propagate)] struct Attack { + entity: Entity, damage: u16, } @@ -68,23 +69,22 @@ struct Armor(u16); /// A normal bevy system that attacks a piece of the goblin's armor on a timer. fn attack_armor(entities: Query>, mut commands: Commands) { let mut rng = rng(); - if let Some(target) = entities.iter().choose(&mut rng) { + if let Some(entity) = entities.iter().choose(&mut rng) { let damage = rng.random_range(1..20); - commands.trigger_targets(Attack { damage }, target); + commands.trigger(Attack { damage, entity }); info!("⚔️ Attack for {} damage", damage); } } -fn attack_hits(event: On, name: Query<&Name>) { - if let Ok(name) = name.get(event.entity()) { +fn attack_hits(attack: On, name: Query<&Name>) { + if let Ok(name) = name.get(attack.entity) { info!("Attack hit {}", name); } } /// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage. -fn block_attack(mut event: On, armor: Query<(&Armor, &Name)>) { - let (armor, name) = armor.get(event.entity()).unwrap(); - let attack = event.event_mut(); +fn block_attack(mut attack: On, armor: Query<(&Armor, &Name)>) { + let (armor, name) = armor.get(attack.entity).unwrap(); let damage = attack.damage.saturating_sub(**armor); if damage > 0 { info!("🩸 {} damage passed through {}", damage, name); @@ -94,7 +94,7 @@ fn block_attack(mut event: On, armor: Query<(&Armor, &Name)>) { } else { info!("🛡️ {} damage blocked by {}", attack.damage, name); // Armor stopped the attack, the event stops here. - event.propagate(false); + attack.propagate(false); info!("(propagation halted early)\n"); } } @@ -102,20 +102,19 @@ fn block_attack(mut event: On, armor: Query<(&Armor, &Name)>) { /// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack, /// or the wearer is attacked directly. fn take_damage( - event: On, + attack: On, mut hp: Query<(&mut HitPoints, &Name)>, mut commands: Commands, mut app_exit: EventWriter, ) { - let attack = event.event(); - let (mut hp, name) = hp.get_mut(event.entity()).unwrap(); + let (mut hp, name) = hp.get_mut(attack.entity).unwrap(); **hp = hp.saturating_sub(attack.damage); if **hp > 0 { info!("{} has {:.1} HP", name, hp.0); } else { warn!("💀 {} has died a gruesome death", name); - commands.entity(event.entity()).despawn(); + commands.entity(attack.entity).despawn(); app_exit.write(AppExit::Success); } diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs index d1055cd3b938c..b3012fb1e8f16 100644 --- a/examples/ecs/observers.rs +++ b/examples/ecs/observers.rs @@ -16,18 +16,18 @@ fn main() { // Observers are systems that run when an event is "triggered". This observer runs whenever // `ExplodeMines` is triggered. .add_observer( - |event: On, + |explode_mines: On, mines: Query<&Mine>, index: Res, mut commands: Commands| { // Access resources - for e in index.get_nearby(event.pos) { + for entity in index.get_nearby(explode_mines.pos) { // Run queries - let mine = mines.get(e).unwrap(); - if mine.pos.distance(event.pos) < mine.size + event.radius { + let mine = mines.get(entity).unwrap(); + if mine.pos.distance(explode_mines.pos) < mine.size + explode_mines.radius { // And queue commands, including triggering additional events // Here we trigger the `Explode` event for entity `e` - commands.trigger_targets(Explode, e); + commands.trigger(Explode { entity }); } } }, @@ -65,7 +65,9 @@ struct ExplodeMines { } #[derive(EntityEvent)] -struct Explode; +struct Explode { + entity: Entity, +} fn setup(mut commands: Commands) { commands.spawn(Camera2d); @@ -110,36 +112,35 @@ fn setup(mut commands: Commands) { commands.spawn(observer); } -fn on_add_mine(event: On, query: Query<&Mine>, mut index: ResMut) { - let mine = query.get(event.entity()).unwrap(); +fn on_add_mine(add: On, query: Query<&Mine>, mut index: ResMut) { + let mine = query.get(add.entity).unwrap(); let tile = ( (mine.pos.x / CELL_SIZE).floor() as i32, (mine.pos.y / CELL_SIZE).floor() as i32, ); - index.map.entry(tile).or_default().insert(event.entity()); + index.map.entry(tile).or_default().insert(add.entity); } // Remove despawned mines from our index -fn on_remove_mine(event: On, query: Query<&Mine>, mut index: ResMut) { - let mine = query.get(event.entity()).unwrap(); +fn on_remove_mine(remove: On, query: Query<&Mine>, mut index: ResMut) { + let mine = query.get(remove.entity).unwrap(); let tile = ( (mine.pos.x / CELL_SIZE).floor() as i32, (mine.pos.y / CELL_SIZE).floor() as i32, ); index.map.entry(tile).and_modify(|set| { - set.remove(&event.entity()); + set.remove(&remove.entity); }); } -fn explode_mine(event: On, query: Query<&Mine>, mut commands: Commands) { - // If a triggered event is targeting a specific entity you can access it with `.entity()` - let id = event.entity(); - let Ok(mut entity) = commands.get_entity(id) else { +fn explode_mine(explode: On, query: Query<&Mine>, mut commands: Commands) { + // If a triggered event is targeting a specific entity you can access it with `.target()` + let Ok(mut entity) = commands.get_entity(explode.entity) else { return; }; - info!("Boom! {} exploded.", id.index()); + info!("Boom! {} exploded.", explode.entity); entity.despawn(); - let mine = query.get(id).unwrap(); + let mine = query.get(explode.entity).unwrap(); // Trigger another explosion cascade. commands.trigger(ExplodeMines { pos: mine.pos, diff --git a/examples/ecs/removal_detection.rs b/examples/ecs/removal_detection.rs index 88d4c4a54dd74..1216c53df37cb 100644 --- a/examples/ecs/removal_detection.rs +++ b/examples/ecs/removal_detection.rs @@ -48,10 +48,9 @@ fn remove_component( } } -fn react_on_removal(event: On, mut query: Query<&mut Sprite>) { +fn react_on_removal(remove: On, mut query: Query<&mut Sprite>) { // The `Remove` event was automatically triggered for the `Entity` that had its `MyComponent` removed. - let entity = event.entity(); - if let Ok(mut sprite) = query.get_mut(entity) { + if let Ok(mut sprite) = query.get_mut(remove.entity) { sprite.color = Color::srgb(0.5, 1., 1.); } } diff --git a/examples/no_std/library/src/lib.rs b/examples/no_std/library/src/lib.rs index 734417571d7b7..1c8cc97cc0be0 100644 --- a/examples/no_std/library/src/lib.rs +++ b/examples/no_std/library/src/lib.rs @@ -106,7 +106,9 @@ struct DelayedComponentTimer(Timer); struct DelayedComponent(B); #[derive(EntityEvent)] -struct Unwrap; +struct Unwrap { + entity: Entity, +} fn tick_timers( mut commands: Commands, @@ -120,7 +122,7 @@ fn tick_timers( commands .entity(entity) .remove::() - .trigger(Unwrap); + .trigger(Unwrap { entity }); } } } diff --git a/examples/picking/debug_picking.rs b/examples/picking/debug_picking.rs index da817c20a8873..c64d4f148cd7a 100644 --- a/examples/picking/debug_picking.rs +++ b/examples/picking/debug_picking.rs @@ -48,13 +48,13 @@ fn setup_scene( .observe(on_click_spawn_cube) .observe( |out: On>, mut texts: Query<&mut TextColor>| { - let mut text_color = texts.get_mut(out.entity()).unwrap(); + let mut text_color = texts.get_mut(out.entity).unwrap(); text_color.0 = Color::WHITE; }, ) .observe( |over: On>, mut texts: Query<&mut TextColor>| { - let mut color = texts.get_mut(over.entity()).unwrap(); + let mut color = texts.get_mut(over.entity).unwrap(); color.0 = bevy::color::palettes::tailwind::CYAN_400.into(); }, ); @@ -102,7 +102,7 @@ fn on_click_spawn_cube( } fn on_drag_rotate(drag: On>, mut transforms: Query<&mut Transform>) { - if let Ok(mut transform) = transforms.get_mut(drag.entity()) { + if let Ok(mut transform) = transforms.get_mut(drag.entity) { transform.rotate_y(drag.delta.x * 0.02); transform.rotate_x(drag.delta.y * 0.02); } diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index da571ddbcd4f0..6d38c32852c89 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -163,8 +163,8 @@ fn update_material_on( // An observer closure that captures `new_material`. We do this to avoid needing to write four // versions of this observer, each triggered by a different event and with a different hardcoded // material. Instead, the event type is a generic, and the material is passed in. - move |trigger, mut query| { - if let Ok(mut material) = query.get_mut(trigger.entity()) { + move |event, mut query| { + if let Ok(mut material) = query.get_mut(event.entity()) { material.0 = new_material.clone(); } } @@ -191,7 +191,7 @@ fn rotate(mut query: Query<&mut Transform, With>, time: Res) { black_box(event); } -fn on_simple_entity_event(event: On) { +#[derive(Clone, EntityEvent)] +struct B { + entity: Entity, +} + +fn on_b(event: On) { black_box(event); } diff --git a/benches/benches/bevy_ecs/observers/lifecycle.rs b/benches/benches/bevy_ecs/observers/lifecycle.rs new file mode 100644 index 0000000000000..7f81aba92b87a --- /dev/null +++ b/benches/benches/bevy_ecs/observers/lifecycle.rs @@ -0,0 +1,35 @@ +use bevy_ecs::{component::Component, lifecycle::Insert, observer::On, world::World}; +use core::hint::black_box; +use criterion::Criterion; +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; + +fn deterministic_rand() -> ChaCha8Rng { + ChaCha8Rng::seed_from_u64(42) +} + +pub fn observer_lifecycle(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("observe"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + group.bench_function("observer_lifecycle_insert", |bencher| { + let mut world = World::new(); + world.add_observer(on_insert); + let mut entity = world.spawn(A); + bencher.iter(|| { + for _ in 0..10000 { + entity.insert(A); + } + }); + }); + + group.finish(); +} + +#[derive(Component)] +struct A; + +fn on_insert(event: On) { + black_box(event); +} diff --git a/benches/benches/bevy_ecs/observers/mod.rs b/benches/benches/bevy_ecs/observers/mod.rs index 16008def7e461..76218072f50f7 100644 --- a/benches/benches/bevy_ecs/observers/mod.rs +++ b/benches/benches/bevy_ecs/observers/mod.rs @@ -1,8 +1,15 @@ +mod custom; +mod lifecycle; mod propagation; -mod simple; use criterion::criterion_group; +use custom::*; +use lifecycle::*; use propagation::*; -use simple::*; -criterion_group!(benches, event_propagation, observe_simple); +criterion_group!( + benches, + event_propagation, + observer_custom, + observer_lifecycle +); From ee819610fc03a4ff337543fa068897e2ced58531 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 30 Aug 2025 13:46:33 -0700 Subject: [PATCH 17/44] Remove trigger methods from EntityWorldMut / EntityCommands --- crates/bevy_ecs/src/system/commands/mod.rs | 9 ------- crates/bevy_ecs/src/world/entity_ref.rs | 31 +--------------------- examples/no_std/library/src/lib.rs | 6 ++--- 3 files changed, 3 insertions(+), 43 deletions(-) diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 52f7fb38d4bc5..87652c3e78539 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1992,15 +1992,6 @@ impl<'a> EntityCommands<'a> { &mut self.commands } - /// Triggers the given [`Event`], which will run any [`Observer`]s watching for it. - /// - /// [`Observer`]: crate::observer::Observer - #[track_caller] - pub fn trigger<'t>(&mut self, event: impl Event: Default>) -> &mut Self { - self.commands_mut().queue(command::trigger(event)); - self - } - /// Creates an [`Observer`] watching for an [`EntityEvent`] of type `E` whose [`EntityEvent::event_target`] /// targets this entity. pub fn observe( diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index c70ec716d8f2c..c7a319c258ba3 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -13,7 +13,7 @@ use crate::{ ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityIdLocation, EntityLocation, OptIn, OptOut, }, - event::{EntityComponentsTrigger, EntityEvent, Event}, + event::{EntityComponentsTrigger, EntityEvent}, lifecycle::{Despawn, Remove, Replace, DESPAWN, REMOVE, REPLACE}, observer::Observer, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, @@ -2025,35 +2025,6 @@ impl<'w> EntityWorldMut<'w> { ) } - /// Triggers the given [`Event`], which will run any [`Observer`]s watching for it. - /// - /// [`Observer`]: crate::observer::Observer - #[track_caller] - pub fn trigger<'a, E: Event: Default>>(&mut self, mut event: E) { - self.world_scope(|world| { - world.trigger_ref_with_caller( - &mut event, - &mut as Default>::default(), - MaybeLocation::caller(), - ); - }); - } - - /// Triggers the given [`Event`] using the given [`Trigger`], which will run any [`Observer`]s watching for it. - /// - /// [`Trigger`]: crate::event::Trigger - /// [`Observer`]: crate::observer::Observer - #[track_caller] - pub fn trigger_with<'a, E: Event: Send + Sync>>( - &mut self, - mut event: E, - mut trigger: E::Trigger<'a>, - ) { - self.world_scope(|world| { - world.trigger_ref_with_caller(&mut event, &mut trigger, MaybeLocation::caller()); - }); - } - /// Split into a new function so we can pass the calling location into the function when using /// as a command. #[inline] diff --git a/examples/no_std/library/src/lib.rs b/examples/no_std/library/src/lib.rs index e4a0c79f9108a..e9759efc1144c 100644 --- a/examples/no_std/library/src/lib.rs +++ b/examples/no_std/library/src/lib.rs @@ -119,10 +119,8 @@ fn tick_timers( timer.tick(time.delta()); if timer.just_finished() { - commands - .entity(entity) - .remove::() - .trigger(Unwrap { entity }); + commands.entity(entity).remove::(); + commands.trigger(Unwrap { entity }); } } } From d400363ed700a862b811742abbe25bf3d17e9e39 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 30 Aug 2025 13:48:36 -0700 Subject: [PATCH 18/44] typos --- crates/bevy_ecs/src/observer/centralized_storage.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs index 92c215b989609..c972ef5b5e1f5 100644 --- a/crates/bevy_ecs/src/observer/centralized_storage.rs +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -148,7 +148,7 @@ pub type ObserverMap = EntityHashMap; pub struct CachedComponentObservers { // Observers watching for events targeting this component, but not a specific entity pub(super) global_observers: ObserverMap, - // Observers wathing for events targeting this component on a specific entity + // Observers watching for events targeting this component on a specific entity pub(super) entity_component_observers: EntityHashMap, } @@ -158,7 +158,7 @@ impl CachedComponentObservers { &self.global_observers } - /// Returns observers wathing for events targeting this component on a specific entity + /// Returns observers watching for events targeting this component on a specific entity pub fn entity_component_observers(&self) -> &EntityHashMap { &self.entity_component_observers } From aa6566794e8847ae296796b77ea6fb81bc478280 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 3 Sep 2025 17:16:33 -0700 Subject: [PATCH 19/44] Add Event / Observer overhaul release notes and consolidate existing notes --- release-content/release-notes/event_split.md | 121 --------- .../release-notes/observer_overhaul.md | 247 +++++++++++++++--- 2 files changed, 217 insertions(+), 151 deletions(-) delete mode 100644 release-content/release-notes/event_split.md diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md deleted file mode 100644 index 8cfb3b3096242..0000000000000 --- a/release-content/release-notes/event_split.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: Event Split -authors: ["@Jondolf"] -pull_requests: [19647, 20101] ---- - -In past releases, all event types were defined by simply deriving the `Event` trait: - -```rust -#[derive(Event)] -struct Speak { - message: String, -} -``` - -You could then use the various event handling tools in Bevy to send and listen to the event. The common options include: - -- Use `trigger` to trigger the event and react to it with a global `Observer` -- Use `trigger_targets` to trigger the event with specific entity target(s) and react to it with an entity `Observer` or global `Observer` -- Use `EventWriter::write` to write the event to an event buffer and `EventReader::read` to read it at a later time - -The first two are observer APIs, while the third is a fully separate "buffered" API for pull-based event handling. -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. - -**Bevy 0.17** aims to solve this ambiguity by splitting the event traits into `Event`, `EntityEvent`, and `BufferedEvent`. - -- `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. - -## Using Events - -A basic `Event` can be defined like before, by deriving the `Event` trait. - -```rust -#[derive(Event)] -struct Speak { - message: String, -} -``` - -You can then `trigger` the event, and use a global observer for reacting to it. - -```rust -app.add_observer(|event: On| { - println!("{}", event.message); -}); - -// ... - -commands.trigger(Speak { - message: "Hello!".to_string(), -}); -``` - -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: - -```rust -// When the `Damage` event is triggered on an entity, bubble the event up to ancestors. -#[derive(EntityEvent)] -#[entity_event(propagate, auto_propagate)] -struct Damage { - amount: f32, -} -``` - -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`: - -```rust -// Spawn an enemy entity. -let enemy = commands.spawn((Enemy, Health(100.0))).id(); - -// Spawn some armor as a child of the enemy entity. -// When the armor takes damage, it will bubble the event up to the enemy, -// which can then handle the event with its own observer. -let armor_piece = commands - .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) - .observe(|event: On, mut query: Query<&mut Health>| { - // Note: `On::entity` only exists because this is an `EntityEvent`. - let mut health = query.get(event.entity()).unwrap(); - health.0 -= event.amount(); - }) - .id(); - -// Trigger the `Damage` event on the armor piece. -commands.trigger_targets(Damage { amount: 10.0 }, armor_piece); -``` - -To allow an event to be used with the buffered API, you can instead derive `BufferedEvent`: - -```rust -#[derive(BufferedEvent)] -struct Message(String); -``` - -The event can then be used with `EventReader`/`EventWriter`: - -```rust -fn write_hello(mut writer: EventWriter) { - writer.write(Message("I hope these examples are alright".to_string())); -} - -fn read_messages(mut reader: EventReader) { -    // Process all buffered events of type `Message`. -   for Message(message) in reader.read() { -        println!("{message}"); -   } -} -``` - -In summary: - -- Need a basic event you can trigger and observe? Derive `Event`! -- 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`! diff --git a/release-content/release-notes/observer_overhaul.md b/release-content/release-notes/observer_overhaul.md index a2d716bc856fe..1dac36edf9ee0 100644 --- a/release-content/release-notes/observer_overhaul.md +++ b/release-content/release-notes/observer_overhaul.md @@ -1,14 +1,158 @@ --- -title: Observer Overhaul -authors: ["@Jondolf", "@alice-i-cecile", "@hukasu", "oscar-benderstone", "Zeophlite", "gwafotapa"] -pull_requests: [19596, 19663, 19611, 19935, 20274] +title: Event / Observer Overhaul +authors: ["@cart, @Jondolf", "@alice-i-cecile", "@hukasu", "oscar-benderstone", "Zeophlite", "gwafotapa"] +pull_requests: [20731, 19596, 19663, 19611, 19935, 20274] --- -TODO: merge with Event split release notes +Bevy's Observer API landed a few releases ago, and it has quickly become one of our most popular features. In **Bevy 0.17** we rearchitected and refined the Event and Observer APIs to be clearer, easier to use, and more performant. We plan on rolling out Bevy's next generation Scene / UI system in the near future, and observers are a key piece! We wanted to ensure they were in a better place for the next phase of Bevy development. The old API had some problems: -## Rename `Trigger` to `On` +1. **Concept names were confusing and ambiguous**: Events could be "observed", "buffered" in `Events` collections, or both. Knowing how to produce or consume a given [`Event`] required too much implied context: "do I write an Observer or use an EventReader system?", "do I trigger the event with or without targets?", what should the targets be?", etc. We need better, less ambiguous ways to refer to events. +2. **The API was not "static" enough**: This relates to (1). Because a given [`Event`] type could be used by and produced for _any context_, we had to provide access to _every possible API_ for _every event type_. It should not be possible to trigger an "entity event" without an entity! An Observer of an event that was not designed to have a target entity should not have an `entity()` field! Every [`Event`] impl had to define an "entity propagation traversal", even it was not designed to propagate (and even if it didn't target entities at all!). Events should be self documenting, impossible to produce or consume in the wrong context, and should only encode the information that is necessary for that event. +3. **The API did too much work**: Because events could be produced and used in any context, this meant that they all branched through code for every possible context. This incurred unnecessary overhead. It also resulted in lots of unnecessary codegen! -In past releases, the observer API looked like this: +In **Bevy 0.17** we have sorted out these issues ... without fundamentally changing the shape of the API. Migrations should generally be very straightforward. + +## The Rearchitecture + +The `Event` trait has been reframed / refocused to increase flexibilty, make the API more static, and remove specialized cruft: + +```rust +// Old: Bevy 0.16 +trait Event { + // this embedded configuration specific to "propagating entity events" in all events! + type Traversal: Traversal; + const AUTO_PROPAGATE: bool = false; + fn register_component_id(world: &mut World); + fn component_id(world: &World) -> Option; +} + +// New: Bevy 0.17 +trait Event { + type Trigger<'a>: Trigger; +} +``` + +Every [`Event`] now has an associated [`Trigger`] implemenation. The [`Trigger`] trait defines the behavior of `world.trigger()` for that event. [`Trigger`] defines which observers will run, the order they will run in, and the data that is passed to them. + +By representing this in the type system, we can constrain behaviors and data to _specific_ types of events statically, making it impossible to "misuse" an [`Event`]. +All of Bevy's existing "flavors" of events have been ported to the new [`Event`] / [`Trigger`] system. + +## Event: global by default + +At a glance, the default [`Event`] derive and usage hasn't changed much. Just some shorter / clearer naming. The old API looked like this: + +```rust +#[derive(Event)] +struct GameOver { + score: u32, +} + +world.add_observer(|trigger: Trigger| { + info!("Game over! You scored {} points", trigger.score); +}); + +world.trigger(GameOver { score: 100 }); +``` + +In **Bevy 0.17**, defining observers has only changed slightly: + +```rust + +world.add_observer(|game_over: On| { + info!("Game over! You scored {} points", game_over.score); +}); + +``` + +`Trigger` is now `On`. `On` encourages developers to think of this parameter _as the event itself_. This is also reflected in the new naming convention, where we name the variable after the `Event` (ex: `game_over`) rather than the `Trigger` (ex: `trigger`). + +Internally things are a bit different though! The [`Event`] derive defaults to being "untargeted" / "global", by setting the `Event::Trigger` to [`GlobalTrigger`]. When it is triggered, only "untargeted" top-level observers will run, and there is _no way_ to trigger it in a different context (ex: events with a [`GlobalTrigger`] cannot target entities!). + +## EntityEvent + +In previous versions of Bevy, _any_ event could optionally be triggered for an entity. It looked like this: + +```rust +#[derive(Event)] +struct Click; + +world.trigger_targets(Click, entity); +``` + +In **Bevy 0.17**, if you want an [`Event`] to target an [`Entity`] (and thus trigger any observers watching for that specific entity), you derive [`EntityEvent`]: + +```rust +#[derive(EntityEvent)] +struct Click { + entity: Entity, +} + +world.trigger(Click { entity }); +``` + +Notice that `Click` now has the target entity as a field _on_ the [`Event`], and it now uses the same `world.trigger()` API that other events use. `world.trigger_targets` is no more ... every event is triggered using the same API! + +```rust +// This observer will run for _all_ Click events targeting any entity +world.add_observer(|mut click: On| {}); + +/// This observer will only run for Click events triggered for `some_entity` +world.entity_mut(some_entity).observe(|mut click: On| {}); +``` + +[`EntityEvent`] is a new trait: + +```rust +trait EntityEvent: Event { + fn event_target(&self) -> Entity; + fn event_target_mut(&mut self) -> &mut Entity; +} +``` + +When it is derived, it defaults to setting the [`Event`] trigger to [`EntityTrigger`]. This will trigger all "untargeted" observers (`world.add_observer()`), just like [`GlobalTrigger`], but it will _also_ trigger any observers that target a specific entity (`world.entity_mut(some_entity).observe()`). + +Deriving [`EntityEvent`] will set the `entity_target` to a field named `entity` by default. In some cases (such as events that have multiple entity fields), it might make sense to use a more descriptive name. You can set the target using the `#[event_target]` field attribute: + +```rust +#[derive(EntityEvent)] +struct Attack { + // This will trigger `attacker` observers + #[event_target] + attacker: Entity, + attacked: Entity, +} +``` + +## EntityEvent Propagation + +An [`EntityEvent`] does not "propagate" by default (and they now statically have no access to APIs that control propagation). Propagation can be enabled using the `propagate` attribute (which defaults to using the [`ChildOf`] relationship to "bubble events up the hierarchy"): + +```rust +#[derive(EntityEvent)] +#[entity_event(propagate)] +struct Click { + entity: Entity +} +``` + +This will set the [`Event`]'s [`Trigger`] to [`PropagatingEntityTrigger`]. + +This enables access to "propagation" functionality like this: + +```rust +world.add_observer(|mut click: On| { + if SOME_CONDITION { + // stop the event from "bubbling up" + click.propagate(false); + } +}); +``` + +Bevy's `Pointer` events have always tracked the "original target" that an "entity event" was targeting. This was handy! We've enabled this functionality for every [`EntityEvent`] with [`PropagatingEntityTrigger`]: simply call `On::original_event_target`. + +## Component Lifecycle Events + +In past releases, the observer API for lifecycle events looked like this: ```rust app.add_observer(|trigger: Trigger| { @@ -16,43 +160,86 @@ app.add_observer(|trigger: Trigger| { }); ``` -In this example, the `Trigger` type contains information about the `OnAdd` event that was triggered -for a `Player`. +We've ported these over to the new system, and renamed them to match our new naming scheme (ex: `OnAdd` is now [`Add`]). They look like this now: + +```rust +app.add_observer(|add: On| { + info!("Added player {}", add.entity); +}); +``` + +Component lifecycle events are an [`EntityEvent`] (and thus store the target entity as a field). They use the [`EntityComponentsTrigger`], which allows them to be triggered for specific components on an entity. -**Bevy 0.17** renames the `Trigger` type to `On`, and removes the `On` prefix from lifecycle events -such as `OnAdd` and `OnRemove`: +## AnimationEvent + +"Animation events" are custom events that are registered with an [`AnimationPlayer`] and triggered at a specific point in the animation. [`AnimationEvent`] is a new event sub-trait / derive (much like [`EntityEvent`]). Animation events use the [`AnimationEventTrigger`]. They behave like an [`EntityEvent`] (they observers on the [`AnimationPlayer`]), but they notably _do not store the entity on the event type_. This allows for directly registering them in [`AnimationPlayer`] without needing to set an entity target: ```rust -app.add_observer(|event: On| { - info!("Added player {}", event.entity()); +animation.add_event( + 0.0, + PrintMessage("Hello".to_string()), +); + +world.entity_mut(animation_player).observe(|print_message: On| { + // The `AnimationEventTrigger` still provides access to the animation_player entity + println!("{} says {}", print_message.trigger().animation_player, print_message.0); }); ``` -This significantly improves readability and ergonomics, and is especially valuable in UI contexts -where observers are very high-traffic APIs. +## Custom Event Triggers + +The new [`Trigger`] trait also enables developers to implement their _own_ specialized [`Event`] [`Trigger`] logic. -One concern that may come to mind is that `Add` can sometimes conflict with the `core::ops::Add` trait. -However, in practice these scenarios should be rare, and when you do get conflicts, it should be straightforward -to disambiguate by using `ops::Add`, for example. +The [`Event`] derive can specify a custom [`Trigger`] like this: + +```rust +#[derive(Event)] +#[event(trigger = CoolTrigger) +struct Jump; +``` + +Alternatively, developers can create specialized event derives / traits, following the same pattern as `EntityEvent`: + +```rust +trait CoolEvent: Event { } + +#[derive(CoolEvent)] +struct Jump; + +// the derive above would generate this code: +impl CoolEvent for Jump {} +impl Event for Jump { + type Trigger<'s> = CoolTrigger; +} +``` -## Original targets +## Concept Clarity: Events vs Messages -`bevy_picking`'s `Pointer` events have always tracked the original target that an entity-event was targeting, -allowing you to bubble events up your hierarchy to see if any of the parents care, -then act on the entity that was actually picked in the first place. +In previous versions of Bevy, the [`Event`] trait was used for both "observable events" (handled with `Observer`) and "buffered events" (handled with `EventReader`). This made _some_ sense, as both concepts could be considered "events" in their own right. But they are also fundamentally _very_ different things functionally: -This was handy! We've enabled this functionality for all entity-events: simply call `On::original_entity`. +1. "Observable events" are consumed one-by-one in Observers, which exist outside of a schedule. "Buffered events" are consumed by iterating over many of them in normal systems, which exist in one or more places inside a schedule. +2. "Observable event" handlers are run _for_ developers. "Buffered event" consumers are responsible for dispatching handler logic themselves. +3. "Observable events" are handled immediately. "Buffered events" are handled at some later moment in time (or not at all). +4. "Observable events" need additional configuration to make them work (ex: `Event::Trigger`). "Buffered events" do not. +5. "Observable events" incur a small amount of per-handler overhead. Handling "buffered events" is as fast as iterating an array. -## Expose name of the Observer's system +Most importantly: there was _no way_ for consumers or producers of these events to know _how_ to handle them, just by looking at the type info. Consider some `ProcessingFinished` event from some 3rd party library. Events could either be "buffered" or "observed" (depending on what the sender of the event chooses), so the consumer has _no way_ to know how to consume `ProcessingFinished`. Is their observer not firing because the event isn't happening, or because the creator of the event was sending it as a buffered event instead of "triggering" it? -The name of the Observer's system is now accessible through `Observer::system_name`, -this opens up the possibility for the debug tools to show more meaningful names for observers. +These are two completely separate systems, with different producer / consumer APIs, different performance considerations, and immediate vs deferred handling. The "things" being sent deserve different concept names to solidify conceptually (and at the type/API level) their intended purpose and context. -## Use `EventKey` instead of `ComponentId` +In **Bevy 0.17**, [`Event`] is now _exclusively_ the name/trait for the concept of something that is "triggered" and "observed". [`Message`] is the name / trait of something that "buffered": it is "written" via a [`MessageWriter`] and "read" via a [`MessageReader`]. -Internally, each `Event` type would generate a `Component` type, allowing us to use the corresponding `ComponentId` to track the event. -We have newtyped this to `EventKey` to help separate these concerns. +It is still possible to support both contexts by implementing _both traits_, but we expect that to be significantly less common than just choosing one. -## Watch multiple entities -To watch multiple entities with the same observer you previously had to call `Observer::with_entity` or `Observer::watch_entity` for each entity. New methods `Observer::with_entities` and `Observer::watch_entities` have been added for your convenience. +[`Event`]: https://dev-docs.bevy.org/bevy/ecs/event/trait.Event.html +[`Trigger`]: `https://dev-docs.bevy.org/bevy/ecs/event/trait.Trigger.html` +[`GlobalTrigger`]: `https://dev-docs.bevy.org/bevy/ecs/event/type.GlobalTrigger.html` +[`EntityEvent`]: https://dev-docs.bevy.org/bevy/ecs/event/trait.EntityEvent.html +[`ChildOf`]: https://dev-docs.bevy.org/bevy/ecs/hierarchy/struct.ChildOf.html +[`PropagatingEntityTrigger`]: `https://dev-docs.bevy.org/bevy/ecs/event/type.PropagatingEntityTrigger.html` +[`Add`]: https://dev-docs.bevy.org/bevy/ecs/lifecycle/struct.Add.html +[`EntityComponentsTrigger`]: `https://dev-docs.bevy.org/bevy/ecs/event/type.EntityComponentsTrigger.html` +[`AnimationPlayer`]: https://dev-docs.bevy.org/bevy/animation/struct.AnimationPlayer.html +[`AnimationEvent`]: https://dev-docs.bevy.org/bevy/animation/trait.AnimationEvent.html +[`AnimationEventTrigger`]: https://dev-docs.bevy.org/bevy/animation/type.AnimationEventTrigger.html \ No newline at end of file From 56c3fe5b111cf7ae998dbff8632ec207019140be Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 3 Sep 2025 18:22:11 -0700 Subject: [PATCH 20/44] Add / update migration guides --- .../migration-guides/event_split.md | 12 +- ...observerState_observer_single_component.md | 2 +- .../observer_and_event_changes.md | 142 ++++++++++++++++++ .../migration-guides/observer_triggers.md | 32 ---- 4 files changed, 145 insertions(+), 43 deletions(-) create mode 100644 release-content/migration-guides/observer_and_event_changes.md delete mode 100644 release-content/migration-guides/observer_triggers.md diff --git a/release-content/migration-guides/event_split.md b/release-content/migration-guides/event_split.md index d30119f75a7db..14ec70cd4130d 100644 --- a/release-content/migration-guides/event_split.md +++ b/release-content/migration-guides/event_split.md @@ -1,14 +1,6 @@ --- -title: "`Event` trait split" +title: "`Event` trait split / Rename" pull_requests: [19647] --- -The `Event` trait was previously used for all types of events: "observer events" with and without targets, -and "buffered events" using `EventReader` and `EventWriter`. - -Buffered events and targeted events have now been split into dedicated `BufferedEvent` and `EntityEvent` traits. -An event with just the `Event` trait implemented only supports non-targeted APIs such as global observers and the `trigger` method. - -If an event is used with `trigger_targets` or an entity observer, make sure you have derived `EntityEvent` for it. - -If an event is used with `EventReader` or `EventWriter`, make sure you have derived `BufferedEvent` for it. +"Buffered events" (things sent/read using `EventWriter` / `EventReader`) are now no longer referred to as "events", in the interest of conceptual clarity and learn-ability (see the release notes for rationale). "Event" as a concept (and the `Event` trait) are now used solely for "observable events". "Buffered events" are now known as "messages" and use the `Message` trait. `EventWriter`, `EventReader`, and `Events`, are now known as `MessageWriter`, `MessageReader`, and `Messages`. Types can be _both_ "messages" and "events" by deriving both `Message` and `Event`, but we expect most types to only be used in one context or the other. diff --git a/release-content/migration-guides/merge_observerState_observer_single_component.md b/release-content/migration-guides/merge_observerState_observer_single_component.md index c0365001bab02..165597a204f5f 100644 --- a/release-content/migration-guides/merge_observerState_observer_single_component.md +++ b/release-content/migration-guides/merge_observerState_observer_single_component.md @@ -8,7 +8,7 @@ now you can use `Observer::with_dynamic_runner` to build custom Observe. ```rust let observe = unsafe { - Observer::with_dynamic_runner(|mut world, trigger, ptr, propagate| { + Observer::with_dynamic_runner(|world, trigger_context, event_ptr, trigger_ptr| { // do something }) .with_event(event_a) diff --git a/release-content/migration-guides/observer_and_event_changes.md b/release-content/migration-guides/observer_and_event_changes.md new file mode 100644 index 0000000000000..147d3ce7385a4 --- /dev/null +++ b/release-content/migration-guides/observer_and_event_changes.md @@ -0,0 +1,142 @@ +--- +title: Observer / Event API Changes +pull_requests: [20731, 19440, 19596] +--- + +The observer "trigger" API has changed a bit to improve clarity and type-safety. + +```rust +// Old +commands.add_observer(|trigger: Trigger| { + info!("Spawned player {}", trigger.entity()); +}); + +// New +commands.add_observer(|add: On| { + info!("Spawned player {}", add.entity); +}); +``` + +The `Trigger` type used inside observers has been renamed to `On` to encourage developers to think about this parameter _as_ the event. We also recommend naming the variable after the event type (ex: `add`). + +To reduce repetition and improve readability, the `OnAdd`, `OnInsert`, `OnReplace`, `OnRemove`, and `OnDespawn` +observer events have also been renamed to `Add`, `Insert`, `Replace`, `Remove`, and `Despawn` respectively. +In rare cases where the `Add` event conflicts with the `std::ops::Add` trait, you may need to disambiguate, +for example by using `ops::Add` for the trait. We encourage removing the "On" from custom events named `OnX`. + +Types implementing `Event` can no longer be triggered from _all contexts. By default `Event` is a "global" / "target-less" event. + +Events that target an entity should now derive `EntityEvent`, and they will now store the target entity _on_ the event type, which is accessible via `EntityEvent::event_target`. Additionally, `world.trigger_targets` has been removed in favor of a single `world.trigger` API: + +```rust +// Old +#[derive(Event)] +struct Explode; + +world.trigger_targets(Explode, entity); + +// New +#[derive(EntityEvent)] +struct Explode { + entity: Entity +} + +world.trigger(Explode { entity }); +``` + +Triggering an entity event for multiple entities now requires multiple calls to `trigger`: + +```rust +// OLd +world.trigger_targets(Explode, [e1, e2]); + +// New - Variant 1 +world.trigger(Explode { entity: e1 }); +world.trigger(Explode { entity: e2 }); + +// New - Variant 2 +for entity in [e1, e2] { + world.trigger(Explode { entity }); +} +``` + +`On::target()` no longer exists for all event types. Instead, you should prefer accessing the "target entity" field on the events that target entities: + +```rust +// Old +commands.add_observer(|trigger: Trigger| { + info!("{} exploded!", trigger.target()); +}); + +// New +commands.add_observer(|explode: On| { + info!("{} exploded!", explode.entity); + // you can also use `EntityEvent::event_target`, but we encourage + // using direct field access when possible, for better documentation and clarity. + info!("{} exploded!", explode.event_target()); +}); +``` + +"Propagation functions", such as `On::propagate` are now _only_ available on `On` when `E: EntityEvent`. + +Enabling propagation is now down using, which defaults to `ChildOf` propagation: + +```rust +#[derive(EntityEvent)] +#[entity_event(propagate)] +struct Click { + entity: Entity, +} +``` + +Setting a custom propagation `Traversal` implementation now uses `propagate` instead of `traversal`: + +```rust +// OLd +#[derive(Event)] +#[event(traversal = &'static ChildOf)] +struct Click; + +// New +#[derive(EntityEvent)] +#[entity_event(propagate = &'static ChildOf)] +struct Click { + entity: Entity, +} +``` + +Animation events (used in `AnimationPlayer`) must now derive `AnimationEvent`. Accessing the animation player entity is now done via the `trigger()`. + +```rust +// Old +#[derive(Event)] +struct SayMessage(String); + +animation.add_event(0.2, SayMessage("hello".to_string())); +world.entity_mut(animation_player).observe(|trigger: Trigger| { + println!("played on", trigger.target()); +}) + +// New +#[derive(AnimationEvent)] +struct SayMessage(String); + +animation.add_event(0.2, SayMessage("hello".to_string())); +world.entity_mut(animation_player).observe(|say_message: On| { + println!("played on", say_message.trigger().animation_player); +}) +``` + +For "component lifecycle events", accessing _all_ of the components that triggered the event has changed: + +```rust +// Old +commands.add_observer(|trigger: Trigger| { + info!("{}", trigger.components()); +}); + +// New +commands.add_observer(|add: On| { + info!("{}", add.trigger().components); +}); +``` \ No newline at end of file diff --git a/release-content/migration-guides/observer_triggers.md b/release-content/migration-guides/observer_triggers.md deleted file mode 100644 index 6a2f7d298c594..0000000000000 --- a/release-content/migration-guides/observer_triggers.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Observer Triggers -pull_requests: [19440, 19596] ---- - -The `Trigger` type used inside observers has been renamed to `On` for a cleaner API. - -```rust -// Old -commands.add_observer(|trigger: Trigger| { - info!("Spawned player {}", trigger.entity()); -}); - -// New -commands.add_observer(|event: On| { - info!("Spawned player {}", event.entity()); -}); -``` - -To reduce repetition and improve readability, the `OnAdd`, `OnInsert`, `OnReplace`, `OnRemove`, and `OnDespawn` -observer events have also been renamed to `Add`, `Insert`, `Replace`, `Remove`, and `Despawn` respectively. -In rare cases where the `Add` event conflicts with the `std::ops::Add` trait, you may need to disambiguate, -for example by using `ops::Add` for the trait. - -Observers may be triggered on particular entities or globally. -Previously, a global trigger would claim to trigger on a particular `Entity`, `Entity::PLACEHOLDER`. -For correctness and transparency, triggers have been changed to `Option`. - -`On::entity` (previously `Trigger::target`) now returns `Option`, and `ObserverTrigger::target` -is now of type `Option`. If you were checking for `Entity::PLACEHOLDER`, migrate to handling the `None` case. -If you were not checking for `Entity::PLACEHOLDER`, migrate to unwrapping, as `Entity::PLACEHOLDER` -would have caused a panic before, at a later point. From a0eec29032a73d7bed14e76417ae9a24ff96e189 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 3 Sep 2025 18:30:18 -0700 Subject: [PATCH 21/44] Use event.trigger().components and remove `event.triggered_components()` --- crates/bevy_ecs/src/bundle/insert.rs | 20 ++++++++++++++------ crates/bevy_ecs/src/bundle/remove.rs | 8 ++++++-- crates/bevy_ecs/src/bundle/spawner.rs | 8 ++++++-- crates/bevy_ecs/src/event/trigger.rs | 9 +++++++-- crates/bevy_ecs/src/observer/mod.rs | 16 +++++++++++----- crates/bevy_ecs/src/observer/system_param.rs | 12 +----------- crates/bevy_ecs/src/world/deferred_world.rs | 8 ++++++-- crates/bevy_ecs/src/world/entity_ref.rs | 12 +++++++++--- 8 files changed, 60 insertions(+), 33 deletions(-) diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index 3080cc35ceefb..0603b1302cdcd 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -169,7 +169,9 @@ impl<'w> BundleInserter<'w> { deferred_world.trigger_raw( REPLACE, &mut Replace { entity }, - &mut EntityComponentsTrigger(&archetype_after_insert.existing), + &mut EntityComponentsTrigger { + components: &archetype_after_insert.existing, + }, caller, ); } @@ -354,7 +356,9 @@ impl<'w> BundleInserter<'w> { deferred_world.trigger_raw( ADD, &mut Add { entity }, - &mut EntityComponentsTrigger(&archetype_after_insert.added), + &mut EntityComponentsTrigger { + components: &archetype_after_insert.added, + }, caller, ); } @@ -374,9 +378,11 @@ impl<'w> BundleInserter<'w> { &mut Insert { entity }, // PERF: this is not a regression from what we were doing before, but ideally we don't // need to collect here - &mut EntityComponentsTrigger( - &archetype_after_insert.iter_inserted().collect::>(), - ), + &mut EntityComponentsTrigger { + components: &archetype_after_insert + .iter_inserted() + .collect::>(), + }, caller, ); } @@ -395,7 +401,9 @@ impl<'w> BundleInserter<'w> { deferred_world.trigger_raw( INSERT, &mut Insert { entity }, - &mut EntityComponentsTrigger(&archetype_after_insert.added), + &mut EntityComponentsTrigger { + components: &archetype_after_insert.added, + }, caller, ); } diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index ca0ea3ac3b83d..9e48e5be4c737 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -155,7 +155,9 @@ impl<'w> BundleRemover<'w> { deferred_world.trigger_raw( REPLACE, &mut Replace { entity }, - &mut EntityComponentsTrigger(&components), + &mut EntityComponentsTrigger { + components: &components, + }, caller, ); } @@ -171,7 +173,9 @@ impl<'w> BundleRemover<'w> { deferred_world.trigger_raw( REMOVE, &mut Remove { entity }, - &mut EntityComponentsTrigger(&components), + &mut EntityComponentsTrigger { + components: &components, + }, caller, ); } diff --git a/crates/bevy_ecs/src/bundle/spawner.rs b/crates/bevy_ecs/src/bundle/spawner.rs index 90b2ddefe0a37..1719725be404a 100644 --- a/crates/bevy_ecs/src/bundle/spawner.rs +++ b/crates/bevy_ecs/src/bundle/spawner.rs @@ -139,7 +139,9 @@ impl<'w> BundleSpawner<'w> { deferred_world.trigger_raw( ADD, &mut Add { entity }, - &mut EntityComponentsTrigger(bundle_info.contributed_components()), + &mut EntityComponentsTrigger { + components: bundle_info.contributed_components(), + }, caller, ); } @@ -154,7 +156,9 @@ impl<'w> BundleSpawner<'w> { deferred_world.trigger_raw( INSERT, &mut Insert { entity }, - &mut EntityComponentsTrigger(bundle_info.contributed_components()), + &mut EntityComponentsTrigger { + components: bundle_info.contributed_components(), + }, caller, ); } diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index a60bbf849cb24..9cfff05fba38c 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -230,7 +230,12 @@ impl> Trigger /// /// This is used by Bevy's built-in [lifecycle events](crate::lifecycle). #[derive(Default)] -pub struct EntityComponentsTrigger<'a>(pub &'a [ComponentId]); +pub struct EntityComponentsTrigger<'a> { + /// All of the components whose observers were triggered together for the target entity. For example, + /// if components `A` and `B` are added together, producing the [`Add`](crate::lifecycle::Add) event, this will + /// contain the [`ComponentId`] for both `A` and `B`. + pub components: &'a [ComponentId], +} impl<'a, E: EntityEvent> Trigger for EntityComponentsTrigger<'a> { fn trigger( @@ -265,7 +270,7 @@ impl<'a> EntityComponentsTrigger<'a> { ); // Trigger observers watching for a specific component - for id in self.0 { + for id in self.components { if let Some(component_observers) = observers.component_observers().get(id) { for (observer, runner) in component_observers.global_observers() { (runner)( diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 03bf2451d080a..2e5262d35ec81 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -605,7 +605,9 @@ mod tests { // trigger for an entity and a component world.trigger_with( EntityComponentsEvent(entity_1), - EntityComponentsTrigger(&[component_a]), + EntityComponentsTrigger { + components: &[component_a], + }, ); // only observer that doesn't trigger is the one only watching entity_2 assert_eq!(1111101, world.resource::().0); @@ -614,11 +616,11 @@ mod tests { // trigger for both entities, but no components: trigger once per entity target world.trigger_with( EntityComponentsEvent(entity_1), - EntityComponentsTrigger(&[]), + EntityComponentsTrigger { components: &[] }, ); world.trigger_with( EntityComponentsEvent(entity_2), - EntityComponentsTrigger(&[]), + EntityComponentsTrigger { components: &[] }, ); // only the observer that doesn't require components triggers - once per entity @@ -629,11 +631,15 @@ mod tests { // we only get 2222211 because a given observer can trigger only once per entity target world.trigger_with( EntityComponentsEvent(entity_1), - EntityComponentsTrigger(&[component_a, component_b]), + EntityComponentsTrigger { + components: &[component_a, component_b], + }, ); world.trigger_with( EntityComponentsEvent(entity_2), - EntityComponentsTrigger(&[component_a, component_b]), + EntityComponentsTrigger { + components: &[component_a, component_b], + }, ); assert_eq!(2222211, world.resource::().0); world.resource_mut::().0 = 0; diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index fe3dda1e07db3..fa453829156fb 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -3,8 +3,7 @@ use crate::{ bundle::Bundle, change_detection::MaybeLocation, - component::ComponentId, - event::{EntityComponentsTrigger, Event, PropagateEntityTrigger}, + event::{Event, PropagateEntityTrigger}, prelude::*, traversal::Traversal, }; @@ -159,15 +158,6 @@ impl< } } -impl<'w, E: EntityEvent + for<'t> Event = EntityComponentsTrigger<'t>>, B: Bundle> - On<'w, E, B> -{ - /// A list of all components that were triggered for this [`EntityEvent`]. - pub fn triggered_components(&self) -> &[ComponentId] { - self.trigger.0 - } -} - impl<'w, E: for<'t> Event: Debug> + Debug, B: Bundle> Debug for On<'w, E, B> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("On") diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 803da2e76288d..96f1e77343dc7 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -172,7 +172,9 @@ impl<'w> DeferredWorld<'w> { self.trigger_raw( REPLACE, &mut Replace { entity }, - &mut EntityComponentsTrigger(&[component_id]), + &mut EntityComponentsTrigger { + components: &[component_id], + }, MaybeLocation::caller(), ); } @@ -212,7 +214,9 @@ impl<'w> DeferredWorld<'w> { self.trigger_raw( INSERT, &mut Insert { entity }, - &mut EntityComponentsTrigger(&[component_id]), + &mut EntityComponentsTrigger { + components: &[component_id], + }, MaybeLocation::caller(), ); } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index c7a319c258ba3..de1a509e7d0b8 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2582,7 +2582,9 @@ impl<'w> EntityWorldMut<'w> { &mut Despawn { entity: self.entity, }, - &mut EntityComponentsTrigger(archetype.components()), + &mut EntityComponentsTrigger { + components: archetype.components(), + }, caller, ); } @@ -2598,7 +2600,9 @@ impl<'w> EntityWorldMut<'w> { &mut Replace { entity: self.entity, }, - &mut EntityComponentsTrigger(archetype.components()), + &mut EntityComponentsTrigger { + components: archetype.components(), + }, caller, ); } @@ -2615,7 +2619,9 @@ impl<'w> EntityWorldMut<'w> { &mut Remove { entity: self.entity, }, - &mut EntityComponentsTrigger(archetype.components()), + &mut EntityComponentsTrigger { + components: archetype.components(), + }, caller, ); } From 4d6970418a6cf2a25191902753c843182350f273 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 3 Sep 2025 18:34:58 -0700 Subject: [PATCH 22/44] typos --- release-content/release-notes/observer_overhaul.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-content/release-notes/observer_overhaul.md b/release-content/release-notes/observer_overhaul.md index 1dac36edf9ee0..2108794da118c 100644 --- a/release-content/release-notes/observer_overhaul.md +++ b/release-content/release-notes/observer_overhaul.md @@ -14,7 +14,7 @@ In **Bevy 0.17** we have sorted out these issues ... without fundamentally chang ## The Rearchitecture -The `Event` trait has been reframed / refocused to increase flexibilty, make the API more static, and remove specialized cruft: +The `Event` trait has been reframed / refocused to increase flexibility, make the API more static, and remove specialized cruft: ```rust // Old: Bevy 0.16 @@ -32,7 +32,7 @@ trait Event { } ``` -Every [`Event`] now has an associated [`Trigger`] implemenation. The [`Trigger`] trait defines the behavior of `world.trigger()` for that event. [`Trigger`] defines which observers will run, the order they will run in, and the data that is passed to them. +Every [`Event`] now has an associated [`Trigger`] implementation. The [`Trigger`] trait defines the behavior of `world.trigger()` for that event. [`Trigger`] defines which observers will run, the order they will run in, and the data that is passed to them. By representing this in the type system, we can constrain behaviors and data to _specific_ types of events statically, making it impossible to "misuse" an [`Event`]. All of Bevy's existing "flavors" of events have been ported to the new [`Event`] / [`Trigger`] system. From 99d6fc451b8cd9375cd5439c38eb261dac6147c6 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 3 Sep 2025 23:13:19 -0700 Subject: [PATCH 23/44] fmt --- release-content/migration-guides/observer_and_event_changes.md | 2 +- release-content/release-notes/observer_overhaul.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/release-content/migration-guides/observer_and_event_changes.md b/release-content/migration-guides/observer_and_event_changes.md index 147d3ce7385a4..bc30a7d25a89a 100644 --- a/release-content/migration-guides/observer_and_event_changes.md +++ b/release-content/migration-guides/observer_and_event_changes.md @@ -139,4 +139,4 @@ commands.add_observer(|trigger: Trigger| { commands.add_observer(|add: On| { info!("{}", add.trigger().components); }); -``` \ No newline at end of file +``` diff --git a/release-content/release-notes/observer_overhaul.md b/release-content/release-notes/observer_overhaul.md index 2108794da118c..c18cd08d493e5 100644 --- a/release-content/release-notes/observer_overhaul.md +++ b/release-content/release-notes/observer_overhaul.md @@ -231,7 +231,6 @@ In **Bevy 0.17**, [`Event`] is now _exclusively_ the name/trait for the concept It is still possible to support both contexts by implementing _both traits_, but we expect that to be significantly less common than just choosing one. - [`Event`]: https://dev-docs.bevy.org/bevy/ecs/event/trait.Event.html [`Trigger`]: `https://dev-docs.bevy.org/bevy/ecs/event/trait.Trigger.html` [`GlobalTrigger`]: `https://dev-docs.bevy.org/bevy/ecs/event/type.GlobalTrigger.html` @@ -242,4 +241,4 @@ It is still possible to support both contexts by implementing _both traits_, but [`EntityComponentsTrigger`]: `https://dev-docs.bevy.org/bevy/ecs/event/type.EntityComponentsTrigger.html` [`AnimationPlayer`]: https://dev-docs.bevy.org/bevy/animation/struct.AnimationPlayer.html [`AnimationEvent`]: https://dev-docs.bevy.org/bevy/animation/trait.AnimationEvent.html -[`AnimationEventTrigger`]: https://dev-docs.bevy.org/bevy/animation/type.AnimationEventTrigger.html \ No newline at end of file +[`AnimationEventTrigger`]: https://dev-docs.bevy.org/bevy/animation/type.AnimationEventTrigger.html From 67b2730b6537628de306b36653937a3c9a6d24a3 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 4 Sep 2025 14:44:07 -0400 Subject: [PATCH 24/44] Simple fixes for migration guide Co-authored-by: Jan Hohenheim --- .../migration-guides/observer_and_event_changes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-content/migration-guides/observer_and_event_changes.md b/release-content/migration-guides/observer_and_event_changes.md index bc30a7d25a89a..9c4d363ff0424 100644 --- a/release-content/migration-guides/observer_and_event_changes.md +++ b/release-content/migration-guides/observer_and_event_changes.md @@ -8,7 +8,7 @@ The observer "trigger" API has changed a bit to improve clarity and type-safety. ```rust // Old commands.add_observer(|trigger: Trigger| { - info!("Spawned player {}", trigger.entity()); + info!("Spawned player {}", trigger.target()); }); // New @@ -47,7 +47,7 @@ world.trigger(Explode { entity }); Triggering an entity event for multiple entities now requires multiple calls to `trigger`: ```rust -// OLd +// Old world.trigger_targets(Explode, [e1, e2]); // New - Variant 1 From 8609182331fb88d0118e0247990229a058bdd6a9 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 4 Sep 2025 13:49:34 -0700 Subject: [PATCH 25/44] Deprecated alias for On::target --- crates/bevy_ecs/src/observer/system_param.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index fa453829156fb..bcf1a5761c035 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -127,6 +127,19 @@ impl< T: Traversal, > On<'w, E, B> { + /// A deprecated way to retrieve the entity that this [`EntityEvent`] targeted at. + /// + /// Access the event via [`On::event`], then read the entity that the event was targeting. + /// Prefer using the field name directly for clarity, + /// but if you are working in a generic context, you can use [`EntityEvent::event_target`]. + #[deprecated( + since = "0.17.0", + note = "Call On::event() to access the event, then read the target entity from the event directly." + )] + pub fn target(&self) -> Entity { + self.event.event_target() + } + /// Returns the original [`Entity`] that this [`EntityEvent`] targeted via [`EntityEvent::event_target`] when it was _first_ triggered, /// prior to any propagation logic. pub fn original_event_target(&self) -> Entity { From a27d862c47c0b23f41d3e1ef10c462cadf1a3d3e Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 4 Sep 2025 13:54:05 -0700 Subject: [PATCH 26/44] Deprecated trigger_targets methods --- crates/bevy_ecs/src/observer/mod.rs | 10 ++++++++++ crates/bevy_ecs/src/system/commands/mod.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 2e5262d35ec81..e478b3272d1d7 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -70,6 +70,16 @@ impl World { ); } + /// A deprecated alias for [`trigger`](Self::trigger) to ease migration. + /// + /// Instead of specifying the trigger target separately, + /// information about the target of the event is embedded in the data held by + /// the event type itself. + #[deprecated(since = "0.17.0", note = "Use `World::trigger` instead.")] + pub fn trigger_targets<'a>(&mut self, event: impl Event: Default>) { + self.trigger(event) + } + /// Triggers the given [`Event`] using the given [`Trigger`](crate::event::Trigger), which will run any [`Observer`]s watching for it. /// /// For a variant that borrows the `event` rather than consuming it, use [`World::trigger_ref`] instead. diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 87652c3e78539..787fca4b58fe3 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1091,6 +1091,16 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::trigger(event)); } + /// A deprecated alias for [`trigger`](Self::trigger) to ease migration. + /// + /// Instead of specifying the trigger target separately, + /// information about the target of the event is embedded in the data held by + /// the event type itself. + #[deprecated(since = "0.17.0", note = "Use `Commands::trigger` instead.")] + pub fn trigger_targets<'a>(&mut self, event: impl Event: Default>) { + self.trigger(event) + } + /// Triggers the given [`Event`] using the given [`Trigger`], which will run any [`Observer`]s watching for it. /// /// [`Trigger`]: crate::event::Trigger From 76e3eefc5245262db72496dbc2994a79d50afa2f Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 4 Sep 2025 15:25:10 -0700 Subject: [PATCH 27/44] Clarify macro error message for unit struct EntityEvent derives --- crates/bevy_ecs/macros/src/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 67bacf3a550c5..4e4ab17f2616c 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -196,7 +196,7 @@ fn get_event_target_field(ast: &DeriveInput) -> Result { )), Fields::Unit => Err(syn::Error::new( fields.span(), - "EntityEvent derive expected named or unnamed struct, found unit struct.", + "EntityEvent derive does not work on unit structs. Your type must have a field to store the Entity target, such as `Message(Entity)` or `Message { entity: entity }`.", )), } } From 3aff45dbf0a1899c2a451dc8dc88ecac656e0a2d Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 4 Sep 2025 15:25:44 -0700 Subject: [PATCH 28/44] Polish for error message --- crates/bevy_ecs/macros/src/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 4e4ab17f2616c..c41582e577570 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -196,7 +196,7 @@ fn get_event_target_field(ast: &DeriveInput) -> Result { )), Fields::Unit => Err(syn::Error::new( fields.span(), - "EntityEvent derive does not work on unit structs. Your type must have a field to store the Entity target, such as `Message(Entity)` or `Message { entity: entity }`.", + "EntityEvent derive does not work on unit structs. Your type must have a field to store the `Entity` target, such as `Message(Entity)` or `Message { entity: Entity }`.", )), } } From 0edeb536a2d9b877aa5cf67e216285c0c37ccf07 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 4 Sep 2025 15:28:54 -0700 Subject: [PATCH 29/44] Don't use confusing Message name in error message --- crates/bevy_ecs/macros/src/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index c41582e577570..1980a065e266b 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -196,7 +196,7 @@ fn get_event_target_field(ast: &DeriveInput) -> Result { )), Fields::Unit => Err(syn::Error::new( fields.span(), - "EntityEvent derive does not work on unit structs. Your type must have a field to store the `Entity` target, such as `Message(Entity)` or `Message { entity: Entity }`.", + "EntityEvent derive does not work on unit structs. Your type must have a field to store the `Entity` target, such as `Attack(Entity)` or `Attack { entity: Entity }`.", )), } } From 572230a04106a2d239bb246c5bb957ca0533f0e5 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 4 Sep 2025 16:27:50 -0700 Subject: [PATCH 30/44] Add 't lifetime to `On` --- crates/bevy_ecs/src/observer/runner.rs | 2 +- crates/bevy_ecs/src/observer/system_param.rs | 27 ++++++++++--------- crates/bevy_ecs/src/system/input.rs | 6 ++--- crates/bevy_ecs/src/system/observer_system.rs | 6 ++--- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index dba3fb9150092..a755c73c7bd14 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -41,7 +41,7 @@ pub(super) fn observer_system_runner` let trigger: &mut E::Trigger<'_> = unsafe { trigger_ptr.deref_mut() }; let on: On = On::new( diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index bcf1a5761c035..49499eb4d0a41 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -28,24 +28,24 @@ use core::{ /// Providing multiple components in this bundle will cause this event to be triggered by any /// matching component in the bundle, /// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). -pub struct On<'w, E: Event, B: Bundle = ()> { +pub struct On<'w, 't, E: Event, B: Bundle = ()> { observer: Entity, event: &'w mut E, - trigger: &'w mut E::Trigger<'w>, - trigger_context: &'w TriggerContext, + trigger: &'w mut E::Trigger<'t>, + pub(crate) trigger_context: &'w TriggerContext, _marker: PhantomData, } /// Deprecated in favor of [`On`]. #[deprecated(since = "0.17.0", note = "Renamed to `On`.")] -pub type Trigger<'w, E, B = ()> = On<'w, E, B>; +pub type Trigger<'w, 't, E, B = ()> = On<'w, 't, E, B>; -impl<'w, E: Event, B: Bundle> On<'w, E, B> { +impl<'w, 't, E: Event, B: Bundle> On<'w, 't, E, B> { /// Creates a new instance of [`On`] for the given triggered event. pub fn new( event: &'w mut E, observer: Entity, - trigger: &'w mut E::Trigger<'w>, + trigger: &'w mut E::Trigger<'t>, trigger_context: &'w TriggerContext, ) -> Self { Self { @@ -78,12 +78,12 @@ impl<'w, E: Event, B: Bundle> On<'w, E, B> { } /// Returns the [`Trigger`](crate::event::Trigger) context for this event. - pub fn trigger(&self) -> &E::Trigger<'w> { + pub fn trigger(&self) -> &E::Trigger<'t> { self.trigger } /// Returns the mutable [`Trigger`](crate::event::Trigger) context for this event. - pub fn trigger_mut(&mut self) -> &mut E::Trigger<'w> { + pub fn trigger_mut(&mut self) -> &mut E::Trigger<'t> { self.trigger } @@ -121,11 +121,12 @@ impl<'w, E: Event, B: Bundle> On<'w, E, B> { impl< 'w, + 't, const AUTO_PROPAGATE: bool, - E: EntityEvent + for<'t> Event = PropagateEntityTrigger>, + E: EntityEvent + for<'a> Event = PropagateEntityTrigger>, B: Bundle, T: Traversal, - > On<'w, E, B> + > On<'w, 't, E, B> { /// A deprecated way to retrieve the entity that this [`EntityEvent`] targeted at. /// @@ -171,7 +172,7 @@ impl< } } -impl<'w, E: for<'t> Event: Debug> + Debug, B: Bundle> Debug for On<'w, E, B> { +impl<'w, 't, E: for<'a> Event: Debug> + Debug, B: Bundle> Debug for On<'w, 't, E, B> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("On") .field("event", &self.event) @@ -181,7 +182,7 @@ impl<'w, E: for<'t> Event: Debug> + Debug, B: Bundle> Debug for On<' } } -impl<'w, E: Event, B: Bundle> Deref for On<'w, E, B> { +impl<'w, 't, E: Event, B: Bundle> Deref for On<'w, 't, E, B> { type Target = E; fn deref(&self) -> &Self::Target { @@ -189,7 +190,7 @@ impl<'w, E: Event, B: Bundle> Deref for On<'w, E, B> { } } -impl<'w, E: Event, B: Bundle> DerefMut for On<'w, E, B> { +impl<'w, 't, E: Event, B: Bundle> DerefMut for On<'w, 't, E, B> { fn deref_mut(&mut self) -> &mut Self::Target { self.event } diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 50f2a85ccde94..1b83afbf2570a 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -222,9 +222,9 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// Used for [`ObserverSystem`]s. /// /// [`ObserverSystem`]: crate::system::ObserverSystem -impl SystemInput for On<'_, E, B> { - type Param<'i> = On<'i, E, B>; - type Inner<'i> = On<'i, E, B>; +impl SystemInput for On<'_, '_, E, B> { + type Param<'i> = On<'i, 'i, E, B>; + type Inner<'i> = On<'i, 'i, E, B>; fn wrap(this: Self::Inner<'_>) -> Self::Param<'_> { this diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 6f4abddd84379..fe612a3d8bc53 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -8,12 +8,12 @@ use super::IntoSystem; /// Implemented for [`System`]s that have [`On`] as the first argument. pub trait ObserverSystem: - System, Out = Out> + Send + 'static + System, Out = Out> + Send + 'static { } impl ObserverSystem for T where - T: System, Out = Out> + Send + 'static + T: System, Out = Out> + Send + 'static { } @@ -39,7 +39,7 @@ pub trait IntoObserverSystem: Send + 'static { impl IntoObserverSystem for S where - S: IntoSystem, Out, M> + Send + 'static, + S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, E: 'static, B: Bundle, From 5cb86a7b72615da0e2a52733a2264ecff96befec Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 4 Sep 2025 16:35:33 -0700 Subject: [PATCH 31/44] Apply suggestions from code review Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/event/mod.rs | 8 ++++---- release-content/release-notes/observer_overhaul.md | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index d2061ab8b376b..9dbb6554ce0f8 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -19,7 +19,7 @@ use core::marker::PhantomData; /// using [`Commands::trigger`](crate::system::Commands::trigger). This causes any [`Observer`](crate::observer::Observer) watching for that /// [`Event`] to run _immediately_, as part of the [`World::trigger`] call. /// -/// The [`Event`] trait should generally be derived: +/// First, we create an [`Event`] type, typically by deriving the trait. /// /// ``` /// # use bevy_ecs::prelude::*; @@ -30,7 +30,7 @@ use core::marker::PhantomData; /// } /// ``` /// -/// An [`Observer`](crate::observer::Observer) can then be added to watch for this event type: +/// Then, we add an [`Observer`](crate::observer::Observer) to watch for this event type: /// /// ``` /// # use bevy_ecs::prelude::*; @@ -47,7 +47,7 @@ use core::marker::PhantomData; /// }); /// ``` /// -/// The event can be triggered on the [`World`] using the [`trigger`](World::trigger) method: +/// Finally, we trigger the event by calling [`World::trigger`](World::trigger): /// /// ``` /// # use bevy_ecs::prelude::*; @@ -72,7 +72,7 @@ use core::marker::PhantomData; /// /// # Triggers /// -/// Every [`Event`] has an associated [`Trigger`] implementation (via [`Event::Trigger`]), which defines which observers will run, +/// Every [`Event`] has an associated [`Trigger`] implementation (set via [`Event::Trigger`]), which defines which observers will run, /// what data will be passed to them, and the order they will be run in. Unless you are an internals developer or you have very specific /// needs, you don't need to worry too much about [`Trigger`]. When you derive [`Event`] (or a more specific event trait like [`EntityEvent`]), /// a [`Trigger`] will be provided for you. diff --git a/release-content/release-notes/observer_overhaul.md b/release-content/release-notes/observer_overhaul.md index c18cd08d493e5..143585f798e0e 100644 --- a/release-content/release-notes/observer_overhaul.md +++ b/release-content/release-notes/observer_overhaul.md @@ -10,7 +10,7 @@ Bevy's Observer API landed a few releases ago, and it has quickly become one of 2. **The API was not "static" enough**: This relates to (1). Because a given [`Event`] type could be used by and produced for _any context_, we had to provide access to _every possible API_ for _every event type_. It should not be possible to trigger an "entity event" without an entity! An Observer of an event that was not designed to have a target entity should not have an `entity()` field! Every [`Event`] impl had to define an "entity propagation traversal", even it was not designed to propagate (and even if it didn't target entities at all!). Events should be self documenting, impossible to produce or consume in the wrong context, and should only encode the information that is necessary for that event. 3. **The API did too much work**: Because events could be produced and used in any context, this meant that they all branched through code for every possible context. This incurred unnecessary overhead. It also resulted in lots of unnecessary codegen! -In **Bevy 0.17** we have sorted out these issues ... without fundamentally changing the shape of the API. Migrations should generally be very straightforward. +In **Bevy 0.17** we have sorted out these issues without fundamentally changing the shape of the API. Migrations should generally be very straightforward. ## The Rearchitecture @@ -37,7 +37,7 @@ Every [`Event`] now has an associated [`Trigger`] implementation. The [`Trigger` By representing this in the type system, we can constrain behaviors and data to _specific_ types of events statically, making it impossible to "misuse" an [`Event`]. All of Bevy's existing "flavors" of events have been ported to the new [`Event`] / [`Trigger`] system. -## Event: global by default +## `Event`: global by default At a glance, the default [`Event`] derive and usage hasn't changed much. Just some shorter / clearer naming. The old API looked like this: @@ -68,7 +68,7 @@ world.add_observer(|game_over: On| { Internally things are a bit different though! The [`Event`] derive defaults to being "untargeted" / "global", by setting the `Event::Trigger` to [`GlobalTrigger`]. When it is triggered, only "untargeted" top-level observers will run, and there is _no way_ to trigger it in a different context (ex: events with a [`GlobalTrigger`] cannot target entities!). -## EntityEvent +## `EntityEvent`: a dedicated trait for entity-targeting events In previous versions of Bevy, _any_ event could optionally be triggered for an entity. It looked like this: From 5a323eafbe78eba10f3d1a83303f3ed24543fd49 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 4 Sep 2025 16:43:03 -0700 Subject: [PATCH 32/44] clippy --- crates/bevy_ecs/src/observer/mod.rs | 2 +- crates/bevy_ecs/src/system/commands/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index e478b3272d1d7..43edfdd4ccef6 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -77,7 +77,7 @@ impl World { /// the event type itself. #[deprecated(since = "0.17.0", note = "Use `World::trigger` instead.")] pub fn trigger_targets<'a>(&mut self, event: impl Event: Default>) { - self.trigger(event) + self.trigger(event); } /// Triggers the given [`Event`] using the given [`Trigger`](crate::event::Trigger), which will run any [`Observer`]s watching for it. diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 787fca4b58fe3..634e3caee8934 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1098,7 +1098,7 @@ impl<'w, 's> Commands<'w, 's> { /// the event type itself. #[deprecated(since = "0.17.0", note = "Use `Commands::trigger` instead.")] pub fn trigger_targets<'a>(&mut self, event: impl Event: Default>) { - self.trigger(event) + self.trigger(event); } /// Triggers the given [`Event`] using the given [`Trigger`], which will run any [`Observer`]s watching for it. From 1f551ba07f24c6b364d6c8c97f6b69dd84bc41d5 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 7 Sep 2025 14:59:10 -0700 Subject: [PATCH 33/44] Mark trait and methods unsafe with stub safety comments --- crates/bevy_animation/src/animation_event.rs | 9 ++++-- crates/bevy_animation/src/lib.rs | 2 +- crates/bevy_ecs/src/event/trigger.rs | 30 +++++++++++++------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/crates/bevy_animation/src/animation_event.rs b/crates/bevy_animation/src/animation_event.rs index 5422ba041af3d..398b516b4f666 100644 --- a/crates/bevy_animation/src/animation_event.rs +++ b/crates/bevy_animation/src/animation_event.rs @@ -20,8 +20,13 @@ pub struct AnimationEventTrigger { pub animation_player: Entity, } -impl Trigger for AnimationEventTrigger { - fn trigger( +#[expect( + unsafe_code, + reason = "We must implement this trait to define a custom Trigger, which is required to be unsafe due to safety considerations within bevy_ecs." +)] +unsafe impl Trigger for AnimationEventTrigger { + /// SAFETY: TODO! + unsafe fn trigger( &mut self, world: DeferredWorld, observers: &CachedObservers, diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 2ddff477b8f5a..ac87e41d6787c 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![forbid(unsafe_code)] +#![warn(unsafe_code)] #![doc( html_logo_url = "https://bevy.org/assets/icon.png", html_favicon_url = "https://bevy.org/assets/icon.png" diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 9cfff05fba38c..e16d33a9444c7 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -21,10 +21,16 @@ use core::marker::PhantomData; /// - [`EntityTrigger`]: The [`EntityEvent`] derive defaults to using this /// - [`PropagateEntityTrigger`]: The [`EntityEvent`] derive uses this when propagation is enabled. /// - [`EntityComponentsTrigger`]: Used by Bevy's [component lifecycle events](crate::lifecycle). -pub trait Trigger { +/// +/// SAFETY: TODO! +pub unsafe trait Trigger { /// Trigger the given `event`, running every [`Observer`](crate::observer::Observer) that matches the `event`, as defined by this /// [`Trigger`] and the state stored on `self`. - fn trigger( + /// + /// SAFETY: TODO! + // The safety requirements of this method were prompted by this comment thread: + // https://github.com/bevyengine/bevy/pull/20731#discussion_r2311907935 + unsafe fn trigger( &mut self, world: DeferredWorld, observers: &CachedObservers, @@ -40,8 +46,9 @@ pub trait Trigger { #[derive(Default)] pub struct GlobalTrigger; -impl Trigger for GlobalTrigger { - fn trigger( +unsafe impl Trigger for GlobalTrigger { + /// SAFETY: TODO! + unsafe fn trigger( &mut self, world: DeferredWorld, observers: &CachedObservers, @@ -87,8 +94,9 @@ impl GlobalTrigger { #[derive(Default)] pub struct EntityTrigger; -impl Trigger for EntityTrigger { - fn trigger( +unsafe impl Trigger for EntityTrigger { + /// SAFETY: TODO! + unsafe fn trigger( &mut self, world: DeferredWorld, observers: &CachedObservers, @@ -177,10 +185,11 @@ impl> Default } } -impl> Trigger +unsafe impl> Trigger for PropagateEntityTrigger { - fn trigger( + /// SAFETY: TODO! + unsafe fn trigger( &mut self, mut world: DeferredWorld, observers: &CachedObservers, @@ -237,8 +246,9 @@ pub struct EntityComponentsTrigger<'a> { pub components: &'a [ComponentId], } -impl<'a, E: EntityEvent> Trigger for EntityComponentsTrigger<'a> { - fn trigger( +unsafe impl<'a, E: EntityEvent> Trigger for EntityComponentsTrigger<'a> { + /// SAFETY: TODO! + unsafe fn trigger( &mut self, world: DeferredWorld, observers: &CachedObservers, From 7004cca4ed691d11d132b984cd71f6fba4562063 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 7 Sep 2025 15:07:31 -0700 Subject: [PATCH 34/44] Leave a safety comment on the On type --- crates/bevy_ecs/src/observer/system_param.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index 49499eb4d0a41..20facdc39b920 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -28,6 +28,8 @@ use core::{ /// Providing multiple components in this bundle will cause this event to be triggered by any /// matching component in the bundle, /// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). +// SAFETY: this type must never expose anything with the 'w lifetime other than `E::trigger<'w>` +// See the safety discussion on `Trigger::trigger` for more details. pub struct On<'w, 't, E: Event, B: Bundle = ()> { observer: Entity, event: &'w mut E, From 29825a6c1804c5f0d60707537c357b72f61f786b Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 7 Sep 2025 15:09:46 -0700 Subject: [PATCH 35/44] More safety notes on the tricky trigger_ptr deref --- crates/bevy_ecs/src/observer/runner.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index a755c73c7bd14..9ddda69f31868 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -42,6 +42,8 @@ pub(super) fn observer_system_runner` + // This is enforced by the safety requirements of `Trigger::trigger` + // See the safety discussion on `Trigger::trigger` for more details. let trigger: &mut E::Trigger<'_> = unsafe { trigger_ptr.deref_mut() }; let on: On = On::new( From 2b94023cc4c4adf249a2e3d7c0c76b90a97e5881 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 7 Sep 2025 15:10:10 -0700 Subject: [PATCH 36/44] Clean up generic name for Event pointer deref --- crates/bevy_ecs/src/observer/runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 9ddda69f31868..af5789afda35e 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -47,7 +47,7 @@ pub(super) fn observer_system_runner = unsafe { trigger_ptr.deref_mut() }; let on: On = On::new( - // SAFETY: Caller ensures `ptr` is castable to `&mut T` + // SAFETY: Caller ensures `ptr` is castable to `&mut E` unsafe { event_ptr.deref_mut() }, observer, trigger, From eebd21f1cfd8292b515bdc1220308a1cbc512cc8 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 7 Sep 2025 15:26:15 -0700 Subject: [PATCH 37/44] More stub safety requirements in trigger_raw --- crates/bevy_ecs/src/world/deferred_world.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 96f1e77343dc7..4a0c04b86d005 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -787,7 +787,9 @@ impl<'w> DeferredWorld<'w> { /// Triggers all `event` observers for the given `targets` /// /// # Safety + /// /// Caller must ensure `E` is accessible as the type represented by `event_key` + /// TODO: capture safety requirements of `E::Trigger::trigger` #[inline] pub unsafe fn trigger_raw<'a, E: Event>( &mut self, @@ -807,7 +809,11 @@ impl<'w> DeferredWorld<'w> { (world.into_deferred(), observers) }; let context = TriggerContext { event_key, caller }; - trigger.trigger(world.reborrow(), observers, &context, event); + + // SAFETY: TODO! + unsafe { + trigger.trigger(world.reborrow(), observers, &context, event); + } } /// Sends a global [`Event`] without any targets. From c875f32dca5c6c8b1940cae20190bc52ceb637c3 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 7 Sep 2025 15:26:51 -0700 Subject: [PATCH 38/44] Try to capture the problem in a concise comment --- crates/bevy_ecs/src/event/trigger.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index e16d33a9444c7..0ddaf0b6634f9 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -28,8 +28,26 @@ pub unsafe trait Trigger { /// [`Trigger`] and the state stored on `self`. /// /// SAFETY: TODO! + // To understand why this must be unsafe, we must think carefully about the lifetimes involved. + // + // The core challenge is that the lifetime of the `&mut E::Trigger<'_>` that we want to create + // within our `observer_system_runner` may not be the same as the lifetime provided by Event::Trigger<'a>. + // + // If the lifetimes are the same, then we can safely create a `&mut E::Trigger<'_>` from the `PtrMut` + // passed to the observer runner function, and pass that to the observer system inside of 'On'. + // + // If the lifetimes are not the same, then we must be careful to ensure that the `&mut E::Trigger<'_>` we create + // does not outlive the `PtrMut` that was passed to the observer runner function. + // Failing to do so could lead to use-after-free bugs. + // + // To avoid this, we require that the caller of this function (i.e. the code that triggers the event) + // ensures that TODO. + // + // This is complex, and ways to simplify this would be welcome in the future! // The safety requirements of this method were prompted by this comment thread: // https://github.com/bevyengine/bevy/pull/20731#discussion_r2311907935 + // + // which also discusses some alternative designs that were considered. unsafe fn trigger( &mut self, world: DeferredWorld, From 6a142c9702c4925af4036e9f5ec2ca141e429b16 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 7 Sep 2025 15:33:52 -0700 Subject: [PATCH 39/44] Make observer_system_runner unsafe --- crates/bevy_ecs/src/event/trigger.rs | 87 ++++++++++++++++---------- crates/bevy_ecs/src/observer/runner.rs | 6 +- 2 files changed, 57 insertions(+), 36 deletions(-) diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 0ddaf0b6634f9..41d5d781993fb 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -40,8 +40,12 @@ pub unsafe trait Trigger { // does not outlive the `PtrMut` that was passed to the observer runner function. // Failing to do so could lead to use-after-free bugs. // - // To avoid this, we require that the caller of this function (i.e. the code that triggers the event) - // ensures that TODO. + // This problem leaks into `Trigger::trigger` because the `observer_system_runner` function + // must be called from within `Trigger::trigger`. + // + // We cannot simply ensure that the lifetimes are the same, because the generic associated type + // in `Event::Trigger<'a>` cannot require that the lifetime `'a` is the same as the lifetime + // TODO. // // This is complex, and ways to simplify this would be welcome in the future! // The safety requirements of this method were prompted by this comment thread: @@ -90,13 +94,16 @@ impl GlobalTrigger { world.as_unsafe_world_cell().increment_trigger_id(); } for (observer, runner) in observers.global_observers() { - (runner)( - world.reborrow(), - *observer, - trigger_context, - event.reborrow(), - self.into(), - ); + // SAFETY: TODO! + unsafe { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + self.into(), + ); + } } } } @@ -150,17 +157,8 @@ pub fn trigger_entity_internal( world.as_unsafe_world_cell().increment_trigger_id(); } for (observer, runner) in observers.global_observers() { - (runner)( - world.reborrow(), - *observer, - trigger_context, - event.reborrow(), - trigger.reborrow(), - ); - } - - if let Some(map) = observers.entity_observers().get(&target_entity) { - for (observer, runner) in map { + // SAFETY: TODO! + unsafe { (runner)( world.reborrow(), *observer, @@ -170,6 +168,21 @@ pub fn trigger_entity_internal( ); } } + + if let Some(map) = observers.entity_observers().get(&target_entity) { + for (observer, runner) in map { + // SAFETY: TODO! + unsafe { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + trigger.reborrow(), + ); + } + } + } } /// An [`EntityEvent`] [`Trigger`] that behaves like [`EntityTrigger`], but "propagates" the event @@ -301,20 +314,8 @@ impl<'a> EntityComponentsTrigger<'a> { for id in self.components { if let Some(component_observers) = observers.component_observers().get(id) { for (observer, runner) in component_observers.global_observers() { - (runner)( - world.reborrow(), - *observer, - trigger_context, - event.reborrow(), - self.into(), - ); - } - - if let Some(map) = component_observers - .entity_component_observers() - .get(&entity) - { - for (observer, runner) in map { + // SAFETY: TODO! + unsafe { (runner)( world.reborrow(), *observer, @@ -324,6 +325,24 @@ impl<'a> EntityComponentsTrigger<'a> { ); } } + + if let Some(map) = component_observers + .entity_component_observers() + .get(&entity) + { + for (observer, runner) in map { + // SAFETY: TODO! + unsafe { + (runner)( + world.reborrow(), + *observer, + trigger_context, + event.reborrow(), + self.into(), + ); + } + } + } } } } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index af5789afda35e..4c2919891d56c 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -18,9 +18,11 @@ use bevy_ptr::PtrMut; /// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component, /// but can be overridden for custom behavior. pub type ObserverRunner = - fn(DeferredWorld, observer: Entity, &TriggerContext, event: PtrMut, trigger: PtrMut); + unsafe fn(DeferredWorld, observer: Entity, &TriggerContext, event: PtrMut, trigger: PtrMut); -pub(super) fn observer_system_runner>( +// SAFETY: TODO! +// See the safety discussion on `Trigger::trigger` for more details. +pub(super) unsafe fn observer_system_runner>( mut world: DeferredWorld, observer: Entity, trigger_context: &TriggerContext, From 1456ad9666a93191cd28f184697285d7c366f9e6 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 8 Sep 2025 19:52:32 -0700 Subject: [PATCH 40/44] Fill in safety docs and make some things unsafe --- crates/bevy_animation/src/animation_event.rs | 32 ++- crates/bevy_ecs/src/event/trigger.rs | 266 +++++++++++++------ crates/bevy_ecs/src/observer/runner.rs | 12 +- crates/bevy_ecs/src/observer/system_param.rs | 8 +- crates/bevy_ecs/src/world/deferred_world.rs | 9 +- 5 files changed, 232 insertions(+), 95 deletions(-) diff --git a/crates/bevy_animation/src/animation_event.rs b/crates/bevy_animation/src/animation_event.rs index 398b516b4f666..4d009f6d53356 100644 --- a/crates/bevy_animation/src/animation_event.rs +++ b/crates/bevy_animation/src/animation_event.rs @@ -24,8 +24,12 @@ pub struct AnimationEventTrigger { unsafe_code, reason = "We must implement this trait to define a custom Trigger, which is required to be unsafe due to safety considerations within bevy_ecs." )] -unsafe impl Trigger for AnimationEventTrigger { - /// SAFETY: TODO! +// SAFETY: +// - `E`'s [`Event::Trigger`] is contrained to [`AnimationEventTrigger`] +// - The implementation abides by the other safety constraints defined in [`Trigger`] +unsafe impl Event = AnimationEventTrigger>> Trigger + for AnimationEventTrigger +{ unsafe fn trigger( &mut self, world: DeferredWorld, @@ -34,13 +38,21 @@ unsafe impl Trigger for AnimationEventTrigger { event: &mut E, ) { let animation_player = self.animation_player; - trigger_entity_internal( - world, - observers, - event.into(), - self.into(), - animation_player, - trigger_context, - ); + // SAFETY: + // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` + // - the passed in event pointer comes from `event`, which is an `Event` + // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` + // - this abides by the nuances defined in the `Trigger` safety docs + unsafe { + trigger_entity_internal( + world, + observers, + event.into(), + self.into(), + animation_player, + trigger_context, + ); + } } } diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 41d5d781993fb..54b2b273eacdb 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -13,7 +13,7 @@ use core::marker::PhantomData; /// This decides which [`Observer`](crate::observer::Observer)s will run, what data gets passed to them, and the order they will /// be executed in. /// -/// Implementing [`Trigger`] is "advanced-level" terrority, and is generally unnecessary unless you are developing highly specialized +/// Implementing [`Trigger`] is "advanced-level" territory, and is generally unnecessary unless you are developing highly specialized /// [`Event`] trigger logic. /// /// Bevy comes with a number of built-in [`Trigger`] implementations (see their documentation for more info): @@ -22,36 +22,53 @@ use core::marker::PhantomData; /// - [`PropagateEntityTrigger`]: The [`EntityEvent`] derive uses this when propagation is enabled. /// - [`EntityComponentsTrigger`]: Used by Bevy's [component lifecycle events](crate::lifecycle). /// -/// SAFETY: TODO! +/// # Safety +/// +/// Implementing this properly is _advanced_ soundness territory! Implementers must abide by the following: +/// +/// - The `E`' [`Event::Trigger`] must be constrained to the implemented [`Trigger`] type, as part of the implementation. +/// This prevents other [`Trigger`] implementations from directly deferring to your implementation, which is a very easy +/// soundness misstep, as most [`Trigger`] implementations will invoke observers that are developed _for their specific [`Trigger`] type_. +/// Without this constraint, something like [`GlobalTrigger`] could be called for _any_ [`Event`] type, even one that expects a different +/// [`Trigger`] type. This would result in an unsound cast of [`GlobalTrigger`] reference. +/// This is not expressed as an explicit type constraint,, as the `for<'a> Event::Trigger<'a>` lifetime can mismatch explicit lifetimes in +/// some impls. +/// - Read, understand, and abide by the lifetime constraints defined in the section below: +/// +/// To understand why this must be unsafe, we must think carefully about the lifetimes involved. +/// +/// The core challenge is that the lifetime of the `&mut E::Trigger<'_>` that we want to create +/// within the [`ObserverRunner`] (which calls the [`Observer`](crate::observer::Observer)) may not be the same as the lifetime provided +/// by [`Event::Trigger<'a>`]. +/// +/// If the lifetimes are the same, then we can safely create a `&mut E::Trigger<'_>` from the [`PtrMut`] +/// passed to the observer runner function, and pass that to the observer system inside of ['On'](crate::observer::On). +/// +/// If the lifetimes are not the same, then we must be careful to ensure that the `&mut E::Trigger<'_>` we create +/// does not outlive the [`PtrMut`] that was passed to the observer runner function. +/// Failing to do so could lead to use-after-free bugs. +/// +/// This problem leaks into [`Trigger::trigger`] because the [`ObserverRunner`] function +/// is called from within [`Trigger::trigger`], for most real-world [`Trigger`] implementations. +/// +/// This is complex, and ways to simplify this would be welcome in the future! +/// The safety requirements of this trait were prompted by this comment thread: +/// +/// +/// which also discusses some alternative designs that were considered. +/// +/// [`ObserverRunner`]: crate::observer::ObserverRunner pub unsafe trait Trigger { /// Trigger the given `event`, running every [`Observer`](crate::observer::Observer) that matches the `event`, as defined by this /// [`Trigger`] and the state stored on `self`. /// - /// SAFETY: TODO! - // To understand why this must be unsafe, we must think carefully about the lifetimes involved. - // - // The core challenge is that the lifetime of the `&mut E::Trigger<'_>` that we want to create - // within our `observer_system_runner` may not be the same as the lifetime provided by Event::Trigger<'a>. - // - // If the lifetimes are the same, then we can safely create a `&mut E::Trigger<'_>` from the `PtrMut` - // passed to the observer runner function, and pass that to the observer system inside of 'On'. - // - // If the lifetimes are not the same, then we must be careful to ensure that the `&mut E::Trigger<'_>` we create - // does not outlive the `PtrMut` that was passed to the observer runner function. - // Failing to do so could lead to use-after-free bugs. - // - // This problem leaks into `Trigger::trigger` because the `observer_system_runner` function - // must be called from within `Trigger::trigger`. - // - // We cannot simply ensure that the lifetimes are the same, because the generic associated type - // in `Event::Trigger<'a>` cannot require that the lifetime `'a` is the same as the lifetime - // TODO. - // - // This is complex, and ways to simplify this would be welcome in the future! - // The safety requirements of this method were prompted by this comment thread: - // https://github.com/bevyengine/bevy/pull/20731#discussion_r2311907935 - // - // which also discusses some alternative designs that were considered. + /// # Safety + /// - The [`CachedObservers`] `observers` must come from the [`DeferredWorld`] `world` + /// - [`TriggerContext`] must contain an [`EventKey`](crate::event::EventKey) that matches the `E` [`Event`] type + /// - `observers` must correspond to observers compatible with the event type `E` + /// - Read and abide by the "Safety" section defined in the top-level [`Trigger`] docs. Calling this function is + /// unintuitively risky. _Do not use it directly unless you know what you are doing_. Importantly, this should only + /// be called for an `event` whose [`Event::Trigger`] matches this trigger. unsafe fn trigger( &mut self, world: DeferredWorld, @@ -68,8 +85,10 @@ pub unsafe trait Trigger { #[derive(Default)] pub struct GlobalTrigger; -unsafe impl Trigger for GlobalTrigger { - /// SAFETY: TODO! +// SAFETY: +// - `E`'s [`Event::Trigger`] is contrained to [`GlobalTrigger`] +// - The implementation abides by the other safety constraints defined in [`Trigger`] +unsafe impl Event = Self>> Trigger for GlobalTrigger { unsafe fn trigger( &mut self, world: DeferredWorld, @@ -82,7 +101,13 @@ unsafe impl Trigger for GlobalTrigger { } impl GlobalTrigger { - fn trigger_internal( + /// # Safety + /// - `observers` must come from the `world` [`DeferredWorld`], and correspond to observers that match the `event` type + /// - `event` must point to an [`Event`] + /// - The `event` [`Event::Trigger`] must be [`GlobalTrigger`] + /// - `trigger_context`'s [`TriggerContext::event_key`] must correspond to the `event` type. + /// - Read, understand, and abide by the [`Trigger`] safety documentation + unsafe fn trigger_internal( &mut self, mut world: DeferredWorld, observers: &CachedObservers, @@ -94,7 +119,12 @@ impl GlobalTrigger { world.as_unsafe_world_cell().increment_trigger_id(); } for (observer, runner) in observers.global_observers() { - // SAFETY: TODO! + // SAFETY: + // - `observers` come from `world` and match the `event` type, enforced by the call to `trigger_internal` + // - the passed in event pointer is an `Event`, enforced by the call to `trigger_internal` + // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `event`, enforced by `trigger_internal` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_internal` + // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), @@ -119,8 +149,10 @@ impl GlobalTrigger { #[derive(Default)] pub struct EntityTrigger; -unsafe impl Trigger for EntityTrigger { - /// SAFETY: TODO! +// SAFETY: +// - `E`'s [`Event::Trigger`] is contrained to [`EntityTrigger`] +// - The implementation abides by the other safety constraints defined in [`Trigger`] +unsafe impl Event = Self>> Trigger for EntityTrigger { unsafe fn trigger( &mut self, world: DeferredWorld, @@ -129,22 +161,37 @@ unsafe impl Trigger for EntityTrigger { event: &mut E, ) { let entity = event.event_target(); - trigger_entity_internal( - world, - observers, - event.into(), - self.into(), - entity, - trigger_context, - ); + // SAFETY: + // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` + // - the passed in event pointer comes from `event`, which is an `Event` + // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` + // - this abides by the nuances defined in the `Trigger` safety docs + unsafe { + trigger_entity_internal( + world, + observers, + event.into(), + self.into(), + entity, + trigger_context, + ); + } } } /// Trigger observers watching for the given entity event. /// The `target_entity` should match the [`EntityEvent::event_target`] on `event` for logical correctness. +/// +/// # Safety +/// - `observers` must come from the `world` [`DeferredWorld`], and correspond to observers that match the `event` type +/// - `event` must point to an [`Event`] +/// - `trigger` must correspond to the [`Event::Trigger`] type expected by the `event` +/// - `trigger_context`'s [`TriggerContext::event_key`] must correspond to the `event` type. +/// - Read, understand, and abide by the [`Trigger`] safety documentation // Note: this is not an EntityTrigger method because we want to reuse this logic for the entity propagation trigger #[inline(never)] -pub fn trigger_entity_internal( +pub unsafe fn trigger_entity_internal( mut world: DeferredWorld, observers: &CachedObservers, mut event: PtrMut, @@ -157,7 +204,12 @@ pub fn trigger_entity_internal( world.as_unsafe_world_cell().increment_trigger_id(); } for (observer, runner) in observers.global_observers() { - // SAFETY: TODO! + // SAFETY: + // - `observers` come from `world` and match the `event` type, enforced by the call to `trigger_entity_internal` + // - the passed in event pointer is an `Event`, enforced by the call to `trigger_entity_internal` + // - `trigger` is a matching trigger type, enforced by the call to `trigger_entity_internal` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_entity_internal` + // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), @@ -171,7 +223,12 @@ pub fn trigger_entity_internal( if let Some(map) = observers.entity_observers().get(&target_entity) { for (observer, runner) in map { - // SAFETY: TODO! + // SAFETY: + // - `observers` come from `world` and match the `event` type, enforced by the call to `trigger_entity_internal` + // - the passed in event pointer is an `Event`, enforced by the call to `trigger_entity_internal` + // - `trigger` is a matching trigger type, enforced by the call to `trigger_entity_internal` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_entity_internal` + // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), @@ -216,10 +273,15 @@ impl> Default } } -unsafe impl> Trigger - for PropagateEntityTrigger +// SAFETY: +// - `E`'s [`Event::Trigger`] is contrained to [`PropagateEntityTrigger`] +// - The implementation abides by the other safety constraints defined in [`Trigger`] +unsafe impl< + const AUTO_PROPAGATE: bool, + E: EntityEvent + for<'a> Event = Self>, + T: Traversal, + > Trigger for PropagateEntityTrigger { - /// SAFETY: TODO! unsafe fn trigger( &mut self, mut world: DeferredWorld, @@ -229,14 +291,22 @@ unsafe impl> Trigger ) { let mut current_entity = event.event_target(); self.original_event_target = current_entity; - trigger_entity_internal( - world.reborrow(), - observers, - event.into(), - self.into(), - current_entity, - trigger_context, - ); + // SAFETY: + // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` + // - the passed in event pointer comes from `event`, which is an `Event` + // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` + // - this abides by the nuances defined in the `Trigger` safety docs + unsafe { + trigger_entity_internal( + world.reborrow(), + observers, + event.into(), + self.into(), + current_entity, + trigger_context, + ); + } loop { if !self.propagate { @@ -252,14 +322,22 @@ unsafe impl> Trigger } *event.event_target_mut() = current_entity; - trigger_entity_internal( - world.reborrow(), - observers, - event.into(), - self.into(), - current_entity, - trigger_context, - ); + // SAFETY: + // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` + // - the passed in event pointer comes from `event`, which is an `Event` + // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` + // - this abides by the nuances defined in the `Trigger` safety docs + unsafe { + trigger_entity_internal( + world.reborrow(), + observers, + event.into(), + self.into(), + current_entity, + trigger_context, + ); + } } } } @@ -277,8 +355,12 @@ pub struct EntityComponentsTrigger<'a> { pub components: &'a [ComponentId], } -unsafe impl<'a, E: EntityEvent> Trigger for EntityComponentsTrigger<'a> { - /// SAFETY: TODO! +// SAFETY: +// - `E`'s [`Event::Trigger`] is contrained to [`EntityComponentsTrigger`] +// - The implementation abides by the other safety constraints defined in [`Trigger`] +unsafe impl<'a, E: EntityEvent + Event = EntityComponentsTrigger<'a>>> Trigger + for EntityComponentsTrigger<'a> +{ unsafe fn trigger( &mut self, world: DeferredWorld, @@ -287,13 +369,25 @@ unsafe impl<'a, E: EntityEvent> Trigger for EntityComponentsTrigger<'a> { event: &mut E, ) { let entity = event.event_target(); - self.trigger_internal(world, observers, event.into(), entity, trigger_context); + // SAFETY: + // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` + // - the passed in event pointer comes from `event`, which is an `Event` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` + // - this abides by the nuances defined in the `Trigger` safety docs + unsafe { + self.trigger_internal(world, observers, event.into(), entity, trigger_context); + } } } impl<'a> EntityComponentsTrigger<'a> { + /// # Safety + /// - `observers` must come from the `world` [`DeferredWorld`] + /// - `event` must point to an [`Event`] whose [`Event::Trigger`] is [`EntityComponentsTrigger`] + /// - `trigger_context`'s [`TriggerContext::event_key`] must correspond to the `event` type. + /// - Read, understand, and abide by the [`Trigger`] safety documentation #[inline(never)] - fn trigger_internal( + unsafe fn trigger_internal( &mut self, mut world: DeferredWorld, observers: &CachedObservers, @@ -301,20 +395,33 @@ impl<'a> EntityComponentsTrigger<'a> { entity: Entity, trigger_context: &TriggerContext, ) { - trigger_entity_internal( - world.reborrow(), - observers, - event.reborrow(), - self.into(), - entity, - trigger_context, - ); + // SAFETY: + // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` + // - the passed in event pointer comes from `event`, which is an `Event` + // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` + // - this abides by the nuances defined in the `Trigger` safety docs + unsafe { + trigger_entity_internal( + world.reborrow(), + observers, + event.reborrow(), + self.into(), + entity, + trigger_context, + ); + } // Trigger observers watching for a specific component for id in self.components { if let Some(component_observers) = observers.component_observers().get(id) { for (observer, runner) in component_observers.global_observers() { - // SAFETY: TODO! + // SAFETY: + // - `observers` come from `world` and match the `event` type, enforced by the call to `trigger_internal` + // - the passed in event pointer is an `Event`, enforced by the call to `trigger_internal` + // - `trigger` is a matching trigger type, enforced by the call to `trigger_internal` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_internal` + // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), @@ -331,7 +438,12 @@ impl<'a> EntityComponentsTrigger<'a> { .get(&entity) { for (observer, runner) in map { - // SAFETY: TODO! + // SAFETY: + // - `observers` come from `world` and match the `event` type, enforced by the call to `trigger_internal` + // - the passed in event pointer is an `Event`, enforced by the call to `trigger_internal` + // - `trigger` is a matching trigger type, enforced by the call to `trigger_internal` + // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_internal` + // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 4c2919891d56c..5dfa7b2ee9d04 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -17,11 +17,19 @@ use bevy_ptr::PtrMut; /// /// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component, /// but can be overridden for custom behavior. +/// +/// See [`observer_system_runner`] for safety considerations. pub type ObserverRunner = unsafe fn(DeferredWorld, observer: Entity, &TriggerContext, event: PtrMut, trigger: PtrMut); -// SAFETY: TODO! -// See the safety discussion on `Trigger::trigger` for more details. +/// # Safety +/// +/// - `world` must be the [`DeferredWorld`] that the `entity` is defined in +/// - `event_ptr` must match the `E` [`Event`] type. +/// - `trigger_ptr` must match the [`Event::Trigger`] type for `E`. +/// - `trigger_context`'s [`TriggerContext::event_key`] must match the `E` event type. +/// +/// See the safety discussion on [`Trigger`] for more details. pub(super) unsafe fn observer_system_runner>( mut world: DeferredWorld, observer: Entity, diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index 20facdc39b920..d206af0179e42 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -28,12 +28,16 @@ use core::{ /// Providing multiple components in this bundle will cause this event to be triggered by any /// matching component in the bundle, /// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). -// SAFETY: this type must never expose anything with the 'w lifetime other than `E::trigger<'w>` -// See the safety discussion on `Trigger::trigger` for more details. +// SAFETY WARNING! +// this type must _never_ expose anything with the 'w lifetime +// See the safety discussion on `Trigger` for more details. pub struct On<'w, 't, E: Event, B: Bundle = ()> { observer: Entity, + // SAFETY WARNING: never expose this 'w lifetime event: &'w mut E, + // SAFETY WARNING: never expose this 'w lifetime trigger: &'w mut E::Trigger<'t>, + // SAFETY WARNING: never expose this 'w lifetime pub(crate) trigger_context: &'w TriggerContext, _marker: PhantomData, } diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 4a0c04b86d005..eef44ef4a9f6a 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -787,9 +787,7 @@ impl<'w> DeferredWorld<'w> { /// Triggers all `event` observers for the given `targets` /// /// # Safety - /// - /// Caller must ensure `E` is accessible as the type represented by `event_key` - /// TODO: capture safety requirements of `E::Trigger::trigger` + /// - Caller must ensure `E` is accessible as the type represented by `event_key` #[inline] pub unsafe fn trigger_raw<'a, E: Event>( &mut self, @@ -810,7 +808,10 @@ impl<'w> DeferredWorld<'w> { }; let context = TriggerContext { event_key, caller }; - // SAFETY: TODO! + // SAFETY: + // - `observers` comes from `world`, and corresponds to the `event_key`, as it was looked up above + // - trigger_context contains the correct event_key for `event`, as enforced by the call to `trigger_raw` + // - This method is being called for an `event` whose `Event::Trigger` matches, as the input trigger is E::Trigger. unsafe { trigger.trigger(world.reborrow(), observers, &context, event); } From 2efcf291d3b339e080ede37cd18fcb5811eb2abb Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 8 Sep 2025 19:53:46 -0700 Subject: [PATCH 41/44] typos --- crates/bevy_animation/src/animation_event.rs | 2 +- crates/bevy_ecs/src/event/trigger.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/animation_event.rs b/crates/bevy_animation/src/animation_event.rs index 4d009f6d53356..55bf5655855a6 100644 --- a/crates/bevy_animation/src/animation_event.rs +++ b/crates/bevy_animation/src/animation_event.rs @@ -25,7 +25,7 @@ pub struct AnimationEventTrigger { reason = "We must implement this trait to define a custom Trigger, which is required to be unsafe due to safety considerations within bevy_ecs." )] // SAFETY: -// - `E`'s [`Event::Trigger`] is contrained to [`AnimationEventTrigger`] +// - `E`'s [`Event::Trigger`] is constrained to [`AnimationEventTrigger`] // - The implementation abides by the other safety constraints defined in [`Trigger`] unsafe impl Event = AnimationEventTrigger>> Trigger for AnimationEventTrigger diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 54b2b273eacdb..56407ff601ef4 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -86,7 +86,7 @@ pub unsafe trait Trigger { pub struct GlobalTrigger; // SAFETY: -// - `E`'s [`Event::Trigger`] is contrained to [`GlobalTrigger`] +// - `E`'s [`Event::Trigger`] is constrained to [`GlobalTrigger`] // - The implementation abides by the other safety constraints defined in [`Trigger`] unsafe impl Event = Self>> Trigger for GlobalTrigger { unsafe fn trigger( @@ -150,7 +150,7 @@ impl GlobalTrigger { pub struct EntityTrigger; // SAFETY: -// - `E`'s [`Event::Trigger`] is contrained to [`EntityTrigger`] +// - `E`'s [`Event::Trigger`] is constrained to [`EntityTrigger`] // - The implementation abides by the other safety constraints defined in [`Trigger`] unsafe impl Event = Self>> Trigger for EntityTrigger { unsafe fn trigger( @@ -274,7 +274,7 @@ impl> Default } // SAFETY: -// - `E`'s [`Event::Trigger`] is contrained to [`PropagateEntityTrigger`] +// - `E`'s [`Event::Trigger`] is constrained to [`PropagateEntityTrigger`] // - The implementation abides by the other safety constraints defined in [`Trigger`] unsafe impl< const AUTO_PROPAGATE: bool, @@ -356,7 +356,7 @@ pub struct EntityComponentsTrigger<'a> { } // SAFETY: -// - `E`'s [`Event::Trigger`] is contrained to [`EntityComponentsTrigger`] +// - `E`'s [`Event::Trigger`] is constrained to [`EntityComponentsTrigger`] // - The implementation abides by the other safety constraints defined in [`Trigger`] unsafe impl<'a, E: EntityEvent + Event = EntityComponentsTrigger<'a>>> Trigger for EntityComponentsTrigger<'a> From 6d1d80ea79f2c4298dffcf8319c5bd307806be42 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 8 Sep 2025 20:05:19 -0700 Subject: [PATCH 42/44] Fix test --- tests/window/desktop_request_redraw.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/window/desktop_request_redraw.rs b/tests/window/desktop_request_redraw.rs index 5cabc42e23a39..299af50ed03f6 100644 --- a/tests/window/desktop_request_redraw.rs +++ b/tests/window/desktop_request_redraw.rs @@ -82,14 +82,12 @@ fn setup( AnimationActive, )) .observe( - |trigger: On>, mut commands: Commands| match trigger.button { + |click: On>, mut commands: Commands| match click.button { PointerButton::Primary => { - commands.entity(trigger.entity()).insert(AnimationActive); + commands.entity(click.entity).insert(AnimationActive); } PointerButton::Secondary => { - commands - .entity(trigger.entity()) - .remove::(); + commands.entity(click.entity).remove::(); } _ => {} }, From 09352a4cece5bbaa24a77f4bf9c6b377d7e9d737 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 8 Sep 2025 21:12:20 -0700 Subject: [PATCH 43/44] Remove private link --- crates/bevy_ecs/src/observer/runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 5dfa7b2ee9d04..96ed4a76e1a9d 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -18,7 +18,7 @@ use bevy_ptr::PtrMut; /// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component, /// but can be overridden for custom behavior. /// -/// See [`observer_system_runner`] for safety considerations. +/// See `observer_system_runner` for safety considerations. pub type ObserverRunner = unsafe fn(DeferredWorld, observer: Entity, &TriggerContext, event: PtrMut, trigger: PtrMut); From 16aa39816347cf062987d2d3946e50cad96943f9 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Tue, 9 Sep 2025 16:23:12 -0700 Subject: [PATCH 44/44] Respond to feedback --- crates/bevy_ecs/src/bundle/insert.rs | 4 ++ crates/bevy_ecs/src/bundle/remove.rs | 2 + crates/bevy_ecs/src/bundle/spawner.rs | 2 + crates/bevy_ecs/src/event/trigger.rs | 47 ++++----------------- crates/bevy_ecs/src/observer/runner.rs | 12 ++++-- crates/bevy_ecs/src/system/input.rs | 4 ++ crates/bevy_ecs/src/world/deferred_world.rs | 2 + crates/bevy_ecs/src/world/entity_ref.rs | 3 ++ 8 files changed, 34 insertions(+), 42 deletions(-) diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index d3280eef201b5..63f70380d76d2 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -170,6 +170,7 @@ impl<'w> BundleInserter<'w> { if insert_mode == InsertMode::Replace { if archetype.has_replace_observer() { + // SAFETY: the REPLACE event_key corresponds to the Replace event's type deferred_world.trigger_raw( REPLACE, &mut Replace { entity }, @@ -357,6 +358,7 @@ impl<'w> BundleInserter<'w> { caller, ); if new_archetype.has_add_observer() { + // SAFETY: the ADD event_key corresponds to the Add event's type deferred_world.trigger_raw( ADD, &mut Add { entity }, @@ -377,6 +379,7 @@ impl<'w> BundleInserter<'w> { relationship_hook_mode, ); if new_archetype.has_insert_observer() { + // SAFETY: the INSERT event_key corresponds to the Insert event's type deferred_world.trigger_raw( INSERT, &mut Insert { entity }, @@ -402,6 +405,7 @@ impl<'w> BundleInserter<'w> { relationship_hook_mode, ); if new_archetype.has_insert_observer() { + // SAFETY: the INSERT event_key corresponds to the Insert event's type deferred_world.trigger_raw( INSERT, &mut Insert { entity }, diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index 28943ef058470..a3da97f5b6c2c 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -157,6 +157,7 @@ impl<'w> BundleRemover<'w> { }; if self.old_archetype.as_ref().has_replace_observer() { let components = bundle_components_in_archetype().collect::>(); + // SAFETY: the REPLACE event_key corresponds to the Replace event's type deferred_world.trigger_raw( REPLACE, &mut Replace { entity }, @@ -175,6 +176,7 @@ impl<'w> BundleRemover<'w> { ); if self.old_archetype.as_ref().has_remove_observer() { let components = bundle_components_in_archetype().collect::>(); + // SAFETY: the REMOVE event_key corresponds to the Remove event's type deferred_world.trigger_raw( REMOVE, &mut Remove { entity }, diff --git a/crates/bevy_ecs/src/bundle/spawner.rs b/crates/bevy_ecs/src/bundle/spawner.rs index ea1ff26ec469e..bf28ad9ce2862 100644 --- a/crates/bevy_ecs/src/bundle/spawner.rs +++ b/crates/bevy_ecs/src/bundle/spawner.rs @@ -140,6 +140,7 @@ impl<'w> BundleSpawner<'w> { caller, ); if archetype.has_add_observer() { + // SAFETY: the ADD event_key corresponds to the Add event's type deferred_world.trigger_raw( ADD, &mut Add { entity }, @@ -157,6 +158,7 @@ impl<'w> BundleSpawner<'w> { RelationshipHookMode::Run, ); if archetype.has_insert_observer() { + // SAFETY: the INSERT event_key corresponds to the Insert event's type deferred_world.trigger_raw( INSERT, &mut Insert { entity }, diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 56407ff601ef4..45b5e52b06b7b 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -33,31 +33,6 @@ use core::marker::PhantomData; /// [`Trigger`] type. This would result in an unsound cast of [`GlobalTrigger`] reference. /// This is not expressed as an explicit type constraint,, as the `for<'a> Event::Trigger<'a>` lifetime can mismatch explicit lifetimes in /// some impls. -/// - Read, understand, and abide by the lifetime constraints defined in the section below: -/// -/// To understand why this must be unsafe, we must think carefully about the lifetimes involved. -/// -/// The core challenge is that the lifetime of the `&mut E::Trigger<'_>` that we want to create -/// within the [`ObserverRunner`] (which calls the [`Observer`](crate::observer::Observer)) may not be the same as the lifetime provided -/// by [`Event::Trigger<'a>`]. -/// -/// If the lifetimes are the same, then we can safely create a `&mut E::Trigger<'_>` from the [`PtrMut`] -/// passed to the observer runner function, and pass that to the observer system inside of ['On'](crate::observer::On). -/// -/// If the lifetimes are not the same, then we must be careful to ensure that the `&mut E::Trigger<'_>` we create -/// does not outlive the [`PtrMut`] that was passed to the observer runner function. -/// Failing to do so could lead to use-after-free bugs. -/// -/// This problem leaks into [`Trigger::trigger`] because the [`ObserverRunner`] function -/// is called from within [`Trigger::trigger`], for most real-world [`Trigger`] implementations. -/// -/// This is complex, and ways to simplify this would be welcome in the future! -/// The safety requirements of this trait were prompted by this comment thread: -/// -/// -/// which also discusses some alternative designs that were considered. -/// -/// [`ObserverRunner`]: crate::observer::ObserverRunner pub unsafe trait Trigger { /// Trigger the given `event`, running every [`Observer`](crate::observer::Observer) that matches the `event`, as defined by this /// [`Trigger`] and the state stored on `self`. @@ -96,7 +71,14 @@ unsafe impl Event = Self>> Trigger for GlobalTrigger { trigger_context: &TriggerContext, event: &mut E, ) { - self.trigger_internal(world, observers, trigger_context, event.into()); + // SAFETY: + // - The caller of `trigger` ensures that `observers` come from the `world` + // - The passed in event ptr comes from `event`, which is E: Event + // - E: Event::Trigger is constrained to GlobalTrigger + // - The caller of `trigger` ensures that `TriggerContext::event_key` matches `event` + unsafe { + self.trigger_internal(world, observers, trigger_context, event.into()); + } } } @@ -106,7 +88,6 @@ impl GlobalTrigger { /// - `event` must point to an [`Event`] /// - The `event` [`Event::Trigger`] must be [`GlobalTrigger`] /// - `trigger_context`'s [`TriggerContext::event_key`] must correspond to the `event` type. - /// - Read, understand, and abide by the [`Trigger`] safety documentation unsafe fn trigger_internal( &mut self, mut world: DeferredWorld, @@ -166,7 +147,6 @@ unsafe impl Event = Self>> Trigger for E // - the passed in event pointer comes from `event`, which is an `Event` // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { trigger_entity_internal( world, @@ -209,7 +189,6 @@ pub unsafe fn trigger_entity_internal( // - the passed in event pointer is an `Event`, enforced by the call to `trigger_entity_internal` // - `trigger` is a matching trigger type, enforced by the call to `trigger_entity_internal` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_entity_internal` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), @@ -228,7 +207,6 @@ pub unsafe fn trigger_entity_internal( // - the passed in event pointer is an `Event`, enforced by the call to `trigger_entity_internal` // - `trigger` is a matching trigger type, enforced by the call to `trigger_entity_internal` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_entity_internal` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), @@ -275,7 +253,6 @@ impl> Default // SAFETY: // - `E`'s [`Event::Trigger`] is constrained to [`PropagateEntityTrigger`] -// - The implementation abides by the other safety constraints defined in [`Trigger`] unsafe impl< const AUTO_PROPAGATE: bool, E: EntityEvent + for<'a> Event = Self>, @@ -296,7 +273,6 @@ unsafe impl< // - the passed in event pointer comes from `event`, which is an `Event` // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { trigger_entity_internal( world.reborrow(), @@ -327,7 +303,6 @@ unsafe impl< // - the passed in event pointer comes from `event`, which is an `Event` // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { trigger_entity_internal( world.reborrow(), @@ -357,7 +332,6 @@ pub struct EntityComponentsTrigger<'a> { // SAFETY: // - `E`'s [`Event::Trigger`] is constrained to [`EntityComponentsTrigger`] -// - The implementation abides by the other safety constraints defined in [`Trigger`] unsafe impl<'a, E: EntityEvent + Event = EntityComponentsTrigger<'a>>> Trigger for EntityComponentsTrigger<'a> { @@ -373,7 +347,6 @@ unsafe impl<'a, E: EntityEvent + Event = EntityComponentsTrigger<'a> // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` // - the passed in event pointer comes from `event`, which is an `Event` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { self.trigger_internal(world, observers, event.into(), entity, trigger_context); } @@ -385,7 +358,6 @@ impl<'a> EntityComponentsTrigger<'a> { /// - `observers` must come from the `world` [`DeferredWorld`] /// - `event` must point to an [`Event`] whose [`Event::Trigger`] is [`EntityComponentsTrigger`] /// - `trigger_context`'s [`TriggerContext::event_key`] must correspond to the `event` type. - /// - Read, understand, and abide by the [`Trigger`] safety documentation #[inline(never)] unsafe fn trigger_internal( &mut self, @@ -400,7 +372,6 @@ impl<'a> EntityComponentsTrigger<'a> { // - the passed in event pointer comes from `event`, which is an `Event` // - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { trigger_entity_internal( world.reborrow(), @@ -421,7 +392,6 @@ impl<'a> EntityComponentsTrigger<'a> { // - the passed in event pointer is an `Event`, enforced by the call to `trigger_internal` // - `trigger` is a matching trigger type, enforced by the call to `trigger_internal` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_internal` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), @@ -443,7 +413,6 @@ impl<'a> EntityComponentsTrigger<'a> { // - the passed in event pointer is an `Event`, enforced by the call to `trigger_internal` // - `trigger` is a matching trigger type, enforced by the call to `trigger_internal` // - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_internal` - // - this abides by the nuances defined in the `Trigger` safety docs unsafe { (runner)( world.reborrow(), diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 96ed4a76e1a9d..dfffe3bec60cd 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -29,7 +29,9 @@ pub type ObserverRunner = /// - `trigger_ptr` must match the [`Event::Trigger`] type for `E`. /// - `trigger_context`'s [`TriggerContext::event_key`] must match the `E` event type. /// -/// See the safety discussion on [`Trigger`] for more details. +// NOTE: The way `Trigger` and `On` interact in this implementation is _subtle_ and _easily invalidated_ +// from a soundness perspective. Please read and understand the safety comments before making any changes, +// either here or in `On`. pub(super) unsafe fn observer_system_runner>( mut world: DeferredWorld, observer: Entity, @@ -52,8 +54,12 @@ pub(super) unsafe fn observer_system_runner` - // This is enforced by the safety requirements of `Trigger::trigger` - // See the safety discussion on `Trigger::trigger` for more details. + // The soundness story here is complicated: This casts to &'a mut E::Trigger<'a> which notably + // casts the _arbitrary lifetimes_ of the passed in `trigger_ptr` (&'w E::Trigger<'t>, which are + // 'w and 't on On<'w, 't>) as the _same_ lifetime 'a, which is _local to this function call_. + // This becomes On<'a, 'a> in practice. This is why `On<'w, 't>` has the strict constraint that + // the 'w lifetime can never be exposed. To do so would make it possible to introduce use-after-free bugs. + // See this thread for more details: let trigger: &mut E::Trigger<'_> = unsafe { trigger_ptr.deref_mut() }; let on: On = On::new( diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 1b83afbf2570a..429f4df018ed6 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -223,6 +223,10 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// /// [`ObserverSystem`]: crate::system::ObserverSystem impl SystemInput for On<'_, '_, E, B> { + // Note: the fact that we must use a shared lifetime here is + // a key piece of the complicated safety story documented above + // the `&mut E::Trigger<'_>` cast in `observer_system_runner` and in + // the `On` implementation. type Param<'i> = On<'i, 'i, E, B>; type Inner<'i> = On<'i, 'i, E, B>; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index eef44ef4a9f6a..22f38c698350d 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -169,6 +169,7 @@ impl<'w> DeferredWorld<'w> { relationship_hook_mode, ); if archetype.has_replace_observer() { + // SAFETY: the REPLACE event_key corresponds to the Replace event's type self.trigger_raw( REPLACE, &mut Replace { entity }, @@ -211,6 +212,7 @@ impl<'w> DeferredWorld<'w> { relationship_hook_mode, ); if archetype.has_insert_observer() { + // SAFETY: the INSERT event_key corresponds to the Insert event's type self.trigger_raw( INSERT, &mut Insert { entity }, diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 31c8f22017a57..b6f70c639cffa 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2583,6 +2583,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: All components in the archetype exist in world unsafe { if archetype.has_despawn_observer() { + // SAFETY: the DESPAWN event_key corresponds to the Despawn event's type deferred_world.trigger_raw( DESPAWN, &mut Despawn { @@ -2601,6 +2602,7 @@ impl<'w> EntityWorldMut<'w> { caller, ); if archetype.has_replace_observer() { + // SAFETY: the REPLACE event_key corresponds to the Replace event's type deferred_world.trigger_raw( REPLACE, &mut Replace { @@ -2620,6 +2622,7 @@ impl<'w> EntityWorldMut<'w> { RelationshipHookMode::Run, ); if archetype.has_remove_observer() { + // SAFETY: the REMOVE event_key corresponds to the Remove event's type deferred_world.trigger_raw( REMOVE, &mut Remove {