Skip to content

Commit 94ff123

Browse files
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::<T>` 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.
1 parent bcdca06 commit 94ff123

15 files changed

+1512
-518
lines changed

Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -1392,6 +1392,17 @@ description = "Change detection on components"
13921392
category = "ECS (Entity Component System)"
13931393
wasm = false
13941394

1395+
[[example]]
1396+
name = "component_hooks"
1397+
path = "examples/ecs/component_hooks.rs"
1398+
doc-scrape-examples = true
1399+
1400+
[package.metadata.example.component_hooks]
1401+
name = "Component Hooks"
1402+
description = "Define component hooks to manage component lifecycle events"
1403+
category = "ECS (Entity Component System)"
1404+
wasm = false
1405+
13951406
[[example]]
13961407
name = "custom_schedule"
13971408
path = "examples/ecs/custom_schedule.rs"

crates/bevy_ecs/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
2222
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
2323
bevy_ecs_macros = { path = "macros", version = "0.14.0-dev" }
2424

25+
bitflags = "2.3"
2526
concurrent-queue = "2.4.0"
2627
fixedbitset = "0.4.2"
2728
rustc-hash = "1.1"

crates/bevy_ecs/src/archetype.rs

+65-8
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
2222
use crate::{
2323
bundle::BundleId,
24-
component::{ComponentId, StorageType},
24+
component::{ComponentId, Components, StorageType},
2525
entity::{Entity, EntityLocation},
2626
storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow},
2727
};
@@ -107,7 +107,7 @@ impl ArchetypeId {
107107
}
108108
}
109109

110-
#[derive(Copy, Clone)]
110+
#[derive(Copy, Clone, Eq, PartialEq)]
111111
pub(crate) enum ComponentStatus {
112112
Added,
113113
Mutated,
@@ -298,6 +298,18 @@ struct ArchetypeComponentInfo {
298298
archetype_component_id: ArchetypeComponentId,
299299
}
300300

301+
bitflags::bitflags! {
302+
/// Flags used to keep track of metadata about the component in this [`Archetype`]
303+
///
304+
/// Used primarily to early-out when there are no [`ComponentHook`] registered for any contained components.
305+
#[derive(Clone, Copy)]
306+
pub(crate) struct ArchetypeFlags: u32 {
307+
const ON_ADD_HOOK = (1 << 0);
308+
const ON_INSERT_HOOK = (1 << 1);
309+
const ON_REMOVE_HOOK = (1 << 2);
310+
}
311+
}
312+
301313
/// Metadata for a single archetype within a [`World`].
302314
///
303315
/// For more information, see the *[module level documentation]*.
@@ -310,20 +322,26 @@ pub struct Archetype {
310322
edges: Edges,
311323
entities: Vec<ArchetypeEntity>,
312324
components: ImmutableSparseSet<ComponentId, ArchetypeComponentInfo>,
325+
flags: ArchetypeFlags,
313326
}
314327

315328
impl Archetype {
316329
pub(crate) fn new(
330+
components: &Components,
317331
id: ArchetypeId,
318332
table_id: TableId,
319333
table_components: impl Iterator<Item = (ComponentId, ArchetypeComponentId)>,
320334
sparse_set_components: impl Iterator<Item = (ComponentId, ArchetypeComponentId)>,
321335
) -> Self {
322336
let (min_table, _) = table_components.size_hint();
323337
let (min_sparse, _) = sparse_set_components.size_hint();
324-
let mut components = SparseSet::with_capacity(min_table + min_sparse);
338+
let mut flags = ArchetypeFlags::empty();
339+
let mut archetype_components = SparseSet::with_capacity(min_table + min_sparse);
325340
for (component_id, archetype_component_id) in table_components {
326-
components.insert(
341+
// SAFETY: We are creating an archetype that includes this component so it must exist
342+
let info = unsafe { components.get_info_unchecked(component_id) };
343+
info.update_archetype_flags(&mut flags);
344+
archetype_components.insert(
327345
component_id,
328346
ArchetypeComponentInfo {
329347
storage_type: StorageType::Table,
@@ -333,7 +351,10 @@ impl Archetype {
333351
}
334352

335353
for (component_id, archetype_component_id) in sparse_set_components {
336-
components.insert(
354+
// SAFETY: We are creating an archetype that includes this component so it must exist
355+
let info = unsafe { components.get_info_unchecked(component_id) };
356+
info.update_archetype_flags(&mut flags);
357+
archetype_components.insert(
337358
component_id,
338359
ArchetypeComponentInfo {
339360
storage_type: StorageType::SparseSet,
@@ -345,8 +366,9 @@ impl Archetype {
345366
id,
346367
table_id,
347368
entities: Vec::new(),
348-
components: components.into_immutable(),
369+
components: archetype_components.into_immutable(),
349370
edges: Default::default(),
371+
flags,
350372
}
351373
}
352374

@@ -356,6 +378,12 @@ impl Archetype {
356378
self.id
357379
}
358380

381+
/// Fetches the flags for the archetype.
382+
#[inline]
383+
pub(crate) fn flags(&self) -> ArchetypeFlags {
384+
self.flags
385+
}
386+
359387
/// Fetches the archetype's [`Table`] ID.
360388
///
361389
/// [`Table`]: crate::storage::Table
@@ -542,6 +570,24 @@ impl Archetype {
542570
pub(crate) fn clear_entities(&mut self) {
543571
self.entities.clear();
544572
}
573+
574+
/// Returns true if any of the components in this archetype have `on_add` hooks
575+
#[inline]
576+
pub(crate) fn has_on_add(&self) -> bool {
577+
self.flags().contains(ArchetypeFlags::ON_ADD_HOOK)
578+
}
579+
580+
/// Returns true if any of the components in this archetype have `on_insert` hooks
581+
#[inline]
582+
pub(crate) fn has_on_insert(&self) -> bool {
583+
self.flags().contains(ArchetypeFlags::ON_INSERT_HOOK)
584+
}
585+
586+
/// Returns true if any of the components in this archetype have `on_remove` hooks
587+
#[inline]
588+
pub(crate) fn has_on_remove(&self) -> bool {
589+
self.flags().contains(ArchetypeFlags::ON_REMOVE_HOOK)
590+
}
545591
}
546592

547593
/// The next [`ArchetypeId`] in an [`Archetypes`] collection.
@@ -624,7 +670,15 @@ impl Archetypes {
624670
by_components: Default::default(),
625671
archetype_component_count: 0,
626672
};
627-
archetypes.get_id_or_insert(TableId::empty(), Vec::new(), Vec::new());
673+
// SAFETY: Empty archetype has no components
674+
unsafe {
675+
archetypes.get_id_or_insert(
676+
&Components::default(),
677+
TableId::empty(),
678+
Vec::new(),
679+
Vec::new(),
680+
);
681+
}
628682
archetypes
629683
}
630684

@@ -717,8 +771,10 @@ impl Archetypes {
717771
///
718772
/// # Safety
719773
/// [`TableId`] must exist in tables
720-
pub(crate) fn get_id_or_insert(
774+
/// `table_components` and `sparse_set_components` must exist in `components`
775+
pub(crate) unsafe fn get_id_or_insert(
721776
&mut self,
777+
components: &Components,
722778
table_id: TableId,
723779
table_components: Vec<ComponentId>,
724780
sparse_set_components: Vec<ComponentId>,
@@ -744,6 +800,7 @@ impl Archetypes {
744800
let sparse_set_archetype_components =
745801
(sparse_start..*archetype_component_count).map(ArchetypeComponentId);
746802
archetypes.push(Archetype::new(
803+
components,
747804
id,
748805
table_id,
749806
table_components.into_iter().zip(table_archetype_components),

0 commit comments

Comments
 (0)