From 1cee14f4ac0eb3db3a02fd13ba0700aa2af27cec Mon Sep 17 00:00:00 2001 From: Zeenobit Date: Sun, 5 Oct 2025 17:30:56 -0400 Subject: [PATCH 1/4] Refactor `EntityEvent` to support `ContainsEntity` See #21384 --- crates/bevy_ecs/macros/src/event.rs | 8 +- crates/bevy_ecs/src/event/mod.rs | 80 +++++++++++++++++++- crates/bevy_ecs/src/event/trigger.rs | 10 ++- crates/bevy_ecs/src/observer/mod.rs | 10 +-- crates/bevy_ecs/src/observer/system_param.rs | 2 +- crates/bevy_ecs/src/world/entity_ref.rs | 2 +- crates/bevy_ecs/src/world/error.rs | 4 +- crates/bevy_scene/src/scene_spawner.rs | 4 +- examples/no_std/library/src/lib.rs | 2 +- examples/picking/mesh_picking.rs | 2 +- examples/picking/sprite_picking.rs | 2 +- examples/ui/ui_drag_and_drop.rs | 12 +-- 12 files changed, 106 insertions(+), 32 deletions(-) diff --git a/crates/bevy_ecs/macros/src/event.rs b/crates/bevy_ecs/macros/src/event.rs index 5137dfe6f408b..38d3b8f13d02d 100644 --- a/crates/bevy_ecs/macros/src/event.rs +++ b/crates/bevy_ecs/macros/src/event.rs @@ -148,12 +148,12 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { } impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { - fn event_target(&self) -> #bevy_ecs_path::entity::Entity { - self.#entity_field + fn event_target(&self) -> &impl #bevy_ecs_path::entity::ContainsEntity { + &self.#entity_field } - fn event_target_mut(&mut self) -> &mut #bevy_ecs_path::entity::Entity { - &mut self.#entity_field + fn set_event_target(&mut self, entity: #bevy_ecs_path::entity::Entity) { + self.#entity_field = entity.into(); } } diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index b7fac6696bd5b..421f374c4ea20 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -4,6 +4,7 @@ mod trigger; pub use bevy_ecs_macros::{EntityEvent, Event}; pub use trigger::*; +use crate::entity::ContainsEntity; use crate::{ component::{Component, ComponentId}, entity::Entity, @@ -283,12 +284,13 @@ pub trait Event: Send + Sync + Sized + 'static { /// [`Observer::watch_entities`]: crate::observer::Observer::watch_entities pub trait EntityEvent: Event { /// The [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. - fn event_target(&self) -> Entity; - /// Returns a mutable reference to the [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. + fn event_target(&self) -> &impl ContainsEntity; + + /// Sets the "target" [`Entity`] of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. /// - /// Note: In general, this should not be mutated from within an [`Observer`](crate::observer::Observer), as this will not "retarget" + /// Note: In general, this should not be used from within an [`Observer`](crate::observer::Observer), as this will not "retarget" /// the event in any of Bevy's built-in [`Trigger`] implementations. - fn event_target_mut(&mut self) -> &mut Entity; + fn set_event_target(&mut self, entity: Entity); } impl World { @@ -959,4 +961,74 @@ mod tests { }); schedule.run(&mut world); } + + #[test] + fn test_derive_entity_event() { + use bevy_ecs::prelude::*; + + struct Entitoid(Entity); + + impl ContainsEntity for Entitoid { + fn entity(&self) -> Entity { + self.0 + } + } + + // Lame :( + impl From for Entitoid { + fn from(value: Entity) -> Self { + Self(value) + } + } + + #[derive(EntityEvent)] + struct A(Entity); + + #[derive(EntityEvent)] + struct B { + entity: Entity, + } + + #[derive(EntityEvent)] + struct C { + #[event_target] + target: Entity, + } + + #[derive(EntityEvent)] + struct D(Entitoid); + + #[derive(EntityEvent)] + struct E { + entity: Entitoid, + } + + #[derive(EntityEvent)] + struct F { + #[event_target] + target: Entitoid, + } + + let mut world = World::new(); + let entity = world.spawn_empty().id(); + + world.entity_mut(entity).trigger(A); + + // Lame :( + world.entity_mut(entity).trigger(|entity| B { entity }); + world + .entity_mut(entity) + .trigger(|entity| C { target: entity }); + world + .entity_mut(entity) + .trigger(|entity| D(Entitoid(entity))); + world.entity_mut(entity).trigger(|entity| E { + entity: Entitoid(entity), + }); + world.entity_mut(entity).trigger(|entity| F { + target: Entitoid(entity), + }); + + // No asserts; test just needs to compile + } } diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 9492380b63159..8b70ceb996cdc 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -1,3 +1,4 @@ +use crate::entity::ContainsEntity; use crate::{ component::ComponentId, entity::Entity, @@ -141,7 +142,7 @@ unsafe impl Event = Self>> Trigger for E trigger_context: &TriggerContext, event: &mut E, ) { - let entity = event.event_target(); + let entity = event.event_target().entity(); // 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` @@ -278,7 +279,7 @@ unsafe impl< trigger_context: &TriggerContext, event: &mut E, ) { - let mut current_entity = event.event_target(); + let mut current_entity = event.event_target().entity(); self.original_event_target = current_entity; // SAFETY: // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` @@ -309,7 +310,8 @@ unsafe impl< break; } - *event.event_target_mut() = current_entity; + event.set_event_target(current_entity); + // 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` @@ -354,7 +356,7 @@ unsafe impl<'a, E: EntityEvent + Event = EntityComponentsTrigger<'a> trigger_context: &TriggerContext, event: &mut E, ) { - let entity = event.event_target(); + let entity = event.event_target().entity(); // 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` diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index d268085c40e8f..526791539975f 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -565,7 +565,7 @@ mod tests { .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); world.add_observer(move |event: On, mut res: ResMut| { - assert_eq!(event.event_target(), entity); + assert_eq!(event.event_target().entity(), entity); res.observed("a_2"); }); @@ -719,7 +719,7 @@ mod tests { move |event: On, mut res: ResMut| { res.observed("parent"); - assert_eq!(event.event_target(), parent); + assert_eq!(event.event_target().entity(), parent); assert_eq!(event.original_event_target(), child); }, ); @@ -727,7 +727,7 @@ mod tests { world.entity_mut(child).observe( move |event: On, mut res: ResMut| { res.observed("child"); - assert_eq!(event.event_target(), child); + assert_eq!(event.event_target().entity(), child); assert_eq!(event.original_event_target(), child); }, ); @@ -937,7 +937,7 @@ mod tests { world.add_observer( |event: On, query: Query<&A>, mut res: ResMut| { - if query.get(event.event_target()).is_ok() { + if query.get(event.event_target().entity()).is_ok() { res.observed("event"); } }, @@ -1127,7 +1127,7 @@ mod tests { let entity = world .spawn_empty() .observe(|event: On| { - assert_eq!(event.target(), event.event_target()); + assert_eq!(event.target(), event.event_target().entity()); }) .id(); world.trigger(EntityEventA(entity)); diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index 3391d3eb4d26c..2037fe045c47b 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -140,7 +140,7 @@ impl<'w, 't, E: EntityEvent, B: Bundle> On<'w, 't, E, B> { 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() + self.event.event_target().entity() } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 3b003ef86ef1a..31611f30e6a56 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -6274,7 +6274,7 @@ mod tests { .spawn_empty() .observe(|event: On, mut commands: Commands| { commands - .entity(event.event_target()) + .entity(event.event_target().entity()) .insert(TestComponent(0)); }) .id(); diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 6378123903a09..7be0bd18a65c0 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -92,13 +92,13 @@ mod tests { struct FollowupEvent(Entity); fn despawn(kill: On, mut commands: Commands) { - commands.entity(kill.event_target()).despawn(); + commands.entity(kill.event_target().entity()).despawn(); } fn followup(kill: 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.queue_silenced(trigger(FollowupEvent(kill.event_target()))); + commands.queue_silenced(trigger(FollowupEvent(kill.event_target().entity()))); } let mut world = World::new(); diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 11018fb4b0b4a..e67733f62f189 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -709,7 +709,7 @@ mod tests { component::Component, hierarchy::Children, observer::On, - prelude::ReflectComponent, + prelude::*, query::With, system::{Commands, Query, Res, ResMut, RunSystemOnce}, }; @@ -898,7 +898,7 @@ mod tests { "`SceneInstanceReady` contains the wrong `InstanceId`" ); assert_eq!( - event.event_target(), + event.event_target().entity(), scene_entity.unwrap_or(Entity::PLACEHOLDER), "`SceneInstanceReady` triggered on the wrong parent entity" ); diff --git a/examples/no_std/library/src/lib.rs b/examples/no_std/library/src/lib.rs index e9759efc1144c..0332b8c7ef665 100644 --- a/examples/no_std/library/src/lib.rs +++ b/examples/no_std/library/src/lib.rs @@ -126,7 +126,7 @@ fn tick_timers( } fn unwrap(event: On, world: &mut World) { - if let Ok(mut target) = world.get_entity_mut(event.event_target()) + if let Ok(mut target) = world.get_entity_mut(event.entity) && let Some(DelayedComponent(bundle)) = target.take::>() { target.insert(bundle); diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index f51b544ee3f2a..09e6290d82ab7 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -164,7 +164,7 @@ fn update_material_on( // 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 |event, mut query| { - if let Ok(mut material) = query.get_mut(event.event_target()) { + if let Ok(mut material) = query.get_mut(event.event_target().entity()) { material.0 = new_material.clone(); } } diff --git a/examples/picking/sprite_picking.rs b/examples/picking/sprite_picking.rs index cae4ba2e082a7..fe1a31fe45049 100644 --- a/examples/picking/sprite_picking.rs +++ b/examples/picking/sprite_picking.rs @@ -154,7 +154,7 @@ fn recolor_on( color: Color, ) -> impl Fn(On, Query<&mut Sprite>) { move |ev, mut sprites| { - let Ok(mut sprite) = sprites.get_mut(ev.event_target()) else { + let Ok(mut sprite) = sprites.get_mut(ev.event_target().entity()) else { return; }; sprite.color = color; diff --git a/examples/ui/ui_drag_and_drop.rs b/examples/ui/ui_drag_and_drop.rs index fef7d0d8fd729..0f0b77cd649b7 100644 --- a/examples/ui/ui_drag_and_drop.rs +++ b/examples/ui/ui_drag_and_drop.rs @@ -58,37 +58,37 @@ fn setup(mut commands: Commands) { GlobalZIndex::default() )) .observe(move |on_over: On>, mut query: Query<(&mut BackgroundColor, &mut BorderColor)>| { - if let Ok((mut background_color, mut border_color)) = query.get_mut(on_over.event_target()) { + if let Ok((mut background_color, mut border_color)) = query.get_mut(on_over.entity) { background_color.0 = tile_color.lighter(0.1); border_color.set_all(tile_border_color.lighter(0.1)); } }) .observe(move |on_out: On>, mut query: Query<(&mut BackgroundColor, &mut BorderColor)>| { - if let Ok((mut background_color, mut border_color)) = query.get_mut(on_out.event_target()) { + if let Ok((mut background_color, mut border_color)) = query.get_mut(on_out.entity) { background_color.0 = tile_color; border_color.set_all(tile_border_color); } }) .observe(|on_drag_start: On>, mut query: Query<(&mut Outline, &mut GlobalZIndex)>| { - if let Ok((mut outline, mut global_zindex, )) = query.get_mut(on_drag_start.event_target()) { + if let Ok((mut outline, mut global_zindex, )) = query.get_mut(on_drag_start.entity) { outline.color = Color::WHITE; global_zindex.0 = 1; } }) .observe(|on_drag: On>, mut query: Query<&mut UiTransform>| { - if let Ok(mut transform) = query.get_mut(on_drag.event_target()) { + if let Ok(mut transform) = query.get_mut(on_drag.entity) { transform.translation = Val2::px(on_drag.distance.x, on_drag.distance.y); } }) .observe(move |on_drag_end: On>, mut query: Query<(&mut UiTransform, &mut Outline, &mut GlobalZIndex)>| { - if let Ok((mut transform, mut outline, mut global_zindex)) = query.get_mut(on_drag_end.event_target()) { + if let Ok((mut transform, mut outline, mut global_zindex)) = query.get_mut(on_drag_end.entity) { transform.translation = Val2::ZERO; outline.color = Color::NONE; global_zindex.0 = 0; } }) .observe(|on_drag_drop: On>, mut query: Query<&mut Node>| { - if let Ok([mut a, mut b]) = query.get_many_mut([on_drag_drop.event_target(), on_drag_drop.dropped]) { + if let Ok([mut a, mut b]) = query.get_many_mut([on_drag_drop.entity, on_drag_drop.dropped]) { core::mem::swap(&mut a.grid_row, &mut b.grid_row); core::mem::swap(&mut a.grid_column, &mut b.grid_column); } From 0320f24b806791c561d2e4b086661ec4f0a039c5 Mon Sep 17 00:00:00 2001 From: Zeenobit Date: Mon, 6 Oct 2025 20:23:51 -0400 Subject: [PATCH 2/4] Revert "Refactor `EntityEvent` to support `ContainsEntity`" This reverts commit 1cee14f4ac0eb3db3a02fd13ba0700aa2af27cec. --- crates/bevy_ecs/macros/src/event.rs | 8 +- crates/bevy_ecs/src/event/mod.rs | 80 +------------------- crates/bevy_ecs/src/event/trigger.rs | 10 +-- crates/bevy_ecs/src/observer/mod.rs | 10 +-- crates/bevy_ecs/src/observer/system_param.rs | 2 +- crates/bevy_ecs/src/world/entity_ref.rs | 2 +- crates/bevy_ecs/src/world/error.rs | 4 +- crates/bevy_scene/src/scene_spawner.rs | 4 +- examples/no_std/library/src/lib.rs | 2 +- examples/picking/mesh_picking.rs | 2 +- examples/picking/sprite_picking.rs | 2 +- examples/ui/ui_drag_and_drop.rs | 12 +-- 12 files changed, 32 insertions(+), 106 deletions(-) diff --git a/crates/bevy_ecs/macros/src/event.rs b/crates/bevy_ecs/macros/src/event.rs index 38d3b8f13d02d..5137dfe6f408b 100644 --- a/crates/bevy_ecs/macros/src/event.rs +++ b/crates/bevy_ecs/macros/src/event.rs @@ -148,12 +148,12 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { } impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { - fn event_target(&self) -> &impl #bevy_ecs_path::entity::ContainsEntity { - &self.#entity_field + fn event_target(&self) -> #bevy_ecs_path::entity::Entity { + self.#entity_field } - fn set_event_target(&mut self, entity: #bevy_ecs_path::entity::Entity) { - self.#entity_field = entity.into(); + fn event_target_mut(&mut self) -> &mut #bevy_ecs_path::entity::Entity { + &mut self.#entity_field } } diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 421f374c4ea20..b7fac6696bd5b 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -4,7 +4,6 @@ mod trigger; pub use bevy_ecs_macros::{EntityEvent, Event}; pub use trigger::*; -use crate::entity::ContainsEntity; use crate::{ component::{Component, ComponentId}, entity::Entity, @@ -284,13 +283,12 @@ pub trait Event: Send + Sync + Sized + 'static { /// [`Observer::watch_entities`]: crate::observer::Observer::watch_entities pub trait EntityEvent: Event { /// The [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. - fn event_target(&self) -> &impl ContainsEntity; - - /// Sets the "target" [`Entity`] of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. + fn event_target(&self) -> Entity; + /// Returns a mutable reference to the [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. /// - /// Note: In general, this should not be used from within an [`Observer`](crate::observer::Observer), as this will not "retarget" + /// Note: In general, this should not be mutated from within an [`Observer`](crate::observer::Observer), as this will not "retarget" /// the event in any of Bevy's built-in [`Trigger`] implementations. - fn set_event_target(&mut self, entity: Entity); + fn event_target_mut(&mut self) -> &mut Entity; } impl World { @@ -961,74 +959,4 @@ mod tests { }); schedule.run(&mut world); } - - #[test] - fn test_derive_entity_event() { - use bevy_ecs::prelude::*; - - struct Entitoid(Entity); - - impl ContainsEntity for Entitoid { - fn entity(&self) -> Entity { - self.0 - } - } - - // Lame :( - impl From for Entitoid { - fn from(value: Entity) -> Self { - Self(value) - } - } - - #[derive(EntityEvent)] - struct A(Entity); - - #[derive(EntityEvent)] - struct B { - entity: Entity, - } - - #[derive(EntityEvent)] - struct C { - #[event_target] - target: Entity, - } - - #[derive(EntityEvent)] - struct D(Entitoid); - - #[derive(EntityEvent)] - struct E { - entity: Entitoid, - } - - #[derive(EntityEvent)] - struct F { - #[event_target] - target: Entitoid, - } - - let mut world = World::new(); - let entity = world.spawn_empty().id(); - - world.entity_mut(entity).trigger(A); - - // Lame :( - world.entity_mut(entity).trigger(|entity| B { entity }); - world - .entity_mut(entity) - .trigger(|entity| C { target: entity }); - world - .entity_mut(entity) - .trigger(|entity| D(Entitoid(entity))); - world.entity_mut(entity).trigger(|entity| E { - entity: Entitoid(entity), - }); - world.entity_mut(entity).trigger(|entity| F { - target: Entitoid(entity), - }); - - // No asserts; test just needs to compile - } } diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 8b70ceb996cdc..9492380b63159 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -1,4 +1,3 @@ -use crate::entity::ContainsEntity; use crate::{ component::ComponentId, entity::Entity, @@ -142,7 +141,7 @@ unsafe impl Event = Self>> Trigger for E trigger_context: &TriggerContext, event: &mut E, ) { - let entity = event.event_target().entity(); + let entity = event.event_target(); // 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` @@ -279,7 +278,7 @@ unsafe impl< trigger_context: &TriggerContext, event: &mut E, ) { - let mut current_entity = event.event_target().entity(); + let mut current_entity = event.event_target(); self.original_event_target = current_entity; // SAFETY: // - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger` @@ -310,8 +309,7 @@ unsafe impl< break; } - event.set_event_target(current_entity); - + *event.event_target_mut() = current_entity; // 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` @@ -356,7 +354,7 @@ unsafe impl<'a, E: EntityEvent + Event = EntityComponentsTrigger<'a> trigger_context: &TriggerContext, event: &mut E, ) { - let entity = event.event_target().entity(); + let entity = event.event_target(); // 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` diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 526791539975f..d268085c40e8f 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -565,7 +565,7 @@ mod tests { .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); world.add_observer(move |event: On, mut res: ResMut| { - assert_eq!(event.event_target().entity(), entity); + assert_eq!(event.event_target(), entity); res.observed("a_2"); }); @@ -719,7 +719,7 @@ mod tests { move |event: On, mut res: ResMut| { res.observed("parent"); - assert_eq!(event.event_target().entity(), parent); + assert_eq!(event.event_target(), parent); assert_eq!(event.original_event_target(), child); }, ); @@ -727,7 +727,7 @@ mod tests { world.entity_mut(child).observe( move |event: On, mut res: ResMut| { res.observed("child"); - assert_eq!(event.event_target().entity(), child); + assert_eq!(event.event_target(), child); assert_eq!(event.original_event_target(), child); }, ); @@ -937,7 +937,7 @@ mod tests { world.add_observer( |event: On, query: Query<&A>, mut res: ResMut| { - if query.get(event.event_target().entity()).is_ok() { + if query.get(event.event_target()).is_ok() { res.observed("event"); } }, @@ -1127,7 +1127,7 @@ mod tests { let entity = world .spawn_empty() .observe(|event: On| { - assert_eq!(event.target(), event.event_target().entity()); + assert_eq!(event.target(), event.event_target()); }) .id(); world.trigger(EntityEventA(entity)); diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index 2037fe045c47b..3391d3eb4d26c 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -140,7 +140,7 @@ impl<'w, 't, E: EntityEvent, B: Bundle> On<'w, 't, E, B> { 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().entity() + self.event.event_target() } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 31611f30e6a56..3b003ef86ef1a 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -6274,7 +6274,7 @@ mod tests { .spawn_empty() .observe(|event: On, mut commands: Commands| { commands - .entity(event.event_target().entity()) + .entity(event.event_target()) .insert(TestComponent(0)); }) .id(); diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 7be0bd18a65c0..6378123903a09 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -92,13 +92,13 @@ mod tests { struct FollowupEvent(Entity); fn despawn(kill: On, mut commands: Commands) { - commands.entity(kill.event_target().entity()).despawn(); + commands.entity(kill.event_target()).despawn(); } fn followup(kill: 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.queue_silenced(trigger(FollowupEvent(kill.event_target().entity()))); + commands.queue_silenced(trigger(FollowupEvent(kill.event_target()))); } let mut world = World::new(); diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index e67733f62f189..11018fb4b0b4a 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -709,7 +709,7 @@ mod tests { component::Component, hierarchy::Children, observer::On, - prelude::*, + prelude::ReflectComponent, query::With, system::{Commands, Query, Res, ResMut, RunSystemOnce}, }; @@ -898,7 +898,7 @@ mod tests { "`SceneInstanceReady` contains the wrong `InstanceId`" ); assert_eq!( - event.event_target().entity(), + event.event_target(), scene_entity.unwrap_or(Entity::PLACEHOLDER), "`SceneInstanceReady` triggered on the wrong parent entity" ); diff --git a/examples/no_std/library/src/lib.rs b/examples/no_std/library/src/lib.rs index 0332b8c7ef665..e9759efc1144c 100644 --- a/examples/no_std/library/src/lib.rs +++ b/examples/no_std/library/src/lib.rs @@ -126,7 +126,7 @@ fn tick_timers( } fn unwrap(event: On, world: &mut World) { - if let Ok(mut target) = world.get_entity_mut(event.entity) + if let Ok(mut target) = world.get_entity_mut(event.event_target()) && let Some(DelayedComponent(bundle)) = target.take::>() { target.insert(bundle); diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index 09e6290d82ab7..f51b544ee3f2a 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -164,7 +164,7 @@ fn update_material_on( // 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 |event, mut query| { - if let Ok(mut material) = query.get_mut(event.event_target().entity()) { + if let Ok(mut material) = query.get_mut(event.event_target()) { material.0 = new_material.clone(); } } diff --git a/examples/picking/sprite_picking.rs b/examples/picking/sprite_picking.rs index fe1a31fe45049..cae4ba2e082a7 100644 --- a/examples/picking/sprite_picking.rs +++ b/examples/picking/sprite_picking.rs @@ -154,7 +154,7 @@ fn recolor_on( color: Color, ) -> impl Fn(On, Query<&mut Sprite>) { move |ev, mut sprites| { - let Ok(mut sprite) = sprites.get_mut(ev.event_target().entity()) else { + let Ok(mut sprite) = sprites.get_mut(ev.event_target()) else { return; }; sprite.color = color; diff --git a/examples/ui/ui_drag_and_drop.rs b/examples/ui/ui_drag_and_drop.rs index 0f0b77cd649b7..fef7d0d8fd729 100644 --- a/examples/ui/ui_drag_and_drop.rs +++ b/examples/ui/ui_drag_and_drop.rs @@ -58,37 +58,37 @@ fn setup(mut commands: Commands) { GlobalZIndex::default() )) .observe(move |on_over: On>, mut query: Query<(&mut BackgroundColor, &mut BorderColor)>| { - if let Ok((mut background_color, mut border_color)) = query.get_mut(on_over.entity) { + if let Ok((mut background_color, mut border_color)) = query.get_mut(on_over.event_target()) { background_color.0 = tile_color.lighter(0.1); border_color.set_all(tile_border_color.lighter(0.1)); } }) .observe(move |on_out: On>, mut query: Query<(&mut BackgroundColor, &mut BorderColor)>| { - if let Ok((mut background_color, mut border_color)) = query.get_mut(on_out.entity) { + if let Ok((mut background_color, mut border_color)) = query.get_mut(on_out.event_target()) { background_color.0 = tile_color; border_color.set_all(tile_border_color); } }) .observe(|on_drag_start: On>, mut query: Query<(&mut Outline, &mut GlobalZIndex)>| { - if let Ok((mut outline, mut global_zindex, )) = query.get_mut(on_drag_start.entity) { + if let Ok((mut outline, mut global_zindex, )) = query.get_mut(on_drag_start.event_target()) { outline.color = Color::WHITE; global_zindex.0 = 1; } }) .observe(|on_drag: On>, mut query: Query<&mut UiTransform>| { - if let Ok(mut transform) = query.get_mut(on_drag.entity) { + if let Ok(mut transform) = query.get_mut(on_drag.event_target()) { transform.translation = Val2::px(on_drag.distance.x, on_drag.distance.y); } }) .observe(move |on_drag_end: On>, mut query: Query<(&mut UiTransform, &mut Outline, &mut GlobalZIndex)>| { - if let Ok((mut transform, mut outline, mut global_zindex)) = query.get_mut(on_drag_end.entity) { + if let Ok((mut transform, mut outline, mut global_zindex)) = query.get_mut(on_drag_end.event_target()) { transform.translation = Val2::ZERO; outline.color = Color::NONE; global_zindex.0 = 0; } }) .observe(|on_drag_drop: On>, mut query: Query<&mut Node>| { - if let Ok([mut a, mut b]) = query.get_many_mut([on_drag_drop.entity, on_drag_drop.dropped]) { + if let Ok([mut a, mut b]) = query.get_many_mut([on_drag_drop.event_target(), on_drag_drop.dropped]) { core::mem::swap(&mut a.grid_row, &mut b.grid_row); core::mem::swap(&mut a.grid_column, &mut b.grid_column); } From 6fb6d557356db9434aff4ea42fbcac577bdc4915 Mon Sep 17 00:00:00 2001 From: Zeenobit Date: Mon, 6 Oct 2025 20:33:26 -0400 Subject: [PATCH 3/4] Simplified integration of `ContainsEntity` with `derive(EntityEvent)` --- crates/bevy_ecs/macros/src/event.rs | 6 +-- crates/bevy_ecs/src/event/mod.rs | 76 ++++++++++++++++++++++++++-- crates/bevy_ecs/src/event/trigger.rs | 2 +- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/macros/src/event.rs b/crates/bevy_ecs/macros/src/event.rs index 5137dfe6f408b..febfe7d82efcb 100644 --- a/crates/bevy_ecs/macros/src/event.rs +++ b/crates/bevy_ecs/macros/src/event.rs @@ -149,11 +149,11 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { fn event_target(&self) -> #bevy_ecs_path::entity::Entity { - self.#entity_field + #bevy_ecs_path::entity::ContainsEntity::entity(&self.#entity_field) } - fn event_target_mut(&mut self) -> &mut #bevy_ecs_path::entity::Entity { - &mut self.#entity_field + fn set_event_target(&mut self, entity: #bevy_ecs_path::entity::Entity) { + self.#entity_field = Into::into(entity); } } diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index b7fac6696bd5b..ce88206562166 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -284,11 +284,11 @@ pub trait Event: Send + Sync + Sized + 'static { pub trait EntityEvent: Event { /// The [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. fn event_target(&self) -> Entity; - /// Returns a mutable reference to the [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. + /// Sets the [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. /// - /// Note: In general, this should not be mutated from within an [`Observer`](crate::observer::Observer), as this will not "retarget" + /// Note: In general, this should not be called from within an [`Observer`](crate::observer::Observer), as this will not "retarget" /// the event in any of Bevy's built-in [`Trigger`] implementations. - fn event_target_mut(&mut self) -> &mut Entity; + fn set_event_target(&mut self, entity: Entity); } impl World { @@ -959,4 +959,74 @@ mod tests { }); schedule.run(&mut world); } + + #[test] + fn test_derive_entity_event() { + use bevy_ecs::prelude::*; + + struct Entitoid(Entity); + + impl ContainsEntity for Entitoid { + fn entity(&self) -> Entity { + self.0 + } + } + + // Lame :( + impl From for Entitoid { + fn from(value: Entity) -> Self { + Self(value) + } + } + + #[derive(EntityEvent)] + struct A(Entity); + + #[derive(EntityEvent)] + struct B { + entity: Entity, + } + + #[derive(EntityEvent)] + struct C { + #[event_target] + target: Entity, + } + + #[derive(EntityEvent)] + struct D(Entitoid); + + #[derive(EntityEvent)] + struct E { + entity: Entitoid, + } + + #[derive(EntityEvent)] + struct F { + #[event_target] + target: Entitoid, + } + + let mut world = World::new(); + let entity = world.spawn_empty().id(); + + world.entity_mut(entity).trigger(A); + + // Lame :( + world.entity_mut(entity).trigger(|entity| B { entity }); + world + .entity_mut(entity) + .trigger(|entity| C { target: entity }); + world + .entity_mut(entity) + .trigger(|entity| D(Entitoid(entity))); + world.entity_mut(entity).trigger(|entity| E { + entity: Entitoid(entity), + }); + world.entity_mut(entity).trigger(|entity| F { + target: Entitoid(entity), + }); + + // No asserts; test just needs to compile + } } diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 9492380b63159..f9a472d89a2b0 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -309,7 +309,7 @@ unsafe impl< break; } - *event.event_target_mut() = current_entity; + event.set_event_target(current_entity); // 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` From 0d09b3d0db0e3c79c120f514fb90c211578e0491 Mon Sep 17 00:00:00 2001 From: Zeenobit Date: Tue, 7 Oct 2025 22:31:19 -0400 Subject: [PATCH 4/4] Add `SetEntityEventTarget` This change adds a new `SetEntityEventTarget` to handle mutable entity events. --- crates/bevy_ecs/macros/src/event.rs | 18 +++++-- crates/bevy_ecs/src/event/mod.rs | 72 +++++++++++++++++++++++++++- crates/bevy_ecs/src/event/trigger.rs | 3 +- 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/macros/src/event.rs b/crates/bevy_ecs/macros/src/event.rs index febfe7d82efcb..90aaff50de0ba 100644 --- a/crates/bevy_ecs/macros/src/event.rs +++ b/crates/bevy_ecs/macros/src/event.rs @@ -142,6 +142,19 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { } else { quote! {#bevy_ecs_path::event::EntityTrigger} }; + + let set_entity_event_target_impl = if propagate { + quote! { + impl #impl_generics #bevy_ecs_path::event::SetEntityEventTarget for #struct_name #type_generics #where_clause { + fn set_event_target(&mut self, entity: #bevy_ecs_path::entity::Entity) { + self.#entity_field = Into::into(entity); + } + } + } + } else { + quote! {} + }; + TokenStream::from(quote! { impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { type Trigger<'a> = #trigger; @@ -151,12 +164,9 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { fn event_target(&self) -> #bevy_ecs_path::entity::Entity { #bevy_ecs_path::entity::ContainsEntity::entity(&self.#entity_field) } - - fn set_event_target(&mut self, entity: #bevy_ecs_path::entity::Entity) { - self.#entity_field = Into::into(entity); - } } + #set_entity_event_target_impl }) } diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index ce88206562166..786cc5020fd83 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -284,6 +284,16 @@ pub trait Event: Send + Sync + Sized + 'static { pub trait EntityEvent: Event { /// The [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. fn event_target(&self) -> Entity; +} + +/// A trait which is used to set the target of an [`EntityEvent`]. +/// +/// By default, entity events are immutable; meaning their target does not change during the lifetime of the event. However, some events +/// may require mutable access to provide features such as event propagation. +/// +/// You should never need to implement this trait manually if you use `#[derive(EntityEvent)]`. It is automatically implemented for you if you +/// use `#[entity_event(propagate)]`. +pub trait SetEntityEventTarget: EntityEvent { /// Sets the [`Entity`] "target" of this [`EntityEvent`]. When triggered, this will run observers that watch for this specific entity. /// /// Note: In general, this should not be called from within an [`Observer`](crate::observer::Observer), as this will not "retarget" @@ -972,8 +982,15 @@ mod tests { } } - // Lame :( - impl From for Entitoid { + struct MutableEntitoid(Entity); + + impl ContainsEntity for MutableEntitoid { + fn entity(&self) -> Entity { + self.0 + } + } + + impl From for MutableEntitoid { fn from(value: Entity) -> Self { Self(value) } @@ -982,41 +999,89 @@ mod tests { #[derive(EntityEvent)] struct A(Entity); + #[derive(EntityEvent)] + #[entity_event(propagate)] + struct AP(Entity); + #[derive(EntityEvent)] struct B { entity: Entity, } + #[derive(EntityEvent)] + #[entity_event(propagate)] + struct BP { + entity: Entity, + } + #[derive(EntityEvent)] struct C { #[event_target] target: Entity, } + #[derive(EntityEvent)] + #[entity_event(propagate)] + struct CP { + #[event_target] + target: Entity, + } + #[derive(EntityEvent)] struct D(Entitoid); + // SHOULD NOT COMPILE: + // #[derive(EntityEvent)] + // #[entity_event(propagate)] + // struct DP(Entitoid); + #[derive(EntityEvent)] struct E { entity: Entitoid, } + // SHOULD NOT COMPILE: + // #[derive(EntityEvent)] + // #[entity_event(propagate)] + // struct EP { + // entity: Entitoid, + // } + #[derive(EntityEvent)] struct F { #[event_target] target: Entitoid, } + // SHOULD NOT COMPILE: + // #[derive(EntityEvent)] + // #[entity_event(propagate)] + // struct FP { + // #[event_target] + // target: Entitoid, + // } + + #[derive(EntityEvent)] + #[entity_event(propagate)] + struct G { + entity: MutableEntitoid, + } + let mut world = World::new(); let entity = world.spawn_empty().id(); world.entity_mut(entity).trigger(A); + world.entity_mut(entity).trigger(AP); // Lame :( world.entity_mut(entity).trigger(|entity| B { entity }); + world.entity_mut(entity).trigger(|entity| BP { entity }); world .entity_mut(entity) .trigger(|entity| C { target: entity }); + world + .entity_mut(entity) + .trigger(|entity| CP { target: entity }); world .entity_mut(entity) .trigger(|entity| D(Entitoid(entity))); @@ -1026,6 +1091,9 @@ mod tests { world.entity_mut(entity).trigger(|entity| F { target: Entitoid(entity), }); + world.entity_mut(entity).trigger(|entity| G { + entity: MutableEntitoid(entity), + }); // No asserts; test just needs to compile } diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index f9a472d89a2b0..69aac4a513793 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -1,3 +1,4 @@ +use crate::event::SetEntityEventTarget; use crate::{ component::ComponentId, entity::Entity, @@ -267,7 +268,7 @@ impl> fmt::Debug // - `E`'s [`Event::Trigger`] is constrained to [`PropagateEntityTrigger`] unsafe impl< const AUTO_PROPAGATE: bool, - E: EntityEvent + for<'a> Event = Self>, + E: EntityEvent + SetEntityEventTarget + for<'a> Event = Self>, T: Traversal, > Trigger for PropagateEntityTrigger {