From eb3c81374a703bb5630869819b382a5a1d5a2e90 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Fri, 14 Jun 2024 18:33:26 -0700 Subject: [PATCH] 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, 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| { // ... }); ``` 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| { // ... }); ``` 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, 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| { }) 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| {}); ``` 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 Co-authored-by: MiniaczQ Co-authored-by: Carter Anderson --- Cargo.toml | 11 + crates/bevy_app/src/app.rs | 11 +- crates/bevy_ecs/README.md | 48 ++ crates/bevy_ecs/macros/src/component.rs | 4 + crates/bevy_ecs/macros/src/lib.rs | 15 + crates/bevy_ecs/src/archetype.rs | 56 +- crates/bevy_ecs/src/bundle.rs | 97 +-- crates/bevy_ecs/src/event/base.rs | 19 +- crates/bevy_ecs/src/event/writer.rs | 7 + crates/bevy_ecs/src/lib.rs | 6 +- .../bevy_ecs/src/observer/entity_observer.rs | 42 ++ crates/bevy_ecs/src/observer/mod.rs | 638 ++++++++++++++++++ crates/bevy_ecs/src/observer/runner.rs | 409 +++++++++++ crates/bevy_ecs/src/observer/trigger_event.rs | 165 +++++ crates/bevy_ecs/src/query/access.rs | 10 + crates/bevy_ecs/src/system/adapter_system.rs | 5 + crates/bevy_ecs/src/system/combinator.rs | 6 + crates/bevy_ecs/src/system/commands/mod.rs | 56 +- .../src/system/exclusive_function_system.rs | 9 +- crates/bevy_ecs/src/system/function_system.rs | 12 +- crates/bevy_ecs/src/system/mod.rs | 2 + crates/bevy_ecs/src/system/observer_system.rs | 71 ++ crates/bevy_ecs/src/system/system.rs | 5 + crates/bevy_ecs/src/system/system_param.rs | 39 +- crates/bevy_ecs/src/world/command_queue.rs | 145 +++- .../bevy_ecs/src/world/component_constants.rs | 23 + crates/bevy_ecs/src/world/deferred_world.rs | 127 +++- crates/bevy_ecs/src/world/entity_ref.rs | 76 ++- crates/bevy_ecs/src/world/mod.rs | 63 +- crates/bevy_ecs/src/world/spawn_batch.rs | 2 +- .../bevy_ecs/src/world/unsafe_world_cell.rs | 16 + examples/README.md | 1 + examples/ecs/observers.rs | 209 ++++++ 33 files changed, 2252 insertions(+), 153 deletions(-) create mode 100644 crates/bevy_ecs/src/observer/entity_observer.rs create mode 100644 crates/bevy_ecs/src/observer/mod.rs create mode 100644 crates/bevy_ecs/src/observer/runner.rs create mode 100644 crates/bevy_ecs/src/observer/trigger_event.rs create mode 100644 crates/bevy_ecs/src/system/observer_system.rs create mode 100644 crates/bevy_ecs/src/world/component_constants.rs create mode 100644 examples/ecs/observers.rs diff --git a/Cargo.toml b/Cargo.toml index fc1e10fc7b83d..27ebefbe940ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 555e30a9928a2..fdd7db483beaa 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -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; @@ -829,6 +829,15 @@ impl App { None } + + /// Spawns an [`Observer`] entity, which will watch for and respond to the given event. + pub fn observe( + &mut self, + observer: impl IntoObserverSystem, + ) -> &mut Self { + self.world_mut().observe(observer); + self + } } type RunnerFn = Box AppExit>; diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index 81cc9104dfe93..138b10d52ee26 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -307,4 +307,52 @@ fn reader(mut reader: EventReader) { 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| { + 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, 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/ diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index b7b7e1582c8b3..dbd28e4b28ac6 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -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; + } }) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index dbca96db17e76..60e8027756731 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -74,6 +74,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { .collect::>(); 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 @@ -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! { @@ -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 @@ -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, diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 5f6302a2e1628..05b0a260cfe4e 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -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::{ @@ -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, + pub added: Vec, } /// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components @@ -202,12 +204,14 @@ impl Edges { bundle_id: BundleId, archetype_id: ArchetypeId, bundle_status: Vec, + added: Vec, ) { self.add_bundle.insert( bundle_id, AddBundle { archetype_id, bundle_status, + added, }, ); } @@ -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); } } @@ -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, @@ -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 { @@ -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. @@ -681,6 +714,7 @@ impl Archetypes { unsafe { archetypes.get_id_or_insert( &Components::default(), + &Observers::default(), TableId::empty(), Vec::new(), Vec::new(), @@ -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, sparse_set_components: Vec, @@ -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), @@ -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> for Archetypes { diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 8c67ae32dd46d..384f7d7a99c76 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2,8 +2,9 @@ //! //! This module contains the [`Bundle`] trait and some other helper types. +use std::any::TypeId; + pub use bevy_ecs_macros::Bundle; -use bevy_utils::{HashMap, HashSet, TypeIdMap}; use crate::{ archetype::{ @@ -12,14 +13,15 @@ use crate::{ }, component::{Component, ComponentId, Components, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, + observer::Observers, prelude::World, query::DebugCheckedUnwrap, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::unsafe_world_cell::UnsafeWorldCell, + world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT}, }; + use bevy_ptr::{ConstNonNull, OwningPtr}; -use bevy_utils::all_tuples; -use std::any::TypeId; +use bevy_utils::{all_tuples, HashMap, HashSet, TypeIdMap}; use std::ptr::NonNull; /// The `Bundle` trait enables insertion and removal of [`Component`]s from an entity. @@ -155,6 +157,9 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { ids: &mut impl FnMut(ComponentId), ); + /// Gets this [`Bundle`]'s component ids. This will be [`None`] if the component has not been registered. + fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)); + /// Calls `func`, which should return data for each component in the bundle, in the order of /// this bundle's [`Component`]s /// @@ -204,6 +209,10 @@ unsafe impl Bundle for C { // Safety: The id given in `component_ids` is for `Self` unsafe { ptr.read() } } + + fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)) { + ids(components.get_id(TypeId::of::())); + } } impl DynamicBundle for C { @@ -227,6 +236,11 @@ macro_rules! tuple_impl { $(<$name as Bundle>::component_ids(components, storages, ids);)* } + #[allow(unused_variables)] + fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)){ + $(<$name as Bundle>::get_component_ids(components, ids);)* + } + #[allow(unused_variables, unused_mut)] #[allow(clippy::unused_unit)] unsafe fn from_components(ctx: &mut T, func: &mut F) -> Self @@ -432,6 +446,7 @@ impl BundleInfo { archetypes: &mut Archetypes, storages: &mut Storages, components: &Components, + observers: &Observers, archetype_id: ArchetypeId, ) -> ArchetypeId { if let Some(add_bundle_id) = archetypes[archetype_id].edges().get_add_bundle(self.id) { @@ -440,6 +455,7 @@ impl BundleInfo { let mut new_table_components = Vec::new(); let mut new_sparse_set_components = Vec::new(); let mut bundle_status = Vec::with_capacity(self.component_ids.len()); + let mut added = Vec::new(); let current_archetype = &mut archetypes[archetype_id]; for component_id in self.component_ids.iter().cloned() { @@ -447,6 +463,7 @@ impl BundleInfo { bundle_status.push(ComponentStatus::Mutated); } else { bundle_status.push(ComponentStatus::Added); + added.push(component_id); // SAFETY: component_id exists let component_info = unsafe { components.get_info_unchecked(component_id) }; match component_info.storage_type() { @@ -459,7 +476,7 @@ impl BundleInfo { if new_table_components.is_empty() && new_sparse_set_components.is_empty() { let edges = current_archetype.edges_mut(); // the archetype does not change when we add this bundle - edges.insert_add_bundle(self.id, archetype_id, bundle_status); + edges.insert_add_bundle(self.id, archetype_id, bundle_status, added); archetype_id } else { let table_id; @@ -498,6 +515,7 @@ impl BundleInfo { // SAFETY: ids in self must be valid let new_archetype_id = archetypes.get_id_or_insert( components, + observers, table_id, table_components, sparse_set_components, @@ -507,6 +525,7 @@ impl BundleInfo { self.id, new_archetype_id, bundle_status, + added, ); new_archetype_id } @@ -567,6 +586,7 @@ impl<'w> BundleInserter<'w> { &mut world.archetypes, &mut world.storages, &world.components, + &world.observers, archetype_id, ); if new_archetype_id == archetype_id { @@ -786,27 +806,21 @@ impl<'w> BundleInserter<'w> { } }; + let new_archetype = &*new_archetype; // SAFETY: We have no outstanding mutable references to world as they were dropped let mut deferred_world = unsafe { self.world.into_deferred() }; - if new_archetype.has_on_add() { - // SAFETY: All components in the bundle are guaranteed to exist in the World - // as they must be initialized before creating the BundleInfo. - unsafe { - deferred_world.trigger_on_add( - entity, - bundle_info - .iter_components() - .zip(add_bundle.bundle_status.iter()) - .filter(|(_, &status)| status == ComponentStatus::Added) - .map(|(id, _)| id), - ); + // SAFETY: All components in the bundle are guaranteed to exist in the World + // as they must be initialized before creating the BundleInfo. + unsafe { + deferred_world.trigger_on_add(new_archetype, entity, add_bundle.added.iter().cloned()); + if new_archetype.has_add_observer() { + deferred_world.trigger_observers(ON_ADD, entity, add_bundle.added.iter().cloned()); + } + deferred_world.trigger_on_insert(new_archetype, entity, bundle_info.iter_components()); + if new_archetype.has_insert_observer() { + deferred_world.trigger_observers(ON_INSERT, entity, bundle_info.iter_components()); } - } - if new_archetype.has_on_insert() { - // SAFETY: All components in the bundle are guaranteed to exist in the World - // as they must be initialized before creating the BundleInfo. - unsafe { deferred_world.trigger_on_insert(entity, bundle_info.iter_components()) } } new_location @@ -853,6 +867,7 @@ impl<'w> BundleSpawner<'w> { &mut world.archetypes, &mut world.storages, &world.components, + &world.observers, ArchetypeId::EMPTY, ); let archetype = &mut world.archetypes[new_archetype_id]; @@ -882,12 +897,12 @@ impl<'w> BundleSpawner<'w> { entity: Entity, bundle: T, ) -> EntityLocation { - let table = self.table.as_mut(); - let archetype = self.archetype.as_mut(); + // SAFETY: We do not make any structural changes to the archetype graph through self.world so these pointers always remain valid let bundle_info = self.bundle_info.as_ref(); - - // SAFETY: We do not make any structural changes to the archetype graph through self.world so this pointer always remain valid let location = { + let table = self.table.as_mut(); + let archetype = self.archetype.as_mut(); + // SAFETY: Mutable references do not alias and will be dropped after this block let (sparse_sets, entities) = { let world = self.world.world_mut(); @@ -910,16 +925,20 @@ impl<'w> BundleSpawner<'w> { // SAFETY: We have no outstanding mutable references to world as they were dropped let mut deferred_world = unsafe { self.world.into_deferred() }; - if archetype.has_on_add() { - // SAFETY: All components in the bundle are guaranteed to exist in the World - // as they must be initialized before creating the BundleInfo. - unsafe { deferred_world.trigger_on_add(entity, bundle_info.iter_components()) }; - } - if archetype.has_on_insert() { - // SAFETY: All components in the bundle are guaranteed to exist in the World - // as they must be initialized before creating the BundleInfo. - unsafe { deferred_world.trigger_on_insert(entity, bundle_info.iter_components()) }; - } + // SAFETY: `DeferredWorld` cannot provide mutable access to `Archetypes`. + let archetype = self.archetype.as_ref(); + // SAFETY: All components in the bundle are guaranteed to exist in the World + // as they must be initialized before creating the BundleInfo. + unsafe { + deferred_world.trigger_on_add(archetype, entity, bundle_info.iter_components()); + if archetype.has_add_observer() { + deferred_world.trigger_observers(ON_ADD, entity, bundle_info.iter_components()); + } + deferred_world.trigger_on_insert(archetype, entity, bundle_info.iter_components()); + if archetype.has_insert_observer() { + deferred_world.trigger_observers(ON_INSERT, entity, bundle_info.iter_components()); + } + }; location } @@ -947,7 +966,7 @@ impl<'w> BundleSpawner<'w> { #[inline] pub(crate) unsafe fn flush_commands(&mut self) { // SAFETY: pointers on self can be invalidated, - self.world.world_mut().flush_commands(); + self.world.world_mut().flush(); } } @@ -1223,13 +1242,13 @@ mod tests { world .register_component_hooks::() .on_add(|mut world, _, _| { - world.resource_mut::().assert_order(2); + world.resource_mut::().assert_order(3); }); world .register_component_hooks::() .on_add(|mut world, _, _| { - world.resource_mut::().assert_order(3); + world.resource_mut::().assert_order(2); }); world.spawn(A).flush(); diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 99598a74ccf9b..acc830da447bd 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -1,3 +1,4 @@ +use crate::component::Component; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use std::{ @@ -7,16 +8,30 @@ use std::{ marker::PhantomData, }; -/// A type that can be stored in an [`Events`] resource +/// Something that "happens" and might be read / observed by app logic. +/// +/// Events can be stored in an [`Events`] resource /// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter. /// +/// Events can also be "triggered" on a [`World`], which will then cause any [`Observer`] of that trigger to run. +/// +/// This trait can be derived. +/// +/// Events implement the [`Component`] type (and they automatically do when they are derived). Events are (generally) +/// not directly inserted as components. More often, the [`ComponentId`] is used to identify the event type within the +/// context of the ECS. +/// /// Events must be thread-safe. +/// +/// [`World`]: crate::world::World +/// [`ComponentId`]: crate::component::ComponentId +/// [`Observer`]: crate::observer::Observer #[diagnostic::on_unimplemented( message = "`{Self}` is not an `Event`", label = "invalid `Event`", note = "consider annotating `{Self}` with `#[derive(Event)]`" )] -pub trait Event: Send + Sync + 'static {} +pub trait Event: Component {} /// An `EventId` uniquely identifies an event stored in a specific [`World`]. /// diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index c5d8e5b9b6d29..0ba8d80778452 100644 --- a/crates/bevy_ecs/src/event/writer.rs +++ b/crates/bevy_ecs/src/event/writer.rs @@ -20,6 +20,11 @@ use bevy_ecs::{ /// /// # bevy_ecs::system::assert_is_system(my_system); /// ``` +/// # Observers +/// +/// "Buffered" Events, such as those sent directly in [`Events`] or sent using [`EventWriter`], do _not_ automatically +/// trigger any [`Observer`]s watching for that event, as each [`Event`] has different requirements regarding _if_ it will +/// be triggered, and if so, _when_ it will be triggered in the schedule. /// /// # Concurrency /// @@ -52,6 +57,8 @@ use bevy_ecs::{ /// } /// ``` /// Note that this is considered *non-idiomatic*, and should only be used when `EventWriter` will not work. +/// +/// [`Observer`]: crate::observer::Observer #[derive(SystemParam)] pub struct EventWriter<'w, E: Event> { events: ResMut<'w, Events>, diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index e6cabde398153..307c7a94a16e5 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -21,6 +21,7 @@ pub mod event; pub mod identifier; pub mod intern; pub mod label; +pub mod observer; pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; @@ -46,6 +47,7 @@ pub mod prelude { component::Component, entity::{Entity, EntityMapper}, event::{Event, EventReader, EventWriter, Events}, + observer::{Observer, Trigger}, query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, removal_detection::RemovedComponents, schedule::{ @@ -57,7 +59,9 @@ pub mod prelude { ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemBuilder, SystemParamFunction, }, - world::{EntityMut, EntityRef, EntityWorldMut, FromWorld, World}, + world::{ + EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, World, + }, }; } diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs new file mode 100644 index 0000000000000..ce30201785758 --- /dev/null +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -0,0 +1,42 @@ +use crate::{ + component::{Component, ComponentHooks, StorageType}, + entity::Entity, + observer::ObserverState, +}; + +/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. +#[derive(Default)] +pub(crate) struct ObservedBy(pub(crate) Vec); + +impl Component for ObservedBy { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_remove(|mut world, entity, _| { + let observed_by = { + let mut component = world.get_mut::(entity).unwrap(); + std::mem::take(&mut component.0) + }; + for e in observed_by { + let (total_entities, despawned_watched_entities) = { + let Some(mut entity_mut) = world.get_entity_mut(e) else { + continue; + }; + let Some(mut state) = entity_mut.get_mut::() else { + continue; + }; + state.despawned_watched_entities += 1; + ( + state.descriptor.entities.len(), + state.despawned_watched_entities as usize, + ) + }; + + // Despawn Observer if it has no more active sources. + if total_entities == despawned_watched_entities { + world.commands().entity(e).despawn(); + } + } + }); + } +} diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs new file mode 100644 index 0000000000000..08e87854c4c56 --- /dev/null +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -0,0 +1,638 @@ +//! Types for creating and storing [`Observer`]s + +mod entity_observer; +mod runner; +mod trigger_event; + +pub use runner::*; +pub use trigger_event::*; + +use crate::observer::entity_observer::ObservedBy; +use crate::{archetype::ArchetypeFlags, system::IntoObserverSystem, world::*}; +use crate::{component::ComponentId, prelude::*, world::DeferredWorld}; +use bevy_ptr::Ptr; +use bevy_utils::{EntityHashMap, HashMap}; +use std::marker::PhantomData; + +/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the +/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. +pub struct Trigger<'w, E, B: Bundle = ()> { + event: &'w mut E, + trigger: ObserverTrigger, + _marker: PhantomData, +} + +impl<'w, E, B: Bundle> Trigger<'w, E, B> { + /// Creates a new trigger for the given event and observer information. + pub fn new(event: &'w mut E, trigger: ObserverTrigger) -> Self { + Self { + event, + trigger, + _marker: PhantomData, + } + } + + /// Returns the event type of this trigger. + pub fn event_type(&self) -> ComponentId { + self.trigger.event_type + } + + /// Returns a reference to the triggered event. + pub fn event(&self) -> &E { + self.event + } + + /// Returns a mutable reference to the triggered event. + pub fn event_mut(&mut self) -> &mut E { + self.event + } + + /// Returns a pointer to the triggered event. + pub fn event_ptr(&self) -> Ptr { + Ptr::from(&self.event) + } + + /// Returns the entity that triggered the observer, could be [`Entity::PLACEHOLDER`]. + pub fn entity(&self) -> Entity { + self.trigger.entity + } +} + +/// A description of what an [`Observer`] observes. +#[derive(Default, Clone)] +pub struct ObserverDescriptor { + /// The events the observer is watching. + events: Vec, + + /// The components the observer is watching. + components: Vec, + + /// The entities the observer is watching. + entities: Vec, +} + +impl ObserverDescriptor { + /// Add the given `triggers` to the descriptor. + pub fn with_triggers(mut self, triggers: Vec) -> Self { + self.events = triggers; + self + } + + /// Add the given `components` to the descriptor. + pub fn with_components(mut self, components: Vec) -> Self { + self.components = components; + self + } + + /// Add the given `entities` to the descriptor. + pub fn with_entities(mut self, entities: Vec) -> Self { + self.entities = entities; + self + } + + pub(crate) fn merge(&mut self, descriptor: &ObserverDescriptor) { + self.events.extend(descriptor.events.iter().copied()); + self.components + .extend(descriptor.components.iter().copied()); + self.entities.extend(descriptor.entities.iter().copied()); + } +} + +/// Event trigger metadata for a given [`Observer`], +#[derive(Debug)] +pub struct ObserverTrigger { + /// The [`Entity`] of the observer handling the trigger. + pub observer: Entity, + + /// The [`ComponentId`] the trigger targeted. + pub event_type: ComponentId, + + /// The entity the trigger targeted. + pub entity: Entity, +} + +// Map between an observer entity and its runner +type ObserverMap = EntityHashMap; + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component. +#[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, +} + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger. +#[derive(Default, Debug)] +pub struct CachedObservers { + // Observers listening for any time this trigger is fired + map: ObserverMap, + // Observers listening for this trigger fired at a specific component + component_observers: HashMap, + // Observers listening for this trigger fired at a specific entity + entity_observers: EntityHashMap, +} + +/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers. +#[derive(Default, Debug)] +pub struct Observers { + // Cached ECS observers to save a lookup most common triggers. + on_add: CachedObservers, + on_insert: CachedObservers, + on_remove: CachedObservers, + // Map from trigger type to set of observers + cache: HashMap, +} + +impl Observers { + pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { + match event_type { + ON_ADD => &mut self.on_add, + ON_INSERT => &mut self.on_insert, + ON_REMOVE => &mut self.on_remove, + _ => self.cache.entry(event_type).or_default(), + } + } + + pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + match event_type { + ON_ADD => Some(&self.on_add), + ON_INSERT => Some(&self.on_insert), + ON_REMOVE => Some(&self.on_remove), + _ => self.cache.get(&event_type), + } + } + + /// This will run the observers of the given `event_type`, targeting the given `entity` and `components`. + pub(crate) fn invoke( + mut world: DeferredWorld, + event_type: ComponentId, + entity: Entity, + components: impl Iterator, + data: &mut T, + ) { + // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` + let (mut world, observers) = unsafe { + let world = world.as_unsafe_world_cell(); + // 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 { + return; + }; + // SAFETY: The only outstanding reference to world is `observers` + (world.into_deferred(), observers) + }; + + let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { + (runner)( + world.reborrow(), + ObserverTrigger { + observer, + event_type, + entity, + }, + data.into(), + ); + }; + + // Trigger observers listening for any kind of this trigger + observers.map.iter().for_each(&mut trigger_observer); + + // Trigger entity observers listening for this kind of trigger + if entity != Entity::PLACEHOLDER { + if let Some(map) = observers.entity_observers.get(&entity) { + map.iter().for_each(&mut trigger_observer); + } + } + + // Trigger observers listening to this trigger targeting a specific component + components.for_each(|id| { + if let Some(component_observers) = observers.component_observers.get(&id) { + component_observers + .map + .iter() + .for_each(&mut trigger_observer); + + if entity != Entity::PLACEHOLDER { + if let Some(map) = component_observers.entity_map.get(&entity) { + map.iter().for_each(&mut trigger_observer); + } + } + } + }); + } + + pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { + match event_type { + ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), + ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), + ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + _ => None, + } + } + + pub(crate) fn update_archetype_flags( + &self, + component_id: ComponentId, + flags: &mut ArchetypeFlags, + ) { + if self.on_add.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); + } + if self + .on_insert + .component_observers + .contains_key(&component_id) + { + flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); + } + if self + .on_remove + .component_observers + .contains_key(&component_id) + { + flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); + } + } +} + +impl World { + /// Spawn a "global" [`Observer`] and returns it's [`Entity`]. + pub fn observe( + &mut self, + system: impl IntoObserverSystem, + ) -> EntityWorldMut { + self.spawn(Observer::new(system)) + } + + /// Triggers the given `event`, which will run any observers watching for it. + pub fn trigger(&mut self, event: impl Event) { + TriggerEvent { event, targets: () }.apply(self); + } + + /// Triggers the given `event` for the given `targets`, which will run any observers watching for it. + pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) { + TriggerEvent { event, targets }.apply(self); + } + + /// 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. + let (observer_state, archetypes, observers) = unsafe { + let observer_state: *const ObserverState = + self.get::(observer_entity).unwrap(); + // Populate ObservedBy for each observed entity. + for watched_entity in &(*observer_state).descriptor.entities { + let mut entity_mut = self.entity_mut(*watched_entity); + let mut observed_by = entity_mut.entry::().or_default(); + observed_by.0.push(observer_entity); + } + (&*observer_state, &mut self.archetypes, &mut self.observers) + }; + let descriptor = &observer_state.descriptor; + + for &event_type in &descriptor.events { + let cache = observers.get_observers(event_type); + + if descriptor.components.is_empty() && descriptor.entities.is_empty() { + cache.map.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 { + let map = cache.entity_observers.entry(watched_entity).or_default(); + map.insert(observer_entity, observer_state.runner); + } + } else { + // Register observer for each watched component + for &component in &descriptor.components { + let observers = + cache + .component_observers + .entry(component) + .or_insert_with(|| { + if let Some(flag) = Observers::is_archetype_cached(event_type) { + archetypes.update_flags(component, flag, true); + } + CachedComponentObservers::default() + }); + if descriptor.entities.is_empty() { + // Register for all triggers targeting the component + observers.map.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(); + map.insert(observer_entity, observer_state.runner); + } + } + } + } + } + } + + /// Remove the observer from the cache, called when an observer gets despawned + pub(crate) fn unregister_observer(&mut self, entity: Entity, descriptor: ObserverDescriptor) { + let archetypes = &mut self.archetypes; + let observers = &mut self.observers; + + for &event_type in &descriptor.events { + let cache = observers.get_observers(event_type); + if descriptor.components.is_empty() && descriptor.entities.is_empty() { + cache.map.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 + let Some(observers) = cache.entity_observers.get_mut(watched_entity) else { + continue; + }; + observers.remove(&entity); + if observers.is_empty() { + cache.entity_observers.remove(watched_entity); + } + } + } else { + for component in &descriptor.components { + let Some(observers) = cache.component_observers.get_mut(component) else { + continue; + }; + if descriptor.entities.is_empty() { + observers.map.remove(&entity); + } else { + for watched_entity in &descriptor.entities { + let Some(map) = observers.entity_map.get_mut(watched_entity) else { + continue; + }; + map.remove(&entity); + if map.is_empty() { + observers.entity_map.remove(watched_entity); + } + } + } + + if observers.map.is_empty() && observers.entity_map.is_empty() { + cache.component_observers.remove(component); + if let Some(flag) = Observers::is_archetype_cached(event_type) { + archetypes.update_flags(*component, flag, false); + } + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy_ptr::OwningPtr; + + use crate as bevy_ecs; + use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState}; + use crate::prelude::*; + + #[derive(Component)] + struct A; + + #[derive(Component)] + struct B; + + #[derive(Component)] + struct C; + + #[derive(Event)] + struct EventA; + + #[derive(Resource, Default)] + struct R(usize); + + impl R { + #[track_caller] + fn assert_order(&mut self, count: usize) { + assert_eq!(count, self.0); + self.0 += 1; + } + } + + #[test] + fn observer_order_spawn_despawn() { + let mut world = World::new(); + world.init_resource::(); + + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(0)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(1)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + + let entity = world.spawn(A).id(); + world.despawn(entity); + assert_eq!(3, world.resource::().0); + } + + #[test] + fn observer_order_insert_remove() { + let mut world = World::new(); + world.init_resource::(); + + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(0)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(1)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + + let mut entity = world.spawn_empty(); + entity.insert(A); + entity.remove::(); + entity.flush(); + assert_eq!(3, world.resource::().0); + } + + #[test] + fn observer_order_recursive() { + let mut world = World::new(); + world.init_resource::(); + world.observe( + |obs: Trigger, mut res: ResMut, mut commands: Commands| { + res.assert_order(0); + commands.entity(obs.entity()).insert(B); + }, + ); + world.observe( + |obs: Trigger, mut res: ResMut, mut commands: Commands| { + res.assert_order(2); + commands.entity(obs.entity()).remove::(); + }, + ); + + world.observe( + |obs: Trigger, mut res: ResMut, mut commands: Commands| { + res.assert_order(1); + commands.entity(obs.entity()).remove::(); + }, + ); + world.observe(|_: Trigger, mut res: ResMut| { + res.assert_order(3); + }); + + let entity = world.spawn(A).flush(); + let entity = world.get_entity(entity).unwrap(); + assert!(!entity.contains::()); + assert!(!entity.contains::()); + assert_eq!(4, world.resource::().0); + } + + #[test] + fn observer_multiple_listeners() { + let mut world = World::new(); + world.init_resource::(); + + world.observe(|_: Trigger, mut res: ResMut| res.0 += 1); + world.observe(|_: Trigger, mut res: ResMut| res.0 += 1); + + world.spawn(A).flush(); + assert_eq!(2, world.resource::().0); + // Our A entity plus our two observers + assert_eq!(world.entities().len(), 3); + } + + #[test] + fn observer_multiple_events() { + let mut world = World::new(); + world.init_resource::(); + let on_remove = world.init_component::(); + world.spawn( + Observer::new(|_: Trigger, mut res: ResMut| res.0 += 1) + .with_event(on_remove), + ); + + let entity = world.spawn(A).id(); + world.despawn(entity); + assert_eq!(2, world.resource::().0); + } + + #[test] + fn observer_multiple_components() { + let mut world = World::new(); + world.init_resource::(); + world.init_component::(); + world.init_component::(); + + world.observe(|_: Trigger, mut res: ResMut| res.0 += 1); + + let entity = world.spawn(A).id(); + world.entity_mut(entity).insert(B); + world.flush(); + assert_eq!(2, world.resource::().0); + } + + #[test] + fn observer_despawn() { + let mut world = World::new(); + world.init_resource::(); + + let observer = world + .observe(|_: Trigger| panic!("Observer triggered after being despawned.")) + .id(); + world.despawn(observer); + world.spawn(A).flush(); + } + + #[test] + fn observer_multiple_matches() { + let mut world = World::new(); + world.init_resource::(); + + world.observe(|_: Trigger, mut res: ResMut| res.0 += 1); + + world.spawn((A, B)).flush(); + assert_eq!(1, world.resource::().0); + } + + #[test] + fn observer_no_target() { + let mut world = World::new(); + world.init_resource::(); + + world + .spawn_empty() + .observe(|_: Trigger| panic!("Trigger routed to non-targeted entity.")); + world.observe(move |obs: Trigger, mut res: ResMut| { + assert_eq!(obs.entity(), Entity::PLACEHOLDER); + res.0 += 1; + }); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger(EventA); + world.flush(); + assert_eq!(1, world.resource::().0); + } + + #[test] + fn observer_entity_routing() { + let mut world = World::new(); + world.init_resource::(); + + world + .spawn_empty() + .observe(|_: Trigger| panic!("Trigger routed to non-targeted entity.")); + let entity = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + world.observe(move |obs: Trigger, mut res: ResMut| { + assert_eq!(obs.entity(), entity); + res.0 += 1; + }); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventA, entity); + world.flush(); + assert_eq!(2, world.resource::().0); + } + + #[test] + fn observer_dynamic_component() { + let mut world = World::new(); + world.init_resource::(); + + let component_id = world.init_component::(); + world.spawn( + Observer::new(|_: Trigger, mut res: ResMut| res.0 += 1) + .with_component(component_id), + ); + + let mut entity = world.spawn_empty(); + OwningPtr::make(A, |ptr| { + // SAFETY: we registered `component_id` above. + unsafe { entity.insert_by_id(component_id, ptr) }; + }); + let entity = entity.flush(); + + world.trigger_targets(EventA, entity); + world.flush(); + assert_eq!(1, world.resource::().0); + } + + #[test] + fn observer_dynamic_trigger() { + let mut world = World::new(); + world.init_resource::(); + let event_a = world.init_component::(); + + world.spawn(ObserverState { + descriptor: ObserverDescriptor::default().with_triggers(vec![event_a]), + runner: |mut world, _trigger, _ptr| { + world.resource_mut::().0 += 1; + }, + ..Default::default() + }); + + world.commands().add( + // SAFETY: we registered `trigger` above and it matches the type of TriggerA + unsafe { EmitDynamicTrigger::new_with_id(event_a, EventA, ()) }, + ); + world.flush(); + assert_eq!(1, world.resource::().0); + } +} diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs new file mode 100644 index 0000000000000..f4d5384348230 --- /dev/null +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -0,0 +1,409 @@ +use crate::{ + component::{ComponentHooks, ComponentId, StorageType}, + observer::{ObserverDescriptor, ObserverTrigger}, + prelude::*, + query::DebugCheckedUnwrap, + system::{IntoObserverSystem, ObserverSystem}, + world::DeferredWorld, +}; +use bevy_ptr::PtrMut; + +/// Contains [`Observer`] information. This defines how a given observer behaves. It is the +/// "source of truth" for a given observer entity's behavior. +pub struct ObserverState { + pub(crate) descriptor: ObserverDescriptor, + pub(crate) runner: ObserverRunner, + pub(crate) last_trigger_id: u32, + pub(crate) despawned_watched_entities: u32, +} + +impl Default for ObserverState { + fn default() -> Self { + Self { + runner: |_, _, _| {}, + last_trigger_id: 0, + despawned_watched_entities: 0, + descriptor: Default::default(), + } + } +} + +impl ObserverState { + /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] + /// is triggered. + pub fn with_event(mut self, event: ComponentId) -> Self { + self.descriptor.events.push(event); + self + } + + /// Observe the given event list. This will cause the [`Observer`] to run whenever an event with any of the given [`ComponentId`]s + /// is triggered. + pub fn with_events(mut self, events: impl IntoIterator) -> Self { + self.descriptor.events.extend(events); + self + } + + /// Observe the given [`Entity`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for any [`Entity`] target in the list. + pub fn with_entities(mut self, entities: impl IntoIterator) -> Self { + self.descriptor.entities.extend(entities); + self + } + + /// Observe the given [`ComponentId`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for any [`ComponentId`] target in the list. + pub fn with_components(mut self, components: impl IntoIterator) -> Self { + self.descriptor.components.extend(components); + self + } +} + +impl Component for ObserverState { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_add(|mut world, entity, _| { + world.commands().add(move |world: &mut World| { + world.register_observer(entity); + }); + }); + hooks.on_remove(|mut world, entity, _| { + let descriptor = std::mem::take( + &mut world + .entity_mut(entity) + .get_mut::() + .unwrap() + .as_mut() + .descriptor, + ); + world.commands().add(move |world: &mut World| { + world.unregister_observer(entity, descriptor); + }); + }); + } +} + +/// Type for function that is run when an observer is triggered. +/// Typically refers to the default runner that runs the system stored in the associated [`ObserverSystemComponent`], +/// but can be overridden for custom behaviour. +pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut); + +/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". +/// +/// Observers listen for a "trigger" of a specific [`Event`]. Events are triggered by calling [`World::trigger`] or [`World::trigger_targets`]. +/// +/// Note that "buffered" events sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. They must be triggered at a specific +/// point in the schedule. +/// +/// # Usage +/// +/// The simplest usage +/// of the observer pattern looks like this: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// #[derive(Event)] +/// struct Speak { +/// message: String, +/// } +/// +/// world.observe(|trigger: Trigger| { +/// println!("{}", trigger.event().message); +/// }); +/// +/// // Observers currently require a flush() to be registered. In the context of schedules, +/// // this will generally be done for you. +/// world.flush(); +/// +/// world.trigger(Speak { +/// message: "Hello!".into(), +/// }); +/// ``` +/// +/// Notice that we used [`World::observe`]. This is just a shorthand for spawning an [`Observer`] manually: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct Speak; +/// // These are functionally the same: +/// world.observe(|trigger: Trigger| {}); +/// world.spawn(Observer::new(|trigger: Trigger| {})); +/// ``` +/// +/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct PrintNames; +/// # #[derive(Component, Debug)] +/// # struct Name; +/// world.observe(|trigger: Trigger, names: Query<&Name>| { +/// for name in &names { +/// println!("{name:?}"); +/// } +/// }); +/// ``` +/// +/// Note that [`Trigger`] must always be the first parameter. +/// +/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct SpawnThing; +/// # #[derive(Component, Debug)] +/// # struct Thing; +/// world.observe(|trigger: Trigger, mut commands: Commands| { +/// commands.spawn(Thing); +/// }); +/// ``` +/// +/// Observers can also trigger new events: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct A; +/// # #[derive(Event)] +/// # struct B; +/// world.observe(|trigger: Trigger, mut commands: Commands| { +/// commands.trigger(B); +/// }); +/// ``` +/// +/// When the commands are flushed (including these "nested triggers") they will be +/// recursively evaluated until there are no commands left, meaning nested triggers all +/// evaluate at the same time! +/// +/// Events can be triggered for entities, which will be passed to the [`Observer`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// #[derive(Event)] +/// struct Explode; +/// +/// world.observe(|trigger: Trigger, mut commands: Commands| { +/// println!("Entity {:?} goes BOOM!", trigger.entity()); +/// commands.entity(trigger.entity()).despawn(); +/// }); +/// +/// world.flush(); +/// +/// world.trigger_targets(Explode, entity); +/// ``` +/// +/// You can trigger multiple entities at once: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event)] +/// # struct Explode; +/// world.trigger_targets(Explode, [e1, e2]); +/// ``` +/// +/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component, Debug)] +/// # struct Name(String); +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event)] +/// # struct Explode; +/// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { +/// println!("Boom!"); +/// commands.entity(trigger.entity()).despawn(); +/// }); +/// +/// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { +/// println!("The explosion fizzles! This entity is immune!"); +/// }); +/// ``` +/// +/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. +/// This protects against observer "garbage" building up over time. +/// +/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) +/// just shorthand for spawning an [`Observer`] directly: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// # #[derive(Event)] +/// # struct Explode; +/// let mut observer = Observer::new(|trigger: Trigger| {}); +/// observer.watch_entity(entity); +/// world.spawn(observer); +/// ``` +/// +/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! +/// +/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. +/// +/// When first added, [`Observer`] will also create an [`ObserverState`] component, which registers the observer with the [`World`] and +/// serves as the "source of truth" of the observer. +/// +/// [`SystemParam`]: crate::system::SystemParam +pub struct Observer { + system: BoxedObserverSystem, + descriptor: ObserverDescriptor, +} + +impl Observer { + /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered + /// for _any_ entity (or no entity). + pub fn new(system: impl IntoObserverSystem) -> Self { + Self { + system: Box::new(IntoObserverSystem::into_system(system)), + descriptor: Default::default(), + } + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + pub fn with_entity(mut self, entity: Entity) -> Self { + self.descriptor.entities.push(entity); + self + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + /// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects. + pub fn watch_entity(&mut self, entity: Entity) { + self.descriptor.entities.push(entity); + } + + /// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// with the given component target. + pub fn with_component(mut self, component: ComponentId) -> Self { + self.descriptor.components.push(component); + self + } + + /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] + /// is triggered. + pub fn with_event(mut self, event: ComponentId) -> Self { + self.descriptor.events.push(event); + self + } +} + +impl Component for Observer { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_add(|mut world, entity, _| { + world.commands().add(move |world: &mut World| { + let event_type = world.init_component::(); + let mut components = Vec::new(); + B::component_ids(&mut world.components, &mut world.storages, &mut |id| { + components.push(id); + }); + let mut descriptor = ObserverDescriptor { + events: vec![event_type], + components, + ..Default::default() + }; + + // Initialize System + let system: *mut dyn ObserverSystem = + if let Some(mut observe) = world.get_mut::(entity) { + descriptor.merge(&observe.descriptor); + &mut *observe.system + } else { + return; + }; + // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias + unsafe { + (*system).initialize(world); + } + + { + let mut entity = world.entity_mut(entity); + if let crate::world::Entry::Vacant(entry) = entity.entry::() { + entry.insert(ObserverState { + descriptor, + runner: observer_system_runner::, + ..Default::default() + }); + } + } + }); + }); + } +} + +/// Equivalent to [`BoxedSystem`](crate::system::BoxedSystem) for [`ObserverSystem`]. +pub type BoxedObserverSystem = Box>; + +fn observer_system_runner( + mut world: DeferredWorld, + observer_trigger: ObserverTrigger, + ptr: PtrMut, +) { + let world = world.as_unsafe_world_cell(); + // SAFETY: Observer was triggered so must still exist in world + let observer_cell = unsafe { + world + .get_entity(observer_trigger.observer) + .debug_checked_unwrap() + }; + // SAFETY: Observer was triggered so must have an `ObserverState` + let mut state = unsafe { + observer_cell + .get_mut::() + .debug_checked_unwrap() + }; + + // TODO: Move this check into the observer cache to avoid dynamic dispatch + // SAFETY: We only access world metadata + let last_trigger = unsafe { world.world_metadata() }.last_trigger_id(); + if state.last_trigger_id == last_trigger { + return; + } + state.last_trigger_id = last_trigger; + + // SAFETY: Caller ensures `ptr` is castable to `&mut T` + let trigger: Trigger = Trigger::new(unsafe { ptr.deref_mut() }, observer_trigger); + // SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out. + // Additionally, IntoObserverSystem is only implemented for functions starting + // with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually, + // allowing the Trigger<'static> to be moved outside of the context of the system. + // This transmute is obviously not ideal, but it is safe. Ideally we can remove the + // static constraint from ObserverSystem, but so far we have not found a way. + let trigger: Trigger<'static, E, B> = unsafe { std::mem::transmute(trigger) }; + // SAFETY: Observer was triggered so must have an `ObserverSystemComponent` + let system = unsafe { + &mut observer_cell + .get_mut::>() + .debug_checked_unwrap() + .system + }; + + system.update_archetype_component_access(world); + + // SAFETY: + // - `update_archetype_component_access` was just called + // - there are no outstanding references to world except a private component + // - system is an `ObserverSystem` so won't mutate world beyond the access of a `DeferredWorld` + // - system is the same type erased system from above + unsafe { + system.run_unsafe(trigger, world); + system.queue_deferred(world.into_deferred()); + } +} diff --git a/crates/bevy_ecs/src/observer/trigger_event.rs b/crates/bevy_ecs/src/observer/trigger_event.rs new file mode 100644 index 0000000000000..ff4000c8ee688 --- /dev/null +++ b/crates/bevy_ecs/src/observer/trigger_event.rs @@ -0,0 +1,165 @@ +use crate::{ + component::ComponentId, + entity::Entity, + event::Event, + world::{Command, DeferredWorld, World}, +}; + +/// A [`Command`] that emits a given trigger for a given set of targets. +pub struct TriggerEvent { + /// The event to trigger. + pub event: E, + + /// The targets to trigger the event for. + pub targets: Targets, +} + +impl Command for TriggerEvent { + fn apply(mut self, world: &mut World) { + let event_type = world.init_component::(); + trigger_event(world, event_type, &mut self.event, self.targets); + } +} + +/// Emit a trigger for a dynamic component id. This is unsafe and must be verified manually. +pub struct EmitDynamicTrigger { + event_type: ComponentId, + event_data: T, + targets: Targets, +} + +impl EmitDynamicTrigger { + /// Sets the event type of the resulting trigger, used for dynamic triggers + /// # Safety + /// Caller must ensure that the component associated with `event_type` is accessible as E + pub unsafe fn new_with_id(event_type: ComponentId, event_data: E, targets: Targets) -> Self { + Self { + event_type, + event_data, + targets, + } + } +} + +impl Command for EmitDynamicTrigger { + fn apply(mut self, world: &mut World) { + trigger_event(world, self.event_type, &mut self.event_data, self.targets); + } +} + +#[inline] +fn trigger_event( + world: &mut World, + event_type: ComponentId, + event_data: &mut E, + targets: Targets, +) { + let mut world = DeferredWorld::from(world); + if targets.entities().len() == 0 { + // SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new` + unsafe { + world.trigger_observers_with_data( + event_type, + Entity::PLACEHOLDER, + targets.components(), + event_data, + ); + }; + } else { + for target in targets.entities() { + // SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new` + unsafe { + world.trigger_observers_with_data( + event_type, + target, + targets.components(), + event_data, + ); + }; + } + } +} + +/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`]. +/// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination +/// will run. +/// +/// [`Trigger`]: crate::observer::Trigger +/// [`Observer`]: crate::observer::Observer +pub trait TriggerTargets: Send + Sync + 'static { + /// The components the trigger should target. + fn components(&self) -> impl ExactSizeIterator; + + /// The entities the trigger should target. + fn entities(&self) -> impl ExactSizeIterator; +} + +impl TriggerTargets for () { + fn components(&self) -> impl ExactSizeIterator { + [].into_iter() + } + + fn entities(&self) -> impl ExactSizeIterator { + [].into_iter() + } +} + +impl TriggerTargets for Entity { + fn components(&self) -> impl ExactSizeIterator { + [].into_iter() + } + + fn entities(&self) -> impl ExactSizeIterator { + std::iter::once(*self) + } +} + +impl TriggerTargets for Vec { + fn components(&self) -> impl ExactSizeIterator { + [].into_iter() + } + + fn entities(&self) -> impl ExactSizeIterator { + self.iter().copied() + } +} + +impl TriggerTargets for [Entity; N] { + fn components(&self) -> impl ExactSizeIterator { + [].into_iter() + } + + fn entities(&self) -> impl ExactSizeIterator { + self.iter().copied() + } +} + +impl TriggerTargets for ComponentId { + fn components(&self) -> impl ExactSizeIterator { + std::iter::once(*self) + } + + fn entities(&self) -> impl ExactSizeIterator { + [].into_iter() + } +} + +impl TriggerTargets for Vec { + fn components(&self) -> impl ExactSizeIterator { + self.iter().copied() + } + + fn entities(&self) -> impl ExactSizeIterator { + [].into_iter() + } +} + +impl TriggerTargets for [ComponentId; N] { + fn components(&self) -> impl ExactSizeIterator { + self.iter().copied() + } + + fn entities(&self) -> impl ExactSizeIterator { + [].into_iter() + } +} diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index e4880c33ad5f2..3ee0e484ab292 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -647,6 +647,16 @@ impl FilteredAccessSet { .extend(filtered_access_set.filtered_accesses); } + /// Marks the set as reading all possible indices of type T. + pub fn read_all(&mut self) { + self.combined_access.read_all(); + } + + /// Marks the set as writing all T. + pub fn write_all(&mut self) { + self.combined_access.write_all(); + } + /// Removes all accesses stored in this set. pub fn clear(&mut self) { self.combined_access.clear(); diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 794db43936506..33ad9aa78a226 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -127,6 +127,11 @@ where self.system.apply_deferred(world); } + #[inline] + fn queue_deferred(&mut self, world: crate::world::DeferredWorld) { + self.system.queue_deferred(world); + } + fn initialize(&mut self, world: &mut crate::prelude::World) { self.system.initialize(world); } diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index dca72127dc9a5..e1de10b3943f3 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -202,6 +202,12 @@ where self.b.apply_deferred(world); } + #[inline] + fn queue_deferred(&mut self, mut world: crate::world::DeferredWorld) { + self.a.queue_deferred(world.reborrow()); + self.b.queue_deferred(world); + } + fn initialize(&mut self, world: &mut World) { self.a.initialize(world); self.b.initialize(world); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 4b74a121ff2d1..028182e69f00c 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,11 +1,13 @@ mod parallel_scope; -use super::{Deferred, IntoSystem, RegisterSystem, Resource}; +use super::{Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource}; use crate::{ self as bevy_ecs, bundle::Bundle, component::ComponentId, entity::{Entities, Entity}, + event::Event, + observer::{Observer, TriggerEvent, TriggerTargets}, system::{RunSystemWithInput, SystemId}, world::command_queue::RawCommandQueue, world::{Command, CommandQueue, EntityWorldMut, FromWorld, World}, @@ -116,6 +118,17 @@ const _: () = { world, ); } + fn queue( + state: &mut Self::State, + system_meta: &bevy_ecs::system::SystemMeta, + world: bevy_ecs::world::DeferredWorld, + ) { + <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::queue( + &mut state.state, + system_meta, + world, + ); + } unsafe fn get_param<'w, 's>( state: &'s mut Self::State, system_meta: &bevy_ecs::system::SystemMeta, @@ -150,7 +163,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// [system parameter]: crate::system::SystemParam pub fn new(queue: &'s mut CommandQueue, world: &'w World) -> Self { - Self::new_from_entities(queue, world.entities()) + Self::new_from_entities(queue, &world.entities) } /// Returns a new `Commands` instance from a [`CommandQueue`] and an [`Entities`] reference. @@ -735,6 +748,26 @@ impl<'w, 's> Commands<'w, 's> { pub fn add(&mut self, command: C) { self.push(command); } + + /// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that + /// isn't scoped to specific targets. + pub fn trigger(&mut self, event: impl Event) { + self.add(TriggerEvent { event, targets: () }); + } + + /// Sends a [`Trigger`] for the given targets. This will run any [`Observer`] of the `event` that + /// watches those targets. + pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) { + self.add(TriggerEvent { event, targets }); + } + + /// Spawn an [`Observer`] and returns the [`EntityCommands`] associated with the entity that stores the observer. + pub fn observe( + &mut self, + observer: impl IntoObserverSystem, + ) -> EntityCommands { + self.spawn(Observer::new(observer)) + } } /// A [`Command`] which gets executed for a given [`Entity`]. @@ -1125,6 +1158,15 @@ impl EntityCommands<'_> { pub fn commands(&mut self) -> Commands { self.commands.reborrow() } + + /// Creates an [`Observer`](crate::observer::Observer) listening for a trigger of type `T` that targets this entity. + pub fn observe( + &mut self, + system: impl IntoObserverSystem, + ) -> &mut Self { + self.add(observe(system)); + self + } } impl Command for F @@ -1285,6 +1327,16 @@ fn log_components(entity: Entity, world: &mut World) { info!("Entity {:?}: {:?}", entity, debug_infos); } +fn observe( + observer: impl IntoObserverSystem, +) -> impl EntityCommand { + move |entity, world: &mut World| { + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.observe(observer); + } + } +} + #[cfg(test)] #[allow(clippy::float_cmp, clippy::approx_constant)] mod tests { diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 85f3af12097e3..3d54e882b5d59 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -110,7 +110,7 @@ where ); let out = self.func.run(world, input, params); - world.flush_commands(); + world.flush(); let change_tick = world.change_tick.get_mut(); self.system_meta.last_run.set(*change_tick); *change_tick = change_tick.wrapping_add(1); @@ -126,6 +126,13 @@ where // might have buffers to apply, but this is handled by `PipeSystem`. } + #[inline] + fn queue_deferred(&mut self, _world: crate::world::DeferredWorld) { + // "pure" exclusive systems do not have any buffers to apply. + // Systems made by piping a normal system with an exclusive system + // might have buffers to apply, but this is handled by `PipeSystem`. + } + #[inline] fn initialize(&mut self, world: &mut World) { self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index e9d7f71736154..bf2b2c4fd5dbc 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -5,7 +5,7 @@ use crate::{ query::{Access, FilteredAccessSet}, schedule::{InternedSystemSet, SystemSet}, system::{check_system_change_tick, ReadOnlySystemParam, System, SystemParam, SystemParamItem}, - world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World, WorldId}, }; use bevy_utils::all_tuples; @@ -399,8 +399,8 @@ where F: SystemParamFunction, { func: F, - param_state: Option<::State>, - system_meta: SystemMeta, + pub(crate) param_state: Option<::State>, + pub(crate) system_meta: SystemMeta, world_id: Option, archetype_generation: ArchetypeGeneration, // NOTE: PhantomData T> gives this safe Send/Sync impls @@ -542,6 +542,12 @@ where F::Param::apply(param_state, &self.system_meta, world); } + #[inline] + fn queue_deferred(&mut self, world: DeferredWorld) { + let param_state = self.param_state.as_mut().expect(Self::PARAM_MESSAGE); + F::Param::queue(param_state, &self.system_meta, world); + } + #[inline] fn initialize(&mut self, world: &mut World) { if let Some(id) = self.world_id { diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index e0a9d6715e640..b41255194cdf0 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -108,6 +108,7 @@ mod commands; mod exclusive_function_system; mod exclusive_system_param; mod function_system; +mod observer_system; mod query; #[allow(clippy::module_inception)] mod system; @@ -124,6 +125,7 @@ pub use commands::*; pub use exclusive_function_system::*; pub use exclusive_system_param::*; pub use function_system::*; +pub use observer_system::*; pub use query::*; pub use system::*; pub use system_name::*; diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs new file mode 100644 index 0000000000000..0e13100f68d9b --- /dev/null +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -0,0 +1,71 @@ +use bevy_utils::all_tuples; + +use crate::{ + prelude::{Bundle, Trigger}, + system::{System, SystemParam, SystemParamFunction, SystemParamItem}, +}; + +use super::IntoSystem; + +/// Implemented for systems that have an [`Observer`] as the first argument. +pub trait ObserverSystem: + System, Out = ()> + Send + 'static +{ +} + +impl, Out = ()> + Send + 'static> + ObserverSystem for T +{ +} + +/// Implemented for systems that convert into [`ObserverSystem`]. +pub trait IntoObserverSystem: Send + 'static { + /// The type of [`System`] that this instance converts into. + type System: ObserverSystem; + + /// Turns this value into its corresponding [`System`]. + fn into_system(this: Self) -> Self::System; +} + +impl, (), M> + Send + 'static, M, E: 'static, B: Bundle> + IntoObserverSystem for S +where + S::System: ObserverSystem, +{ + type System = , (), M>>::System; + + fn into_system(this: Self) -> Self::System { + IntoSystem::into_system(this) + } +} + +macro_rules! impl_system_function { + ($($param: ident),*) => { + #[allow(non_snake_case)] + impl SystemParamFunction, $($param,)*)> for Func + where + for <'a> &'a mut Func: + FnMut(Trigger, $($param),*) + + FnMut(Trigger, $(SystemParamItem<$param>),*) + { + type In = Trigger<'static, E, B>; + type Out = (); + type Param = ($($param,)*); + #[inline] + fn run(&mut self, input: Trigger<'static, E, B>, param_value: SystemParamItem< ($($param,)*)>) { + #[allow(clippy::too_many_arguments)] + fn call_inner( + mut f: impl FnMut(Trigger<'static, E, B>, $($param,)*), + input: Trigger<'static, E, B>, + $($param: $param,)* + ){ + f(input, $($param,)*) + } + let ($($param,)*) = param_value; + call_inner(self, input, $($param),*) + } + } + } +} + +all_tuples!(impl_system_function, 0, 16, F); diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index f8321a5d93389..a36900a353824 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -4,6 +4,7 @@ use core::fmt::Debug; use crate::component::Tick; use crate::schedule::InternedSystemSet; use crate::world::unsafe_world_cell::UnsafeWorldCell; +use crate::world::DeferredWorld; use crate::{archetype::ArchetypeComponentId, component::ComponentId, query::Access, world::World}; use std::any::TypeId; @@ -89,6 +90,10 @@ pub trait System: Send + Sync + 'static { /// This is where [`Commands`](crate::system::Commands) get applied. fn apply_deferred(&mut self, world: &mut World); + /// Enqueues any [`Deferred`](crate::system::Deferred) system parameters (or other system buffers) + /// of this system into the world's command buffer. + fn queue_deferred(&mut self, world: DeferredWorld); + /// Initialize the system. fn initialize(&mut self, _world: &mut World); diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 7ec42cfb6acd3..aae8e4e2e26f3 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -11,7 +11,7 @@ use crate::{ ReadOnlyQueryData, }, system::{Query, SystemMeta}, - world::{unsafe_world_cell::UnsafeWorldCell, FromWorld, World}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; use bevy_ecs_macros::impl_param_set; pub use bevy_ecs_macros::Resource; @@ -159,6 +159,11 @@ pub unsafe trait SystemParam: Sized { #[allow(unused_variables)] fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {} + /// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred). + #[inline] + #[allow(unused_variables)] + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {} + /// Creates a parameter to be passed into a [`SystemParamFunction`]. /// /// [`SystemParamFunction`]: super::SystemParamFunction @@ -712,6 +717,27 @@ unsafe impl SystemParam for &'_ World { } } +/// SAFETY: `DeferredWorld` can read all components and resources but cannot be used to gain any other mutable references. +unsafe impl<'w> SystemParam for DeferredWorld<'w> { + type State = (); + type Item<'world, 'state> = DeferredWorld<'world>; + + fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + system_meta.component_access_set.read_all(); + system_meta.component_access_set.write_all(); + system_meta.set_has_deferred(); + } + + unsafe fn get_param<'world, 'state>( + _state: &'state mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + _change_tick: Tick, + ) -> Self::Item<'world, 'state> { + world.into_deferred() + } +} + /// A system local [`SystemParam`]. /// /// A local may only be accessed by the system itself and is therefore not visible to other systems. @@ -848,6 +874,8 @@ impl<'w, T: FromWorld + Send + 'static> BuildableSystemParam for Local<'w, T> { pub trait SystemBuffer: FromWorld + Send + 'static { /// Applies any deferred mutations to the [`World`]. fn apply(&mut self, system_meta: &SystemMeta, world: &mut World); + /// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred). + fn queue(&mut self, _system_meta: &SystemMeta, _world: DeferredWorld) {} } /// A [`SystemParam`] that stores a buffer which gets applied to the [`World`] during @@ -1012,6 +1040,10 @@ unsafe impl SystemParam for Deferred<'_, T> { state.get().apply(system_meta, world); } + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) { + state.get().queue(system_meta, world); + } + unsafe fn get_param<'w, 's>( state: &'s mut Self::State, _system_meta: &SystemMeta, @@ -1424,6 +1456,11 @@ macro_rules! impl_system_param_tuple { $($param::apply($param, _system_meta, _world);)* } + #[inline] + fn queue(($($param,)*): &mut Self::State, _system_meta: &SystemMeta, mut _world: DeferredWorld) { + $($param::queue($param, _system_meta, _world.reborrow());)* + } + #[inline] #[allow(clippy::unused_unit)] unsafe fn get_param<'w, 's>( diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index 0c783be27fdff..721248ea859ad 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -3,6 +3,7 @@ use crate::system::{SystemBuffer, SystemMeta}; use std::{ fmt::Debug, mem::MaybeUninit, + panic::{self, AssertUnwindSafe}, ptr::{addr_of_mut, NonNull}, }; @@ -11,6 +12,8 @@ use bevy_utils::tracing::warn; use crate::world::{Command, World}; +use super::DeferredWorld; + struct CommandMeta { /// SAFETY: The `value` must point to a value of type `T: Command`, /// where `T` is some specific type that was used to produce this metadata. @@ -18,11 +21,8 @@ struct CommandMeta { /// `world` is optional to allow this one function pointer to perform double-duty as a drop. /// /// Advances `cursor` by the size of `T` in bytes. - consume_command_and_get_size: unsafe fn( - value: OwningPtr, - world: Option>, - cursor: NonNull, - ), + consume_command_and_get_size: + unsafe fn(value: OwningPtr, world: Option>, cursor: &mut usize), } /// Densely and efficiently stores a queue of heterogenous types implementing [`Command`]. @@ -41,6 +41,7 @@ pub struct CommandQueue { // be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer. pub(crate) bytes: Vec>, pub(crate) cursor: usize, + pub(crate) panic_recovery: Vec>, } /// Wraps pointers to a [`CommandQueue`], used internally to avoid stacked borrow rules when @@ -49,6 +50,7 @@ pub struct CommandQueue { pub(crate) struct RawCommandQueue { pub(crate) bytes: NonNull>>, pub(crate) cursor: NonNull, + pub(crate) panic_recovery: NonNull>>, } // CommandQueue needs to implement Debug manually, rather than deriving it, because the derived impl just prints @@ -117,6 +119,7 @@ impl CommandQueue { RawCommandQueue { bytes: NonNull::new_unchecked(addr_of_mut!(self.bytes)), cursor: NonNull::new_unchecked(addr_of_mut!(self.cursor)), + panic_recovery: NonNull::new_unchecked(addr_of_mut!(self.panic_recovery)), } } } @@ -130,6 +133,7 @@ impl RawCommandQueue { Self { bytes: NonNull::new_unchecked(Box::into_raw(Box::default())), cursor: NonNull::new_unchecked(Box::into_raw(Box::new(0usize))), + panic_recovery: NonNull::new_unchecked(Box::into_raw(Box::default())), } } } @@ -164,17 +168,23 @@ impl RawCommandQueue { } let meta = CommandMeta { - consume_command_and_get_size: |command, world, mut cursor| { - // SAFETY: Pointer is assured to be valid in `CommandQueue.apply_or_drop_queued` - unsafe { *cursor.as_mut() += std::mem::size_of::() } + consume_command_and_get_size: |command, world, cursor| { + *cursor += std::mem::size_of::(); // SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`, // `command` must point to a value of type `C`. let command: C = unsafe { command.read_unaligned() }; match world { // Apply command to the provided world... - // SAFETY: Calller ensures pointer is not null - Some(mut world) => command.apply(unsafe { world.as_mut() }), + Some(mut world) => { + // SAFETY: Caller ensures pointer is not null + let world = unsafe { world.as_mut() }; + command.apply(world); + // The command may have queued up world commands, which we flush here to ensure they are also picked up. + // If the current command queue already the World Command queue, this will still behave appropriately because the global cursor + // is still at the current `stop`, ensuring only the newly queued Commands will be applied. + world.flush(); + } // ...or discard it. None => drop(command), } @@ -222,50 +232,79 @@ impl RawCommandQueue { pub(crate) unsafe fn apply_or_drop_queued(&mut self, world: Option>) { // SAFETY: If this is the command queue on world, world will not be dropped as we have a mutable reference // If this is not the command queue on world we have exclusive ownership and self will not be mutated - while *self.cursor.as_ref() < self.bytes.as_ref().len() { + let start = *self.cursor.as_ref(); + let stop = self.bytes.as_ref().len(); + let mut local_cursor = start; + // SAFETY: we are setting the global cursor to the current length to prevent the executing commands from applying + // the remaining commands currently in this list. This is safe. + *self.cursor.as_mut() = stop; + + while local_cursor < stop { // SAFETY: The cursor is either at the start of the buffer, or just after the previous command. // Since we know that the cursor is in bounds, it must point to the start of a new command. let meta = unsafe { self.bytes .as_mut() .as_mut_ptr() - .add(*self.cursor.as_ref()) + .add(local_cursor) .cast::() .read_unaligned() }; // Advance to the bytes just after `meta`, which represent a type-erased command. - // SAFETY: For most types of `Command`, the pointer immediately following the metadata - // is guaranteed to be in bounds. If the command is a zero-sized type (ZST), then the cursor - // might be 1 byte past the end of the buffer, which is safe. - unsafe { *self.cursor.as_mut() += std::mem::size_of::() }; + local_cursor += std::mem::size_of::(); // Construct an owned pointer to the command. // SAFETY: It is safe to transfer ownership out of `self.bytes`, since the increment of `cursor` above // guarantees that nothing stored in the buffer will get observed after this function ends. // `cmd` points to a valid address of a stored command, so it must be non-null. let cmd = unsafe { OwningPtr::::new(std::ptr::NonNull::new_unchecked( - self.bytes - .as_mut() - .as_mut_ptr() - .add(*self.cursor.as_ref()) - .cast(), + self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(), )) }; - // SAFETY: The data underneath the cursor must correspond to the type erased in metadata, - // since they were stored next to each other by `.push()`. - // For ZSTs, the type doesn't matter as long as the pointer is non-null. - // This also advances the cursor past the command. For ZSTs, the cursor will not move. - // At this point, it will either point to the next `CommandMeta`, - // or the cursor will be out of bounds and the loop will end. - unsafe { (meta.consume_command_and_get_size)(cmd, world, self.cursor) }; + let result = panic::catch_unwind(AssertUnwindSafe(|| { + // SAFETY: The data underneath the cursor must correspond to the type erased in metadata, + // since they were stored next to each other by `.push()`. + // For ZSTs, the type doesn't matter as long as the pointer is non-null. + // This also advances the cursor past the command. For ZSTs, the cursor will not move. + // At this point, it will either point to the next `CommandMeta`, + // or the cursor will be out of bounds and the loop will end. + unsafe { (meta.consume_command_and_get_size)(cmd, world, &mut local_cursor) }; + })); + + if let Err(payload) = result { + // local_cursor now points to the location _after_ the panicked command. + // Add the remaining commands that _would have_ been applied to the + // panic_recovery queue. + // + // This uses `current_stop` instead of `stop` to account for any commands + // that were queued _during_ this panic. + // + // This is implemented in such a way that if apply_or_drop_queued() are nested recursively in, + // an applied Command, the correct command order will be retained. + let panic_recovery = self.panic_recovery.as_mut(); + let bytes = self.bytes.as_mut(); + let current_stop = bytes.len(); + panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]); + bytes.set_len(start); + *self.cursor.as_mut() = start; + + // This was the "top of the apply stack". If we are _not_ at the top of the apply stack, + // when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check, + // until we reach the top. + if start == 0 { + bytes.append(panic_recovery); + } + panic::resume_unwind(payload); + } } - // Reset the buffer, so it can be reused after this function ends. - // SAFETY: `set_len(0)` is always valid. + // Reset the buffer: all commands past the original `start` cursor have been applied. + // SAFETY: we are setting the length of bytes to the original length, minus the length of the original + // list of commands being considered. All bytes remaining in the Vec are still valid, unapplied commands. unsafe { - self.bytes.as_mut().set_len(0); - *self.cursor.as_mut() = 0; + self.bytes.as_mut().set_len(start); + *self.cursor.as_mut() = start; }; } } @@ -287,11 +326,18 @@ impl SystemBuffer for CommandQueue { let _span_guard = _system_meta.commands_span.enter(); self.apply(world); } + + #[inline] + fn queue(&mut self, _system_meta: &SystemMeta, mut world: DeferredWorld) { + world.commands().append(self); + } } #[cfg(test)] mod test { use super::*; + use crate as bevy_ecs; + use crate::system::Resource; use std::{ panic::AssertUnwindSafe, sync::{ @@ -412,10 +458,6 @@ mod test { queue.apply(&mut world); })); - // even though the first command panicking. - // the cursor was incremented. - assert!(queue.cursor > 0); - // Even though the first command panicked, it's still ok to push // more commands. queue.push(SpawnCommand); @@ -424,6 +466,37 @@ mod test { assert_eq!(world.entities().len(), 3); } + #[test] + fn test_command_queue_inner_nested_panic_safe() { + std::panic::set_hook(Box::new(|_| {})); + + #[derive(Resource, Default)] + struct Order(Vec); + + let mut world = World::new(); + world.init_resource::(); + + fn add_index(index: usize) -> impl Command { + move |world: &mut World| world.resource_mut::().0.push(index) + } + world.commands().add(add_index(1)); + world.commands().add(|world: &mut World| { + world.commands().add(add_index(2)); + world.commands().add(PanicCommand("I panic!".to_owned())); + world.commands().add(add_index(3)); + world.flush_commands(); + }); + world.commands().add(add_index(4)); + + let _ = std::panic::catch_unwind(AssertUnwindSafe(|| { + world.flush_commands(); + })); + + world.commands().add(add_index(5)); + world.flush_commands(); + assert_eq!(&world.resource::().0, &[1, 2, 3, 4, 5]); + } + // NOTE: `CommandQueue` is `Send` because `Command` is send. // If the `Command` trait gets reworked to be non-send, `CommandQueue` // should be reworked. diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs new file mode 100644 index 0000000000000..640ff1929d7a6 --- /dev/null +++ b/crates/bevy_ecs/src/world/component_constants.rs @@ -0,0 +1,23 @@ +use super::*; +use crate::{self as bevy_ecs}; +/// Internal components used by bevy with a fixed component id. +/// Constants are used to skip [`TypeId`] lookups in hot paths. + +/// [`ComponentId`] for [`OnAdd`] +pub const ON_ADD: ComponentId = ComponentId::new(0); +/// [`ComponentId`] for [`OnInsert`] +pub const ON_INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`OnRemove`] +pub const ON_REMOVE: ComponentId = ComponentId::new(2); + +/// Trigger emitted when a component is added to an entity. +#[derive(Event)] +pub struct OnAdd; + +/// Trigger emitted when a component is inserted on to to an entity. +#[derive(Event)] +pub struct OnInsert; + +/// Trigger emitted when a component is removed from an entity. +#[derive(Event)] +pub struct OnRemove; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 5bf02b3c8bd2a..71f5a059aed20 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -1,10 +1,12 @@ use std::ops::Deref; use crate::{ + archetype::Archetype, change_detection::MutUntyped, component::ComponentId, entity::Entity, event::{Event, EventId, Events, SendBatchIds}, + observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, system::{Commands, Query, Resource}, @@ -52,6 +54,12 @@ impl<'w> From<&'w mut World> for DeferredWorld<'w> { } impl<'w> DeferredWorld<'w> { + /// Reborrow self as a new instance of [`DeferredWorld`] + #[inline] + pub fn reborrow(&mut self) -> DeferredWorld { + DeferredWorld { world: self.world } + } + /// Creates a [`Commands`] instance that pushes to the world's command queue #[inline] pub fn commands(&mut self) -> Commands { @@ -65,19 +73,34 @@ impl<'w> DeferredWorld<'w> { /// Returns `None` if the `entity` does not have a [`Component`] of the given type. #[inline] pub fn get_mut(&mut self, entity: Entity) -> Option> { - // SAFETY: &mut self ensure that there are no outstanding accesses to the component + // SAFETY: + // - `as_unsafe_world_cell` is the only thing that is borrowing world + // - `as_unsafe_world_cell` provides mutable permission to everything + // - `&mut self` ensures no other borrows on world data unsafe { self.world.get_entity(entity)?.get_mut() } } + /// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`. + /// Returns [`None`] if the `entity` does not exist. + /// Instead of unwrapping the value returned from this function, prefer [`Self::entity_mut`]. + #[inline] + pub fn get_entity_mut(&mut self, entity: Entity) -> Option { + let location = self.entities.get(entity)?; + // SAFETY: if the Entity is invalid, the function returns early. + // Additionally, Entities::get(entity) returns the correct EntityLocation if the entity exists. + let entity_cell = UnsafeEntityCell::new(self.as_unsafe_world_cell(), entity, location); + // SAFETY: The UnsafeEntityCell has read access to the entire world. + let entity_ref = unsafe { EntityMut::new(entity_cell) }; + Some(entity_ref) + } + /// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`. /// This will panic if the `entity` does not exist. Use [`Self::get_entity_mut`] if you want /// to check for entity existence instead of implicitly panic-ing. #[inline] - #[track_caller] pub fn entity_mut(&mut self, entity: Entity) -> EntityMut { #[inline(never)] #[cold] - #[track_caller] fn panic_no_entity(entity: Entity) -> ! { panic!("Entity {entity:?} does not exist"); } @@ -88,16 +111,6 @@ impl<'w> DeferredWorld<'w> { } } - /// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`. - /// Returns [`None`] if the `entity` does not exist. - /// Instead of unwrapping the value returned from this function, prefer [`Self::entity_mut`]. - #[inline] - pub fn get_entity_mut(&mut self, entity: Entity) -> Option { - let location = self.entities.get(entity)?; - // SAFETY: `entity` exists and `location` is that entity's location - Some(unsafe { EntityMut::new(UnsafeEntityCell::new(self.world, entity, location)) }) - } - /// Returns [`Query`] for the given [`QueryState`], which is used to efficiently /// run queries on the [`World`] by storing and reusing the [`QueryState`]. /// @@ -266,14 +279,17 @@ impl<'w> DeferredWorld<'w> { #[inline] pub(crate) unsafe fn trigger_on_add( &mut self, + archetype: &Archetype, entity: Entity, targets: impl Iterator, ) { - for component_id in targets { - // SAFETY: Caller ensures that these components exist - let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); - if let Some(hook) = hooks.on_add { - hook(DeferredWorld { world: self.world }, entity, component_id); + if archetype.has_add_hook() { + for component_id in targets { + // SAFETY: Caller ensures that these components exist + let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); + if let Some(hook) = hooks.on_add { + hook(DeferredWorld { world: self.world }, entity, component_id); + } } } } @@ -285,14 +301,17 @@ impl<'w> DeferredWorld<'w> { #[inline] pub(crate) unsafe fn trigger_on_insert( &mut self, + archetype: &Archetype, entity: Entity, targets: impl Iterator, ) { - for component_id in targets { - // SAFETY: Caller ensures that these components exist - let hooks = unsafe { self.world.components().get_info_unchecked(component_id) }.hooks(); - if let Some(hook) = hooks.on_insert { - hook(DeferredWorld { world: self.world }, entity, component_id); + if archetype.has_insert_hook() { + for component_id in targets { + // SAFETY: Caller ensures that these components exist + let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); + if let Some(hook) = hooks.on_insert { + hook(DeferredWorld { world: self.world }, entity, component_id); + } } } } @@ -304,15 +323,67 @@ impl<'w> DeferredWorld<'w> { #[inline] pub(crate) unsafe fn trigger_on_remove( &mut self, + archetype: &Archetype, entity: Entity, targets: impl Iterator, ) { - for component_id in targets { - // SAFETY: Caller ensures that these components exist - let hooks = unsafe { self.world.components().get_info_unchecked(component_id) }.hooks(); - if let Some(hook) = hooks.on_remove { - hook(DeferredWorld { world: self.world }, entity, component_id); + if archetype.has_remove_hook() { + for component_id in targets { + let hooks = + // SAFETY: Caller ensures that these components exist + unsafe { self.world.components().get_info_unchecked(component_id) }.hooks(); + if let Some(hook) = hooks.on_remove { + hook(DeferredWorld { world: self.world }, entity, component_id); + } } } } + + /// Triggers all event observers for [`ComponentId`] in target. + /// + /// # Safety + /// Caller must ensure observers listening for `event` can accept ZST pointers + #[inline] + pub(crate) unsafe fn trigger_observers( + &mut self, + event: ComponentId, + entity: Entity, + components: impl Iterator, + ) { + Observers::invoke(self.reborrow(), event, entity, components, &mut ()); + } + + /// Triggers all event observers for [`ComponentId`] in target. + /// + /// # Safety + /// Caller must ensure `E` is accessible as the type represented by `event` + #[inline] + pub(crate) unsafe fn trigger_observers_with_data( + &mut self, + event: ComponentId, + entity: Entity, + components: impl Iterator, + data: &mut E, + ) { + Observers::invoke(self.reborrow(), event, entity, components, data); + } + + /// Sends a "global" [`Trigger`] without any targets. + pub fn trigger(&mut self, trigger: impl Event) { + self.commands().trigger(trigger); + } + + /// Sends a [`Trigger`] with the given `targets`. + pub fn trigger_targets(&mut self, trigger: impl Event, targets: impl TriggerTargets) { + self.commands().trigger_targets(trigger, targets); + } + + /// Gets an [`UnsafeWorldCell`] containing the underlying world. + /// + /// # Safety + /// - must only be used to to make non-structural ECS changes + #[inline] + pub(crate) fn as_unsafe_world_cell(&mut self) -> UnsafeWorldCell { + self.world + } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index e7396bf3c7bc7..9fa1021923522 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -4,16 +4,19 @@ use crate::{ change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{Entities, Entity, EntityLocation}, + event::Event, + observer::{Observer, Observers}, query::Access, removal_detection::RemovedComponentEvents, storage::Storages, - world::{Mut, World}, + system::IntoObserverSystem, + world::{DeferredWorld, Mut, World}, }; use bevy_ptr::{OwningPtr, Ptr}; use std::{any::TypeId, marker::PhantomData}; use thiserror::Error; -use super::{unsafe_world_cell::UnsafeEntityCell, Ref}; +use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE}; /// A read-only reference to a particular [`Entity`] and all of its components. /// @@ -876,6 +879,7 @@ impl<'w> EntityWorldMut<'w> { &mut world.archetypes, storages, components, + &world.observers, old_location.archetype_id, bundle_info, false, @@ -898,11 +902,14 @@ impl<'w> EntityWorldMut<'w> { ) }; - if old_archetype.has_on_remove() { - // SAFETY: All components in the archetype exist in world - unsafe { - deferred_world.trigger_on_remove(entity, bundle_info.iter_components()); - } + // SAFETY: all bundle components exist in World + unsafe { + trigger_on_remove_hooks_and_observers( + &mut deferred_world, + old_archetype, + entity, + bundle_info, + ); } let archetypes = &mut world.archetypes; @@ -1053,6 +1060,7 @@ impl<'w> EntityWorldMut<'w> { &mut world.archetypes, &mut world.storages, &world.components, + &world.observers, location.archetype_id, bundle_info, // components from the bundle that are not present on the entity are ignored @@ -1075,11 +1083,14 @@ impl<'w> EntityWorldMut<'w> { ) }; - if old_archetype.has_on_remove() { - // SAFETY: All components in the archetype exist in world - unsafe { - deferred_world.trigger_on_remove(entity, bundle_info.iter_components()); - } + // SAFETY: all bundle components exist in World + unsafe { + trigger_on_remove_hooks_and_observers( + &mut deferred_world, + old_archetype, + entity, + bundle_info, + ); } let old_archetype = &world.archetypes[location.archetype_id]; @@ -1209,10 +1220,11 @@ impl<'w> EntityWorldMut<'w> { (&*archetype, world.into_deferred()) }; - if archetype.has_on_remove() { - // SAFETY: All components in the archetype exist in world - unsafe { - deferred_world.trigger_on_remove(self.entity, archetype.components()); + // SAFETY: All components in the archetype exist in world + unsafe { + deferred_world.trigger_on_remove(archetype, self.entity, archetype.components()); + if archetype.has_remove_observer() { + deferred_world.trigger_observers(ON_REMOVE, self.entity, archetype.components()); } } @@ -1276,12 +1288,12 @@ impl<'w> EntityWorldMut<'w> { world.archetypes[moved_location.archetype_id] .set_entity_table_row(moved_location.archetype_row, table_row); } - world.flush_commands(); + world.flush(); } - /// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush_commands`] + /// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush`] pub fn flush(self) -> Entity { - self.world.flush_commands(); + self.world.flush(); self.entity } @@ -1397,6 +1409,30 @@ impl<'w> EntityWorldMut<'w> { }) } } + + /// Creates an [`Observer`](crate::observer::Observer) listening for events of type `E` targeting this entity. + /// In order to trigger the callback the entity must also match the query when the event is fired. + pub fn observe( + &mut self, + observer: impl IntoObserverSystem, + ) -> &mut Self { + self.world + .spawn(Observer::new(observer).with_entity(self.entity)); + self + } +} + +/// SAFETY: all components in the archetype must exist in world +unsafe fn trigger_on_remove_hooks_and_observers( + deferred_world: &mut DeferredWorld, + archetype: &Archetype, + entity: Entity, + bundle_info: &BundleInfo, +) { + deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_components()); + if archetype.has_remove_observer() { + deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.iter_components()); + } } /// A view into a single entity and component in a world, which may either be vacant or occupied. @@ -2292,6 +2328,7 @@ unsafe fn remove_bundle_from_archetype( archetypes: &mut Archetypes, storages: &mut Storages, components: &Components, + observers: &Observers, archetype_id: ArchetypeId, bundle_info: &BundleInfo, intersection: bool, @@ -2362,6 +2399,7 @@ unsafe fn remove_bundle_from_archetype( let new_archetype_id = archetypes.get_id_or_insert( components, + observers, next_table_id, next_table_components, next_sparse_set_components, diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 099534a4272fe..d509eaa349c65 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,20 +1,25 @@ //! Defines the [`World`] and APIs for accessing it directly. pub(crate) mod command_queue; +mod component_constants; mod deferred_world; mod entity_ref; pub mod error; +mod identifier; mod spawn_batch; pub mod unsafe_world_cell; -pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}; -use crate::entity::EntityHashSet; -pub use crate::world::command_queue::CommandQueue; +pub use crate::{ + change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, + world::command_queue::CommandQueue, +}; +pub use component_constants::*; pub use deferred_world::DeferredWorld; pub use entity_ref::{ EntityMut, EntityRef, EntityWorldMut, Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, VacantEntry, }; +pub use identifier::WorldId; pub use spawn_batch::*; use crate::{ @@ -25,8 +30,9 @@ use crate::{ Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentInfo, ComponentTicks, Components, Tick, }, - entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation}, + entity::{AllocAtWithoutReplacement, Entities, Entity, EntityHashSet, EntityLocation}, event::{Event, EventId, Events, SendBatchIds}, + observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryEntityError, QueryFilter, QueryState}, removal_detection::RemovedComponentEvents, schedule::{Schedule, ScheduleLabel, Schedules}, @@ -43,10 +49,7 @@ use std::{ mem::MaybeUninit, sync::atomic::{AtomicU32, Ordering}, }; -mod identifier; - -use self::unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; -pub use identifier::WorldId; +use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; /// A [`World`] mutation. /// @@ -110,30 +113,36 @@ pub struct World { pub(crate) archetypes: Archetypes, pub(crate) storages: Storages, pub(crate) bundles: Bundles, + pub(crate) observers: Observers, pub(crate) removed_components: RemovedComponentEvents, pub(crate) change_tick: AtomicU32, pub(crate) last_change_tick: Tick, pub(crate) last_check_tick: Tick, + pub(crate) last_trigger_id: u32, pub(crate) command_queue: RawCommandQueue, } impl Default for World { fn default() -> Self { - Self { + let mut world = Self { id: WorldId::new().expect("More `bevy` `World`s have been created than is supported"), entities: Entities::new(), components: Default::default(), archetypes: Archetypes::new(), storages: Default::default(), bundles: Default::default(), + observers: Observers::default(), removed_components: Default::default(), // Default value is `1`, and `last_change_tick`s default to `0`, such that changes // are detected on first system runs and for direct world queries. change_tick: AtomicU32::new(1), last_change_tick: Tick::new(0), last_check_tick: Tick::new(0), + last_trigger_id: 0, command_queue: RawCommandQueue::new(), - } + }; + world.bootstrap(); + world } } @@ -149,6 +158,14 @@ impl Drop for World { } impl World { + /// This performs initialization that _must_ happen for every [`World`] immediately upon creation (such as claiming specific component ids). + /// This _must_ be run as part of constructing a [`World`], before it is returned to the caller. + #[inline] + fn bootstrap(&mut self) { + assert_eq!(ON_ADD, self.init_component::()); + assert_eq!(ON_INSERT, self.init_component::()); + assert_eq!(ON_REMOVE, self.init_component::()); + } /// Creates a new empty [`World`]. /// /// # Panics @@ -226,7 +243,7 @@ impl World { } /// Creates a new [`Commands`] instance that writes to the world's command queue - /// Use [`World::flush_commands`] to apply all queued commands + /// Use [`World::flush`] to apply all queued commands #[inline] pub fn commands(&mut self) -> Commands { // SAFETY: command_queue is stored on world and always valid while the world exists @@ -493,7 +510,7 @@ impl World { /// scheme worked out to share an ID space (which doesn't happen by default). #[inline] pub fn get_or_spawn(&mut self, entity: Entity) -> Option { - self.flush_entities(); + self.flush(); match self.entities.alloc_at_without_replacement(entity) { AllocAtWithoutReplacement::Exists(location) => { // SAFETY: `entity` exists and `location` is that entity's location @@ -886,7 +903,7 @@ impl World { /// assert_eq!(position.x, 0.0); /// ``` pub fn spawn_empty(&mut self) -> EntityWorldMut { - self.flush_entities(); + self.flush(); let entity = self.entities.alloc(); // SAFETY: entity was just allocated unsafe { self.spawn_at_empty_internal(entity) } @@ -952,7 +969,7 @@ impl World { /// assert_eq!(position.x, 2.0); /// ``` pub fn spawn(&mut self, bundle: B) -> EntityWorldMut { - self.flush_entities(); + self.flush(); let change_tick = self.change_tick(); let entity = self.entities.alloc(); let entity_location = { @@ -1083,6 +1100,7 @@ impl World { /// ``` #[inline] pub fn despawn(&mut self, entity: Entity) -> bool { + self.flush(); if let Some(entity) = self.get_entity_mut(entity) { entity.despawn(); true @@ -1734,7 +1752,7 @@ impl World { I::IntoIter: Iterator, B: Bundle, { - self.flush_entities(); + self.flush(); let change_tick = self.change_tick(); @@ -2020,9 +2038,15 @@ impl World { } } + /// Calls both [`World::flush_entities`] and [`World::flush_commands`]. + #[inline] + pub fn flush(&mut self) { + self.flush_entities(); + self.flush_commands(); + } + /// Applies any commands in the world's internal [`CommandQueue`]. /// This does not apply commands from any systems, only those stored in the world. - #[inline] pub fn flush_commands(&mut self) { // SAFETY: `self.command_queue` is only de-allocated in `World`'s `Drop` if !unsafe { self.command_queue.is_empty() } { @@ -2073,6 +2097,13 @@ impl World { self.last_change_tick } + /// Returns the id of the last ECS event that was fired. + /// Used internally to ensure observers don't trigger multiple times for the same event. + #[inline] + pub(crate) fn last_trigger_id(&self) -> u32 { + self.last_trigger_id + } + /// Sets [`World::last_change_tick()`] to the specified value during a scope. /// When the scope terminates, it will return to its old value. /// diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index ab9cb8f2db01f..f6cc9c9a2e6eb 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -27,7 +27,7 @@ where pub(crate) fn new(world: &'w mut World, iter: I) -> Self { // Ensure all entity allocations are accounted for so `self.entities` can realloc if // necessary - world.flush_entities(); + world.flush(); let change_tick = world.change_tick(); diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index d6c400171c88c..227846340c36e 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -9,6 +9,7 @@ use crate::{ change_detection::{MutUntyped, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, StorageType, Tick, TickCells}, entity::{Entities, Entity, EntityLocation}, + observer::Observers, prelude::Component, removal_detection::RemovedComponentEvents, storage::{Column, ComponentSparseSet, Storages}, @@ -231,6 +232,13 @@ impl<'w> UnsafeWorldCell<'w> { &unsafe { self.world_metadata() }.removed_components } + /// Retrieves this world's [`Observers`] collection. + pub(crate) unsafe fn observers(self) -> &'w Observers { + // SAFETY: + // - we only access world metadata + &unsafe { self.world_metadata() }.observers + } + /// Retrieves this world's [`Bundles`] collection. #[inline] pub fn bundles(self) -> &'w Bundles { @@ -571,6 +579,14 @@ impl<'w> UnsafeWorldCell<'w> { // - caller ensures that we have permission to access the queue unsafe { (*self.0).command_queue.clone() } } + + /// # Safety + /// It is the callers responsibility to ensure that there are no outstanding + /// references to `last_trigger_id`. + pub(crate) unsafe fn increment_trigger_id(self) { + // SAFETY: Caller ensure there are no outstanding references + unsafe { (*self.0).last_trigger_id += 1 } + } } impl Debug for UnsafeWorldCell<'_> { diff --git a/examples/README.md b/examples/README.md index 95cf4b465b22a..78b909ff4a4ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -276,6 +276,7 @@ Example | Description [Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities [Iter Combinations](../examples/ecs/iter_combinations.rs) | Shows how to iterate over combinations of query results [Nondeterministic System Order](../examples/ecs/nondeterministic_system_order.rs) | Systems run in parallel, but their order isn't always deterministic. Here's how to detect and fix this. +[Observers](../examples/ecs/observers.rs) | Demonstrates observers that react to events (both built-in life-cycle events and custom events) [One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them [Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator` [Removal Detection](../examples/ecs/removal_detection.rs) | Query for entities that had a specific component removed earlier in the current frame diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs new file mode 100644 index 0000000000000..0ebdfac701afd --- /dev/null +++ b/examples/ecs/observers.rs @@ -0,0 +1,209 @@ +//! Demonstrates how to observe life-cycle triggers as well as define custom ones. + +use bevy::{ + prelude::*, + utils::{HashMap, HashSet}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .init_resource::() + .add_systems(Startup, setup) + .add_systems(Update, (draw_shapes, handle_click)) + // Observers are systems that run when an event is "triggered". This observer runs whenever + // `ExplodeMines` is triggered. + .observe( + |trigger: Trigger, + mines: Query<&Mine>, + index: Res, + mut commands: Commands| { + // You can access the trigger data via the `Observer` + let event = trigger.event(); + // Access resources + for e in index.get_nearby(event.pos) { + // Run queries + let mine = mines.get(e).unwrap(); + if mine.pos.distance(event.pos) < mine.size + event.radius { + // And queue commands, including triggering additional events + // Here we trigger the `Explode` event for entity `e` + commands.trigger_targets(Explode, e); + } + } + }, + ) + // This observer runs whenever the `Mine` component is added to an entity, and places it in a simple spatial index. + .observe(on_add_mine) + // This observer runs whenever the `Mine` component is removed from an entity (including despawning it) + // and removes it from the spatial index. + .observe(on_remove_mine) + .run(); +} + +#[derive(Component)] +struct Mine { + pos: Vec2, + size: f32, +} + +impl Mine { + fn random() -> Self { + Mine { + pos: Vec2::new( + (rand::random::() - 0.5) * 1200.0, + (rand::random::() - 0.5) * 600.0, + ), + size: 4.0 + rand::random::() * 16.0, + } + } +} + +#[derive(Event)] +struct ExplodeMines { + pos: Vec2, + radius: f32, +} + +#[derive(Event)] +struct Explode; + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2dBundle::default()); + commands.spawn(TextBundle::from_section( + "Click on a \"Mine\" to trigger it.\n\ + When it explodes it will trigger all overlapping mines.", + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 24., + color: Color::WHITE, + }, + )); + + commands + .spawn(Mine::random()) + // Observers can watch for events targeting a specific entity. + // This will create a new observer that runs whenever the Explode event + // is triggered for this spawned entity. + .observe(explode_mine); + + // We want to spawn a bunch of mines. We could just call the code above for each of them. + // That would create a new observer instance for every Mine entity. Having duplicate observers + // generally isn't worth worrying about as the overhead is low. But if you want to be maximally efficient, + // you can reuse observers across entities. + // + // First, observers are actually just entities with the Observer component! The `observe()` functions + // you've seen so far in this example are just shorthand for manually spawning an observer. + let mut observer = Observer::new(explode_mine); + + // As we spawn entities, we can make this observer watch each of them: + for _ in 0..1000 { + let entity = commands.spawn(Mine::random()).id(); + observer.watch_entity(entity); + } + + // By spawning the Observer component, it becomes active! + commands.spawn(observer); +} + +fn on_add_mine( + trigger: Trigger, + query: Query<&Mine>, + mut index: ResMut, +) { + let mine = query.get(trigger.entity()).unwrap(); + let tile = ( + (mine.pos.x / CELL_SIZE).floor() as i32, + (mine.pos.y / CELL_SIZE).floor() as i32, + ); + index.map.entry(tile).or_default().insert(trigger.entity()); +} + +// Remove despawned mines from our index +fn on_remove_mine( + trigger: Trigger, + query: Query<&Mine>, + mut index: ResMut, +) { + let mine = query.get(trigger.entity()).unwrap(); + let tile = ( + (mine.pos.x / CELL_SIZE).floor() as i32, + (mine.pos.y / CELL_SIZE).floor() as i32, + ); + index.map.entry(tile).and_modify(|set| { + set.remove(&trigger.entity()); + }); +} + +fn explode_mine(trigger: Trigger, query: Query<&Mine>, mut commands: Commands) { + // If a triggered event is targeting a specific entity you can access it with `.entity()` + let id = trigger.entity(); + let Some(mut entity) = commands.get_entity(id) else { + return; + }; + println!("Boom! {:?} exploded.", id.index()); + entity.despawn(); + let mine = query.get(id).unwrap(); + // Trigger another explosion cascade. + commands.trigger(ExplodeMines { + pos: mine.pos, + radius: mine.size, + }); +} + +// Draw a circle for each mine using `Gizmos` +fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) { + for mine in &mines { + gizmos.circle_2d( + mine.pos, + mine.size, + Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8), + ); + } +} + +// Trigger `ExplodeMines` at the position of a given click +fn handle_click( + mouse_button_input: Res>, + camera: Query<(&Camera, &GlobalTransform)>, + windows: Query<&Window>, + mut commands: Commands, +) { + let (camera, camera_transform) = camera.single(); + if let Some(pos) = windows + .single() + .cursor_position() + .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor)) + .map(|ray| ray.origin.truncate()) + { + if mouse_button_input.just_pressed(MouseButton::Left) { + commands.trigger(ExplodeMines { pos, radius: 1.0 }); + } + } +} + +#[derive(Resource, Default)] +struct SpatialIndex { + map: HashMap<(i32, i32), HashSet>, +} + +/// Cell size has to be bigger than any `TriggerMine::radius` +const CELL_SIZE: f32 = 64.0; + +impl SpatialIndex { + // Lookup all entities within adjacent cells of our spatial index + fn get_nearby(&self, pos: Vec2) -> Vec { + let tile = ( + (pos.x / CELL_SIZE).floor() as i32, + (pos.y / CELL_SIZE).floor() as i32, + ); + let mut nearby = Vec::new(); + for x in -1..2 { + for y in -1..2 { + if let Some(mines) = self.map.get(&(tile.0 + x, tile.1 + y)) { + nearby.extend(mines.iter()); + } + } + } + nearby + } +}