From dda7a744cfa693ce309217b77eefe211fc253955 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 22 May 2024 14:04:56 -0400 Subject: [PATCH] Further improve docs for component hooks (#13475) # Objective While reviewing the other open hooks-related PRs, I found that the docs on the `ComponentHooks` struct itself didn't give enough information about how and why the feature could be used. ## Solution 1. Clean up the docs to add additional context. 2. Add a doc test demonstrating simple usage. ## Testing The doc test passes locally. --------- Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/component.rs | 79 ++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index ba0500e0b3ced..1a61820f31f62 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -182,7 +182,55 @@ pub enum StorageType { /// 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 its [`ComponentInfo`] +/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. +/// +/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. +/// These are intended to be used for structural side effects that need to happen when a component is added or removed, +/// and are not intended for general-purpose logic. +/// +/// For example, you might use a hook to update a cached index when a component is added, +/// to clean up resources when a component is removed, +/// or to keep hierarchical data structures across entities in sync. +/// +/// This information is stored in the [`ComponentInfo`] of the associated component. +/// +/// # Example +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_utils::HashSet; +/// +/// #[derive(Component)] +/// struct MyTrackedComponent; +/// +/// #[derive(Resource, Default)] +/// struct TrackedEntities(HashSet); +/// +/// let mut world = World::new(); +/// world.init_resource::(); +/// +/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks +/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); +/// assert!(tracked_component_query.iter(&world).next().is_none()); +/// +/// world.register_component_hooks::().on_add(|mut world, entity, _component_id| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.insert(entity); +/// }); +/// +/// world.register_component_hooks::().on_remove(|mut world, entity, _component_id| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.remove(&entity); +/// }); +/// +/// let entity = world.spawn(MyTrackedComponent).id(); +/// let tracked_entities = world.resource::(); +/// assert!(tracked_entities.0.contains(&entity)); +/// +/// world.despawn(entity); +/// let tracked_entities = world.resource::(); +/// assert!(!tracked_entities.0.contains(&entity)); +/// ``` #[derive(Debug, Clone, Default)] pub struct ComponentHooks { pub(crate) on_add: Option, @@ -195,6 +243,8 @@ impl ComponentHooks { /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as /// adding all of its components. /// + /// # Panics + /// /// 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) @@ -202,9 +252,17 @@ impl ComponentHooks { } /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// or replaced. The hook won't run if the component is already present and is only mutated. + /// or replaced. + /// /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// /// 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) @@ -214,13 +272,18 @@ impl ComponentHooks { /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. /// Despawning an entity counts as removing all of its components. /// + /// # Panics + /// /// 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`]. + /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. + /// + /// This is a 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() { @@ -230,7 +293,10 @@ impl ComponentHooks { Some(self) } - /// Fallible version of [`Self::on_insert`]. + /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// + /// This is a 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() { @@ -240,7 +306,10 @@ impl ComponentHooks { Some(self) } - /// Fallible version of [`Self::on_remove`]. + /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// + /// This is a 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() {