Observer trigger refactor (#19935)

# Objective

- The usage of ComponentId is quite confusing: events are not
components. By newtyping this, we can prevent stupid mistakes, avoid
leaking internal details and make the code clearer for users and engine
devs reading it.
- Adopts https://github.com/bevyengine/bevy/pull/19755

---------

Co-authored-by: oscar-benderstone <oscarbenderstone@gmail.com>
Co-authored-by: Oscar Bender-Stone <88625129+oscar-benderstone@users.noreply.github.com>
This commit is contained in:
Daniel Skates 2025-07-05 00:27:21 +08:00 committed by GitHub
parent 1380a3b7f2
commit 560429ebd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 129 additions and 102 deletions

View File

@ -94,10 +94,10 @@ use core::{
note = "consider annotating `{Self}` with `#[derive(Event)]`"
)]
pub trait Event: Send + Sync + 'static {
/// Generates the [`ComponentId`] for this event type.
/// Generates the [`EventKey`] for this event type.
///
/// If this type has already been registered,
/// this will return the existing [`ComponentId`].
/// this will return the existing [`EventKey`].
///
/// This is used by various dynamically typed observer APIs,
/// such as [`World::trigger_targets_dynamic`].
@ -105,12 +105,12 @@ pub trait Event: Send + Sync + 'static {
/// # Warning
///
/// This method should not be overridden by implementers,
/// and should always correspond to the implementation of [`component_id`](Event::component_id).
fn register_component_id(world: &mut World) -> ComponentId {
world.register_component::<EventWrapperComponent<Self>>()
/// 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::<EventWrapperComponent<Self>>())
}
/// Fetches the [`ComponentId`] for this event type,
/// Fetches the [`EventKey`] for this event type,
/// if it has already been generated.
///
/// This is used by various dynamically typed observer APIs,
@ -119,9 +119,12 @@ pub trait Event: Send + Sync + 'static {
/// # Warning
///
/// This method should not be overridden by implementers,
/// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id).
fn component_id(world: &World) -> Option<ComponentId> {
world.component_id::<EventWrapperComponent<Self>>()
/// and should always correspond to the implementation of
/// [`register_event_key`](Event::register_event_key).
fn event_key(world: &World) -> Option<EventKey> {
world
.component_id::<EventWrapperComponent<Self>>()
.map(EventKey)
}
}
@ -421,3 +424,19 @@ pub(crate) struct EventInstance<E: BufferedEvent> {
pub event_id: EventId<E>,
pub event: E,
}
/// A unique identifier for an [`Event`], used by [observers].
///
/// You can look up the key for your event by calling the [`Event::event_key`] method.
///
/// [observers]: crate::observer
#[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct EventKey(pub(crate) ComponentId);
impl EventKey {
/// Returns the internal [`ComponentId`].
#[inline]
pub(crate) fn component_id(&self) -> ComponentId {
self.0
}
}

View File

@ -11,7 +11,7 @@ mod update;
mod writer;
pub(crate) use base::EventInstance;
pub use base::{BufferedEvent, EntityEvent, Event, EventId};
pub use base::{BufferedEvent, EntityEvent, Event, EventId, EventKey};
pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event};
pub use collections::{Events, SendBatchIds};
pub use event_cursor::EventCursor;

View File

