Skip to content

Commit eb3c813

Browse files
james-j-obrienalice-i-cecileMiniaczQcart
authored
Generalised ECS reactivity with Observers (#10839)
# Objective - Provide an expressive way to register dynamic behavior in response to ECS changes that is consistent with existing bevy types and traits as to provide a smooth user experience. - Provide a mechanism for immediate changes in response to events during command application in order to facilitate improved query caching on the path to relations. ## Solution - A new fundamental ECS construct, the `Observer`; inspired by flec's observers but adapted to better fit bevy's access patterns and rust's type system. --- ## Examples There are 3 main ways to register observers. The first is a "component observer" that looks like this: ```rust world.observe(|trigger: Trigger<OnAdd, Transform>, query: Query<&Transform>| { let transform = query.get(trigger.entity()).unwrap(); }); ``` The above code will spawn a new entity representing the observer that will run it's callback whenever the `Transform` component is added to an entity. This is a system-like function that supports dependency injection for all the standard bevy types: `Query`, `Res`, `Commands` etc. It also has a `Trigger` parameter that provides information about the trigger such as the target entity, and the event being triggered. Importantly these systems run during command application which is key for their future use to keep ECS internals up to date. There are similar events for `OnInsert` and `OnRemove`, and this will be expanded with things such as `ArchetypeCreated`, `TableEmpty` etc. in follow up PRs. Another way to register an observer is an "entity observer" that looks like this: ```rust world.entity_mut(entity).observe(|trigger: Trigger<Resize>| { // ... }); ``` Entity observers run whenever an event of their type is triggered targeting that specific entity. This type of observer will de-spawn itself if the entity (or entities) it is observing is ever de-spawned so as to not leave dangling observers. Entity observers can also be spawned from deferred contexts such as other observers, systems, or hooks using commands: ```rust commands.entity(entity).observe(|trigger: Trigger<Resize>| { // ... }); ``` Observers are not limited to in built event types, they can be used with any type that implements `Event` (which has been extended to implement Component). This means events can also carry data: ```rust #[derive(Event)] struct Resize { x: u32, y: u32 } commands.entity(entity).observe(|trigger: Trigger<Resize>, query: Query<&mut Size>| { let event = trigger.event(); // ... }); // Will trigger the observer when commands are applied. commands.trigger_targets(Resize { x: 10, y: 10 }, entity); ``` You can also trigger events that target more than one entity at a time: ```rust commands.trigger_targets(Resize { x: 10, y: 10 }, [e1, e2]); ``` Additionally, Observers don't _need_ entity targets: ```rust app.observe(|trigger: Trigger<Quit>| { }) commands.trigger(Quit); ``` In these cases, `trigger.entity()` will be a placeholder. Observers are actually just normal entities with an `ObserverState` and `Observer` component! The `observe()` functions above are just shorthand for: ```rust world.spawn(Observer::new(|trigger: Trigger<Resize>| {}); ``` This will spawn the `Observer` system and use an `on_add` hook to add the `ObserverState` component. Dynamic components and trigger types are also fully supported allowing for runtime defined trigger types. ## Possible Follow-ups 1. Deprecate `RemovedComponents`, observers should fulfill all use cases while being more flexible and performant. 2. Queries as entities: Swap queries to entities and begin using observers listening to archetype creation triggers to keep their caches in sync, this allows unification of `ObserverState` and `QueryState` as well as unlocking several API improvements for `Query` and the management of `QueryState`. 3. Trigger bubbling: For some UI use cases in particular users are likely to want some form of bubbling for entity observers, this is trivial to implement naively but ideally this includes an acceleration structure to cache hierarchy traversals. 4. All kinds of other in-built trigger types. 5. Optimization; in order to not bloat the complexity of the PR I have kept the implementation straightforward, there are several areas where performance can be improved. The focus for this PR is to get the behavior implemented and not incur a performance cost for users who don't use observers. I am leaving each of these to follow up PR's in order to keep each of them reviewable as this already includes significant changes. --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: MiniaczQ <[email protected]> Co-authored-by: Carter Anderson <[email protected]>
1 parent 1a1b22e commit eb3c813

33 files changed

+2252
-153
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2534,6 +2534,17 @@ description = "Systems run in parallel, but their order isn't always determinist
25342534
category = "ECS (Entity Component System)"
25352535
wasm = false
25362536

2537+
[[example]]
2538+
name = "observers"
2539+
path = "examples/ecs/observers.rs"
2540+
doc-scrape-examples = true
2541+
2542+
[package.metadata.example.observers]
2543+
name = "Observers"
2544+
description = "Demonstrates observers that react to events (both built-in life-cycle events and custom events)"
2545+
category = "ECS (Entity Component System)"
2546+
wasm = true
2547+
25372548
[[example]]
25382549
name = "3d_rotation"
25392550
path = "examples/transforms/3d_rotation.rs"

crates/bevy_app/src/app.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use bevy_ecs::{
88
intern::Interned,
99
prelude::*,
1010
schedule::{ScheduleBuildSettings, ScheduleLabel},
11-
system::SystemId,
11+
system::{IntoObserverSystem, SystemId},
1212
};
1313
#[cfg(feature = "trace")]
1414
use bevy_utils::tracing::info_span;
@@ -829,6 +829,15 @@ impl App {
829829

830830
None
831831
}
832+
833+
/// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
834+
pub fn observe<E: Event, B: Bundle, M>(
835+
&mut self,
836+
observer: impl IntoObserverSystem<E, B, M>,
837+
) -> &mut Self {
838+
self.world_mut().observe(observer);
839+
self
840+
}
832841
}
833842

