Enable dynamic triggers for observers (#23870)

# Objective

Finish the bevy_ecs dynamic story. Currently you can register observers
with dynamic triggers and runners, but there's no way to actually
trigger these observers dynamically.

## Solution

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.

## Testing

Several new tests

## Showcase

See updated example
This commit is contained in:
Mark Old
2026-04-19 15:55:59 +00:00
committed by GitHub
parent 15a6b5eec5
commit 334c242c96
3 changed files with 592 additions and 4 deletions
+30
View File
@@ -386,10 +386,40 @@ struct EventWrapperComponent<E: Event>(PhantomData<E>);
///
/// 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};
+462
View File
@@ -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::<Order>().0);
}
/// Collects `u32` values read by dynamic observers through `PtrMut`.
#[derive(Resource, Default)]
struct DynamicValues(Vec<u32>);
#[test]
fn observer_fully_dynamic_trigger() {
use core::alloc::Layout;
let mut world = World::new();
world.init_resource::<Order>();
world.init_resource::<DynamicValues>();
// 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::<u32>(),
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::<u32>();
world.resource_mut::<Order>().observed("dynamic_event");
world.resource_mut::<DynamicValues>().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::<Order>().0);
assert_eq!(vec![42], world.resource::<DynamicValues>().0);
}
#[test]
fn observer_fully_dynamic_trigger_targets() {
use core::alloc::Layout;
let mut world = World::new();
world.init_resource::<Order>();
world.init_resource::<DynamicValues>();
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::<u32>(),
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::<u32>();
world.resource_mut::<Order>().observed("global");
world.resource_mut::<DynamicValues>().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::<u32>();
world.resource_mut::<Order>().observed("entity_scoped");
world.resource_mut::<DynamicValues>().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::<Order>().0);
assert_eq!(vec![7, 7], world.resource::<DynamicValues>().0);
// Trigger targeting `other`: only global should fire.
world.resource_mut::<Order>().0.clear();
world.resource_mut::<DynamicValues>().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::<Order>().0);
assert_eq!(vec![99], world.resource::<DynamicValues>().0);
}
#[test]
fn observer_fully_dynamic_trigger_targets_components() {
use core::alloc::Layout;
let mut world = World::new();
world.init_resource::<Order>();
world.init_resource::<DynamicValues>();
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::<u32>(),
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::<u32>();
world.resource_mut::<Order>().observed("global");
world.resource_mut::<DynamicValues>().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::<u32>();
world.resource_mut::<Order>().observed("comp_scoped");
world.resource_mut::<DynamicValues>().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::<Order>().0);
assert_eq!(vec![5, 5], world.resource::<DynamicValues>().0);
// Trigger without components: only global should fire.
world.resource_mut::<Order>().0.clear();
world.resource_mut::<DynamicValues>().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::<Order>().0);
assert_eq!(vec![10], world.resource::<DynamicValues>().0);
}
#[test]
fn observer_propagating() {
let mut world = World::new();
+100 -4
View File
@@ -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::<String, ComponentId>::new();
let mut component_info = HashMap::<ComponentId, ComponentInfo>::new();
let mut event_names = HashMap::<String, EventKey>::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::<EventFireCount>() {
*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::<EventFireCount>();
}
"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::<EventFireCount>()
.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<EventKey, u32>);
// 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<u64>]) -> Vec<OwningPtr<'_, Aligned>> {