Skip to content

Make observers metadata / storage publicly visible #19608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 16, 2025
8 changes: 5 additions & 3 deletions crates/bevy_ecs/src/observer/entity_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo
let event_types = observer_state.descriptor.events.clone();
let components = observer_state.descriptor.components.clone();
for event_type in event_types {
let observers = world.observers.get_observers(event_type);
let observers = world.observers.get_observers_mut(event_type);
if components.is_empty() {
if let Some(map) = observers.entity_observers.get(&source).cloned() {
observers.entity_observers.insert(target, map);
Expand All @@ -108,8 +108,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo
else {
continue;
};
if let Some(map) = observers.entity_map.get(&source).cloned() {
observers.entity_map.insert(target, map);
if let Some(map) =
observers.entity_component_observers.get(&source).cloned()
{
observers.entity_component_observers.insert(target, map);
}
}
}
Expand Down
105 changes: 79 additions & 26 deletions crates/bevy_ecs/src/observer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,40 +515,72 @@ impl ObserverTrigger {
}
}

// Map between an observer entity and its runner
type ObserverMap = EntityHashMap<ObserverRunner>;
/// Map between an observer entity and its [`ObserverRunner`]
pub type ObserverMap = EntityHashMap<ObserverRunner>;

/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component.
/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event targeted at a specific component.
///
/// This is stored inside of [`CachedObservers`].
#[derive(Default, Debug)]
pub struct CachedComponentObservers {
// Observers listening to triggers targeting this component
map: ObserverMap,
// Observers listening to triggers targeting this component on a specific entity
entity_map: EntityHashMap<ObserverMap>,
// Observers listening to events targeting this component, but not a specific entity
global_observers: ObserverMap,
// Observers listening to events targeting this component on a specific entity
entity_component_observers: EntityHashMap<ObserverMap>,
}

/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger.
impl CachedComponentObservers {
/// Returns the observers listening for this trigger, regardless of target.
Copy link
Member

@Vrixyz Vrixyz Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you changed trigger to event on a precedent line, but here trigger is back, it's a bit confusing ; maybe doc linking to actual type may help, so documentation for what those terms refer to would be easily reached and consulted 🤔

/// These observers will also respond to events targeting specific entities.
pub fn global_observers(&self) -> &ObserverMap {
&self.global_observers
}

/// Returns the observers listening for this trigger targeting this component on a specific entity.
pub fn entity_component_observers(&self) -> &EntityHashMap<ObserverMap> {
&self.entity_component_observers
}
}

/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event.
///
/// This is stored inside of [`Observers`], specialized for each kind of observer.
#[derive(Default, Debug)]
pub struct CachedObservers {
// Observers listening for any time this trigger is fired
map: ObserverMap,
// Observers listening for any time this event is fired, regardless of target
// This will also respond to events targeting specific components or entities
global_observers: ObserverMap,
// Observers listening for this trigger fired at a specific component
component_observers: HashMap<ComponentId, CachedComponentObservers>,
// Observers listening for this trigger fired at a specific entity
entity_observers: EntityHashMap<ObserverMap>,
}

impl CachedObservers {
/// Returns the observers listening for this trigger, regardless of target.
/// These observers will also respond to events targeting specific components or entities.
pub fn global_observers(&self) -> &ObserverMap {
&self.global_observers
}

/// Returns the observers listening for this trigger targeting components.
pub fn get_component_observers(&self) -> &HashMap<ComponentId, CachedComponentObservers> {
&self.component_observers
}

/// Returns the observers listening for this trigger targeting entities.
pub fn entity_observers(&self) -> &HashMap<ComponentId, CachedComponentObservers> {
&self.component_observers
}
}

/// An internal lookup table tracking all of the observers in the world.
///
/// Stores a cache mapping trigger ids to the registered observers.
/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field,
/// saving lookups for the most common triggers.
///
/// This is stored as a field of the [`World`].
/// This can be accessed via [`World::observers`].
#[derive(Default, Debug)]
pub struct Observers {
// Cached ECS observers to save a lookup most common triggers.
Expand All @@ -562,7 +594,7 @@ pub struct Observers {
}

impl Observers {
pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers {
pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers {
use crate::lifecycle::*;

match event_type {
Expand All @@ -575,7 +607,11 @@ impl Observers {
}
}

pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> {
/// Attempts to get the observers for the given `event_type`.
///
/// 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 crate::lifecycle::*;

match event_type {
Expand Down Expand Up @@ -630,7 +666,10 @@ impl Observers {
);
};
// Trigger observers listening for any kind of this trigger
observers.map.iter().for_each(&mut trigger_observer);
observers
.global_observers
.iter()
.for_each(&mut trigger_observer);

// Trigger entity observers listening for this kind of trigger
if let Some(target_entity) = current_target {
Expand All @@ -643,12 +682,15 @@ impl Observers {
trigger_for_components.for_each(|id| {
if let Some(component_observers) = observers.component_observers.get(&id) {
component_observers
.map
.global_observers
.iter()
.for_each(&mut trigger_observer);

if let Some(target_entity) = current_target {
if let Some(map) = component_observers.entity_map.get(&target_entity) {
if let Some(map) = component_observers
.entity_component_observers
.get(&target_entity)
{
map.iter().for_each(&mut trigger_observer);
}
}
Expand Down Expand Up @@ -926,10 +968,12 @@ impl World {
let descriptor = &observer_state.descriptor;

for &event_type in &descriptor.events {
let cache = observers.get_observers(event_type);
let cache = observers.get_observers_mut(event_type);

if descriptor.components.is_empty() && descriptor.entities.is_empty() {
cache.map.insert(observer_entity, observer_state.runner);
cache
.global_observers
.insert(observer_entity, observer_state.runner);
} else if descriptor.components.is_empty() {
// Observer is not targeting any components so register it as an entity observer
for &watched_entity in &observer_state.descriptor.entities {
Expand All @@ -951,11 +995,16 @@ impl World {
});
if descriptor.entities.is_empty() {
// Register for all triggers targeting the component
observers.map.insert(observer_entity, observer_state.runner);
observers
.global_observers
.insert(observer_entity, observer_state.runner);
} else {
// Register for each watched entity
for &watched_entity in &descriptor.entities {
let map = observers.entity_map.entry(watched_entity).or_default();
let map = observers
.entity_component_observers
.entry(watched_entity)
.or_default();
map.insert(observer_entity, observer_state.runner);
}
}
Expand All @@ -970,9 +1019,9 @@ impl World {
let observers = &mut self.observers;

for &event_type in &descriptor.events {
let cache = observers.get_observers(event_type);
let cache = observers.get_observers_mut(event_type);
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
cache.map.remove(&entity);
cache.global_observers.remove(&entity);
} else if descriptor.components.is_empty() {
for watched_entity in &descriptor.entities {
// This check should be unnecessary since this observer hasn't been unregistered yet
Expand All @@ -990,20 +1039,24 @@ impl World {
continue;
};
if descriptor.entities.is_empty() {
observers.map.remove(&entity);
observers.global_observers.remove(&entity);
} else {
for watched_entity in &descriptor.entities {
let Some(map) = observers.entity_map.get_mut(watched_entity) else {
let Some(map) =
observers.entity_component_observers.get_mut(watched_entity)
else {
continue;
};
map.remove(&entity);
if map.is_empty() {
observers.entity_map.remove(watched_entity);
observers.entity_component_observers.remove(watched_entity);
}
}
}

if observers.map.is_empty() && observers.entity_map.is_empty() {
if observers.global_observers.is_empty()
&& observers.entity_component_observers.is_empty()
{
cache.component_observers.remove(component);
if let Some(flag) = Observers::is_archetype_cached(event_type) {
if let Some(by_component) = archetypes.by_component.get(component) {
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ impl World {
&self.removed_components
}

/// Retrieves this world's [`Observers`] list
#[inline]
pub fn observers(&self) -> &Observers {
&self.observers
}

/// Creates a new [`Commands`] instance that writes to the world's command queue
/// Use [`World::flush`] to apply all queued commands
#[inline]
Expand Down