834843
type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;

crates/bevy_ecs/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,52 @@ fn reader(mut reader: EventReader<MyEvent>) {
307307

308308
A minimal set up using events can be seen in [`events.rs`](examples/events.rs).
309309

310+
### Observers
311+
312+
Observers are systems that listen for a "trigger" of a specific `Event`:
313+
314+
```rust
315+
use bevy_ecs::prelude::*;
316+
317+
#[derive(Event)]
318+
struct MyEvent {
319+
message: String
320+
}
321+
322+
let mut world = World::new();
323+
324+
world.observe(|trigger: Trigger<MyEvent>| {
325+
println!("{}", trigger.event().message);
326+
});
327+
328+
world.flush();
329+
330+
world.trigger(MyEvent {
331+
message: "hello!".to_string(),
332+
});
333+
```
334+
335+
These differ from `EventReader` and `EventWriter` in that they are "reactive". Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. Triggers can trigger other triggers, and they all will be evaluated at the same time!
336+
337+
Events can also be triggered to target specific entities:
338+
339+
```rust
340+
use bevy_ecs::prelude::*;
341+
342+
#[derive(Event)]
343+
struct Explode;
344+
345+
let mut world = World::new();
346+
let entity = world.spawn_empty().id();
347+
348+
world.observe(|trigger: Trigger<Explode>, mut commands: Commands| {
349+
println!("Entity {:?} goes BOOM!", trigger.entity());
350+
commands.entity(trigger.entity()).despawn();
351+
});
352+
353+
world.flush();
354+
355+
world.trigger_targets(Explode, entity);
356+
```
357+
310358
[bevy]: https://bevyengine.org/

crates/bevy_ecs/macros/src/component.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ pub fn derive_event(input: TokenStream) -> TokenStream {
1818
TokenStream::from(quote! {
1919
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
2020
}
21+
22+
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
23+
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet;
24+
}
2125
})
2226
}
2327

crates/bevy_ecs/macros/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
7474
.collect::<Vec<_>>();
7575

