diff --git a/Cargo.toml b/Cargo.toml index 4bb5ace1b9729..866324e7914b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3857,6 +3857,10 @@ doc-scrape-examples = true [package.metadata.example.testbed_3d] hidden = true +[[example]] +name = "playground" +path = "examples/playground.rs" +doc-scrape-examples = false [[example]] name = "testbed_ui_layout_rounding" diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 2c2241e29ec6d..a78d9ece6a02c 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -352,6 +352,7 @@ bitflags::bitflags! { const ON_INSERT_OBSERVER = (1 << 5); const ON_REPLACE_OBSERVER = (1 << 6); const ON_REMOVE_OBSERVER = (1 << 7); + const ON_MUTATE_OBSERVER = (1 << 8); } } @@ -697,6 +698,11 @@ impl Archetype { pub fn has_remove_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER) } + + #[inline] + pub fn has_mutate_observer(&self) -> bool { + self.flags().contains(ArchetypeFlags::ON_MUTATE_OBSERVER) + } } /// The next [`ArchetypeId`] in an [`Archetypes`] collection. diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 384b7517c77b2..2f0c8db2ea90a 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -18,7 +18,7 @@ use crate::{ prelude::World, query::DebugCheckedUnwrap, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_REPLACE}, + world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_MUTATE, ON_REPLACE}, }; use bevy_ptr::{ConstNonNull, OwningPtr}; use bevy_utils::{all_tuples, HashMap, HashSet, TypeIdMap}; @@ -1093,6 +1093,13 @@ impl<'w> BundleInserter<'w> { add_bundle.iter_inserted(), ); } + if new_archetype.has_mutate_observer() { + deferred_world.trigger_observers( + ON_MUTATE, + entity, + add_bundle.iter_inserted(), + ); + } } InsertMode::Keep => { // insert triggers only for new components if we're not replacing them (since @@ -1109,6 +1116,13 @@ impl<'w> BundleInserter<'w> { add_bundle.iter_added(), ); } + if new_archetype.has_mutate_observer() { + deferred_world.trigger_observers( + ON_MUTATE, + entity, + add_bundle.iter_added(), + ); + } } } } @@ -1249,6 +1263,13 @@ impl<'w> BundleSpawner<'w> { bundle_info.iter_contributed_components(), ); } + if archetype.has_mutate_observer() { + deferred_world.trigger_observers( + ON_MUTATE, + entity, + bundle_info.iter_contributed_components(), + ); + } }; location diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 67254d6298f11..ff2276ea0ef4e 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -64,7 +64,7 @@ pub mod prelude { }, world::{ Command, EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, - FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, + FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, OnMutate, World, }, }; diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 674f98f650725..7b5724ce87bb2 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -247,6 +247,7 @@ pub struct Observers { on_insert: CachedObservers, on_replace: CachedObservers, on_remove: CachedObservers, + on_mutate: CachedObservers, // Map from trigger type to set of observers cache: HashMap, } @@ -258,6 +259,7 @@ impl Observers { ON_INSERT => &mut self.on_insert, ON_REPLACE => &mut self.on_replace, ON_REMOVE => &mut self.on_remove, + ON_MUTATE => &mut self.on_mutate, _ => self.cache.entry(event_type).or_default(), } } @@ -268,6 +270,7 @@ impl Observers { ON_INSERT => Some(&self.on_insert), ON_REPLACE => Some(&self.on_replace), ON_REMOVE => Some(&self.on_remove), + ON_MUTATE => Some(&self.on_mutate), _ => self.cache.get(&event_type), } } @@ -342,6 +345,7 @@ impl Observers { ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + ON_MUTATE => Some(ArchetypeFlags::ON_MUTATE_OBSERVER), _ => None, } } @@ -378,6 +382,14 @@ impl Observers { { flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); } + + if self + .on_mutate + .component_observers + .contains_key(&component_id) + { + flags.insert(ArchetypeFlags::ON_MUTATE_OBSERVER); + } } } @@ -565,6 +577,7 @@ impl World { } } +// TODO GRACE: write tests here #[cfg(test)] mod tests { use alloc::vec; diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index c3e2645864094..a5b355acb4544 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -4,7 +4,7 @@ use crate::{ bundle::Bundles, change_detection::{Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Tick}, - entity::Entities, + entity::{Entities, Entity}, query::{ Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QuerySingleError, QueryState, ReadOnlyQueryData, @@ -12,14 +12,13 @@ use crate::{ storage::ResourceData, system::{Query, Single, SystemMeta}, world::{ - unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, FilteredResourcesMut, - FromWorld, World, + unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, FilteredResourcesMut, FromWorld, Mut, World, ON_MUTATE }, }; use bevy_ecs_macros::impl_param_set; pub use bevy_ecs_macros::{Resource, SystemParam}; use bevy_ptr::UnsafeCellDeref; -use bevy_utils::{all_tuples, synccell::SyncCell}; +use bevy_utils::{all_tuples, synccell::SyncCell, Entry, HashMap, HashSet}; #[cfg(feature = "track_change_detection")] use core::panic::Location; use core::{ @@ -194,7 +193,7 @@ pub unsafe trait SystemParam: Sized { /// and creates a new instance of this param's [`State`](SystemParam::State). fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State; - /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a + /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable). /// /// # Safety /// `archetype` must be from the [`World`] used to initialize `state` in [`SystemParam::init_state`]. @@ -320,8 +319,59 @@ unsafe impl SystemParam for Qu // world data that the query needs. unsafe { Query::new(world, state, system_meta.last_run, change_tick) } } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + world.init_resource::(); + world.resource_scope(|world, mut buffer: Mut| { + let (writes, cant_write) = state.component_access.access.component_writes(); + let mutable_components = if cant_write { Vec::new() } else { writes.collect() }; + if mutable_components.is_empty() { return } + + let buffer_started_empty = buffer.0.is_empty(); + + for archetype_id in state.matched_archetypes() { + let Some(archetype) = world.archetypes().get(archetype_id) else { continue; }; + if !archetype.has_mutate_observer() { continue; } + + for archetype_entity in archetype.entities() { + let entity = archetype_entity.id(); + let entity_ref = world.entity(entity); + let mutated_observers = mutable_components + .iter() + .cloned() + .filter(|c| entity_ref + .get_change_ticks_by_id(*c) + .map(|ticks| ticks.changed.get() >= system_meta.last_run.get()) // FIXME GRACE: get this_run + .unwrap_or(false) + ); + let values = buffer.0.entry(entity).or_default(); + values.extend(mutated_observers); + } + } + + if buffer_started_empty && !buffer.0.is_empty() { + world.commands().queue(flush_mutations); + } + }); + } } +fn flush_mutations(world: &mut World) { + world.resource_scope(|world, mut buffer: Mut| { + let mut deferred_world = DeferredWorld::from(world); + for (entity, components) in buffer.0.drain() { + unsafe { + deferred_world.trigger_observers(ON_MUTATE, entity, components.iter().cloned()) + } + } + }); +} + +#[derive(Default)] +pub(crate) struct ComponentMutationBuffer(HashMap>); + +impl Resource for ComponentMutationBuffer {} + pub(crate) fn init_query_param( world: &mut World, system_meta: &mut SystemMeta, diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs index 5eea8dc6229ef..73b21e5ca53ce 100644 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ b/crates/bevy_ecs/src/world/component_constants.rs @@ -13,6 +13,8 @@ pub const ON_INSERT: ComponentId = ComponentId::new(1); pub const ON_REPLACE: ComponentId = ComponentId::new(2); /// [`ComponentId`] for [`OnRemove`] pub const ON_REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`OnMutate`] +pub const ON_MUTATE: ComponentId = ComponentId::new(4); /// Trigger emitted when a component is added to an entity. See [`crate::component::ComponentHooks::on_add`] /// for more information. @@ -41,3 +43,9 @@ pub struct OnReplace; #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] pub struct OnRemove; + +/// Trigger emitted when a component is mutated. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnMutate; diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index d50f8f421cfc2..8f9de144214ff 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -182,6 +182,7 @@ impl World { assert_eq!(ON_INSERT, self.register_component::()); assert_eq!(ON_REPLACE, self.register_component::()); assert_eq!(ON_REMOVE, self.register_component::()); + assert_eq!(ON_MUTATE, self.register_component::()); } /// Creates a new empty [`World`]. /// diff --git a/examples/playground.rs b/examples/playground.rs new file mode 100644 index 0000000000000..a85d1fcd9ba69 --- /dev/null +++ b/examples/playground.rs @@ -0,0 +1,55 @@ + +#![allow(missing_docs)] + +use bevy::prelude::*; + +// TODO GRACE: remove this file + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, on_space_press) + .add_observer(on_a_mutated) + .run(); +} + +#[derive(Component)] +struct A(i32); + +fn setup(mut commands: Commands) { + commands.spawn(A(0)); +} + +fn on_space_press(input: Res>, mut query: Query<&mut A>) { + if input.just_pressed(KeyCode::Space) { + for mut a in query.iter_mut() { + println!("asdfjkaskldfjlksadfljsdfk"); + a.0 = 100; + } + } +} + +fn on_a_mutated(trigger: Trigger) { + println!("A mutation happened!"); +} + + +// fn main() { +// App::new() +// .add_plugins(DefaultPlugins) +// .add_systems(Startup, setup) +// .add_observer(on_a_added) +// .run(); +// } + +// #[derive(Component)] +// struct A(i32); + +// fn setup(mut commands: Commands) { +// commands.spawn_empty().insert(A(0)).insert(A(1)); +// } + +// fn on_a_added(trigger: Trigger) { +// println!("Abafjgklasdfgjp;kl") +// } \ No newline at end of file