From 38cb1d44306958e9f0cfac0f8eae5feb82038387 Mon Sep 17 00:00:00 2001 From: Mark Old Date: Fri, 17 Apr 2026 22:19:52 -0700 Subject: [PATCH] Enable dynamic triggers for observers Adds three new unsafe `World` functions: - `trigger_dynamic()` - `trigger_dynamic_targets()` - `trigger_dynamic_targets_components()` These enable observers to be triggered with untyped events and trigger data. Their implementations are just wiring up some existing internal structure. Structurally, they are based on their non-dynamic counterparts. Also exposes `EventKey::new()` and `EventKey::component_id()` for constructing event keys from dynamic `ComponentId`s. Extends the dynamic example with event registration and triggering. --- crates/bevy_ecs/src/event/mod.rs | 30 ++ crates/bevy_ecs/src/observer/mod.rs | 462 ++++++++++++++++++++++++++++ examples/ecs/dynamic.rs | 104 ++++++- 3 files changed, 592 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index b80d54e6e8dc7..2bad38049368e 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -386,10 +386,40 @@ struct EventWrapperComponent(PhantomData); /// /// You can look up the key for your event by calling the [`World::event_key`] method. /// +/// For dynamic events not backed by a Rust type, create an `EventKey` from +/// a [`ComponentId`] using [`EventKey::new`]. Obtain a [`ComponentId`] via +/// [`World::register_component_with_descriptor`]. +/// /// [observers]: crate::observer #[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] pub struct EventKey(pub(crate) ComponentId); +impl EventKey { + /// Creates a new [`EventKey`] from a [`ComponentId`]. + /// + /// Useful for dynamic events not backed by a Rust type. Obtain a + /// [`ComponentId`] via [`World::register_component_with_descriptor`]. + /// + /// # Safety + /// + /// The caller must ensure that `component_id` was registered for use as + /// an event (e.g. via [`World::register_component_with_descriptor`]). + /// Using an unrelated [`ComponentId`] may cause observers to receive + /// data with an unexpected layout. + /// + /// [`World::register_component_with_descriptor`]: crate::world::World::register_component_with_descriptor + #[inline] + pub const unsafe fn new(component_id: ComponentId) -> Self { + Self(component_id) + } + + /// Returns the underlying [`ComponentId`] for this event key. + #[inline] + pub const fn component_id(self) -> ComponentId { + self.0 + } +} + #[cfg(test)] mod tests { use alloc::{vec, vec::Vec}; diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index a1e1b9217e70c..2a2189810d074 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -111,6 +111,212 @@ impl World { } } + /// Splits `&mut self` into a [`DeferredWorld`] and the [`CachedObservers`] + /// registered for `event_key`, or returns `None` if no observers exist. + /// + /// # Safety + /// + /// Caller must not use the returned [`DeferredWorld`] to access observer + /// storage, as it aliases with the returned [`CachedObservers`] reference. + unsafe fn split_for_event( + &mut self, + event_key: crate::event::EventKey, + ) -> Option<(DeferredWorld<'_>, &CachedObservers)> { + let world_cell = self.as_unsafe_world_cell(); + let observers = world_cell.observers(); + let observers = observers.try_get_observers(event_key)?; + // SAFETY: The caller guarantees the returned `DeferredWorld` will not + // be used to access observer storage (which `observers` borrows). + Some((unsafe { world_cell.into_deferred() }, observers)) + } + + /// Triggers global [`Observer`]s for `event_key` with untyped event and + /// trigger data. + /// + /// Dynamic equivalent of [`World::trigger`]. Only fires global observers, + /// not entity- or component-scoped ones. + /// + /// Use [`World::trigger_dynamic_targets`] to also fire entity-scoped + /// observers. + /// + /// # Safety + /// + /// - `event_data` must point to a valid, aligned value whose layout matches + /// what observers registered for this `event_key` expect. + /// - `trigger_data` must point to a valid, aligned value whose layout + /// matches what observers registered for this `event_key` expect. + #[track_caller] + pub unsafe fn trigger_dynamic( + &mut self, + event_key: crate::event::EventKey, + mut event_data: bevy_ptr::PtrMut, + mut trigger_data: bevy_ptr::PtrMut, + ) { + // SAFETY: We have exclusive access via `&mut self` and will not + // access observer storage through the returned `DeferredWorld`. + let Some((mut world, observers)) = (unsafe { self.split_for_event(event_key) }) else { + return; + }; + + let context = TriggerContext { + event_key, + caller: MaybeLocation::caller(), + }; + + // SAFETY: no outstanding world references besides `observers` + unsafe { + world.as_unsafe_world_cell().increment_trigger_id(); + } + + for (observer, runner) in observers.global_observers() { + // SAFETY: + // - `observers` come from `world` and correspond to `event_key` + // - caller guarantees `event_data` and `trigger_data` are valid + unsafe { + (runner)( + world.reborrow(), + *observer, + &context, + event_data.reborrow(), + trigger_data.reborrow(), + ); + } + } + } + + /// Triggers [`Observer`]s for `event_key` targeting `entity`, with untyped + /// event and trigger data. + /// + /// Fires global and entity-scoped observers. Dynamic equivalent of + /// [`EntityWorldMut::trigger`]. + /// + /// # Safety + /// + /// - `event_data` must point to a valid, aligned value whose layout matches + /// what observers registered for this `event_key` expect. + /// - `trigger_data` must point to a valid, aligned value whose layout + /// matches what observers registered for this `event_key` expect. + #[track_caller] + pub unsafe fn trigger_dynamic_targets( + &mut self, + event_key: crate::event::EventKey, + entity: Entity, + event_data: bevy_ptr::PtrMut, + trigger_data: bevy_ptr::PtrMut, + ) { + // SAFETY: We have exclusive access via `&mut self` and will not + // access observer storage through the returned `DeferredWorld`. + let Some((world, observers)) = (unsafe { self.split_for_event(event_key) }) else { + return; + }; + + let context = TriggerContext { + event_key, + caller: MaybeLocation::caller(), + }; + + // SAFETY: + // - `observers` come from `world` and correspond to `event_key` + // - caller guarantees `event_data` and `trigger_data` are valid + // - `trigger_entity_internal` increments the trigger id + unsafe { + crate::event::trigger_entity_internal( + world, + observers, + event_data, + trigger_data, + entity, + &context, + ); + } + } + + /// Triggers [`Observer`]s for `event_key` targeting `entity` and + /// `components`, with untyped event and trigger data. + /// + /// Fires global, entity-scoped, and component-scoped observers. + /// Dynamic equivalent of [`EntityComponentsTrigger`]. + /// + /// [`EntityComponentsTrigger`]: crate::event::EntityComponentsTrigger + /// + /// # Safety + /// + /// - `event_data` must point to a valid, aligned value whose layout matches + /// what observers registered for this `event_key` expect. + /// - `trigger_data` must point to a valid, aligned value whose layout + /// matches what observers registered for this `event_key` expect. + #[track_caller] + pub unsafe fn trigger_dynamic_targets_components( + &mut self, + event_key: crate::event::EventKey, + entity: Entity, + components: &[crate::component::ComponentId], + mut event_data: bevy_ptr::PtrMut, + mut trigger_data: bevy_ptr::PtrMut, + ) { + // SAFETY: We have exclusive access via `&mut self` and will not + // access observer storage through the returned `DeferredWorld`. + let Some((mut world, observers)) = (unsafe { self.split_for_event(event_key) }) else { + return; + }; + + let context = TriggerContext { + event_key, + caller: MaybeLocation::caller(), + }; + + // SAFETY: + // - `observers` come from `world` and correspond to `event_key` + // - caller guarantees `event_data` and `trigger_data` are valid + // - `trigger_entity_internal` increments the trigger id + unsafe { + crate::event::trigger_entity_internal( + world.reborrow(), + observers, + event_data.reborrow(), + trigger_data.reborrow(), + entity, + &context, + ); + } + + // Trigger observers watching for specific components. + for id in components { + if let Some(component_observers) = observers.component_observers().get(id) { + for (observer, runner) in component_observers.global_observers() { + // SAFETY: same as above, caller guarantees data validity + unsafe { + (runner)( + world.reborrow(), + *observer, + &context, + event_data.reborrow(), + trigger_data.reborrow(), + ); + } + } + + if let Some(map) = component_observers + .entity_component_observers() + .get(&entity) + { + for (observer, runner) in map { + // SAFETY: same as above, caller guarantees data validity + unsafe { + (runner)( + world.reborrow(), + *observer, + &context, + event_data.reborrow(), + trigger_data.reborrow(), + ); + } + } + } + } + } + } + /// Register an observer to the cache, called when an observer is created pub(crate) fn register_observer(&mut self, observer_entity: Entity) { // SAFETY: References do not alias. @@ -705,6 +911,262 @@ mod tests { assert_eq!(vec!["event_a"], world.resource::().0); } + /// Collects `u32` values read by dynamic observers through `PtrMut`. + #[derive(Resource, Default)] + struct DynamicValues(Vec); + + #[test] + fn observer_fully_dynamic_trigger() { + use core::alloc::Layout; + + let mut world = World::new(); + world.init_resource::(); + world.init_resource::(); + + // Register a dynamic event whose data is a u32. + let event_id = world.register_component_with_descriptor( + // SAFETY: u32 layout with no drop + unsafe { + crate::component::ComponentDescriptor::new_with_layout( + "DynamicEvent", + crate::component::StorageType::Table, + Layout::new::(), + None, + false, + crate::component::ComponentCloneBehavior::Ignore, + None, + ) + }, + ); + // SAFETY: event_id was just registered for use as an event + let event_key = unsafe { crate::event::EventKey::new(event_id) }; + + // SAFETY: event_key was just created, observer reads event_data as u32 + let observe = unsafe { + Observer::with_dynamic_runner( + |mut world, _observer, _trigger_context, event, _trigger| { + // SAFETY: caller passes a valid u32 pointer as event data + let value = *event.as_ref().deref::(); + world.resource_mut::().observed("dynamic_event"); + world.resource_mut::().0.push(value); + }, + ) + .with_event_key(event_key) + }; + world.spawn(observe); + + let mut event_data: u32 = 42; + let mut trigger_data: u32 = 0; + // SAFETY: pointers are valid u32s matching the registered layout + unsafe { + world.trigger_dynamic( + event_key, + bevy_ptr::PtrMut::from(&mut event_data), + bevy_ptr::PtrMut::from(&mut trigger_data), + ); + } + + assert_eq!(vec!["dynamic_event"], world.resource::().0); + assert_eq!(vec![42], world.resource::().0); + } + + #[test] + fn observer_fully_dynamic_trigger_targets() { + use core::alloc::Layout; + + let mut world = World::new(); + world.init_resource::(); + world.init_resource::(); + + let event_id = world.register_component_with_descriptor( + // SAFETY: u32 layout with no drop + unsafe { + crate::component::ComponentDescriptor::new_with_layout( + "DynamicEntityEvent", + crate::component::StorageType::Table, + Layout::new::(), + None, + false, + crate::component::ComponentCloneBehavior::Ignore, + None, + ) + }, + ); + // SAFETY: event_id was just registered for use as an event + let event_key = unsafe { crate::event::EventKey::new(event_id) }; + + let target = world.spawn_empty().id(); + let other = world.spawn_empty().id(); + + // SAFETY: event_key was just created, observer reads event_data as u32 + let global = unsafe { + Observer::with_dynamic_runner( + |mut world, _observer, _trigger_context, event, _trigger| { + let value = *event.as_ref().deref::(); + world.resource_mut::().observed("global"); + world.resource_mut::().0.push(value); + }, + ) + .with_event_key(event_key) + }; + world.spawn(global); + + // SAFETY: event_key was just created, observer reads event_data as u32 + let entity_scoped = unsafe { + Observer::with_dynamic_runner( + |mut world, _observer, _trigger_context, event, _trigger| { + let value = *event.as_ref().deref::(); + world.resource_mut::().observed("entity_scoped"); + world.resource_mut::().0.push(value); + }, + ) + .with_event_key(event_key) + .with_entity(target) + }; + world.spawn(entity_scoped); + + // Trigger targeting `target`: both global and entity-scoped should fire. + let mut event_data: u32 = 7; + let mut trigger_data: u32 = 0; + // SAFETY: pointers are valid u32s matching the registered layout + unsafe { + world.trigger_dynamic_targets( + event_key, + target, + bevy_ptr::PtrMut::from(&mut event_data), + bevy_ptr::PtrMut::from(&mut trigger_data), + ); + } + + assert_eq!(vec!["global", "entity_scoped"], world.resource::().0); + assert_eq!(vec![7, 7], world.resource::().0); + + // Trigger targeting `other`: only global should fire. + world.resource_mut::().0.clear(); + world.resource_mut::().0.clear(); + let mut event_data: u32 = 99; + let mut trigger_data: u32 = 0; + // SAFETY: pointers are valid u32s matching the registered layout + unsafe { + world.trigger_dynamic_targets( + event_key, + other, + bevy_ptr::PtrMut::from(&mut event_data), + bevy_ptr::PtrMut::from(&mut trigger_data), + ); + } + + assert_eq!(vec!["global"], world.resource::().0); + assert_eq!(vec![99], world.resource::().0); + } + + #[test] + fn observer_fully_dynamic_trigger_targets_components() { + use core::alloc::Layout; + + let mut world = World::new(); + world.init_resource::(); + world.init_resource::(); + + let event_id = world.register_component_with_descriptor( + // SAFETY: u32 layout with no drop + unsafe { + crate::component::ComponentDescriptor::new_with_layout( + "DynamicComponentEvent", + crate::component::StorageType::Table, + Layout::new::(), + None, + false, + crate::component::ComponentCloneBehavior::Ignore, + None, + ) + }, + ); + // SAFETY: event_id was just registered for use as an event + let event_key = unsafe { crate::event::EventKey::new(event_id) }; + + // Register a dynamic component to scope an observer to. + let comp_id = world.register_component_with_descriptor( + // SAFETY: ZST layout with no drop + unsafe { + crate::component::ComponentDescriptor::new_with_layout( + "DynamicComp", + crate::component::StorageType::Table, + Layout::new::<()>(), + None, + false, + crate::component::ComponentCloneBehavior::Ignore, + None, + ) + }, + ); + + let target = world.spawn_empty().id(); + + // SAFETY: event_key was just created, observer reads event_data as u32 + let global = unsafe { + Observer::with_dynamic_runner( + |mut world, _observer, _trigger_context, event, _trigger| { + let value = *event.as_ref().deref::(); + world.resource_mut::().observed("global"); + world.resource_mut::().0.push(value); + }, + ) + .with_event_key(event_key) + }; + world.spawn(global); + + // SAFETY: event_key was just created, observer reads event_data as u32 + let comp_scoped = unsafe { + Observer::with_dynamic_runner( + |mut world, _observer, _trigger_context, event, _trigger| { + let value = *event.as_ref().deref::(); + world.resource_mut::().observed("comp_scoped"); + world.resource_mut::().0.push(value); + }, + ) + .with_event_key(event_key) + .with_component(comp_id) + }; + world.spawn(comp_scoped); + + // Trigger with `comp_id` in the components list: both should fire. + let mut event_data: u32 = 5; + let mut trigger_data: u32 = 0; + // SAFETY: pointers are valid u32s matching the registered layout + unsafe { + world.trigger_dynamic_targets_components( + event_key, + target, + &[comp_id], + bevy_ptr::PtrMut::from(&mut event_data), + bevy_ptr::PtrMut::from(&mut trigger_data), + ); + } + + assert_eq!(vec!["global", "comp_scoped"], world.resource::().0); + assert_eq!(vec![5, 5], world.resource::().0); + + // Trigger without components: only global should fire. + world.resource_mut::().0.clear(); + world.resource_mut::().0.clear(); + let mut event_data: u32 = 10; + let mut trigger_data: u32 = 0; + // SAFETY: pointers are valid u32s matching the registered layout + unsafe { + world.trigger_dynamic_targets_components( + event_key, + target, + &[], + bevy_ptr::PtrMut::from(&mut event_data), + bevy_ptr::PtrMut::from(&mut trigger_data), + ); + } + + assert_eq!(vec!["global"], world.resource::().0); + assert_eq!(vec![10], world.resource::().0); + } + #[test] fn observer_propagating() { let mut world = World::new(); diff --git a/examples/ecs/dynamic.rs b/examples/ecs/dynamic.rs index 777e7c60477b6..2663c9413d537 100644 --- a/examples/ecs/dynamic.rs +++ b/examples/ecs/dynamic.rs @@ -5,6 +5,9 @@ //! This example show how you can create components dynamically, spawn entities with those components //! as well as query for entities with those components. +//! +//! It also demonstrates dynamic observers: registering events and observers at +//! runtime without compile-time event types, and triggering them with raw data. use std::{alloc::Layout, collections::HashMap, io::Write, ptr::NonNull}; @@ -13,18 +16,22 @@ use bevy::{ component::{ ComponentCloneBehavior, ComponentDescriptor, ComponentId, ComponentInfo, StorageType, }, + event::EventKey, + observer::{Observer, ObserverRunner}, query::{ComponentAccessKind, QueryData}, world::FilteredEntityMut, }, prelude::*, - ptr::{Aligned, OwningPtr}, + ptr::{Aligned, OwningPtr, PtrMut}, }; const PROMPT: &str = " Commands: - comp, c Create new components - spawn, s Spawn entities - query, q Query for entities + comp, c Create new components + spawn, s Spawn entities + query, q Query for entities + event, e Register dynamic events and observers + emit, t Trigger a dynamic event Enter a command with no parameters for usage."; const COMPONENT_PROMPT: &str = " @@ -48,11 +55,23 @@ query, q Query for entities e.g. &A || &B, &mut C, D, ?E"; +const EVENT_PROMPT: &str = " +event, e Register dynamic events and observers + Enter a comma separated list of event names. + Each event gets a dynamic observer that prints when fired. + e.g. OnDamage, OnHeal, OnDeath"; + +const EMIT_PROMPT: &str = " +emit, t Trigger a dynamic event + Enter the name of a previously registered event. + e.g. OnDamage"; + fn main() { let mut world = World::new(); let mut lines = std::io::stdin().lines(); let mut component_names = HashMap::::new(); let mut component_info = HashMap::::new(); + let mut event_names = HashMap::::new(); println!("{PROMPT}"); loop { @@ -71,6 +90,8 @@ fn main() { Some('c') => println!("{COMPONENT_PROMPT}"), Some('s') => println!("{ENTITY_PROMPT}"), Some('q') => println!("{QUERY_PROMPT}"), + Some('e') => println!("{EVENT_PROMPT}"), + Some('t') => println!("{EMIT_PROMPT}"), _ => println!("{PROMPT}"), } continue; @@ -191,11 +212,86 @@ fn main() { println!("{}: {}", filtered_entity.id(), terms); }); } + "e" => { + rest.split(',').for_each(|event| { + let name = event.trim(); + if name.is_empty() { + return; + } + + // Register a ComponentId for this event, no Rust type needed. + // SAFETY: ZST with no drop + let event_component_id = world.register_component_with_descriptor(unsafe { + ComponentDescriptor::new_with_layout( + format!("event:{name}"), + StorageType::Table, + Layout::new::<()>(), + None, + false, + ComponentCloneBehavior::Ignore, + None, + ) + }); + // SAFETY: event_component_id was just registered for this event + let event_key = unsafe { EventKey::new(event_component_id) }; + event_names.insert(name.to_string(), event_key); + + // Build a dynamic observer that prints when the event fires. + let runner: ObserverRunner = |mut world, _observer, ctx, _event, _trigger| { + println!(" Observer fired!"); + if let Some(mut counts) = world.get_resource_mut::() { + *counts.0.entry(ctx.event_key).or_insert(0) += 1; + } + }; + + // SAFETY: event_key was just registered, runner ignores pointers + let observer = + unsafe { Observer::with_dynamic_runner(runner).with_event_key(event_key) }; + world.spawn(observer); + + println!( + "Event '{name}' registered (key: {}) with a dynamic observer", + event_component_id.index() + ); + }); + + // Ensure the counter resource exists. + world.init_resource::(); + } + "t" => { + let name = rest.trim(); + let Some(&event_key) = event_names.get(name) else { + println!( + "Event '{name}' does not exist. Register it first with 'event {name}'" + ); + continue; + }; + + let mut event_data = (); + let mut trigger_data = (); + // SAFETY: event_key was registered in this world, both pointers are valid ZSTs + unsafe { + world.trigger_dynamic( + event_key, + PtrMut::from(&mut event_data), + PtrMut::from(&mut trigger_data), + ); + } + + let count = world + .get_resource::() + .map_or(0, |c| c.0.get(&event_key).copied().unwrap_or(0)); + println!("Event '{name}' triggered ({count} fires)"); + } _ => continue, } } } +/// Tracks how many times each dynamic event's observer has fired. +#[derive(Resource, Default)] +struct EventFireCount(HashMap); + // Constructs `OwningPtr` for each item in `components` // By sharing the lifetime of `components` with the resulting ptrs we ensure we don't drop the data before use fn to_owning_ptrs(components: &mut [Vec]) -> Vec> {