Skip to content
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

Generalised ECS reactivity with Observers (#10839) #13873

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2534,6 +2534,17 @@ description = "Systems run in parallel, but their order isn't always determinist
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "observers"
path = "examples/ecs/observers.rs"
doc-scrape-examples = true

[package.metadata.example.observers]
name = "Observers"
description = "Demonstrates observers that react to events (both built-in life-cycle events and custom events)"
category = "ECS (Entity Component System)"
wasm = true

[[example]]
name = "3d_rotation"
path = "examples/transforms/3d_rotation.rs"
Expand Down
11 changes: 10 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bevy_ecs::{
intern::Interned,
prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel},
system::SystemId,
system::{IntoObserverSystem, SystemId},
};
#[cfg(feature = "trace")]
use bevy_utils::tracing::info_span;
Expand Down Expand Up @@ -829,6 +829,15 @@ impl App {

None
}

/// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
pub fn observe<E: Event, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
self.world_mut().observe(observer);
self
}
}

type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
Expand Down
48 changes: 48 additions & 0 deletions crates/bevy_ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,52 @@ fn reader(mut reader: EventReader<MyEvent>) {

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

### Observers

Observers are systems that listen for a "trigger" of a specific `Event`:

```rust
use bevy_ecs::prelude::*;

#[derive(Event)]
struct MyEvent {
message: String
}

let mut world = World::new();

world.observe(|trigger: Trigger<MyEvent>| {
println!("{}", trigger.event().message);
});

world.flush();

world.trigger(MyEvent {
message: "hello!".to_string(),
});
```

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!

Events can also be triggered to target specific entities:

```rust
use bevy_ecs::prelude::*;

#[derive(Event)]
struct Explode;

let mut world = World::new();
let entity = world.spawn_empty().id();

world.observe(|trigger: Trigger<Explode>, mut commands: Commands| {
println!("Entity {:?} goes BOOM!", trigger.entity());
commands.entity(trigger.entity()).despawn();
});

world.flush();

world.trigger_targets(Explode, entity);
```

[bevy]: https://bevyengine.org/
4 changes: 4 additions & 0 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub fn derive_event(input: TokenStream) -> TokenStream {
TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
}

impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet;
}
})
}

Expand Down
15 changes: 15 additions & 0 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
.collect::<Vec<_>>();

let mut field_component_ids = Vec::new();
let mut field_get_component_ids = Vec::new();
let mut field_get_components = Vec::new();
let mut field_from_components = Vec::new();
for (((i, field_type), field_kind), field) in field_type
Expand All @@ -87,6 +88,9 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
field_component_ids.push(quote! {
<#field_type as #ecs_path::bundle::Bundle>::component_ids(components, storages, &mut *ids);
});
field_get_component_ids.push(quote! {
<#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);
});
match field {
Some(field) => {
field_get_components.push(quote! {
Expand Down Expand Up @@ -133,6 +137,13 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
#(#field_component_ids)*
}

fn get_component_ids(
components: &#ecs_path::component::Components,
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
){
#(#field_get_component_ids)*
}

#[allow(unused_variables, non_snake_case)]
unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
where
Expand Down Expand Up @@ -435,6 +446,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::apply(&mut state.state, system_meta, world);
}

fn queue(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: #path::world::DeferredWorld) {
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::queue(&mut state.state, system_meta, world);
}

unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &#path::system::SystemMeta,
Expand Down
56 changes: 53 additions & 3 deletions crates/bevy_ecs/src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::{
bundle::BundleId,
component::{ComponentId, Components, StorageType},
entity::{Entity, EntityLocation},
observer::Observers,
storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow},
};
use std::{
Expand Down Expand Up @@ -119,6 +120,7 @@ pub(crate) struct AddBundle {
/// For each component iterated in the same order as the source [`Bundle`](crate::bundle::Bundle),
/// indicate if the component is newly added to the target archetype or if it already existed
pub bundle_status: Vec<ComponentStatus>,
pub added: Vec<ComponentId>,
}

/// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components
Expand Down Expand Up @@ -202,12 +204,14 @@ impl Edges {
bundle_id: BundleId,
archetype_id: ArchetypeId,
bundle_status: Vec<ComponentStatus>,
added: Vec<ComponentId>,
) {
self.add_bundle.insert(
bundle_id,
AddBundle {
archetype_id,
bundle_status,
added,
},
);
}
Expand Down Expand Up @@ -314,6 +318,9 @@ bitflags::bitflags! {
const ON_ADD_HOOK = (1 << 0);
const ON_INSERT_HOOK = (1 << 1);
const ON_REMOVE_HOOK = (1 << 2);
const ON_ADD_OBSERVER = (1 << 3);
const ON_INSERT_OBSERVER = (1 << 4);
const ON_REMOVE_OBSERVER = (1 << 5);
}
}

Expand All @@ -335,6 +342,7 @@ pub struct Archetype {
impl Archetype {
pub(crate) fn new(
components: &Components,
observers: &Observers,
id: ArchetypeId,
table_id: TableId,
table_components: impl Iterator<Item = (ComponentId, ArchetypeComponentId)>,
Expand All @@ -348,6 +356,7 @@ impl Archetype {
// SAFETY: We are creating an archetype that includes this component so it must exist
let info = unsafe { components.get_info_unchecked(component_id) };
info.update_archetype_flags(&mut flags);
observers.update_archetype_flags(component_id, &mut flags);
archetype_components.insert(
component_id,
ArchetypeComponentInfo {
Expand Down Expand Up @@ -580,21 +589,45 @@ impl Archetype {

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

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

/// Returns true if any of the components in this archetype have `on_remove` hooks
#[inline]
pub(crate) fn has_on_remove(&self) -> bool {
pub fn has_remove_hook(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_HOOK)
}

/// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer
///
/// [`OnAdd`]: crate::world::OnAdd
#[inline]
pub fn has_add_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER)
}

/// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer
///
/// [`OnInsert`]: crate::world::OnInsert
#[inline]
pub fn has_insert_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
}

/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
///
/// [`OnRemove`]: crate::world::OnRemove
#[inline]
pub fn has_remove_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
}
}

/// The next [`ArchetypeId`] in an [`Archetypes`] collection.
Expand Down Expand Up @@ -681,6 +714,7 @@ impl Archetypes {
unsafe {
archetypes.get_id_or_insert(
&Components::default(),
&Observers::default(),
TableId::empty(),
Vec::new(),
Vec::new(),
Expand Down Expand Up @@ -782,6 +816,7 @@ impl Archetypes {
pub(crate) unsafe fn get_id_or_insert(
&mut self,
components: &Components,
observers: &Observers,
table_id: TableId,
table_components: Vec<ComponentId>,
sparse_set_components: Vec<ComponentId>,
Expand All @@ -808,6 +843,7 @@ impl Archetypes {
(sparse_start..*archetype_component_count).map(ArchetypeComponentId);
archetypes.push(Archetype::new(
components,
observers,
id,
table_id,
table_components.into_iter().zip(table_archetype_components),
Expand All @@ -832,6 +868,20 @@ impl Archetypes {
archetype.clear_entities();
}
}

pub(crate) fn update_flags(
&mut self,
component_id: ComponentId,
flags: ArchetypeFlags,
set: bool,
) {
// TODO: Refactor component index to speed this up.
for archetype in &mut self.archetypes {
if archetype.contains(component_id) {
archetype.flags.set(flags, set);
}
}
}
}

impl Index<RangeFrom<ArchetypeGeneration>> for Archetypes {
Expand Down
Loading