@ -1,15 +1,15 @@
use alloc::vec::Vec;
use bevy_ecs::{
change_detection::{DetectChangesMut, MutUntyped},
component::{ComponentId, Tick},
event::{BufferedEvent, Events},
component::Tick,
event::{BufferedEvent, EventKey, Events},
resource::Resource,
world::World,
};
#[doc(hidden)]
struct RegisteredEvent {
component_id: ComponentId,
event_key: EventKey,
// Required to flush the secondary buffer and drop events even if left unchanged.
previously_updated: bool,
// SAFETY: The component ID and the function must be used to fetch the Events<T> resource
@ -51,7 +51,7 @@ impl EventRegistry {
let component_id = world.init_resource::<Events<T>>();
let mut registry = world.get_resource_or_init::<Self>();
registry.event_updates.push(RegisteredEvent {
component_id,
event_key: EventKey(component_id),
previously_updated: false,
update: |ptr| {
// SAFETY: The resource was initialized with the type Events<T>.
@ -66,7 +66,9 @@ impl EventRegistry {
pub fn run_updates(&mut self, world: &mut World, last_change_tick: Tick) {
for registered_event in &mut self.event_updates {
// Bypass the type ID -> Component ID lookup with the cached component ID.
if let Some(events) = world.get_resource_mut_by_id(registered_event.component_id) {
if let Some(events) =
world.get_resource_mut_by_id(registered_event.event_key.component_id())
{
let has_changed = events.has_changed_since(last_change_tick);
if registered_event.previously_updated || has_changed {
// SAFETY: The update function pointer is called with the resource
@ -87,7 +89,7 @@ impl EventRegistry {
let mut registry = world.get_resource_or_init::<Self>();
registry
.event_updates
.retain(|e| e.component_id != component_id);
.retain(|e| e.event_key.component_id() != component_id);
world.remove_resource::<Events<T>>();
}
}

View File

@ -79,7 +79,8 @@ pub mod prelude {
entity::{ContainsEntity, Entity, EntityMapper},
error::{BevyError, Result},
event::{
BufferedEvent, EntityEvent, Event, EventMutator, EventReader, EventWriter, Events,
BufferedEvent, EntityEvent, Event, EventKey, EventMutator, EventReader, EventWriter,
Events,
},
hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children},
lifecycle::{

View File

@ -55,7 +55,7 @@ use crate::{
entity::Entity,
event::{
BufferedEvent, EntityEvent, Event, EventCursor, EventId, EventIterator,
EventIteratorWithId, Events,
EventIteratorWithId, EventKey, Events,
},
query::FilteredAccessSet,
relationship::RelationshipHookMode,
@ -314,16 +314,16 @@ impl ComponentHooks {
}
}
/// [`ComponentId`] for [`Add`]
pub const ADD: ComponentId = ComponentId::new(0);
/// [`ComponentId`] for [`Insert`]
pub const INSERT: ComponentId = ComponentId::new(1);
/// [`ComponentId`] for [`Replace`]
pub const REPLACE: ComponentId = ComponentId::new(2);
/// [`ComponentId`] for [`Remove`]
pub const REMOVE: ComponentId = ComponentId::new(3);
/// [`ComponentId`] for [`Despawn`]
pub const DESPAWN: ComponentId = ComponentId::new(4);
/// [`EventKey`] for [`Add`]
pub const ADD: EventKey = EventKey(ComponentId::new(0));
/// [`EventKey`] for [`Insert`]
pub const INSERT: EventKey = EventKey(ComponentId::new(1));
/// [`EventKey`] for [`Replace`]
pub const REPLACE: EventKey = EventKey(ComponentId::new(2));
/// [`EventKey`] for [`Remove`]
pub const REMOVE: EventKey = EventKey(ComponentId::new(3));
/// [`EventKey`] for [`Despawn`]
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`.

View File

@ -37,44 +37,44 @@ pub struct Observers {
remove: CachedObservers,
despawn: CachedObservers,
// Map from trigger type to set of observers listening to that trigger
cache: HashMap<ComponentId, CachedObservers>,
cache: HashMap<EventKey, CachedObservers>,
}
impl Observers {
pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers {
pub(crate) fn get_observers_mut(&mut self, event_key: EventKey) -> &mut CachedObservers {
use crate::lifecycle::*;
match event_type {
match event_key {
ADD => &mut self.add,
INSERT => &mut self.insert,
REPLACE => &mut self.replace,
REMOVE => &mut self.remove,
DESPAWN => &mut self.despawn,
_ => self.cache.entry(event_type).or_default(),
_ => self.cache.entry(event_key).or_default(),
}
}
/// Attempts to get the observers for the given `event_type`.
/// Attempts to get the observers for the given `event_key`.
///
/// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`],
/// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module.
pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> {
/// use the [`EventKey`] constants from the [`lifecycle`](crate::lifecycle) module.
pub fn try_get_observers(&self, event_key: EventKey) -> Option<&CachedObservers> {
use crate::lifecycle::*;
match event_type {
match event_key {
ADD => Some(&self.add),
INSERT => Some(&self.insert),
REPLACE => Some(&self.replace),
REMOVE => Some(&self.remove),
DESPAWN => Some(&self.despawn),
_ => self.cache.get(&event_type),
_ => self.cache.get(&event_key),
}
}
/// This will run the observers of the given `event_type`, targeting the given `entity` and `components`.
/// This will run the observers of the given `event_key`, targeting the given `entity` and `components`.
pub(crate) fn invoke<T>(
mut world: DeferredWorld,
event_type: ComponentId,
event_key: EventKey,
current_target: Option<Entity>,
original_target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone,
@ -88,7 +88,7 @@ impl Observers {
// SAFETY: There are no outstanding world references
world.increment_trigger_id();
let observers = world.observers();
let Some(observers) = observers.try_get_observers(event_type) else {
let Some(observers) = observers.try_get_observers(event_key) else {
return;
};
// SAFETY: The only outstanding reference to world is `observers`
@ -102,7 +102,7 @@ impl Observers {
world.reborrow(),
ObserverTrigger {
observer,
event_type,
event_key,
components: components.clone().collect(),
current_target,
original_target,
@ -145,10 +145,10 @@ impl Observers {
});
}
pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option<ArchetypeFlags> {
pub(crate) fn is_archetype_cached(event_key: EventKey) -> Option<ArchetypeFlags> {
use crate::lifecycle::*;
match event_type {
match event_key {
ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER),
INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),
REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER),

View File

@ -289,9 +289,9 @@ impl Observer {
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
/// is triggered.
/// # Safety
/// The type of the `event` [`ComponentId`] _must_ match the actual value
/// The type of the `event` [`EventKey`] _must_ match the actual value
/// of the event passed into the observer system.
pub unsafe fn with_event(mut self, event: ComponentId) -> Self {
pub unsafe fn with_event(mut self, event: EventKey) -> Self {
self.descriptor.events.push(event);
self
}
@ -350,7 +350,7 @@ impl Component for Observer {
#[derive(Default, Clone)]
pub struct ObserverDescriptor {
/// The events the observer is watching.
pub(super) events: Vec<ComponentId>,
pub(super) events: Vec<EventKey>,
/// The components the observer is watching.
pub(super) components: Vec<ComponentId>,
@ -362,9 +362,9 @@ pub struct ObserverDescriptor {
impl ObserverDescriptor {
/// Add the given `events` to the descriptor.
/// # Safety
/// The type of each [`ComponentId`] in `events` _must_ match the actual value
/// The type of each [`EventKey`] in `events` _must_ match the actual value
/// of the event passed into the observer.
pub unsafe fn with_events(mut self, events: Vec<ComponentId>) -> Self {
pub unsafe fn with_events(mut self, events: Vec<EventKey>) -> Self {
self.events = events;
self
}
@ -382,7 +382,7 @@ impl ObserverDescriptor {
}
/// Returns the `events` that the observer is watching.
pub fn events(&self) -> &[ComponentId] {
pub fn events(&self) -> &[EventKey] {
&self.events
}
@ -410,13 +410,13 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
HookContext { entity, .. }: HookContext,
) {
world.commands().queue(move |world: &mut World| {
let event_id = E::register_component_id(world);
let event_key = E::register_event_key(world);
let mut components = alloc::vec![];
B::component_ids(&mut world.components_registrator(), &mut |id| {
components.push(id);
});
if let Some(mut observer) = world.get_mut::<Observer>(entity) {
observer.descriptor.events.push(event_id);
observer.descriptor.events.push(event_key);
observer.descriptor.components.extend(components);
let system: &mut dyn Any = observer.system.as_mut();

View File

@ -43,10 +43,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo
.get_mut::<Observer>(observer_entity)
.expect("Source observer entity must have Observer");
observer_state.descriptor.entities.push(target);
let event_types = observer_state.descriptor.events.clone();
let event_keys = observer_state.descriptor.events.clone();
let components = observer_state.descriptor.components.clone();
for event_type in event_types {
let observers = world.observers.get_observers_mut(event_type);
for event_key in event_keys {
let observers = world.observers.get_observers_mut(event_key);
if components.is_empty() {
if let Some(map) = observers.entity_observers.get(&source).cloned() {
observers.entity_observers.insert(target, map);

View File

@ -197,10 +197,10 @@ impl World {
}
pub(crate) fn trigger_with_caller<E: Event>(&mut self, mut event: E, caller: MaybeLocation) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
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_id, &mut event, caller);
self.trigger_dynamic_ref_with_caller(event_key, &mut event, caller);
}
}
@ -210,22 +210,22 @@ impl World {
/// or use the event after it has been modified by observers.
#[track_caller]
pub fn trigger_ref<E: Event>(&mut self, event: &mut E) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
unsafe { self.trigger_dynamic_ref_with_caller(event_id, event, MaybeLocation::caller()) };
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<E: Event>(
&mut self,
event_id: ComponentId,
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_id`
// SAFETY: `event_data` is accessible as the type represented by `event_key`
unsafe {
world.trigger_observers_with_data::<_, ()>(
event_id,
event_key,
None,
None,
core::iter::empty::<ComponentId>(),
@ -252,10 +252,10 @@ impl World {
targets: impl TriggerTargets,
caller: MaybeLocation,
) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
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_id, &mut event, targets, caller);
self.trigger_targets_dynamic_ref_with_caller(event_key, &mut event, targets, caller);
}
}
@ -270,9 +270,9 @@ impl World {
event: &mut E,
targets: impl TriggerTargets,
) {
let event_id = E::register_component_id(self);
// SAFETY: We just registered `event_id` with the type of `event`
unsafe { self.trigger_targets_dynamic_ref(event_id, event, targets) };
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.
@ -283,17 +283,17 @@ impl World {
///
/// # Safety
///
/// Caller must ensure that `event_data` is accessible as the type represented by `event_id`.
/// Caller must ensure that `event_data` is accessible as the type represented by `event_key`.
#[track_caller]
pub unsafe fn trigger_targets_dynamic<E: EntityEvent, Targets: TriggerTargets>(
&mut self,
event_id: ComponentId,
event_key: EventKey,
mut event_data: E,
targets: Targets,
) {
// SAFETY: `event_data` is accessible as the type represented by `event_id`
// SAFETY: `event_data` is accessible as the type represented by `event_key`
unsafe {
self.trigger_targets_dynamic_ref(event_id, &mut event_data, targets);
self.trigger_targets_dynamic_ref(event_key, &mut event_data, targets);
};
}
@ -305,16 +305,16 @@ impl World {
///
/// # Safety
///
/// Caller must ensure that `event_data` is accessible as the type represented by `event_id`.
/// Caller must ensure that `event_data` is accessible as the type represented by `event_key`.
#[track_caller]
pub unsafe fn trigger_targets_dynamic_ref<E: EntityEvent, Targets: TriggerTargets>(
&mut self,
event_id: ComponentId,
event_key: EventKey,
event_data: &mut E,
targets: Targets,
) {
self.trigger_targets_dynamic_ref_with_caller(
event_id,
event_key,
event_data,
targets,
MaybeLocation::caller(),
@ -326,7 +326,7 @@ impl World {
/// See `trigger_targets_dynamic_ref`
unsafe fn trigger_targets_dynamic_ref_with_caller<E: EntityEvent, Targets: TriggerTargets>(
&mut self,
event_id: ComponentId,
event_key: EventKey,
event_data: &mut E,
targets: Targets,
caller: MaybeLocation,
@ -334,10 +334,10 @@ impl World {
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_id`
// SAFETY: `event_data` is accessible as the type represented by `event_key`
unsafe {
world.trigger_observers_with_data::<_, E::Traversal>(
event_id,
event_key,
None,
None,
targets.components(),
@ -348,10 +348,10 @@ impl World {
};
} else {
for target_entity in entity_targets {
// SAFETY: `event_data` is accessible as the type represented by `event_id`
// SAFETY: `event_data` is accessible as the type represented by `event_key`
unsafe {
world.trigger_observers_with_data::<_, E::Traversal>(
event_id,
event_key,
Some(target_entity),
Some(target_entity),
targets.components(),
@ -379,8 +379,8 @@ impl World {
};
let descriptor = &observer_state.descriptor;
for &event_type in &descriptor.events {
let cache = observers.get_observers_mut(event_type);
for &event_key in &descriptor.events {
let cache = observers.get_observers_mut(event_key);
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
cache
@ -400,7 +400,7 @@ impl World {
.component_observers
.entry(component)
.or_insert_with(|| {
if let Some(flag) = Observers::is_archetype_cached(event_type) {
if let Some(flag) = Observers::is_archetype_cached(event_key) {
archetypes.update_flags(component, flag, true);
}
CachedComponentObservers::default()
@ -430,8 +430,8 @@ impl World {
let archetypes = &mut self.archetypes;
let observers = &mut self.observers;
for &event_type in &descriptor.events {
let cache = observers.get_observers_mut(event_type);
for &event_key in &descriptor.events {
let cache = observers.get_observers_mut(event_key);
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
cache.global_observers.remove(&entity);
} else if descriptor.components.is_empty() {
@ -470,7 +470,7 @@ impl World {
&& observers.entity_component_observers.is_empty()
{
cache.component_observers.remove(component);
if let Some(flag) = Observers::is_archetype_cached(event_type) {
if let Some(flag) = Observers::is_archetype_cached(event_key) {
if let Some(by_component) = archetypes.by_component.get(component) {
for archetype in by_component.keys() {
let archetype = &mut archetypes.archetypes[archetype.index()];
@ -734,7 +734,7 @@ mod tests {
fn observer_multiple_events() {
let mut world = World::new();
world.init_resource::<Order>();
let on_remove = Remove::register_component_id(&mut world);
let on_remove = Remove::register_event_key(&mut world);
world.spawn(
// SAFETY: Add and Remove are both unit types, so this is safe
unsafe {
@ -1008,7 +1008,7 @@ mod tests {
fn observer_dynamic_trigger() {
let mut world = World::new();
world.init_resource::<Order>();
let event_a = Remove::register_component_id(&mut world);
let event_a = Remove::register_event_key(&mut world);
// SAFETY: we registered `event_a` above and it matches the type of EventA
let observe = unsafe {

View File

@ -49,8 +49,8 @@ impl<'w, E, B: Bundle> On<'w, E, B> {
}
/// Returns the event type of this [`On`] instance.
pub fn event_type(&self) -> ComponentId {
self.trigger.event_type
pub fn event_key(&self) -> EventKey {
self.trigger.event_key
}
/// Returns a reference to the triggered event.
@ -182,7 +182,7 @@ pub struct ObserverTrigger {
/// The [`Entity`] of the observer handling the trigger.
pub observer: Entity,
/// The [`Event`] the trigger targeted.
pub event_type: ComponentId,
pub event_key: EventKey,
/// The [`ComponentId`]s the trigger targeted.
pub components: SmallVec<[ComponentId; 2]>,
/// The entity that the entity-event targeted, if any.

View File

@ -7,7 +7,7 @@ use crate::{
change_detection::{MaybeLocation, MutUntyped},
component::{ComponentId, Mutable},
entity::Entity,
event::{BufferedEvent, EntityEvent, Event, EventId, Events, SendBatchIds},
event::{BufferedEvent, EntityEvent, Event, EventId, EventKey, Events, SendBatchIds},
lifecycle::{HookContext, INSERT, REPLACE},
observer::{Observers, TriggerTargets},
prelude::{Component, QueryState},
@ -749,7 +749,7 @@ impl<'w> DeferredWorld<'w> {
#[inline]
pub(crate) unsafe fn trigger_observers(
&mut self,
event: ComponentId,
event: EventKey,
target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone,
caller: MaybeLocation,
@ -773,7 +773,7 @@ impl<'w> DeferredWorld<'w> {
#[inline]
pub(crate) unsafe fn trigger_observers_with_data<E, T>(
&mut self,
event: ComponentId,
event: EventKey,
current_target: Option<Entity>,
original_target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone,

View File

@ -152,19 +152,19 @@ impl World {
#[inline]
fn bootstrap(&mut self) {
// The order that we register these events is vital to ensure that the constants are correct!
let on_add = Add::register_component_id(self);
let on_add = Add::register_event_key(self);
assert_eq!(ADD, on_add);
let on_insert = Insert::register_component_id(self);
let on_insert = Insert::register_event_key(self);
assert_eq!(INSERT, on_insert);
let on_replace = Replace::register_component_id(self);
let on_replace = Replace::register_event_key(self);
assert_eq!(REPLACE, on_replace);
let on_remove = Remove::register_component_id(self);
let on_remove = Remove::register_event_key(self);
assert_eq!(REMOVE, on_remove);
let on_despawn = Despawn::register_component_id(self);
let on_despawn = Despawn::register_event_key(self);
assert_eq!(DESPAWN, on_despawn);
// This sets up `Disabled` as a disabling component, via the FromWorld impl

View File

@ -1,7 +1,7 @@
---
title: Observer Overhaul
authors: ["@Jondolf", "@alice-i-cecile", "@hukasu]
pull_requests: [19596, 19663, 19611]
authors: ["@Jondolf", "@alice-i-cecile", "@hukasu", "oscar-benderstone", "Zeophlite"]
pull_requests: [19596, 19663, 19611, 19935]
---
## Rename `Trigger` to `On`
@ -45,3 +45,8 @@ This was handy! We've enabled this functionality for all entity-events: simply c
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.
## Use `EventKey` instead of `ComponentId`
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.