From 94ff123d7fddf989e308b082fadedcc3b9ce1b8f Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Fri, 1 Mar 2024 06:59:22 -0800 Subject: [PATCH] Component Lifecycle Hooks and a Deferred World (#10756) # Objective - Provide a reliable and performant mechanism to allows users to keep components synchronized with external sources: closing/opening sockets, updating indexes, debugging etc. - Implement a generic mechanism to provide mutable access to the world without allowing structural changes; this will not only be used here but is a foundational piece for observers, which are key for a performant implementation of relations. ## Solution - Implement a new type `DeferredWorld` (naming is not important, `StaticWorld` is also suitable) that wraps a world pointer and prevents user code from making any structural changes to the ECS; spawning entities, creating components, initializing resources etc. - Add component lifecycle hooks `on_add`, `on_insert` and `on_remove` that can be assigned callbacks in user code. --- ## Changelog - Add new `DeferredWorld` type. - Add new world methods: `register_component::` and `register_component_with_descriptor`. These differ from `init_component` in that they provide mutable access to the created `ComponentInfo` but will panic if the component is already in any archetypes. These restrictions serve two purposes: 1. Prevent users from defining hooks for components that may already have associated hooks provided in another plugin. (a use case better served by observers) 2. Ensure that when an `Archetype` is created it gets the appropriate flags to early-out when triggering hooks. - Add methods to `ComponentInfo`: `on_add`, `on_insert` and `on_remove` to be used to register hooks of the form `fn(DeferredWorld, Entity, ComponentId)` - Modify `BundleInserter`, `BundleSpawner` and `EntityWorldMut` to trigger component hooks when appropriate. - Add bit flags to `Archetype` indicating whether or not any contained components have each type of hook, this can be expanded for other flags as needed. - Add `component_hooks` example to illustrate usage. Try it out! It's fun to mash keys. ## Safety The changes to component insertion, removal and deletion involve a large amount of unsafe code and it's fair for that to raise some concern. I have attempted to document it as clearly as possible and have confirmed that all the hooks examples are accepted by `cargo miri` as not causing any undefined behavior. The largest issue is in ensuring there are no outstanding references when passing a `DeferredWorld` to the hooks which requires some use of raw pointers (as was already happening to some degree in those places) and I have taken some time to ensure that is the case but feel free to let me know if I've missed anything. ## Performance These changes come with a small but measurable performance cost of between 1-5% on `add_remove` benchmarks and between 1-3% on `insert` benchmarks. One consideration to be made is the existence of the current `RemovedComponents` which is on average more costly than the addition of `on_remove` hooks due to the early-out, however hooks doesn't completely remove the need for `RemovedComponents` as there is a chance you want to respond to the removal of a component that already has an `on_remove` hook defined in another plugin, so I have not removed it here. I do intend to deprecate it with the introduction of observers in a follow up PR. ## Discussion Questions - Currently `DeferredWorld` implements `Deref` to `&World` which makes sense conceptually, however it does cause some issues with rust-analyzer providing autocomplete for `&mut World` references which is annoying. There are alternative implementations that may address this but involve more code churn so I have attempted them here. The other alternative is to not implement `Deref` at all but that leads to a large amount of API duplication. - `DeferredWorld`, `StaticWorld`, something else? - In adding support for hooks to `EntityWorldMut` I encountered some unfortunate difficulties with my desired API. If commands are flushed after each call i.e. `world.spawn() // flush commands .insert(A) // flush commands` the entity may be despawned while `EntityWorldMut` still exists which is invalid. An alternative was then to add `self.world.flush_commands()` to the drop implementation for `EntityWorldMut` but that runs into other problems for implementing functions like `into_unsafe_entity_cell`. For now I have implemented a `.flush()` which will flush the commands and consume `EntityWorldMut` or users can manually run `world.flush_commands()` after using `EntityWorldMut`. - In order to allowing querying on a deferred world we need implementations of `WorldQuery` to not break our guarantees of no structural changes through their `UnsafeWorldCell`. All our implementations do this, but there isn't currently any safety documentation specifying what is or isn't allowed for an implementation, just for the caller, (they also shouldn't be aliasing components they didn't specify access for etc.) is that something we should start doing? (see 10752) Please check out the example `component_hooks` or the tests in `bundle.rs` for usage examples. I will continue to expand this description as I go. See #10839 for a more ergonomic API built on top of this one that isn't subject to the same restrictions and supports `SystemParam` dependency injection. --- Cargo.toml | 11 + crates/bevy_ecs/Cargo.toml | 1 + crates/bevy_ecs/src/archetype.rs | 73 +- crates/bevy_ecs/src/bundle.rs | 841 ++++++++++++------ crates/bevy_ecs/src/component.rs | 118 ++- crates/bevy_ecs/src/lib.rs | 4 +- .../src/system/commands/command_queue.rs | 110 ++- .../src/system/exclusive_function_system.rs | 1 + crates/bevy_ecs/src/world/deferred_world.rs | 317 +++++++ crates/bevy_ecs/src/world/entity_ref.rs | 294 +++--- crates/bevy_ecs/src/world/mod.rs | 124 ++- crates/bevy_ecs/src/world/spawn_batch.rs | 23 +- .../bevy_ecs/src/world/unsafe_world_cell.rs | 16 +- examples/README.md | 1 + examples/ecs/component_hooks.rs | 96 ++ 15 files changed, 1512 insertions(+), 518 deletions(-) create mode 100644 crates/bevy_ecs/src/world/deferred_world.rs create mode 100644 examples/ecs/component_hooks.rs diff --git a/Cargo.toml b/Cargo.toml index 16010567e3e87..4d77987d6184b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1392,6 +1392,17 @@ description = "Change detection on components" category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "component_hooks" +path = "examples/ecs/component_hooks.rs" +doc-scrape-examples = true + +[package.metadata.example.component_hooks] +name = "Component Hooks" +description = "Define component hooks to manage component lifecycle events" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "custom_schedule" path = "examples/ecs/custom_schedule.rs" diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 7699829425238..b3372469694d0 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -22,6 +22,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_ecs_macros = { path = "macros", version = "0.14.0-dev" } +bitflags = "2.3" concurrent-queue = "2.4.0" fixedbitset = "0.4.2" rustc-hash = "1.1" diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 62643cf77a066..baa16e2503baa 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -21,7 +21,7 @@ use crate::{ bundle::BundleId, - component::{ComponentId, StorageType}, + component::{ComponentId, Components, StorageType}, entity::{Entity, EntityLocation}, storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow}, }; @@ -107,7 +107,7 @@ impl ArchetypeId { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Eq, PartialEq)] pub(crate) enum ComponentStatus { Added, Mutated, @@ -298,6 +298,18 @@ struct ArchetypeComponentInfo { archetype_component_id: ArchetypeComponentId, } +bitflags::bitflags! { + /// Flags used to keep track of metadata about the component in this [`Archetype`] + /// + /// Used primarily to early-out when there are no [`ComponentHook`] registered for any contained components. + #[derive(Clone, Copy)] + pub(crate) struct ArchetypeFlags: u32 { + const ON_ADD_HOOK = (1 << 0); + const ON_INSERT_HOOK = (1 << 1); + const ON_REMOVE_HOOK = (1 << 2); + } +} + /// Metadata for a single archetype within a [`World`]. /// /// For more information, see the *[module level documentation]*. @@ -310,10 +322,12 @@ pub struct Archetype { edges: Edges, entities: Vec, components: ImmutableSparseSet, + flags: ArchetypeFlags, } impl Archetype { pub(crate) fn new( + components: &Components, id: ArchetypeId, table_id: TableId, table_components: impl Iterator, @@ -321,9 +335,13 @@ impl Archetype { ) -> Self { let (min_table, _) = table_components.size_hint(); let (min_sparse, _) = sparse_set_components.size_hint(); - let mut components = SparseSet::with_capacity(min_table + min_sparse); + let mut flags = ArchetypeFlags::empty(); + let mut archetype_components = SparseSet::with_capacity(min_table + min_sparse); for (component_id, archetype_component_id) in table_components { - components.insert( + // 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); + archetype_components.insert( component_id, ArchetypeComponentInfo { storage_type: StorageType::Table, @@ -333,7 +351,10 @@ impl Archetype { } for (component_id, archetype_component_id) in sparse_set_components { - components.insert( + // 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); + archetype_components.insert( component_id, ArchetypeComponentInfo { storage_type: StorageType::SparseSet, @@ -345,8 +366,9 @@ impl Archetype { id, table_id, entities: Vec::new(), - components: components.into_immutable(), + components: archetype_components.into_immutable(), edges: Default::default(), + flags, } } @@ -356,6 +378,12 @@ impl Archetype { self.id } + /// Fetches the flags for the archetype. + #[inline] + pub(crate) fn flags(&self) -> ArchetypeFlags { + self.flags + } + /// Fetches the archetype's [`Table`] ID. /// /// [`Table`]: crate::storage::Table @@ -542,6 +570,24 @@ impl Archetype { pub(crate) fn clear_entities(&mut self) { self.entities.clear(); } + + /// Returns true if any of the components in this archetype have `on_add` hooks + #[inline] + pub(crate) fn has_on_add(&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 { + 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 { + self.flags().contains(ArchetypeFlags::ON_REMOVE_HOOK) + } } /// The next [`ArchetypeId`] in an [`Archetypes`] collection. @@ -624,7 +670,15 @@ impl Archetypes { by_components: Default::default(), archetype_component_count: 0, }; - archetypes.get_id_or_insert(TableId::empty(), Vec::new(), Vec::new()); + // SAFETY: Empty archetype has no components + unsafe { + archetypes.get_id_or_insert( + &Components::default(), + TableId::empty(), + Vec::new(), + Vec::new(), + ); + } archetypes } @@ -717,8 +771,10 @@ impl Archetypes { /// /// # Safety /// [`TableId`] must exist in tables - pub(crate) fn get_id_or_insert( + /// `table_components` and `sparse_set_components` must exist in `components` + pub(crate) unsafe fn get_id_or_insert( &mut self, + components: &Components, table_id: TableId, table_components: Vec, sparse_set_components: Vec, @@ -744,6 +800,7 @@ impl Archetypes { let sparse_set_archetype_components = (sparse_start..*archetype_component_count).map(ArchetypeComponentId); archetypes.push(Archetype::new( + components, id, table_id, table_components.into_iter().zip(table_archetype_components), diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 81f025d1024a5..f6af2e9be024d 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -7,13 +7,15 @@ use bevy_utils::{HashMap, HashSet, TypeIdMap}; use crate::{ archetype::{ - Archetype, ArchetypeId, Archetypes, BundleComponentStatus, ComponentStatus, + AddBundle, Archetype, ArchetypeId, Archetypes, BundleComponentStatus, ComponentStatus, SpawnBundleStatus, }, component::{Component, ComponentId, ComponentStorage, Components, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, + prelude::World, query::DebugCheckedUnwrap, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld}, }; use bevy_ptr::OwningPtr; use bevy_utils::all_tuples; @@ -346,86 +348,10 @@ impl BundleInfo { &self.component_ids } - pub(crate) fn get_bundle_inserter<'a, 'b>( - &'b self, - entities: &'a mut Entities, - archetypes: &'a mut Archetypes, - components: &Components, - storages: &'a mut Storages, - archetype_id: ArchetypeId, - change_tick: Tick, - ) -> BundleInserter<'a, 'b> { - let new_archetype_id = - self.add_bundle_to_archetype(archetypes, storages, components, archetype_id); - let archetypes_ptr = archetypes.archetypes.as_mut_ptr(); - if new_archetype_id == archetype_id { - let archetype = &mut archetypes[archetype_id]; - let table_id = archetype.table_id(); - BundleInserter { - bundle_info: self, - archetype, - entities, - sparse_sets: &mut storages.sparse_sets, - table: &mut storages.tables[table_id], - archetypes_ptr, - change_tick, - result: InsertBundleResult::SameArchetype, - } - } else { - let (archetype, new_archetype) = archetypes.get_2_mut(archetype_id, new_archetype_id); - let table_id = archetype.table_id(); - if table_id == new_archetype.table_id() { - BundleInserter { - bundle_info: self, - archetype, - archetypes_ptr, - entities, - sparse_sets: &mut storages.sparse_sets, - table: &mut storages.tables[table_id], - change_tick, - result: InsertBundleResult::NewArchetypeSameTable { new_archetype }, - } - } else { - let (table, new_table) = storages - .tables - .get_2_mut(table_id, new_archetype.table_id()); - BundleInserter { - bundle_info: self, - archetype, - sparse_sets: &mut storages.sparse_sets, - entities, - archetypes_ptr, - table, - change_tick, - result: InsertBundleResult::NewArchetypeNewTable { - new_archetype, - new_table, - }, - } - } - } - } - - pub(crate) fn get_bundle_spawner<'a, 'b>( - &'b self, - entities: &'a mut Entities, - archetypes: &'a mut Archetypes, - components: &Components, - storages: &'a mut Storages, - change_tick: Tick, - ) -> BundleSpawner<'a, 'b> { - let new_archetype_id = - self.add_bundle_to_archetype(archetypes, storages, components, ArchetypeId::EMPTY); - let archetype = &mut archetypes[new_archetype_id]; - let table = &mut storages.tables[archetype.table_id()]; - BundleSpawner { - archetype, - bundle_info: self, - table, - entities, - sparse_sets: &mut storages.sparse_sets, - change_tick, - } + /// Returns an iterator over the the [ID](ComponentId) of each component stored in this bundle. + #[inline] + pub fn iter_components(&self) -> impl Iterator + '_ { + self.component_ids.iter().cloned() } /// This writes components from a given [`Bundle`] to the given entity. @@ -436,10 +362,10 @@ impl BundleInfo { /// in the [`Bundle`], with respect to the entity's original archetype (prior to the bundle being added) /// For example, if the original archetype already has `ComponentA` and `T` also has `ComponentA`, the status /// should be `Mutated`. If the original archetype does not have `ComponentA`, the status should be `Added`. - /// When "inserting" a bundle into an existing entity, [`AddBundle`](crate::archetype::AddBundle) + /// When "inserting" a bundle into an existing entity, [`AddBundle`] /// should be used, which will report `Added` vs `Mutated` status based on the current archetype's structure. /// When spawning a bundle, [`SpawnBundleStatus`] can be used instead, which removes the need - /// to look up the [`AddBundle`](crate::archetype::AddBundle) in the archetype graph, which requires + /// to look up the [`AddBundle`] in the archetype graph, which requires /// ownership of the entity's current archetype. /// /// `table` must be the "new" table for `entity`. `table_row` must have space allocated for the @@ -493,7 +419,9 @@ impl BundleInfo { /// Adds a bundle to the given archetype and returns the resulting archetype. This could be the /// same [`ArchetypeId`], in the event that adding the given bundle does not result in an /// [`Archetype`] change. Results are cached in the [`Archetype`] graph to avoid redundant work. - pub(crate) fn add_bundle_to_archetype( + /// # Safety + /// `components` must be the same components as passed in [`Self::new`] + pub(crate) unsafe fn add_bundle_to_archetype( &self, archetypes: &mut Archetypes, storages: &mut Storages, @@ -561,8 +489,13 @@ impl BundleInfo { new_sparse_set_components }; }; - let new_archetype_id = - archetypes.get_id_or_insert(table_id, table_components, sparse_set_components); + // SAFETY: ids in self must be valid + let new_archetype_id = archetypes.get_id_or_insert( + components, + table_id, + table_components, + sparse_set_components, + ); // add an edge from the old archetype to the new archetype archetypes[archetype_id].edges_mut().insert_add_bundle( self.id, @@ -574,194 +507,381 @@ impl BundleInfo { } } -pub(crate) struct BundleInserter<'a, 'b> { - pub(crate) archetype: &'a mut Archetype, - pub(crate) entities: &'a mut Entities, - bundle_info: &'b BundleInfo, - table: &'a mut Table, - sparse_sets: &'a mut SparseSets, - result: InsertBundleResult<'a>, - archetypes_ptr: *mut Archetype, +// SAFETY: We have exclusive world access so our pointers can't be invalidated externally +pub(crate) struct BundleInserter<'w> { + world: UnsafeWorldCell<'w>, + bundle_info: *const BundleInfo, + add_bundle: *const AddBundle, + table: *mut Table, + archetype: *mut Archetype, + result: InsertBundleResult, change_tick: Tick, } -pub(crate) enum InsertBundleResult<'a> { +pub(crate) enum InsertBundleResult { SameArchetype, NewArchetypeSameTable { - new_archetype: &'a mut Archetype, + new_archetype: *mut Archetype, }, NewArchetypeNewTable { - new_archetype: &'a mut Archetype, - new_table: &'a mut Table, + new_archetype: *mut Archetype, + new_table: *mut Table, }, } -impl<'a, 'b> BundleInserter<'a, 'b> { +impl<'w> BundleInserter<'w> { + #[inline] + pub(crate) fn new( + world: &'w mut World, + archetype_id: ArchetypeId, + change_tick: Tick, + ) -> Self { + let bundle_id = world + .bundles + .init_info::(&mut world.components, &mut world.storages); + // SAFETY: We just ensured this bundle exists + unsafe { Self::new_with_id(world, archetype_id, bundle_id, change_tick) } + } + + /// Creates a new [`BundleInserter`]. + /// + /// # Safety + /// Caller must ensure that `bundle_id` exists in `world.bundles` + #[inline] + pub(crate) unsafe fn new_with_id( + world: &'w mut World, + archetype_id: ArchetypeId, + bundle_id: BundleId, + change_tick: Tick, + ) -> Self { + // SAFETY: We will not make any accesses to the command queue, component or resource data of this world + let bundle_info = world.bundles.get_unchecked(bundle_id); + let bundle_id = bundle_info.id(); + let new_archetype_id = bundle_info.add_bundle_to_archetype( + &mut world.archetypes, + &mut world.storages, + &world.components, + archetype_id, + ); + if new_archetype_id == archetype_id { + let archetype = &mut world.archetypes[archetype_id]; + // SAFETY: The edge is assured to be initialized when we called add_bundle_to_archetype + let add_bundle = unsafe { + archetype + .edges() + .get_add_bundle_internal(bundle_id) + .debug_checked_unwrap() + }; + let table_id = archetype.table_id(); + let table = &mut world.storages.tables[table_id]; + Self { + add_bundle, + archetype, + bundle_info, + table, + result: InsertBundleResult::SameArchetype, + change_tick, + world: world.as_unsafe_world_cell(), + } + } else { + let (archetype, new_archetype) = + world.archetypes.get_2_mut(archetype_id, new_archetype_id); + // SAFETY: The edge is assured to be initialized when we called add_bundle_to_archetype + let add_bundle = unsafe { + archetype + .edges() + .get_add_bundle_internal(bundle_id) + .debug_checked_unwrap() + }; + let table_id = archetype.table_id(); + let new_table_id = new_archetype.table_id(); + if table_id == new_table_id { + let table = &mut world.storages.tables[table_id]; + Self { + add_bundle, + archetype, + bundle_info, + table, + result: InsertBundleResult::NewArchetypeSameTable { new_archetype }, + change_tick, + world: world.as_unsafe_world_cell(), + } + } else { + let (table, new_table) = world.storages.tables.get_2_mut(table_id, new_table_id); + Self { + add_bundle, + archetype, + bundle_info, + table, + result: InsertBundleResult::NewArchetypeNewTable { + new_archetype, + new_table, + }, + change_tick, + world: world.as_unsafe_world_cell(), + } + } + } + } /// # Safety - /// `entity` must currently exist in the source archetype for this inserter. `archetype_row` + /// `entity` must currently exist in the source archetype for this inserter. `location` /// must be `entity`'s location in the archetype. `T` must match this [`BundleInfo`]'s type #[inline] - pub unsafe fn insert( + pub(crate) unsafe fn insert( &mut self, entity: Entity, location: EntityLocation, bundle: T, ) -> EntityLocation { - match &mut self.result { - InsertBundleResult::SameArchetype => { - // PERF: this could be looked up during Inserter construction and stored (but borrowing makes this nasty) - // SAFETY: The edge is assured to be initialized when creating the BundleInserter - let add_bundle = unsafe { - self.archetype - .edges() - .get_add_bundle_internal(self.bundle_info.id) - .debug_checked_unwrap() - }; - self.bundle_info.write_components( - self.table, - self.sparse_sets, - add_bundle, + // SAFETY: We do not make any structural changes to the archetype graph through self.world so these pointers always remain valid + let trigger_hooks = |archetype: &Archetype, mut world: DeferredWorld| { + let bundle_info = &*self.bundle_info; + let add_bundle = &*self.add_bundle; + + if archetype.has_on_add() { + world.trigger_on_add( entity, - location.table_row, - self.change_tick, - bundle, + bundle_info + .iter_components() + .zip(add_bundle.bundle_status.iter()) + .filter(|(_, &status)| status == ComponentStatus::Added) + .map(|(id, _)| id), ); + } + if archetype.has_on_insert() { + world.trigger_on_insert(entity, bundle_info.iter_components()); + } + }; + match &mut self.result { + InsertBundleResult::SameArchetype => { + { + // SAFETY: Mutable references do not alias and will be dropped after this block + let sparse_sets = { + let world = self.world.world_mut(); + &mut world.storages.sparse_sets + }; + let table = &mut *self.table; + let bundle_info = &*self.bundle_info; + let add_bundle = &*self.add_bundle; + + bundle_info.write_components( + table, + sparse_sets, + add_bundle, + entity, + location.table_row, + self.change_tick, + bundle, + ); + } + // SAFETY: We have no outstanding mutable references to world as they were dropped + unsafe { + let archetype = &*self.archetype; + trigger_hooks(archetype, self.world.into_deferred()); + } location } InsertBundleResult::NewArchetypeSameTable { new_archetype } => { - let result = self.archetype.swap_remove(location.archetype_row); - if let Some(swapped_entity) = result.swapped_entity { - let swapped_location = - // SAFETY: If the swap was successful, swapped_entity must be valid. - unsafe { self.entities.get(swapped_entity).debug_checked_unwrap() }; - self.entities.set( - swapped_entity.index(), - EntityLocation { - archetype_id: swapped_location.archetype_id, - archetype_row: location.archetype_row, - table_id: swapped_location.table_id, - table_row: swapped_location.table_row, - }, + let new_location = { + // SAFETY: Mutable references do not alias and will be dropped after this block + let (sparse_sets, entities) = { + let world = self.world.world_mut(); + (&mut world.storages.sparse_sets, &mut world.entities) + }; + let table = &mut *self.table; + let archetype = &mut *self.archetype; + let new_archetype = &mut **new_archetype; + + let result = archetype.swap_remove(location.archetype_row); + if let Some(swapped_entity) = result.swapped_entity { + let swapped_location = + // SAFETY: If the swap was successful, swapped_entity must be valid. + unsafe { entities.get(swapped_entity).debug_checked_unwrap() }; + entities.set( + swapped_entity.index(), + EntityLocation { + archetype_id: swapped_location.archetype_id, + archetype_row: location.archetype_row, + table_id: swapped_location.table_id, + table_row: swapped_location.table_row, + }, + ); + } + let new_location = new_archetype.allocate(entity, result.table_row); + entities.set(entity.index(), new_location); + + let bundle_info = &*self.bundle_info; + let add_bundle = &*self.add_bundle; + bundle_info.write_components( + table, + sparse_sets, + add_bundle, + entity, + result.table_row, + self.change_tick, + bundle, ); - } - let new_location = new_archetype.allocate(entity, result.table_row); - self.entities.set(entity.index(), new_location); - - // PERF: this could be looked up during Inserter construction and stored (but borrowing makes this nasty) - // SAFETY: The edge is assured to be initialized when creating the BundleInserter - let add_bundle = unsafe { - self.archetype - .edges() - .get_add_bundle_internal(self.bundle_info.id) - .debug_checked_unwrap() + new_location }; - self.bundle_info.write_components( - self.table, - self.sparse_sets, - add_bundle, - entity, - result.table_row, - self.change_tick, - bundle, - ); + + // SAFETY: We have no outstanding mutable references to world as they were dropped + unsafe { trigger_hooks(&**new_archetype, self.world.into_deferred()) }; + new_location } InsertBundleResult::NewArchetypeNewTable { new_archetype, new_table, } => { - let result = self.archetype.swap_remove(location.archetype_row); - if let Some(swapped_entity) = result.swapped_entity { - let swapped_location = - // SAFETY: If the swap was successful, swapped_entity must be valid. - unsafe { self.entities.get(swapped_entity).debug_checked_unwrap() }; - self.entities.set( - swapped_entity.index(), - EntityLocation { - archetype_id: swapped_location.archetype_id, - archetype_row: location.archetype_row, - table_id: swapped_location.table_id, - table_row: swapped_location.table_row, - }, - ); - } - // PERF: store "non bundle" components in edge, then just move those to avoid - // redundant copies - let move_result = self - .table - .move_to_superset_unchecked(result.table_row, new_table); - let new_location = new_archetype.allocate(entity, move_result.new_row); - self.entities.set(entity.index(), new_location); - - // if an entity was moved into this entity's table spot, update its table row - if let Some(swapped_entity) = move_result.swapped_entity { - let swapped_location = - // SAFETY: If the swap was successful, swapped_entity must be valid. - unsafe { self.entities.get(swapped_entity).debug_checked_unwrap() }; - let swapped_archetype = if self.archetype.id() == swapped_location.archetype_id - { - &mut *self.archetype - } else if new_archetype.id() == swapped_location.archetype_id { - new_archetype - } else { - // SAFETY: the only two borrowed archetypes are above and we just did collision checks - unsafe { - &mut *self - .archetypes_ptr - .add(swapped_location.archetype_id.index()) - } + let new_location = { + // SAFETY: Mutable references do not alias and will be dropped after this block + let (archetypes_ptr, sparse_sets, entities) = { + let world = self.world.world_mut(); + let archetype_ptr: *mut Archetype = + world.archetypes.archetypes.as_mut_ptr(); + ( + archetype_ptr, + &mut world.storages.sparse_sets, + &mut world.entities, + ) }; + let table = &mut *self.table; + let new_table = &mut **new_table; + let archetype = &mut *self.archetype; + let new_archetype = &mut **new_archetype; + let result = archetype.swap_remove(location.archetype_row); + if let Some(swapped_entity) = result.swapped_entity { + let swapped_location = + // SAFETY: If the swap was successful, swapped_entity must be valid. + unsafe { entities.get(swapped_entity).debug_checked_unwrap() }; + entities.set( + swapped_entity.index(), + EntityLocation { + archetype_id: swapped_location.archetype_id, + archetype_row: location.archetype_row, + table_id: swapped_location.table_id, + table_row: swapped_location.table_row, + }, + ); + } + // PERF: store "non bundle" components in edge, then just move those to avoid + // redundant copies + let move_result = table.move_to_superset_unchecked(result.table_row, new_table); + let new_location = new_archetype.allocate(entity, move_result.new_row); + entities.set(entity.index(), new_location); + + // if an entity was moved into this entity's table spot, update its table row + if let Some(swapped_entity) = move_result.swapped_entity { + let swapped_location = + // SAFETY: If the swap was successful, swapped_entity must be valid. + unsafe { entities.get(swapped_entity).debug_checked_unwrap() }; + let swapped_archetype = if archetype.id() == swapped_location.archetype_id { + archetype + } else if new_archetype.id() == swapped_location.archetype_id { + new_archetype + } else { + // SAFETY: the only two borrowed archetypes are above and we just did collision checks + &mut *archetypes_ptr.add(swapped_location.archetype_id.index()) + }; + + entities.set( + swapped_entity.index(), + EntityLocation { + archetype_id: swapped_location.archetype_id, + archetype_row: swapped_location.archetype_row, + table_id: swapped_location.table_id, + table_row: result.table_row, + }, + ); + swapped_archetype + .set_entity_table_row(swapped_location.archetype_row, result.table_row); + } - self.entities.set( - swapped_entity.index(), - EntityLocation { - archetype_id: swapped_location.archetype_id, - archetype_row: swapped_location.archetype_row, - table_id: swapped_location.table_id, - table_row: result.table_row, - }, + let bundle_info = &*self.bundle_info; + let add_bundle = &*self.add_bundle; + bundle_info.write_components( + new_table, + sparse_sets, + add_bundle, + entity, + move_result.new_row, + self.change_tick, + bundle, ); - swapped_archetype - .set_entity_table_row(swapped_location.archetype_row, result.table_row); - } - // PERF: this could be looked up during Inserter construction and stored (but borrowing makes this nasty) - // SAFETY: The edge is assured to be initialized when creating the BundleInserter - let add_bundle = unsafe { - self.archetype - .edges() - .get_add_bundle_internal(self.bundle_info.id) - .debug_checked_unwrap() + new_location }; - self.bundle_info.write_components( - new_table, - self.sparse_sets, - add_bundle, - entity, - move_result.new_row, - self.change_tick, - bundle, - ); + + // SAFETY: We have no outstanding mutable references to world as they were dropped + unsafe { trigger_hooks(&**new_archetype, self.world.into_deferred()) }; + new_location } } } + + #[inline] + pub(crate) fn entities(&mut self) -> &mut Entities { + // SAFETY: No outstanding references to self.world, changes to entities cannot invalidate our internal pointers + unsafe { &mut self.world.world_mut().entities } + } } -pub(crate) struct BundleSpawner<'a, 'b> { - pub(crate) archetype: &'a mut Archetype, - pub(crate) entities: &'a mut Entities, - bundle_info: &'b BundleInfo, - table: &'a mut Table, - sparse_sets: &'a mut SparseSets, +// SAFETY: We have exclusive world access so our pointers can't be invalidated externally +pub(crate) struct BundleSpawner<'w> { + world: UnsafeWorldCell<'w>, + bundle_info: *const BundleInfo, + table: *mut Table, + archetype: *mut Archetype, change_tick: Tick, } -impl<'a, 'b> BundleSpawner<'a, 'b> { +impl<'w> BundleSpawner<'w> { + #[inline] + pub fn new(world: &'w mut World, change_tick: Tick) -> Self { + let bundle_id = world + .bundles + .init_info::(&mut world.components, &mut world.storages); + // SAFETY: we initialized this bundle_id in `init_info` + unsafe { Self::new_with_id(world, bundle_id, change_tick) } + } + + /// Creates a new [`BundleSpawner`]. + /// + /// # Safety + /// Caller must ensure that `bundle_id` exists in `world.bundles` + #[inline] + pub(crate) unsafe fn new_with_id( + world: &'w mut World, + bundle_id: BundleId, + change_tick: Tick, + ) -> Self { + let bundle_info = world.bundles.get_unchecked(bundle_id); + let new_archetype_id = bundle_info.add_bundle_to_archetype( + &mut world.archetypes, + &mut world.storages, + &world.components, + ArchetypeId::EMPTY, + ); + let archetype = &mut world.archetypes[new_archetype_id]; + let table = &mut world.storages.tables[archetype.table_id()]; + Self { + bundle_info, + table, + archetype, + change_tick, + world: world.as_unsafe_world_cell(), + } + } + + #[inline] pub fn reserve_storage(&mut self, additional: usize) { - self.archetype.reserve(additional); - self.table.reserve(additional); + // SAFETY: There are no outstanding world references + let (archetype, table) = unsafe { (&mut *self.archetype, &mut *self.table) }; + archetype.reserve(additional); + table.reserve(additional); } + /// # Safety /// `entity` must be allocated (but non-existent), `T` must match this [`BundleInfo`]'s type #[inline] @@ -770,18 +890,43 @@ impl<'a, 'b> BundleSpawner<'a, 'b> { entity: Entity, bundle: T, ) -> EntityLocation { - let table_row = self.table.allocate(entity); - let location = self.archetype.allocate(entity, table_row); - self.bundle_info.write_components( - self.table, - self.sparse_sets, - &SpawnBundleStatus, - entity, - table_row, - self.change_tick, - bundle, - ); - self.entities.set(entity.index(), location); + // SAFETY: We do not make any structural changes to the archetype graph through self.world so this pointer always remain valid + let location = { + // SAFETY: Mutable references do not alias and will be dropped after this block + let (sparse_sets, entities) = { + let world = self.world.world_mut(); + (&mut world.storages.sparse_sets, &mut world.entities) + }; + let table = &mut *self.table; + let archetype = &mut *self.archetype; + let table_row = table.allocate(entity); + let location = archetype.allocate(entity, table_row); + let bundle_info = &*self.bundle_info; + bundle_info.write_components( + table, + sparse_sets, + &SpawnBundleStatus, + entity, + table_row, + self.change_tick, + bundle, + ); + entities.set(entity.index(), location); + location + }; + + // SAFETY: We have no outstanding mutable references to world as they were dropped + unsafe { + let archetype = &*self.archetype; + let bundle_info = &*self.bundle_info; + let mut world = self.world.into_deferred(); + if archetype.has_on_add() { + world.trigger_on_add(entity, bundle_info.iter_components()); + } + if archetype.has_on_insert() { + world.trigger_on_insert(entity, bundle_info.iter_components()); + } + } location } @@ -790,13 +935,27 @@ impl<'a, 'b> BundleSpawner<'a, 'b> { /// `T` must match this [`BundleInfo`]'s type #[inline] pub unsafe fn spawn(&mut self, bundle: T) -> Entity { - let entity = self.entities.alloc(); + let entity = self.entities().alloc(); // SAFETY: entity is allocated (but non-existent), `T` matches this BundleInfo's type unsafe { self.spawn_non_existent(entity, bundle); } entity } + + #[inline] + pub(crate) fn entities(&mut self) -> &mut Entities { + // SAFETY: No outstanding references to self.world, changes to entities cannot invalidate our internal pointers + unsafe { &mut self.world.world_mut().entities } + } + + /// # Safety: + /// - `Self` must be dropped after running this function as it may invalidate internal pointers. + #[inline] + pub(crate) unsafe fn flush_commands(&mut self) { + // SAFETY: pointers on self can be invalidated, + self.world.world_mut().flush_commands(); + } } /// Metadata for bundles. Stores a [`BundleInfo`] for each type of [`Bundle`] in a given world. @@ -806,9 +965,11 @@ pub struct Bundles { /// Cache static [`BundleId`] bundle_ids: TypeIdMap, /// Cache dynamic [`BundleId`] with multiple components - dynamic_bundle_ids: HashMap, (BundleId, Vec)>, + dynamic_bundle_ids: HashMap, BundleId>, + dynamic_bundle_storages: HashMap>, /// Cache optimized dynamic [`BundleId`] with single component - dynamic_component_bundle_ids: HashMap, + dynamic_component_bundle_ids: HashMap, + dynamic_component_storages: HashMap, } impl Bundles { @@ -828,13 +989,13 @@ impl Bundles { } /// Initializes a new [`BundleInfo`] for a statically known type. - pub(crate) fn init_info<'a, T: Bundle>( - &'a mut self, + pub(crate) fn init_info( + &mut self, components: &mut Components, storages: &mut Storages, - ) -> &'a BundleInfo { + ) -> BundleId { let bundle_infos = &mut self.bundle_infos; - let id = self.bundle_ids.entry(TypeId::of::()).or_insert_with(|| { + let id = *self.bundle_ids.entry(TypeId::of::()).or_insert_with(|| { let mut component_ids = Vec::new(); T::component_ids(components, storages, &mut |id| component_ids.push(id)); let id = BundleId(bundle_infos.len()); @@ -847,8 +1008,24 @@ impl Bundles { bundle_infos.push(bundle_info); id }); - // SAFETY: index either exists, or was initialized - unsafe { self.bundle_infos.get_unchecked(id.0) } + id + } + + pub(crate) unsafe fn get_unchecked(&self, id: BundleId) -> &BundleInfo { + self.bundle_infos.get_unchecked(id.0) + } + + pub(crate) unsafe fn get_storage_unchecked(&self, id: BundleId) -> StorageType { + *self + .dynamic_component_storages + .get(&id) + .debug_checked_unwrap() + } + + pub(crate) unsafe fn get_storages_unchecked(&mut self, id: BundleId) -> &mut Vec { + self.dynamic_bundle_storages + .get_mut(&id) + .debug_checked_unwrap() } /// Initializes a new [`BundleInfo`] for a dynamic [`Bundle`]. @@ -861,25 +1038,22 @@ impl Bundles { &mut self, components: &Components, component_ids: &[ComponentId], - ) -> (&BundleInfo, &Vec) { + ) -> BundleId { let bundle_infos = &mut self.bundle_infos; // Use `raw_entry_mut` to avoid cloning `component_ids` to access `Entry` - let (_, (bundle_id, storage_types)) = self + let (_, bundle_id) = self .dynamic_bundle_ids .raw_entry_mut() .from_key(component_ids) .or_insert_with(|| { - ( - component_ids.into(), - initialize_dynamic_bundle(bundle_infos, components, Vec::from(component_ids)), - ) + let (id, storages) = + initialize_dynamic_bundle(bundle_infos, components, Vec::from(component_ids)); + self.dynamic_bundle_storages + .insert_unique_unchecked(id, storages); + (component_ids.into(), id) }); - - // SAFETY: index either exists, or was initialized - let bundle_info = unsafe { bundle_infos.get_unchecked(bundle_id.0) }; - - (bundle_info, storage_types) + *bundle_id } /// Initializes a new [`BundleInfo`] for a dynamic [`Bundle`] with single component. @@ -891,22 +1065,18 @@ impl Bundles { &mut self, components: &Components, component_id: ComponentId, - ) -> (&BundleInfo, StorageType) { + ) -> BundleId { let bundle_infos = &mut self.bundle_infos; - let (bundle_id, storage_types) = self + let bundle_id = self .dynamic_component_bundle_ids .entry(component_id) .or_insert_with(|| { let (id, storage_type) = initialize_dynamic_bundle(bundle_infos, components, vec![component_id]); - // SAFETY: `storage_type` guaranteed to have length 1 - (id, storage_type[0]) + self.dynamic_component_storages.insert(id, storage_type[0]); + id }); - - // SAFETY: index either exists, or was initialized - let bundle_info = unsafe { bundle_infos.get_unchecked(bundle_id.0) }; - - (bundle_info, *storage_types) + *bundle_id } } @@ -934,3 +1104,138 @@ fn initialize_dynamic_bundle( (id, storage_types) } + +#[cfg(test)] +mod tests { + use crate as bevy_ecs; + use crate::prelude::*; + + #[derive(Component)] + struct A; + + #[derive(Component)] + struct B; + + #[derive(Component)] + struct C; + + #[derive(Component)] + struct D; + + #[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 component_hook_order_spawn_despawn() { + let mut world = World::new(); + world.init_resource::(); + world + .register_component_hooks::() + .on_add(|mut world, _, _| { + world.resource_mut::().assert_order(0); + }) + .on_insert(|mut world, _, _| world.resource_mut::().assert_order(1)) + .on_remove(|mut world, _, _| world.resource_mut::().assert_order(2)); + + let entity = world.spawn(A).id(); + world.despawn(entity); + assert_eq!(3, world.resource::().0); + } + + #[test] + fn component_hook_order_insert_remove() { + let mut world = World::new(); + world.init_resource::(); + world + .register_component_hooks::() + .on_add(|mut world, _, _| { + world.resource_mut::().assert_order(0); + }) + .on_insert(|mut world, _, _| { + world.resource_mut::().assert_order(1); + }) + .on_remove(|mut world, _, _| { + world.resource_mut::().assert_order(2); + }); + + let mut entity = world.spawn_empty(); + entity.insert(A); + entity.remove::(); + entity.flush(); + assert_eq!(3, world.resource::().0); + } + + #[test] + fn component_hook_order_recursive() { + let mut world = World::new(); + world.init_resource::(); + world + .register_component_hooks::() + .on_add(|mut world, entity, _| { + world.resource_mut::().assert_order(0); + world.commands().entity(entity).insert(B); + }) + .on_remove(|mut world, entity, _| { + world.resource_mut::().assert_order(2); + world.commands().entity(entity).remove::(); + }); + + world + .register_component_hooks::() + .on_add(|mut world, entity, _| { + world.resource_mut::().assert_order(1); + world.commands().entity(entity).remove::(); + }) + .on_remove(|mut world, _, _| { + world.resource_mut::().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 component_hook_order_recursive_multiple() { + let mut world = World::new(); + world.init_resource::(); + world + .register_component_hooks::() + .on_add(|mut world, entity, _| { + world.resource_mut::().assert_order(0); + world.commands().entity(entity).insert(B).insert(D); + }); + + world + .register_component_hooks::() + .on_add(|mut world, entity, _| { + world.resource_mut::().assert_order(1); + world.commands().entity(entity).insert(C); + }); + + world + .register_component_hooks::() + .on_add(|mut world, _, _| { + world.resource_mut::().assert_order(2); + }); + + world + .register_component_hooks::() + .on_add(|mut world, _, _| { + world.resource_mut::().assert_order(3); + }); + + world.spawn(A).flush(); + assert_eq!(4, world.resource::().0); + } +} diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 61349f4c86e0f..a65599bf7a043 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -2,10 +2,12 @@ use crate::{ self as bevy_ecs, + archetype::ArchetypeFlags, change_detection::MAX_CHANGE_AGE, + entity::Entity, storage::{SparseSetIndex, Storages}, system::{Local, Resource, SystemParam}, - world::{FromWorld, World}, + world::{DeferredWorld, FromWorld, World}, }; pub use bevy_ecs_macros::Component; use bevy_ptr::{OwningPtr, UnsafeCellDeref}; @@ -152,6 +154,10 @@ pub trait Component: Send + Sync + 'static { /// A marker type indicating the storage type used for this component. /// This must be either [`TableStorage`] or [`SparseStorage`]. type Storage: ComponentStorage; + + /// Called when registering this component, allowing mutable access to it's [`ComponentInfo`]. + /// This is currently used for registering hooks. + fn init_component_info(_info: &mut ComponentInfo) {} } /// Marker type for components stored in a [`Table`](crate::storage::Table). @@ -203,11 +209,83 @@ pub enum StorageType { SparseSet, } +/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove` +pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, Entity, ComponentId); + +/// Lifecycle hooks for a given [`Component`], stored in it's [`ComponentInfo`] +#[derive(Debug, Clone, Default)] +pub struct ComponentHooks { + pub(crate) on_add: Option, + pub(crate) on_insert: Option, + pub(crate) on_remove: Option, +} + +impl ComponentHooks { + /// Register a [`ComponentHook`] that will be run when this component is added to an entity. + /// An `on_add` hook will always be followed by `on_insert`. + /// + /// Will panic if the component already has an `on_add` hook + pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_add(hook) + .expect("Component id: {:?}, already has an on_add hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is added or set by `.insert` + /// An `on_insert` hook will run even if the entity already has the component unlike `on_add`, + /// `on_insert` also always runs after any `on_add` hooks. + /// + /// Will panic if the component already has an `on_insert` hook + pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_insert(hook) + .expect("Component id: {:?}, already has an on_insert hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// Despawning an entity counts as removing all of it's components. + /// + /// Will panic if the component already has an `on_remove` hook + pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_remove(hook) + .expect("Component id: {:?}, already has an on_remove hook") + } + + /// Fallible version of [`Self::on_add`]. + /// Returns `None` if the component already has an `on_add` hook. + pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_add.is_some() { + return None; + } + self.on_add = Some(hook); + Some(self) + } + + /// Fallible version of [`Self::on_insert`]. + /// Returns `None` if the component already has an `on_insert` hook. + pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_insert.is_some() { + return None; + } + self.on_insert = Some(hook); + Some(self) + } + + /// Fallible version of [`Self::on_remove`]. + /// Returns `None` if the component already has an `on_remove` hook. + pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_remove.is_some() { + return None; + } + self.on_remove = Some(hook); + Some(self) + } +} + /// Stores metadata for a type of component or resource stored in a specific [`World`]. #[derive(Debug, Clone)] pub struct ComponentInfo { id: ComponentId, descriptor: ComponentDescriptor, + hooks: ComponentHooks, } impl ComponentInfo { @@ -263,7 +341,30 @@ impl ComponentInfo { /// Create a new [`ComponentInfo`]. pub(crate) fn new(id: ComponentId, descriptor: ComponentDescriptor) -> Self { - ComponentInfo { id, descriptor } + ComponentInfo { + id, + descriptor, + hooks: ComponentHooks::default(), + } + } + + /// Update the given flags to include any [`ComponentHook`] registered to self + #[inline] + pub(crate) fn update_archetype_flags(&self, flags: &mut ArchetypeFlags) { + if self.hooks().on_add.is_some() { + flags.insert(ArchetypeFlags::ON_ADD_HOOK); + } + if self.hooks().on_insert.is_some() { + flags.insert(ArchetypeFlags::ON_INSERT_HOOK); + } + if self.hooks().on_remove.is_some() { + flags.insert(ArchetypeFlags::ON_REMOVE_HOOK); + } + } + + /// Provides a reference to the collection of hooks associated with this [`Component`] + pub fn hooks(&self) -> &ComponentHooks { + &self.hooks } } @@ -474,7 +575,13 @@ impl Components { .. } = self; *indices.entry(type_id).or_insert_with(|| { - Components::init_component_inner(components, storages, ComponentDescriptor::new::()) + let index = Components::init_component_inner( + components, + storages, + ComponentDescriptor::new::(), + ); + T::init_component_info(&mut components[index.index()]); + index }) } @@ -551,6 +658,11 @@ impl Components { unsafe { self.components.get_unchecked(id.0) } } + #[inline] + pub(crate) fn get_hooks_mut(&mut self, id: ComponentId) -> Option<&mut ComponentHooks> { + self.components.get_mut(id.0).map(|info| &mut info.hooks) + } + /// Type-erased equivalent of [`Components::component_id()`]. #[inline] pub fn get_id(&self, type_id: TypeId) -> Option { diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 3cd89b77e2b93..1f3464997bb04 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1067,7 +1067,7 @@ mod tests { fn reserve_and_spawn() { let mut world = World::default(); let e = world.entities().reserve_entity(); - world.flush(); + world.flush_entities(); let mut e_mut = world.entity_mut(e); e_mut.insert(A(0)); assert_eq!(e_mut.get::().unwrap(), &A(0)); @@ -1550,7 +1550,7 @@ mod tests { let e1 = world_a.spawn(A(1)).id(); let e2 = world_a.spawn(A(2)).id(); let e3 = world_a.entities().reserve_entity(); - world_a.flush(); + world_a.flush_entities(); let world_a_max_entities = world_a.entities().len(); world_b.entities.reserve_entities(world_a_max_entities); diff --git a/crates/bevy_ecs/src/system/commands/command_queue.rs b/crates/bevy_ecs/src/system/commands/command_queue.rs index 5e246e8a66e69..a9dcd6f965de2 100644 --- a/crates/bevy_ecs/src/system/commands/command_queue.rs +++ b/crates/bevy_ecs/src/system/commands/command_queue.rs @@ -112,12 +112,12 @@ impl CommandQueue { } } - /// Execute the queued [`Command`]s in the world. + /// Execute the queued [`Command`]s in the world after applying any commands in the world's internal queue. /// This clears the queue. #[inline] pub fn apply(&mut self, world: &mut World) { // flush the previously queued entities - world.flush(); + world.flush_entities(); self.apply_or_drop_queued(Some(world)); } @@ -131,39 +131,85 @@ impl CommandQueue { let bytes_range = self.bytes.as_mut_ptr_range(); // Pointer that will iterate over the entries of the buffer. - let mut cursor = bytes_range.start; + let cursor = bytes_range.start; + + let end = bytes_range.end; // Reset the buffer, so it can be reused after this function ends. // In the loop below, ownership of each command will be transferred into user code. // SAFETY: `set_len(0)` is always valid. unsafe { self.bytes.set_len(0) }; - while cursor < bytes_range.end { - // 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 { 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. - cursor = unsafe { cursor.add(std::mem::size_of::()) }; - // Construct an owned pointer to the command. - // SAFETY: It is safe to transfer ownership out of `self.bytes`, since the call to `set_len(0)` 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(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. - let size = unsafe { (meta.consume_command_and_get_size)(cmd, world.as_deref_mut()) }; - // Advance 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. - // SAFETY: The address just past the command is either within the buffer, - // or 1 byte past the end, so this addition will not overflow the pointer's allocation. - cursor = unsafe { cursor.add(size) }; + // Create a stack for the command queue's we will be applying as commands may queue additional commands. + // This is preferred over recursion to avoid stack overflows. + let mut resolving_commands = vec![(cursor, end)]; + // Take ownership of any additional buffers so they are not free'd uintil they are iterated. + let mut buffers = Vec::new(); + + // Add any commands in the world's internal queue to the top of the stack. + if let Some(world) = &mut world { + if !world.command_queue.is_empty() { + let mut bytes = std::mem::take(&mut world.command_queue.bytes); + let bytes_range = bytes.as_mut_ptr_range(); + resolving_commands.push((bytes_range.start, bytes_range.end)); + buffers.push(bytes); + } + } + + while let Some((mut cursor, mut end)) = resolving_commands.pop() { + while cursor < end { + // 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 { 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. + cursor = unsafe { cursor.add(std::mem::size_of::()) }; + // Construct an owned pointer to the command. + // SAFETY: It is safe to transfer ownership out of `self.bytes`, since the call to `set_len(0)` 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(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. + let size = + unsafe { (meta.consume_command_and_get_size)(cmd, world.as_deref_mut()) }; + // Advance 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. + // SAFETY: The address just past the command is either within the buffer, + // or 1 byte past the end, so this addition will not overflow the pointer's allocation. + cursor = unsafe { cursor.add(size) }; + + if let Some(world) = &mut world { + // If the command we just applied generated more commands we must apply those first + if !world.command_queue.is_empty() { + // If our current list of commands isn't complete push it to the `resolving_commands` stack to be applied after + if cursor < end { + resolving_commands.push((cursor, end)); + } + let mut bytes = std::mem::take(&mut world.command_queue.bytes); + + // Start applying the new queue + let bytes_range = bytes.as_mut_ptr_range(); + cursor = bytes_range.start; + end = bytes_range.end; + + // Store our buffer so it is not dropped; + buffers.push(bytes); + } + } + } + // Re-use last buffer to avoid re-allocation + if let (Some(world), Some(buffer)) = (&mut world, buffers.pop()) { + world.command_queue.bytes = buffer; + // SAFETY: `set_len(0)` is always valid. + unsafe { world.command_queue.bytes.set_len(0) }; + } } } @@ -171,6 +217,12 @@ impl CommandQueue { pub fn append(&mut self, other: &mut CommandQueue) { self.bytes.append(&mut other.bytes); } + + /// Returns false if there are any commands in the queue + #[inline] + pub fn is_empty(&self) -> bool { + self.bytes.is_empty() + } } impl Drop for CommandQueue { diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index a019330856aec..d4351923adaf4 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -110,6 +110,7 @@ where ); let out = self.func.run(world, input, params); + world.flush_commands(); let change_tick = world.change_tick.get_mut(); self.system_meta.last_run.set(*change_tick); *change_tick = change_tick.wrapping_add(1); diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs new file mode 100644 index 0000000000000..81352445a9047 --- /dev/null +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -0,0 +1,317 @@ +use std::ops::Deref; + +use crate::{ + change_detection::MutUntyped, + component::ComponentId, + entity::Entity, + event::{Event, EventId, Events, SendBatchIds}, + prelude::{Component, QueryState}, + query::{QueryData, QueryFilter}, + system::{Commands, Query, Resource}, +}; + +use super::{ + unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}, + EntityMut, Mut, World, +}; + +/// A [`World`] reference that disallows structural ECS changes. +/// This includes initializing resources, registering components or spawning entities. +pub struct DeferredWorld<'w> { + // SAFETY: Implementors must not use this reference to make structural changes + world: UnsafeWorldCell<'w>, +} + +impl<'w> Deref for DeferredWorld<'w> { + type Target = World; + + fn deref(&self) -> &Self::Target { + // SAFETY: Structural changes cannot be made through &World + unsafe { self.world.world() } + } +} + +impl<'w> UnsafeWorldCell<'w> { + /// Turn self into a [`DeferredWorld`] + /// + /// # Safety + /// Caller must ensure there are no outstanding mutable references to world and no + /// outstanding references to the world's command queue, resource or component data + #[inline] + pub unsafe fn into_deferred(self) -> DeferredWorld<'w> { + DeferredWorld { world: self } + } +} + +impl<'w> From<&'w mut World> for DeferredWorld<'w> { + fn from(world: &'w mut World) -> DeferredWorld<'w> { + DeferredWorld { + world: world.as_unsafe_world_cell(), + } + } +} + +impl<'w> DeferredWorld<'w> { + /// Creates a [`Commands`] instance that pushes to the world's command queue + #[inline] + pub fn commands(&mut self) -> Commands { + // SAFETY: &mut self ensure that there are no outstanding accesses to the queue + let queue = unsafe { self.world.get_command_queue() }; + Commands::new_from_entities(queue, self.world.entities()) + } + + /// Retrieves a mutable reference to the given `entity`'s [`Component`] of the given type. + /// 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 + unsafe { self.world.get_entity(entity)?.get_mut() } + } + + /// 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"); + } + + match self.get_entity_mut(entity) { + Some(entity) => entity, + None => panic_no_entity(entity), + } + } + + /// 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`]. + /// + /// # Panics + /// If state is from a different world then self + #[inline] + pub fn query<'s, D: QueryData, F: QueryFilter>( + &'w mut self, + state: &'s mut QueryState, + ) -> Query<'w, 's, D, F> { + state.validate_world(self.world.id()); + state.update_archetypes(self); + // SAFETY: We ran validate_world to ensure our state matches + unsafe { + let world_cell = self.world; + Query::new( + world_cell, + state, + world_cell.last_change_tick(), + world_cell.change_tick(), + ) + } + } + + /// Gets a mutable reference to the resource of the given type + /// + /// # Panics + /// + /// Panics if the resource does not exist. + /// Use [`get_resource_mut`](DeferredWorld::get_resource_mut) instead if you want to handle this case. + #[inline] + #[track_caller] + pub fn resource_mut(&mut self) -> Mut<'_, R> { + match self.get_resource_mut() { + Some(x) => x, + None => panic!( + "Requested resource {} does not exist in the `World`. + Did you forget to add it using `app.insert_resource` / `app.init_resource`? + Resources are also implicitly added via `app.add_event`, + and can be added by plugins.", + std::any::type_name::() + ), + } + } + + /// Gets a mutable reference to the resource of the given type if it exists + #[inline] + pub fn get_resource_mut(&mut self) -> Option> { + // SAFETY: &mut self ensure that there are no outstanding accesses to the resource + unsafe { self.world.get_resource_mut() } + } + + /// Gets a mutable reference to the non-send resource of the given type, if it exists. + /// + /// # Panics + /// + /// Panics if the resource does not exist. + /// Use [`get_non_send_resource_mut`](World::get_non_send_resource_mut) instead if you want to handle this case. + /// + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + #[inline] + #[track_caller] + pub fn non_send_resource_mut(&mut self) -> Mut<'_, R> { + match self.get_non_send_resource_mut() { + Some(x) => x, + None => panic!( + "Requested non-send resource {} does not exist in the `World`. + Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? + Non-send resources can also be be added by plugins.", + std::any::type_name::() + ), + } + } + + /// Gets a mutable reference to the non-send resource of the given type, if it exists. + /// Otherwise returns `None`. + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + #[inline] + pub fn get_non_send_resource_mut(&mut self) -> Option> { + // SAFETY: &mut self ensure that there are no outstanding accesses to the resource + unsafe { self.world.get_non_send_resource_mut() } + } + + /// Sends an [`Event`]. + /// This method returns the [ID](`EventId`) of the sent `event`, + /// or [`None`] if the `event` could not be sent. + #[inline] + pub fn send_event(&mut self, event: E) -> Option> { + self.send_event_batch(std::iter::once(event))?.next() + } + + /// Sends the default value of the [`Event`] of type `E`. + /// This method returns the [ID](`EventId`) of the sent `event`, + /// or [`None`] if the `event` could not be sent. + #[inline] + pub fn send_event_default(&mut self) -> Option> { + self.send_event(E::default()) + } + + /// Sends a batch of [`Event`]s from an iterator. + /// This method returns the [IDs](`EventId`) of the sent `events`, + /// or [`None`] if the `event` could not be sent. + #[inline] + pub fn send_event_batch( + &mut self, + events: impl IntoIterator, + ) -> Option> { + let Some(mut events_resource) = self.get_resource_mut::>() else { + bevy_utils::tracing::error!( + "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", + std::any::type_name::() + ); + return None; + }; + Some(events_resource.send_batch(events)) + } + + /// Gets a pointer to the resource with the id [`ComponentId`] if it exists. + /// The returned pointer may be used to modify the resource, as long as the mutable borrow + /// of the [`World`] is still valid. + /// + /// **You should prefer to use the typed API [`World::get_resource_mut`] where possible and only + /// use this in cases where the actual types are not known at compile time.** + #[inline] + pub fn get_resource_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + // SAFETY: &mut self ensure that there are no outstanding accesses to the resource + unsafe { self.world.get_resource_mut_by_id(component_id) } + } + + /// Gets a `!Send` resource to the resource with the id [`ComponentId`] if it exists. + /// The returned pointer may be used to modify the resource, as long as the mutable borrow + /// of the [`World`] is still valid. + /// + /// **You should prefer to use the typed API [`World::get_resource_mut`] where possible and only + /// use this in cases where the actual types are not known at compile time.** + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + #[inline] + pub fn get_non_send_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + // SAFETY: &mut self ensure that there are no outstanding accesses to the resource + unsafe { self.world.get_non_send_resource_mut_by_id(component_id) } + } + + /// Retrieves a mutable untyped reference to the given `entity`'s [`Component`] of the given [`ComponentId`]. + /// Returns `None` if the `entity` does not have a [`Component`] of the given type. + /// + /// **You should prefer to use the typed API [`World::get_mut`] where possible and only + /// use this in cases where the actual types are not known at compile time.** + #[inline] + pub fn get_mut_by_id( + &mut self, + entity: Entity, + component_id: ComponentId, + ) -> Option> { + // SAFETY: &mut self ensure that there are no outstanding accesses to the resource + unsafe { self.world.get_entity(entity)?.get_mut_by_id(component_id) } + } + + /// Triggers all `on_add` hooks for [`ComponentId`] in target. + /// + /// # Safety + /// Caller must ensure [`ComponentId`] in target exist in self. + #[inline] + pub(crate) unsafe fn trigger_on_add( + &mut self, + 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); + } + } + } + + /// Triggers all `on_insert` hooks for [`ComponentId`] in target. + /// + /// # Safety + /// Caller must ensure [`ComponentId`] in target exist in self. + #[inline] + pub(crate) unsafe fn trigger_on_insert( + &mut self, + 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); + } + } + } + + /// Triggers all `on_remove` hooks for [`ComponentId`] in target. + /// + /// # Safety + /// Caller must ensure [`ComponentId`] in target exist in self. + #[inline] + pub(crate) unsafe fn trigger_on_remove( + &mut self, + 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); + } + } + } +} diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index bc12b3343c93b..7448dddd8bec2 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,6 +1,6 @@ use crate::{ archetype::{Archetype, ArchetypeId, Archetypes}, - bundle::{Bundle, BundleInfo, BundleInserter, DynamicBundle}, + bundle::{Bundle, BundleId, BundleInfo, BundleInserter, DynamicBundle}, change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{Entities, Entity, EntityLocation}, @@ -10,7 +10,6 @@ use crate::{ world::{Mut, World}, }; use bevy_ptr::{OwningPtr, Ptr}; -use bevy_utils::tracing::debug; use std::{any::TypeId, marker::PhantomData}; use thiserror::Error; @@ -653,23 +652,10 @@ impl<'w> EntityWorldMut<'w> { /// This will overwrite any previous value(s) of the same component type. pub fn insert(&mut self, bundle: T) -> &mut Self { let change_tick = self.world.change_tick(); - let bundle_info = self - .world - .bundles - .init_info::(&mut self.world.components, &mut self.world.storages); - let mut bundle_inserter = bundle_info.get_bundle_inserter( - &mut self.world.entities, - &mut self.world.archetypes, - &self.world.components, - &mut self.world.storages, - self.location.archetype_id, - change_tick, - ); + let mut bundle_inserter = + BundleInserter::new::(self.world, self.location.archetype_id, change_tick); // SAFETY: location matches current entity. `T` matches `bundle_info` - unsafe { - self.location = bundle_inserter.insert(self.entity, self.location, bundle); - } - + self.location = unsafe { bundle_inserter.insert(self.entity, self.location, bundle) }; self } @@ -689,17 +675,16 @@ impl<'w> EntityWorldMut<'w> { component: OwningPtr<'_>, ) -> &mut Self { let change_tick = self.world.change_tick(); + let bundle_id = self + .world + .bundles + .init_component_info(&self.world.components, component_id); + let storage_type = self.world.bundles.get_storage_unchecked(bundle_id); - let bundles = &mut self.world.bundles; - let components = &mut self.world.components; - - let (bundle_info, storage_type) = bundles.init_component_info(components, component_id); - let bundle_inserter = bundle_info.get_bundle_inserter( - &mut self.world.entities, - &mut self.world.archetypes, - &self.world.components, - &mut self.world.storages, + let bundle_inserter = BundleInserter::new_with_id( + self.world, self.location.archetype_id, + bundle_id, change_tick, ); @@ -708,9 +693,8 @@ impl<'w> EntityWorldMut<'w> { self.entity, self.location, Some(component).into_iter(), - Some(storage_type).into_iter(), + Some(storage_type).iter().cloned(), ); - self } @@ -732,17 +716,16 @@ impl<'w> EntityWorldMut<'w> { iter_components: I, ) -> &mut Self { let change_tick = self.world.change_tick(); - - let bundles = &mut self.world.bundles; - let components = &mut self.world.components; - - let (bundle_info, storage_types) = bundles.init_dynamic_info(components, component_ids); - let bundle_inserter = bundle_info.get_bundle_inserter( - &mut self.world.entities, - &mut self.world.archetypes, - &self.world.components, - &mut self.world.storages, + let bundle_id = self + .world + .bundles + .init_dynamic_info(&self.world.components, component_ids); + let mut storage_types = + std::mem::take(self.world.bundles.get_storages_unchecked(bundle_id)); + let bundle_inserter = BundleInserter::new_with_id( + self.world, self.location.archetype_id, + bundle_id, change_tick, ); @@ -751,9 +734,9 @@ impl<'w> EntityWorldMut<'w> { self.entity, self.location, iter_components, - storage_types.iter().cloned(), + (*storage_types).iter().cloned(), ); - + *self.world.bundles.get_storages_unchecked(bundle_id) = std::mem::take(&mut storage_types); self } @@ -764,19 +747,18 @@ impl<'w> EntityWorldMut<'w> { // TODO: BundleRemover? #[must_use] pub fn take(&mut self) -> Option { - let archetypes = &mut self.world.archetypes; - let storages = &mut self.world.storages; - let components = &mut self.world.components; - let entities = &mut self.world.entities; - let removed_components = &mut self.world.removed_components; - - let bundle_info = self.world.bundles.init_info::(components, storages); + let world = &mut self.world; + let storages = &mut world.storages; + let components = &mut world.components; + let bundle_id = world.bundles.init_info::(components, storages); + // SAFETY: We just ensured this bundle exists + let bundle_info = unsafe { world.bundles.get_unchecked(bundle_id) }; let old_location = self.location; // SAFETY: `archetype_id` exists because it is referenced in the old `EntityLocation` which is valid, // components exist in `bundle_info` because `Bundles::init_info` initializes a `BundleInfo` containing all components of the bundle type `T` let new_archetype_id = unsafe { remove_bundle_from_archetype( - archetypes, + &mut world.archetypes, storages, components, old_location.archetype_id, @@ -789,8 +771,33 @@ impl<'w> EntityWorldMut<'w> { return None; } - let mut bundle_components = bundle_info.components().iter().cloned(); let entity = self.entity; + // SAFETY: Archetypes and Bundles cannot be mutably aliased through DeferredWorld + let (old_archetype, bundle_info, mut deferred_world) = unsafe { + let bundle_info: *const BundleInfo = bundle_info; + let world = world.as_unsafe_world_cell(); + ( + &world.archetypes()[old_location.archetype_id], + &*bundle_info, + world.into_deferred(), + ) + }; + + 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()); + } + } + + let archetypes = &mut world.archetypes; + let storages = &mut world.storages; + let components = &mut world.components; + let entities = &mut world.entities; + let removed_components = &mut world.removed_components; + + let entity = self.entity; + let mut bundle_components = bundle_info.iter_components(); // SAFETY: bundle components are iterated in order, which guarantees that the component type // matches let result = unsafe { @@ -824,7 +831,6 @@ impl<'w> EntityWorldMut<'w> { new_archetype_id, ); } - Some(result) } @@ -914,49 +920,60 @@ impl<'w> EntityWorldMut<'w> { } } - /// Remove the components of `bundle_info` from `entity`, where `self_location` and `old_location` - /// are the location of this entity, and `self_location` is updated to the new location. + /// Remove the components of `bundle` from `entity`. /// - /// SAFETY: `old_location` must be valid and the components in `bundle_info` must exist. + /// SAFETY: The components in `bundle_info` must exist. #[allow(clippy::too_many_arguments)] - unsafe fn remove_bundle_info( - entity: Entity, - self_location: &mut EntityLocation, - old_location: EntityLocation, - bundle_info: &BundleInfo, - archetypes: &mut Archetypes, - storages: &mut Storages, - components: &Components, - entities: &mut Entities, - removed_components: &mut RemovedComponentEvents, - ) { - // SAFETY: `archetype_id` exists because it is referenced in `old_location` which is valid + unsafe fn remove_bundle(&mut self, bundle: BundleId) -> EntityLocation { + let entity = self.entity; + let world = &mut self.world; + let location = self.location; + let bundle_info = world.bundles.get_unchecked(bundle); + + // SAFETY: `archetype_id` exists because it is referenced in `location` which is valid // and components in `bundle_info` must exist due to this functions safety invariants. - let new_archetype_id = unsafe { - remove_bundle_from_archetype( - archetypes, - storages, - components, - old_location.archetype_id, - bundle_info, - true, - ) - } + let new_archetype_id = remove_bundle_from_archetype( + &mut world.archetypes, + &mut world.storages, + &world.components, + location.archetype_id, + bundle_info, + true, + ) .expect("intersections should always return a result"); - if new_archetype_id == old_location.archetype_id { - return; + if new_archetype_id == location.archetype_id { + return location; } - let old_archetype = &mut archetypes[old_location.archetype_id]; - for component_id in bundle_info.components().iter().cloned() { + // SAFETY: Archetypes and Bundles cannot be mutably aliased through DeferredWorld + let (old_archetype, bundle_info, mut deferred_world) = unsafe { + let bundle_info: *const BundleInfo = bundle_info; + let world = world.as_unsafe_world_cell(); + ( + &world.archetypes()[location.archetype_id], + &*bundle_info, + world.into_deferred(), + ) + }; + + 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()); + } + } + + let old_archetype = &world.archetypes[location.archetype_id]; + for component_id in bundle_info.iter_components() { if old_archetype.contains(component_id) { - removed_components.send(component_id, entity); + world.removed_components.send(component_id, entity); // Make sure to drop components stored in sparse sets. // Dense components are dropped later in `move_to_and_drop_missing_unchecked`. if let Some(StorageType::SparseSet) = old_archetype.get_storage_type(component_id) { - storages + world + .storages .sparse_sets .get_mut(component_id) .unwrap() @@ -967,18 +984,19 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: `new_archetype_id` is a subset of the components in `old_location.archetype_id` // because it is created by removing a bundle from these components. - unsafe { - Self::move_entity_from_remove::( - entity, - self_location, - old_location.archetype_id, - old_location, - entities, - archetypes, - storages, - new_archetype_id, - ); - } + let mut new_location = location; + Self::move_entity_from_remove::( + entity, + &mut new_location, + location.archetype_id, + location, + &mut world.entities, + &mut world.archetypes, + &mut world.storages, + new_archetype_id, + ); + + new_location } /// Removes any components in the [`Bundle`] from the entity. @@ -986,30 +1004,13 @@ impl<'w> EntityWorldMut<'w> { /// See [`EntityCommands::remove`](crate::system::EntityCommands::remove) for more details. // TODO: BundleRemover? pub fn remove(&mut self) -> &mut Self { - let archetypes = &mut self.world.archetypes; let storages = &mut self.world.storages; let components = &mut self.world.components; - let entities = &mut self.world.entities; - let removed_components = &mut self.world.removed_components; - let bundle_info = self.world.bundles.init_info::(components, storages); - let old_location = self.location; // SAFETY: Components exist in `bundle_info` because `Bundles::init_info` - // initializes a `BundleInfo` containing all components of the bundle type `T`. - unsafe { - Self::remove_bundle_info( - self.entity, - &mut self.location, - old_location, - bundle_info, - archetypes, - storages, - components, - entities, - removed_components, - ); - } + // initializes a: EntityLocation `BundleInfo` containing all components of the bundle type `T`. + self.location = unsafe { self.remove_bundle(bundle_info) }; self } @@ -1021,10 +1022,10 @@ impl<'w> EntityWorldMut<'w> { let archetypes = &mut self.world.archetypes; let storages = &mut self.world.storages; let components = &mut self.world.components; - let entities = &mut self.world.entities; - let removed_components = &mut self.world.removed_components; - let retained_bundle_info = self.world.bundles.init_info::(components, storages); + let retained_bundle = self.world.bundles.init_info::(components, storages); + // SAFETY: `retained_bundle` exists as we just initialized it. + let retained_bundle_info = unsafe { self.world.bundles.get_unchecked(retained_bundle) }; let old_location = self.location; let old_archetype = &mut archetypes[old_location.archetype_id]; @@ -1032,28 +1033,11 @@ impl<'w> EntityWorldMut<'w> { .components() .filter(|c| !retained_bundle_info.components().contains(c)) .collect::>(); - let remove_bundle_info = self - .world - .bundles - .init_dynamic_info(components, to_remove) - .0; + let remove_bundle = self.world.bundles.init_dynamic_info(components, to_remove); - // SAFETY: Components exist in `remove_bundle_info` because `Bundles::init_dynamic_info` + // SAFETY: Components exist in `remove_bundle` because `Bundles::init_dynamic_info` // initializes a `BundleInfo` containing all components in the to_remove Bundle. - unsafe { - Self::remove_bundle_info( - self.entity, - &mut self.location, - old_location, - remove_bundle_info, - archetypes, - storages, - components, - entities, - removed_components, - ); - } - + self.location = unsafe { self.remove_bundle(remove_bundle) }; self } @@ -1061,9 +1045,28 @@ impl<'w> EntityWorldMut<'w> { /// /// See [`World::despawn`] for more details. pub fn despawn(self) { - debug!("Despawning entity {:?}", self.entity); let world = self.world; - world.flush(); + world.flush_entities(); + let archetype = &world.archetypes[self.location.archetype_id]; + + // SAFETY: Archetype cannot be mutably aliased by DeferredWorld + let (archetype, mut deferred_world) = unsafe { + let archetype: *const Archetype = archetype; + let world = world.as_unsafe_world_cell(); + (&*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()); + } + } + + for component_id in archetype.components() { + world.removed_components.send(component_id, self.entity); + } + let location = world .entities .free(self.entity) @@ -1072,10 +1075,7 @@ impl<'w> EntityWorldMut<'w> { let moved_entity; { - let archetype = &mut world.archetypes[location.archetype_id]; - for component_id in archetype.components() { - world.removed_components.send(component_id, self.entity); - } + let archetype = &mut world.archetypes[self.location.archetype_id]; let remove_result = archetype.swap_remove(location.archetype_row); if let Some(swapped_entity) = remove_result.swapped_entity { let swapped_location = world.entities.get(swapped_entity).unwrap(); @@ -1123,6 +1123,13 @@ impl<'w> EntityWorldMut<'w> { world.archetypes[moved_location.archetype_id] .set_entity_table_row(moved_location.archetype_row, table_row); } + world.flush_commands(); + } + + /// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush_commands`] + pub fn flush(self) -> Entity { + self.world.flush_commands(); + self.entity } /// Gets read-only access to the world that the current entity belongs to. @@ -2077,7 +2084,7 @@ unsafe fn insert_dynamic_bundle< I: Iterator>, S: Iterator, >( - mut bundle_inserter: BundleInserter<'_, '_>, + mut bundle_inserter: BundleInserter<'_>, entity: Entity, location: EntityLocation, components: I, @@ -2185,6 +2192,7 @@ unsafe fn remove_bundle_from_archetype( } let new_archetype_id = archetypes.get_id_or_insert( + components, 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 4d8e1af57f8f6..81691f6a6a70c 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,5 +1,6 @@ //! Defines the [`World`] and APIs for accessing it directly. +mod deferred_world; mod entity_ref; pub mod error; mod spawn_batch; @@ -7,6 +8,7 @@ pub mod unsafe_world_cell; mod world_cell; pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}; +pub use deferred_world::DeferredWorld; pub use entity_ref::{ EntityMut, EntityRef, EntityWorldMut, Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, VacantEntry, @@ -19,8 +21,8 @@ use crate::{ bundle::{Bundle, BundleInserter, BundleSpawner, Bundles}, change_detection::{MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentId, ComponentInfo, ComponentTicks, Components, - Tick, + Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentInfo, ComponentTicks, + Components, Tick, }, entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation}, event::{Event, EventId, Events, SendBatchIds}, @@ -28,7 +30,7 @@ use crate::{ removal_detection::RemovedComponentEvents, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, - system::{Res, Resource}, + system::{CommandQueue, Commands, Res, Resource}, world::error::TryRunScheduleError, }; use bevy_ptr::{OwningPtr, Ptr}; @@ -77,6 +79,7 @@ pub struct World { pub(crate) change_tick: AtomicU32, pub(crate) last_change_tick: Tick, pub(crate) last_check_tick: Tick, + pub(crate) command_queue: CommandQueue, } impl Default for World { @@ -95,6 +98,7 @@ impl Default for World { change_tick: AtomicU32::new(1), last_change_tick: Tick::new(0), last_check_tick: Tick::new(0), + command_queue: CommandQueue::default(), } } } @@ -183,11 +187,39 @@ impl World { WorldCell::new(self) } + /// Creates a new [`Commands`] instance that writes to the world's command queue + /// Use [`World::flush_commands`] to apply all queued commands + #[inline] + pub fn commands(&mut self) -> Commands { + Commands::new_from_entities(&mut self.command_queue, &self.entities) + } + /// Initializes a new [`Component`] type and returns the [`ComponentId`] created for it. pub fn init_component(&mut self) -> ComponentId { self.components.init_component::(&mut self.storages) } + /// Returns a mutable reference to the [`ComponentHooks`] for a [`Component`] type. + /// + /// Will panic if `T` exists in any archetypes. + pub fn register_component_hooks(&mut self) -> &mut ComponentHooks { + let index = self.init_component::(); + assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(index)), "Components hooks cannot be modified if the component already exists in an archetype, use init_component if {} may already be in use", std::any::type_name::()); + // SAFETY: We just created this component + unsafe { self.components.get_hooks_mut(index).debug_checked_unwrap() } + } + + /// Returns a mutable reference to the [`ComponentHooks`] for a [`Component`] with the given id if it exists. + /// + /// Will panic if `id` exists in any archetypes. + pub fn register_component_hooks_by_id( + &mut self, + id: ComponentId, + ) -> Option<&mut ComponentHooks> { + assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(id)), "Components hooks cannot be modified if the component already exists in an archetype, use init_component if the component with id {:?} may already be in use", id); + self.components.get_hooks_mut(id) + } + /// Initializes a new [`Component`] type and returns the [`ComponentId`] created for it. /// /// This method differs from [`World::init_component`] in that it uses a [`ComponentDescriptor`] @@ -422,7 +454,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(); + self.flush_entities(); match self.entities.alloc_at_without_replacement(entity) { AllocAtWithoutReplacement::Exists(location) => { // SAFETY: `entity` exists and `location` is that entity's location @@ -676,7 +708,7 @@ impl World { /// assert_eq!(position.x, 0.0); /// ``` pub fn spawn_empty(&mut self) -> EntityWorldMut { - self.flush(); + self.flush_entities(); let entity = self.entities.alloc(); // SAFETY: entity was just allocated unsafe { self.spawn_at_empty_internal(entity) } @@ -742,23 +774,13 @@ impl World { /// assert_eq!(position.x, 2.0); /// ``` pub fn spawn(&mut self, bundle: B) -> EntityWorldMut { - self.flush(); + self.flush_entities(); let change_tick = self.change_tick(); let entity = self.entities.alloc(); let entity_location = { - let bundle_info = self - .bundles - .init_info::(&mut self.components, &mut self.storages); - let mut spawner = bundle_info.get_bundle_spawner( - &mut self.entities, - &mut self.archetypes, - &self.components, - &mut self.storages, - change_tick, - ); - + let mut bundle_spawner = BundleSpawner::new::(self, change_tick); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent - unsafe { spawner.spawn_non_existent(entity, bundle) } + unsafe { bundle_spawner.spawn_non_existent(entity, bundle) } }; // SAFETY: entity and location are valid, as they were just created above @@ -1534,33 +1556,30 @@ impl World { I::IntoIter: Iterator, B: Bundle, { - self.flush(); + self.flush_entities(); let change_tick = self.change_tick(); - let bundle_info = self + let bundle_id = self .bundles .init_info::(&mut self.components, &mut self.storages); - enum SpawnOrInsert<'a, 'b> { - Spawn(BundleSpawner<'a, 'b>), - Insert(BundleInserter<'a, 'b>, ArchetypeId), + enum SpawnOrInsert<'w> { + Spawn(BundleSpawner<'w>), + Insert(BundleInserter<'w>, ArchetypeId), } - impl<'a, 'b> SpawnOrInsert<'a, 'b> { + impl<'w> SpawnOrInsert<'w> { fn entities(&mut self) -> &mut Entities { match self { - SpawnOrInsert::Spawn(spawner) => spawner.entities, - SpawnOrInsert::Insert(inserter, _) => inserter.entities, + SpawnOrInsert::Spawn(spawner) => spawner.entities(), + SpawnOrInsert::Insert(inserter, _) => inserter.entities(), } } } - let mut spawn_or_insert = SpawnOrInsert::Spawn(bundle_info.get_bundle_spawner( - &mut self.entities, - &mut self.archetypes, - &self.components, - &mut self.storages, - change_tick, - )); + // SAFETY: we initialized this bundle_id in `init_info` + let mut spawn_or_insert = SpawnOrInsert::Spawn(unsafe { + BundleSpawner::new_with_id(self, bundle_id, change_tick) + }); let mut invalid_entities = Vec::new(); for (entity, bundle) in iter { @@ -1577,14 +1596,15 @@ impl World { unsafe { inserter.insert(entity, location, bundle) }; } _ => { - let mut inserter = bundle_info.get_bundle_inserter( - &mut self.entities, - &mut self.archetypes, - &self.components, - &mut self.storages, - location.archetype_id, - change_tick, - ); + // SAFETY: we initialized this bundle_id in `init_info` + let mut inserter = unsafe { + BundleInserter::new_with_id( + self, + location.archetype_id, + bundle_id, + change_tick, + ) + }; // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter unsafe { inserter.insert(entity, location, bundle) }; spawn_or_insert = @@ -1597,13 +1617,9 @@ impl World { // SAFETY: `entity` is allocated (but non existent), bundle matches inserter unsafe { spawner.spawn_non_existent(entity, bundle) }; } else { - let mut spawner = bundle_info.get_bundle_spawner( - &mut self.entities, - &mut self.archetypes, - &self.components, - &mut self.storages, - change_tick, - ); + // SAFETY: we initialized this bundle_id in `init_info` + let mut spawner = + unsafe { BundleSpawner::new_with_id(self, bundle_id, change_tick) }; // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter unsafe { spawner.spawn_non_existent(entity, bundle) }; spawn_or_insert = SpawnOrInsert::Spawn(spawner); @@ -1824,7 +1840,7 @@ impl World { /// Empties queued entities and adds them to the empty [`Archetype`](crate::archetype::Archetype). /// This should be called before doing operations that might operate on queued entities, /// such as inserting a [`Component`]. - pub(crate) fn flush(&mut self) { + pub(crate) fn flush_entities(&mut self) { let empty_archetype = self.archetypes.empty_mut(); let table = &mut self.storages.tables[empty_archetype.table_id()]; // PERF: consider pre-allocating space for flushed entities @@ -1838,6 +1854,16 @@ impl World { } } + /// 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) { + if !self.command_queue.is_empty() { + // `CommandQueue` application always applies commands from the world queue first so this will apply all stored commands + CommandQueue::default().apply(self); + } + } + /// Increments the world's current change tick and returns the old value. #[inline] pub fn increment_change_tick(&self) -> Tick { diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index e21199f56444c..ab9cb8f2db01f 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -15,7 +15,7 @@ where I::Item: Bundle, { inner: I, - spawner: BundleSpawner<'w, 'w>, + spawner: BundleSpawner<'w>, } impl<'w, I> SpawnBatchIter<'w, I> @@ -27,24 +27,15 @@ 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(); + world.flush_entities(); let change_tick = world.change_tick(); let (lower, upper) = iter.size_hint(); let length = upper.unwrap_or(lower); - - let bundle_info = world - .bundles - .init_info::(&mut world.components, &mut world.storages); world.entities.reserve(length as u32); - let mut spawner = bundle_info.get_bundle_spawner( - &mut world.entities, - &mut world.archetypes, - &world.components, - &mut world.storages, - change_tick, - ); + + let mut spawner = BundleSpawner::new::(world, change_tick); spawner.reserve_storage(length); Self { @@ -60,7 +51,11 @@ where I::Item: Bundle, { fn drop(&mut self) { - for _ in self {} + // Iterate through self in order to spawn remaining bundles. + for _ in &mut *self {} + // Apply any commands from those operations. + // SAFETY: `self.spawner` will be dropped immediately after this call. + unsafe { self.spawner.flush_commands() }; } } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 7ae085d0b26ab..b786a3cfb4425 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -14,10 +14,10 @@ use crate::{ prelude::Component, removal_detection::RemovedComponentEvents, storage::{Column, ComponentSparseSet, Storages}, - system::{Res, Resource}, + system::{CommandQueue, Res, Resource}, }; use bevy_ptr::Ptr; -use std::{any::TypeId, cell::UnsafeCell, fmt::Debug, marker::PhantomData, ptr}; +use std::{any::TypeId, cell::UnsafeCell, fmt::Debug, marker::PhantomData, ptr, ptr::addr_of_mut}; /// Variant of the [`World`] where resource and component accesses take `&self`, and the responsibility to avoid /// aliasing violations are given to the caller instead of being checked at compile-time by rust's unique XOR shared rule. @@ -590,6 +590,18 @@ impl<'w> UnsafeWorldCell<'w> { .get(component_id)? .get_with_ticks() } + + // Returns a mutable reference to the underlying world's [`CommandQueue`]. + /// # Safety + /// It is the callers responsibility to ensure that + /// - the [`UnsafeWorldCell`] has permission to access the queue mutably + /// - no mutable references to the queue exist at the same time + pub(crate) unsafe fn get_command_queue(self) -> &'w mut CommandQueue { + // SAFETY: + // - caller ensures there are no existing mutable references + // - caller ensures that we have permission to access the queue + unsafe { &mut *addr_of_mut!((*self.0).command_queue) } + } } impl Debug for UnsafeWorldCell<'_> { diff --git a/examples/README.md b/examples/README.md index 7a605b114e0ad..b8f0b0f4ed6b8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -228,6 +228,7 @@ Example | Description Example | Description --- | --- [Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components +[Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events [Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type [Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components diff --git a/examples/ecs/component_hooks.rs b/examples/ecs/component_hooks.rs new file mode 100644 index 0000000000000..7794ca84a8c3f --- /dev/null +++ b/examples/ecs/component_hooks.rs @@ -0,0 +1,96 @@ +//! This examples illustrates the different ways you can employ component lifecycle hooks + +use bevy::ecs::component::{ComponentInfo, TableStorage}; +use bevy::prelude::*; +use std::collections::HashMap; + +#[derive(Debug)] +struct MyComponent(KeyCode); + +impl Component for MyComponent { + type Storage = TableStorage; + + /// Hooks can also be registered during component initialisation by + /// implementing `init_component_info` + fn init_component_info(_info: &mut ComponentInfo) { + // Register hooks... + } +} + +#[derive(Resource, Default, Debug, Deref, DerefMut)] +struct MyComponentIndex(HashMap); + +#[derive(Event)] +struct MyEvent; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, trigger_hooks) + .init_resource::() + .add_event::() + .run(); +} + +fn setup(world: &mut World) { + // In order to register component hooks the component must: + // - not belong to any created archetypes + // - not already have a hook of that kind registered + // This is to prevent overriding hooks defined in plugins and other crates as well as keeping things fast + world + .register_component_hooks::() + // There are 3 component lifecycle hooks: `on_add`, `on_insert` and `on_remove` + // A hook has 3 arguments: + // - a `DeferredWorld`, this allows access to resource and component data as well as `Commands` + // - the entity that triggered the hook + // - the component id of the triggering component, this is mostly used for dynamic components + // + // `on_add` will trigger when a component is inserted onto an entity without it + .on_add(|mut world, entity, component_id| { + // You can access component data from within the hook + let value = world.get::(entity).unwrap().0; + println!( + "Component: {:?} added to: {:?} with value {:?}", + component_id, entity, value + ); + // Or access resources + world + .resource_mut::() + .insert(value, entity); + // Or send events + world.send_event(MyEvent); + }) + // `on_insert` will trigger when a component is inserted onto an entity, + // regardless of whether or not it already had it and after `on_add` if it ran + .on_insert(|world, _, _| { + println!("Current Index: {:?}", world.resource::()); + }) + // `on_remove` will trigger when a component is removed from an entity, + // since it runs before the component is removed you can still access the component data + .on_remove(|mut world, entity, component_id| { + let value = world.get::(entity).unwrap().0; + println!( + "Component: {:?} removed from: {:?} with value {:?}", + component_id, entity, value + ); + world.resource_mut::().remove(&value); + // You can also issue commands through `.commands()` + world.commands().entity(entity).despawn(); + }); +} + +fn trigger_hooks( + mut commands: Commands, + keys: Res>, + index: Res, +) { + for (key, entity) in index.iter() { + if !keys.pressed(*key) { + commands.entity(*entity).remove::(); + } + } + for key in keys.get_just_pressed() { + commands.spawn(MyComponent(*key)); + } +}