7676
let mut field_component_ids = Vec::new();
77+
let mut field_get_component_ids = Vec::new();
7778
let mut field_get_components = Vec::new();
7879
let mut field_from_components = Vec::new();
7980
for (((i, field_type), field_kind), field) in field_type
@@ -87,6 +88,9 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
8788
field_component_ids.push(quote! {
8889
<#field_type as #ecs_path::bundle::Bundle>::component_ids(components, storages, &mut *ids);
8990
});
91+
field_get_component_ids.push(quote! {
92+
<#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);
93+
});
9094
match field {
9195
Some(field) => {
9296
field_get_components.push(quote! {
@@ -133,6 +137,13 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
133137
#(#field_component_ids)*
134138
}
135139

140+
fn get_component_ids(
141+
components: &#ecs_path::component::Components,
142+
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
143+
){
144+
#(#field_get_component_ids)*
145+
}
146+
136147
#[allow(unused_variables, non_snake_case)]
137148
unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
138149
where
@@ -435,6 +446,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
435446
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::apply(&mut state.state, system_meta, world);
436447
}
437448

449+
fn queue(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: #path::world::DeferredWorld) {
450+
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::queue(&mut state.state, system_meta, world);
451+
}
452+
438453
unsafe fn get_param<'w, 's>(
439454
state: &'s mut Self::State,
440455
system_meta: &#path::system::SystemMeta,

crates/bevy_ecs/src/archetype.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{
2323
bundle::BundleId,
2424
component::{ComponentId, Components, StorageType},
2525
entity::{Entity, EntityLocation},
26+
observer::Observers,
2627
storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow},
2728
};
2829
use std::{
@@ -119,6 +120,7 @@ pub(crate) struct AddBundle {
119120
/// For each component iterated in the same order as the source [`Bundle`](crate::bundle::Bundle),
120121
/// indicate if the component is newly added to the target archetype or if it already existed
121122
pub bundle_status: Vec<ComponentStatus>,
123+
pub added: Vec<ComponentId>,
122124
}
123125

124126
/// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components
@@ -202,12 +204,14 @@ impl Edges {
202204
bundle_id: BundleId,
203205
archetype_id: ArchetypeId,
204206
bundle_status: Vec<ComponentStatus>,
207+
added: Vec<ComponentId>,
205208
) {
206209
self.add_bundle.insert(
207210
bundle_id,
208211
AddBundle {
209212
archetype_id,
210213
bundle_status,
214+
added,
211215
},
212216
);
213217
}
@@ -314,6 +318,9 @@ bitflags::bitflags! {
314318
const ON_ADD_HOOK = (1 << 0);
315319
const ON_INSERT_HOOK = (1 << 1);
316320
const ON_REMOVE_HOOK = (1 << 2);
321+
const ON_ADD_OBSERVER = (1 << 3);
322+
const ON_INSERT_OBSERVER = (1 << 4);
323+
const ON_REMOVE_OBSERVER = (1 << 5);
317324
}
318325
}
319326

@@ -335,6 +342,7 @@ pub struct Archetype {
335342
impl Archetype {
336343
pub(crate) fn new(
337344
components: &Components,
345+
observers: &Observers,
338346
id: ArchetypeId,
339347
table_id: TableId,
340348
table_components: impl Iterator<Item = (ComponentId, ArchetypeComponentId)>,
@@ -348,6 +356,7 @@ impl Archetype {
348356
// SAFETY: We are creating an archetype that includes this component so it must exist
349357
let info = unsafe { components.get_info_unchecked(component_id) };
350358
info.update_archetype_flags(&mut flags);
359+
observers.update_archetype_flags(component_id, &mut flags);
351360
archetype_components.insert(
352361
component_id,
353362
ArchetypeComponentInfo {
@@ -580,21 +589,45 @@ impl Archetype {
580589

581590
/// Returns true if any of the components in this archetype have `on_add` hooks
582591
#[inline]
583-
pub(crate) fn has_on_add(&self) -> bool {
592+
pub fn has_add_hook(&self) -> bool {
584593
self.flags().contains(ArchetypeFlags::ON_ADD_HOOK)
585594
}
586595

587596
/// Returns true if any of the components in this archetype have `on_insert` hooks
588597
#[inline]
589-
pub(crate) fn has_on_insert(&self) -> bool {
598+
pub fn has_insert_hook(&self) -> bool {
590599
self.flags().contains(ArchetypeFlags::ON_INSERT_HOOK)
591600
}
592601

593602
/// Returns true if any of the components in this archetype have `on_remove` hooks
594603
#[inline]
595-
pub(crate) fn has_on_remove(&self) -> bool {
604+
pub fn has_remove_hook(&self) -> bool {
596605
self.flags().contains(ArchetypeFlags::ON_REMOVE_HOOK)
597606
}
607+
608+
/// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer
609+
///
610+
/// [`OnAdd`]: crate::world::OnAdd
611+
#[inline]
612+
pub fn has_add_observer(&self) -> bool {
613+
self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER)
614+
}
615+
616+
/// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer
617+
///
618+
/// [`OnInsert`]: crate::world::OnInsert
619+
#[inline]
620+
pub fn has_insert_observer(&self) -> bool {
621+
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
622+
}
623+
624+
/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
625+
///
626+
/// [`OnRemove`]: crate::world::OnRemove
627+
#[inline]
628+
pub fn has_remove_observer(&self) -> bool {
629+
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
630+
}
598631
}
599632

600633
/// The next [`ArchetypeId`] in an [`Archetypes`] collection.
@@ -681,6 +714,7 @@ impl Archetypes {
681714
unsafe {
682715
archetypes.get_id_or_insert(
683716
&Components::default(),
717+
&Observers::default(),
684718
TableId::empty(),
685719
Vec::new(),
686720
Vec::new(),
@@ -782,6 +816,7 @@ impl Archetypes {
782816
pub(crate) unsafe fn get_id_or_insert(
783817
&mut self,
784818
components: &Components,
819+
observers: &Observers,
785820
table_id: TableId,
786821
table_components: Vec<ComponentId>,
787822
sparse_set_components: Vec<ComponentId>,
@@ -808,6 +843,7 @@ impl Archetypes {
808843
(sparse_start..*archetype_component_count).map(ArchetypeComponentId);
809844
archetypes.push(Archetype::new(
810845
components,
846+
observers,
811847
id,
812848
table_id,
813849
table_components.into_iter().zip(table_archetype_components),
@@ -832,6 +868,20 @@ impl Archetypes {
832868
archetype.clear_entities();
833869
}
834870
}
871+
872+
pub(crate) fn update_flags(
873+
&mut self,
874+
component_id: ComponentId,
875+
flags: ArchetypeFlags,
876+
set: bool,
877+
) {
878+
// TODO: Refactor component index to speed this up.
879+
for archetype in &mut self.archetypes {
880+
if archetype.contains(component_id) {
881+
archetype.flags.set(flags, set);
882+
}
883+
}
884+
}
835885
}
836886

837887
impl Index<RangeFrom<ArchetypeGeneration>> for Archetypes {

0 commit comments

Comments
 (0)