Component lifecycle reorganization and documentation (#19543)

# Objective

I set out with one simple goal: clearly document the differences between
each of the component lifecycle events via module docs.

Unfortunately, no such module existed: the various lifecycle code was
scattered to the wind.
Without a unified module, it's very hard to discover the related types,
and there's nowhere good to put my shiny new documentation.

## Solution

1. Unify the assorted types into a single
`bevy_ecs::component_lifecycle` module.
2. Write docs.
3. Write a migration guide.

## Testing

Thanks CI!

## Follow-up

1. The lifecycle event names are pretty confusing, especially
`OnReplace`. We should consider renaming those. No bikeshedding in my PR
though!
2. Observers need real module docs too :(
3. Any additional functional changes should be done elsewhere; this is a
simple docs and re-org PR.

---------

Co-authored-by: theotherphil <phil.j.ellison@gmail.com>
This commit is contained in:
Alice Cecile 2025-06-09 17:59:16 -07:00 committed by GitHub
parent c35df3ca66
commit 6ddd0f16a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 703 additions and 630 deletions

View File

@ -1483,8 +1483,8 @@ mod tests {
component::Component, component::Component,
entity::Entity, entity::Entity,
event::{Event, EventWriter, Events}, event::{Event, EventWriter, Events},
lifecycle::RemovedComponents,
query::With, query::With,
removal_detection::RemovedComponents,
resource::Resource, resource::Resource,
schedule::{IntoScheduleConfigs, ScheduleLabel}, schedule::{IntoScheduleConfigs, ScheduleLabel},
system::{Commands, Query}, system::{Commands, Query},

View File

@ -6,9 +6,9 @@ use bevy_ecs::{
component::Component, component::Component,
entity::Entity, entity::Entity,
hierarchy::ChildOf, hierarchy::ChildOf,
lifecycle::RemovedComponents,
query::{Changed, Or, QueryFilter, With, Without}, query::{Changed, Or, QueryFilter, With, Without},
relationship::{Relationship, RelationshipTarget}, relationship::{Relationship, RelationshipTarget},
removal_detection::RemovedComponents,
schedule::{IntoScheduleConfigs, SystemSet}, schedule::{IntoScheduleConfigs, SystemSet},
system::{Commands, Local, Query}, system::{Commands, Local, Query},
}; };

View File

@ -1,7 +1,7 @@
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details. //! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_ecs::{component::*, prelude::*}; use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*};
use bevy_math::UVec2; use bevy_math::UVec2;
use bevy_platform::collections::HashSet; use bevy_platform::collections::HashSet;
use bevy_platform::time::Instant; use bevy_platform::time::Instant;

View File

@ -60,4 +60,4 @@ mod case4 {
pub struct BarTargetOf(Entity); pub struct BarTargetOf(Entity);
} }
fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {} fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::lifecycle::HookContext) {}

View File

@ -434,7 +434,7 @@ impl HookAttributeKind {
HookAttributeKind::Path(path) => path.to_token_stream(), HookAttributeKind::Path(path) => path.to_token_stream(),
HookAttributeKind::Call(call) => { HookAttributeKind::Call(call) => {
quote!({ quote!({
fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) { fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::lifecycle::HookContext) {
(#call)(world, ctx) (#call)(world, ctx)
} }
_internal_hook _internal_hook
@ -658,7 +658,7 @@ fn hook_register_function_call(
) -> Option<TokenStream2> { ) -> Option<TokenStream2> {
function.map(|meta| { function.map(|meta| {
quote! { quote! {
fn #hook() -> ::core::option::Option<#bevy_ecs_path::component::ComponentHook> { fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> {
::core::option::Option::Some(#meta) ::core::option::Option::Some(#meta)
} }
} }

View File

@ -693,7 +693,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer /// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer
/// ///
/// [`OnAdd`]: crate::world::OnAdd /// [`OnAdd`]: crate::lifecycle::OnAdd
#[inline] #[inline]
pub fn has_add_observer(&self) -> bool { pub fn has_add_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER) self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER)
@ -701,7 +701,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer /// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer
/// ///
/// [`OnInsert`]: crate::world::OnInsert /// [`OnInsert`]: crate::lifecycle::OnInsert
#[inline] #[inline]
pub fn has_insert_observer(&self) -> bool { pub fn has_insert_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER) self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
@ -709,7 +709,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer /// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer
/// ///
/// [`OnReplace`]: crate::world::OnReplace /// [`OnReplace`]: crate::lifecycle::OnReplace
#[inline] #[inline]
pub fn has_replace_observer(&self) -> bool { pub fn has_replace_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER) self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER)
@ -717,7 +717,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer /// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
/// ///
/// [`OnRemove`]: crate::world::OnRemove /// [`OnRemove`]: crate::lifecycle::OnRemove
#[inline] #[inline]
pub fn has_remove_observer(&self) -> bool { pub fn has_remove_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER) self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
@ -725,7 +725,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer /// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer
/// ///
/// [`OnDespawn`]: crate::world::OnDespawn /// [`OnDespawn`]: crate::lifecycle::OnDespawn
#[inline] #[inline]
pub fn has_despawn_observer(&self) -> bool { pub fn has_despawn_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER) self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER)

View File

@ -66,15 +66,13 @@ use crate::{
RequiredComponents, StorageType, Tick, RequiredComponents, StorageType, Tick,
}, },
entity::{Entities, Entity, EntityLocation}, entity::{Entities, Entity, EntityLocation},
lifecycle::{ON_ADD, ON_INSERT, ON_REMOVE, ON_REPLACE},
observer::Observers, observer::Observers,
prelude::World, prelude::World,
query::DebugCheckedUnwrap, query::DebugCheckedUnwrap,
relationship::RelationshipHookMode, relationship::RelationshipHookMode,
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
world::{ world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut},
unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REMOVE,
ON_REPLACE,
},
}; };
use alloc::{boxed::Box, vec, vec::Vec}; use alloc::{boxed::Box, vec, vec::Vec};
use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::collections::{HashMap, HashSet};
@ -2123,7 +2121,7 @@ fn sorted_remove<T: Eq + Ord + Copy>(source: &mut Vec<T>, remove: &[T]) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
archetype::ArchetypeCreated, component::HookContext, prelude::*, world::DeferredWorld, archetype::ArchetypeCreated, lifecycle::HookContext, prelude::*, world::DeferredWorld,
}; };
use alloc::vec; use alloc::vec;

View File

@ -5,12 +5,12 @@ use crate::{
bundle::BundleInfo, bundle::BundleInfo,
change_detection::{MaybeLocation, MAX_CHANGE_AGE}, change_detection::{MaybeLocation, MAX_CHANGE_AGE},
entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent}, entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent},
lifecycle::{ComponentHook, ComponentHooks},
query::DebugCheckedUnwrap, query::DebugCheckedUnwrap,
relationship::RelationshipHookMode,
resource::Resource, resource::Resource,
storage::{SparseSetIndex, SparseSets, Table, TableRow}, storage::{SparseSetIndex, SparseSets, Table, TableRow},
system::{Local, SystemParam}, system::{Local, SystemParam},
world::{DeferredWorld, FromWorld, World}, world::{FromWorld, World},
}; };
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::{borrow::Cow, format, vec::Vec}; use alloc::{borrow::Cow, format, vec::Vec};
@ -376,7 +376,8 @@ use thiserror::Error;
/// - `#[component(on_remove = on_remove_function)]` /// - `#[component(on_remove = on_remove_function)]`
/// ///
/// ``` /// ```
/// # use bevy_ecs::component::{Component, HookContext}; /// # use bevy_ecs::component::Component;
/// # use bevy_ecs::lifecycle::HookContext;
/// # use bevy_ecs::world::DeferredWorld; /// # use bevy_ecs::world::DeferredWorld;
/// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::entity::Entity;
/// # use bevy_ecs::component::ComponentId; /// # use bevy_ecs::component::ComponentId;
@ -405,7 +406,8 @@ use thiserror::Error;
/// This also supports function calls that yield closures /// This also supports function calls that yield closures
/// ///
/// ``` /// ```
/// # use bevy_ecs::component::{Component, HookContext}; /// # use bevy_ecs::component::Component;
/// # use bevy_ecs::lifecycle::HookContext;
/// # use bevy_ecs::world::DeferredWorld; /// # use bevy_ecs::world::DeferredWorld;
/// # /// #
/// #[derive(Component)] /// #[derive(Component)]
@ -657,244 +659,6 @@ pub enum StorageType {
SparseSet, 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>, HookContext);
/// Context provided to a [`ComponentHook`].
#[derive(Clone, Copy, Debug)]
pub struct HookContext {
/// The [`Entity`] this hook was invoked for.
pub entity: Entity,
/// The [`ComponentId`] this hook was invoked for.
pub component_id: ComponentId,
/// The caller location is `Some` if the `track_caller` feature is enabled.
pub caller: MaybeLocation,
/// Configures how relationship hooks will run
pub relationship_hook_mode: RelationshipHookMode,
}
/// [`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.
///
/// There is two ways of configuring hooks for a component:
/// 1. Defining the relevant hooks on the [`Component`] implementation
/// 2. Using the [`World::register_component_hooks`] method
///
/// # Example 2
///
/// ```
/// use bevy_ecs::prelude::*;
/// use bevy_platform::collections::HashSet;
///
/// #[derive(Component)]
/// struct MyTrackedComponent;
///
/// #[derive(Resource, Default)]
/// struct TrackedEntities(HashSet<Entity>);
///
/// let mut world = World::new();
/// world.init_resource::<TrackedEntities>();
///
/// // 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::<MyTrackedComponent>().on_add(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.insert(context.entity);
/// });
///
/// world.register_component_hooks::<MyTrackedComponent>().on_remove(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.remove(&context.entity);
/// });
///
/// let entity = world.spawn(MyTrackedComponent).id();
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(tracked_entities.0.contains(&entity));
///
/// world.despawn(entity);
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(!tracked_entities.0.contains(&entity));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ComponentHooks {
pub(crate) on_add: Option<ComponentHook>,
pub(crate) on_insert: Option<ComponentHook>,
pub(crate) on_replace: Option<ComponentHook>,
pub(crate) on_remove: Option<ComponentHook>,
pub(crate) on_despawn: Option<ComponentHook>,
}
impl ComponentHooks {
pub(crate) fn update_from_component<C: Component + ?Sized>(&mut self) -> &mut Self {
if let Some(hook) = C::on_add() {
self.on_add(hook);
}
if let Some(hook) = C::on_insert() {
self.on_insert(hook);
}
if let Some(hook) = C::on_replace() {
self.on_replace(hook);
}
if let Some(hook) = C::on_remove() {
self.on_remove(hook);
}
if let Some(hook) = C::on_despawn() {
self.on_despawn(hook);
}
self
}
/// Register a [`ComponentHook`] that will be run when this component is added to an entity.
/// 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)
.expect("Component already has an on_add hook")
}
/// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`)
/// 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)
.expect("Component already has an on_insert hook")
}
/// Register a [`ComponentHook`] that will be run when this component is about to be dropped,
/// such as being replaced (with `.insert`) or removed.
///
/// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced,
/// allowing access to the previous data just before it is dropped.
/// This hook does *not* run if the entity did not already have this component.
///
/// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity).
///
/// # 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_replace` hook
pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_replace(hook)
.expect("Component already has an on_replace hook")
}
/// 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 already has an on_remove hook")
}
/// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
///
/// # Panics
///
/// Will panic if the component already has an `on_despawn` hook
pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_despawn(hook)
.expect("Component already has an on_despawn hook")
}
/// 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() {
return None;
}
self.on_add = Some(hook);
Some(self)
}
/// 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() {
return None;
}
self.on_insert = Some(hook);
Some(self)
}
/// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed
///
/// This is a fallible version of [`Self::on_replace`].
///
/// Returns `None` if the component already has an `on_replace` hook.
pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> {
if self.on_replace.is_some() {
return None;
}
self.on_replace = Some(hook);
Some(self)
}
/// 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() {
return None;
}
self.on_remove = Some(hook);
Some(self)
}
/// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
///
/// This is a fallible version of [`Self::on_despawn`].
///
/// Returns `None` if the component already has an `on_despawn` hook.
pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> {
if self.on_despawn.is_some() {
return None;
}
self.on_despawn = Some(hook);
Some(self)
}
}
/// Stores metadata for a type of component or resource stored in a specific [`World`]. /// Stores metadata for a type of component or resource stored in a specific [`World`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ComponentInfo { pub struct ComponentInfo {

View File

@ -68,7 +68,7 @@ pub trait Event: Send + Sync + 'static {
/// ///
/// # Warning /// # Warning
/// ///
/// This method should not be overridden by implementors, /// This method should not be overridden by implementers,
/// and should always correspond to the implementation of [`component_id`](Event::component_id). /// and should always correspond to the implementation of [`component_id`](Event::component_id).
fn register_component_id(world: &mut World) -> ComponentId { fn register_component_id(world: &mut World) -> ComponentId {
world.register_component::<EventWrapperComponent<Self>>() world.register_component::<EventWrapperComponent<Self>>()
@ -82,7 +82,7 @@ pub trait Event: Send + Sync + 'static {
/// ///
/// # Warning /// # Warning
/// ///
/// This method should not be overridden by implementors, /// This method should not be overridden by implementers,
/// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id). /// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id).
fn component_id(world: &World) -> Option<ComponentId> { fn component_id(world: &World) -> Option<ComponentId> {
world.component_id::<EventWrapperComponent<Self>>() world.component_id::<EventWrapperComponent<Self>>()

View File

@ -10,8 +10,9 @@
use crate::reflect::{ReflectComponent, ReflectFromWorld}; use crate::reflect::{ReflectComponent, ReflectFromWorld};
use crate::{ use crate::{
bundle::Bundle, bundle::Bundle,
component::{Component, HookContext}, component::Component,
entity::Entity, entity::Entity,
lifecycle::HookContext,
relationship::{RelatedSpawner, RelatedSpawnerCommands}, relationship::{RelatedSpawner, RelatedSpawnerCommands},
system::EntityCommands, system::EntityCommands,
world::{DeferredWorld, EntityWorldMut, FromWorld, World}, world::{DeferredWorld, EntityWorldMut, FromWorld, World},

View File

@ -41,6 +41,7 @@ pub mod event;
pub mod hierarchy; pub mod hierarchy;
pub mod intern; pub mod intern;
pub mod label; pub mod label;
pub mod lifecycle;
pub mod name; pub mod name;
pub mod never; pub mod never;
pub mod observer; pub mod observer;
@ -48,7 +49,6 @@ pub mod query;
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
pub mod reflect; pub mod reflect;
pub mod relationship; pub mod relationship;
pub mod removal_detection;
pub mod resource; pub mod resource;
pub mod schedule; pub mod schedule;
pub mod spawn; pub mod spawn;
@ -76,12 +76,12 @@ pub mod prelude {
error::{BevyError, Result}, error::{BevyError, Result},
event::{Event, EventMutator, EventReader, EventWriter, Events}, event::{Event, EventMutator, EventReader, EventWriter, Events},
hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children},
lifecycle::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, RemovedComponents},
name::{Name, NameOrEntity}, name::{Name, NameOrEntity},
observer::{Observer, Trigger}, observer::{Observer, Trigger},
query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
related, related,
relationship::RelationshipTarget, relationship::RelationshipTarget,
removal_detection::RemovedComponents,
resource::Resource, resource::Resource,
schedule::{ schedule::{
common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule,
@ -96,7 +96,7 @@ pub mod prelude {
}, },
world::{ world::{
EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut,
FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, FromWorld, World,
}, },
}; };

View File

@ -0,0 +1,606 @@
//! This module contains various tools to allow you to react to component insertion or removal,
//! as well as entity spawning and despawning.
//!
//! There are four main ways to react to these lifecycle events:
//!
//! 1. Using component hooks, which act as inherent constructors and destructors for components.
//! 2. Using [observers], which are a user-extensible way to respond to events, including component lifecycle events.
//! 3. Using the [`RemovedComponents`] system parameter, which offers an event-style interface.
//! 4. Using the [`Added`] query filter, which checks each component to see if it has been added since the last time a system ran.
//!
//! [observers]: crate::observer
//! [`Added`]: crate::query::Added
//!
//! # Types of lifecycle events
//!
//! There are five types of lifecycle events, split into two categories. First, we have lifecycle events that are triggered
//! when a component is added to an entity:
//!
//! - [`OnAdd`]: Triggered when a component is added to an entity that did not already have it.
//! - [`OnInsert`]: Triggered when a component is added to an entity, regardless of whether it already had it.
//!
//! When both events occur, [`OnAdd`] hooks are evaluated before [`OnInsert`].
//!
//! Next, we have lifecycle events that are triggered when a component is removed from an entity:
//!
//! - [`OnReplace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value.
//! - [`OnRemove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed.
//! - [`OnDespawn`]: Triggered for each component on an entity when it is despawned.
//!
//! [`OnReplace`] hooks are evaluated before [`OnRemove`], then finally [`OnDespawn`] hooks are evaluated.
//!
//! [`OnAdd`] and [`OnRemove`] are counterparts: they are only triggered when a component is added or removed
//! from an entity in such a way as to cause a change in the component's presence on that entity.
//! Similarly, [`OnInsert`] and [`OnReplace`] are counterparts: they are triggered when a component is added or replaced
//! on an entity, regardless of whether this results in a change in the component's presence on that entity.
//!
//! To reliably synchronize data structures using with component lifecycle events,
//! you can combine [`OnInsert`] and [`OnReplace`] to fully capture any changes to the data.
//! This is particularly useful in combination with immutable components,
//! to avoid any lifecycle-bypassing mutations.
//!
//! ## Lifecycle events and component types
//!
//! Despite the absence of generics, each lifecycle event is associated with a specific component.
//! When defining a component hook for a [`Component`] type, that component is used.
//! When listening to lifecycle events for observers, the `B: Bundle` generic is used.
//!
//! Each of these lifecycle events also corresponds to a fixed [`ComponentId`],
//! which are assigned during [`World`] initialization.
//! For example, [`OnAdd`] corresponds to [`ON_ADD`].
//! This is used to skip [`TypeId`](core::any::TypeId) lookups in hot paths.
use crate::{
change_detection::MaybeLocation,
component::{Component, ComponentId, ComponentIdFor, Tick},
entity::Entity,
event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events},
relationship::RelationshipHookMode,
storage::SparseSet,
system::{Local, ReadOnlySystemParam, SystemMeta, SystemParam},
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
};
use derive_more::derive::Into;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use core::{
fmt::Debug,
iter,
marker::PhantomData,
ops::{Deref, DerefMut},
option,
};
/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`.
pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext);
/// Context provided to a [`ComponentHook`].
#[derive(Clone, Copy, Debug)]
pub struct HookContext {
/// The [`Entity`] this hook was invoked for.
pub entity: Entity,
/// The [`ComponentId`] this hook was invoked for.
pub component_id: ComponentId,
/// The caller location is `Some` if the `track_caller` feature is enabled.
pub caller: MaybeLocation,
/// Configures how relationship hooks will run
pub relationship_hook_mode: RelationshipHookMode,
}
/// [`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`](crate::component::ComponentInfo) of the associated component.
///
/// There are two ways of configuring hooks for a component:
/// 1. Defining the relevant hooks on the [`Component`] implementation
/// 2. Using the [`World::register_component_hooks`] method
///
/// # Example
///
/// ```
/// use bevy_ecs::prelude::*;
/// use bevy_platform::collections::HashSet;
///
/// #[derive(Component)]
/// struct MyTrackedComponent;
///
/// #[derive(Resource, Default)]
/// struct TrackedEntities(HashSet<Entity>);
///
/// let mut world = World::new();
/// world.init_resource::<TrackedEntities>();
///
/// // 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::<MyTrackedComponent>().on_add(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.insert(context.entity);
/// });
///
/// world.register_component_hooks::<MyTrackedComponent>().on_remove(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.remove(&context.entity);
/// });
///
/// let entity = world.spawn(MyTrackedComponent).id();
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(tracked_entities.0.contains(&entity));
///
/// world.despawn(entity);
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(!tracked_entities.0.contains(&entity));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ComponentHooks {
pub(crate) on_add: Option<ComponentHook>,
pub(crate) on_insert: Option<ComponentHook>,
pub(crate) on_replace: Option<ComponentHook>,
pub(crate) on_remove: Option<ComponentHook>,
pub(crate) on_despawn: Option<ComponentHook>,
}
impl ComponentHooks {
pub(crate) fn update_from_component<C: Component + ?Sized>(&mut self) -> &mut Self {
if let Some(hook) = C::on_add() {
self.on_add(hook);
}
if let Some(hook) = C::on_insert() {
self.on_insert(hook);
}
if let Some(hook) = C::on_replace() {
self.on_replace(hook);
}
if let Some(hook) = C::on_remove() {
self.on_remove(hook);
}
if let Some(hook) = C::on_despawn() {
self.on_despawn(hook);
}
self
}
/// Register a [`ComponentHook`] that will be run when this component is added to an entity.
/// 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)
.expect("Component already has an on_add hook")
}
/// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`)
/// 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 needs to be combined with immutable components to serve as a 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)
.expect("Component already has an on_insert hook")
}
/// Register a [`ComponentHook`] that will be run when this component is about to be dropped,
/// such as being replaced (with `.insert`) or removed.
///
/// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced,
/// allowing access to the previous data just before it is dropped.
/// This hook does *not* run if the entity did not already have this component.
///
/// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity).
///
/// # 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 needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches.
///
/// # Panics
///
/// Will panic if the component already has an `on_replace` hook
pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_replace(hook)
.expect("Component already has an on_replace hook")
}
/// 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 already has an on_remove hook")
}
/// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
///
/// # Panics
///
/// Will panic if the component already has an `on_despawn` hook
pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_despawn(hook)
.expect("Component already has an on_despawn hook")
}
/// 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() {
return None;
}
self.on_add = Some(hook);
Some(self)
}
/// 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() {
return None;
}
self.on_insert = Some(hook);
Some(self)
}
/// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed
///
/// This is a fallible version of [`Self::on_replace`].
///
/// Returns `None` if the component already has an `on_replace` hook.
pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> {
if self.on_replace.is_some() {
return None;
}
self.on_replace = Some(hook);
Some(self)
}
/// 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() {
return None;
}
self.on_remove = Some(hook);
Some(self)
}
/// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
///
/// This is a fallible version of [`Self::on_despawn`].
///
/// Returns `None` if the component already has an `on_despawn` hook.
pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> {
if self.on_despawn.is_some() {
return None;
}
self.on_despawn = Some(hook);
Some(self)
}
}
/// [`ComponentId`] for [`OnAdd`]
pub const ON_ADD: ComponentId = ComponentId::new(0);
/// [`ComponentId`] for [`OnInsert`]
pub const ON_INSERT: ComponentId = ComponentId::new(1);
/// [`ComponentId`] for [`OnReplace`]
pub const ON_REPLACE: ComponentId = ComponentId::new(2);
/// [`ComponentId`] for [`OnRemove`]
pub const ON_REMOVE: ComponentId = ComponentId::new(3);
/// [`ComponentId`] for [`OnDespawn`]
pub const ON_DESPAWN: ComponentId = ComponentId::new(4);
/// Trigger emitted when a component is inserted onto an entity that does not already have that
/// component. Runs before `OnInsert`.
/// See [`crate::lifecycle::ComponentHooks::on_add`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnAdd;
/// Trigger emitted when a component is inserted, regardless of whether or not the entity already
/// had that component. Runs after `OnAdd`, if it ran.
/// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnInsert;
/// Trigger emitted when a component is removed from an entity, regardless
/// of whether or not it is later replaced.
///
/// Runs before the value is replaced, so you can still access the original component data.
/// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnReplace;
/// Trigger emitted when a component is removed from an entity, and runs before the component is
/// removed, so you can still access the component data.
/// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnRemove;
/// Trigger emitted for each component on an entity when it is despawned.
/// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnDespawn;
/// Wrapper around [`Entity`] for [`RemovedComponents`].
/// Internally, `RemovedComponents` uses these as an `Events<RemovedComponentEntity>`.
#[derive(Event, Debug, Clone, Into)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))]
pub struct RemovedComponentEntity(Entity);
/// Wrapper around a [`EventCursor<RemovedComponentEntity>`] so that we
/// can differentiate events between components.
#[derive(Debug)]
pub struct RemovedComponentReader<T>
where
T: Component,
{
reader: EventCursor<RemovedComponentEntity>,
marker: PhantomData<T>,
}
impl<T: Component> Default for RemovedComponentReader<T> {
fn default() -> Self {
Self {
reader: Default::default(),
marker: PhantomData,
}
}
}
impl<T: Component> Deref for RemovedComponentReader<T> {
type Target = EventCursor<RemovedComponentEntity>;
fn deref(&self) -> &Self::Target {
&self.reader
}
}
impl<T: Component> DerefMut for RemovedComponentReader<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.reader
}
}
/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`].
#[derive(Default, Debug)]
pub struct RemovedComponentEvents {
event_sets: SparseSet<ComponentId, Events<RemovedComponentEntity>>,
}
impl RemovedComponentEvents {
/// Creates an empty storage buffer for component removal events.
pub fn new() -> Self {
Self::default()
}
/// For each type of component, swaps the event buffers and clears the oldest event buffer.
/// In general, this should be called once per frame/update.
pub fn update(&mut self) {
for (_component_id, events) in self.event_sets.iter_mut() {
events.update();
}
}
/// Returns an iterator over components and their entity events.
pub fn iter(&self) -> impl Iterator<Item = (&ComponentId, &Events<RemovedComponentEntity>)> {
self.event_sets.iter()
}
/// Gets the event storage for a given component.
pub fn get(
&self,
component_id: impl Into<ComponentId>,
) -> Option<&Events<RemovedComponentEntity>> {
self.event_sets.get(component_id.into())
}
/// Sends a removal event for the specified component.
pub fn send(&mut self, component_id: impl Into<ComponentId>, entity: Entity) {
self.event_sets
.get_or_insert_with(component_id.into(), Default::default)
.send(RemovedComponentEntity(entity));
}
}
/// A [`SystemParam`] that yields entities that had their `T` [`Component`]
/// removed or have been despawned with it.
///
/// This acts effectively the same as an [`EventReader`](crate::event::EventReader).
///
/// Unlike hooks or observers (see the [lifecycle](crate) module docs),
/// this does not allow you to see which data existed before removal.
///
/// If you are using `bevy_ecs` as a standalone crate,
/// note that the [`RemovedComponents`] list will not be automatically cleared for you,
/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers).
///
/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is
/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`.
/// For the main world, this is delayed until after all `SubApp`s have run.
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// # use bevy_ecs::component::Component;
/// # use bevy_ecs::system::IntoSystem;
/// # use bevy_ecs::lifecycle::RemovedComponents;
/// #
/// # #[derive(Component)]
/// # struct MyComponent;
/// fn react_on_removal(mut removed: RemovedComponents<MyComponent>) {
/// removed.read().for_each(|removed_entity| println!("{}", removed_entity));
/// }
/// # bevy_ecs::system::assert_is_system(react_on_removal);
/// ```
#[derive(SystemParam)]
pub struct RemovedComponents<'w, 's, T: Component> {
component_id: ComponentIdFor<'s, T>,
reader: Local<'s, RemovedComponentReader<T>>,
event_sets: &'w RemovedComponentEvents,
}
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIter<'a> = iter::Map<
iter::Flatten<option::IntoIter<iter::Cloned<EventIterator<'a, RemovedComponentEntity>>>>,
fn(RemovedComponentEntity) -> Entity,
>;
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIterWithId<'a> = iter::Map<
iter::Flatten<option::IntoIter<EventIteratorWithId<'a, RemovedComponentEntity>>>,
fn(
(&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>),
>;
fn map_id_events(
(entity, id): (&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>) {
(entity.clone().into(), id)
}
// For all practical purposes, the api surface of `RemovedComponents<T>`
// should be similar to `EventReader<T>` to reduce confusion.
impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> {
/// Fetch underlying [`EventCursor`].
pub fn reader(&self) -> &EventCursor<RemovedComponentEntity> {
&self.reader
}
/// Fetch underlying [`EventCursor`] mutably.
pub fn reader_mut(&mut self) -> &mut EventCursor<RemovedComponentEntity> {
&mut self.reader
}
/// Fetch underlying [`Events`].
pub fn events(&self) -> Option<&Events<RemovedComponentEntity>> {
self.event_sets.get(self.component_id.get())
}
/// Destructures to get a mutable reference to the `EventCursor`
/// and a reference to `Events`.
///
/// This is necessary since Rust can't detect destructuring through methods and most
/// usecases of the reader uses the `Events` as well.
pub fn reader_mut_with_events(
&mut self,
) -> Option<(
&mut RemovedComponentReader<T>,
&Events<RemovedComponentEntity>,
)> {
self.event_sets
.get(self.component_id.get())
.map(|events| (&mut *self.reader, events))
}
/// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the
/// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events
/// that happened before now.
pub fn read(&mut self) -> RemovedIter<'_> {
self.reader_mut_with_events()
.map(|(reader, events)| reader.read(events).cloned())
.into_iter()
.flatten()
.map(RemovedComponentEntity::into)
}
/// Like [`read`](Self::read), except also returning the [`EventId`] of the events.
pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> {
self.reader_mut_with_events()
.map(|(reader, events)| reader.read_with_id(events))
.into_iter()
.flatten()
.map(map_id_events)
}
/// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any.
pub fn len(&self) -> usize {
self.events()
.map(|events| self.reader.len(events))
.unwrap_or(0)
}
/// Returns `true` if there are no events available to read.
pub fn is_empty(&self) -> bool {
self.events()
.is_none_or(|events| self.reader.is_empty(events))
}
/// Consumes all available events.
///
/// This means these events will not appear in calls to [`RemovedComponents::read()`] or
/// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`.
pub fn clear(&mut self) {
if let Some((reader, events)) = self.reader_mut_with_events() {
reader.clear(events);
}
}
}
// SAFETY: Only reads World removed component events
unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {}
// SAFETY: no component value access.
unsafe impl<'a> SystemParam for &'a RemovedComponentEvents {
type State = ();
type Item<'w, 's> = &'w RemovedComponentEvents;
fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {}
#[inline]
unsafe fn get_param<'w, 's>(
_state: &'s mut Self::State,
_system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
_change_tick: Tick,
) -> Self::Item<'w, 's> {
world.removed_components()
}
}

View File

@ -1,8 +1,7 @@
use crate::{ use crate::{
component::{ component::{Component, ComponentCloneBehavior, Mutable, StorageType},
Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType,
},
entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent},
lifecycle::{ComponentHook, HookContext},
world::World, world::World,
}; };
use alloc::vec::Vec; use alloc::vec::Vec;

View File

@ -391,6 +391,8 @@ pub struct Observers {
impl Observers { impl Observers {
pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers {
use crate::lifecycle::*;
match event_type { match event_type {
ON_ADD => &mut self.on_add, ON_ADD => &mut self.on_add,
ON_INSERT => &mut self.on_insert, ON_INSERT => &mut self.on_insert,
@ -402,6 +404,8 @@ impl Observers {
} }
pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> {
use crate::lifecycle::*;
match event_type { match event_type {
ON_ADD => Some(&self.on_add), ON_ADD => Some(&self.on_add),
ON_INSERT => Some(&self.on_insert), ON_INSERT => Some(&self.on_insert),
@ -479,6 +483,8 @@ impl Observers {
} }
pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option<ArchetypeFlags> { pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option<ArchetypeFlags> {
use crate::lifecycle::*;
match event_type { match event_type {
ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER),
ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),

View File

@ -2,8 +2,9 @@ use alloc::{boxed::Box, vec};
use core::any::Any; use core::any::Any;
use crate::{ use crate::{
component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType}, component::{ComponentId, Mutable, StorageType},
error::{ErrorContext, ErrorHandler}, error::{ErrorContext, ErrorHandler},
lifecycle::{ComponentHook, HookContext},
observer::{ObserverDescriptor, ObserverTrigger}, observer::{ObserverDescriptor, ObserverTrigger},
prelude::*, prelude::*,
query::DebugCheckedUnwrap, query::DebugCheckedUnwrap,
@ -410,7 +411,7 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
} }
} }
/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::component::ComponentHooks::on_add`). /// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`).
/// ///
/// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters /// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters
/// erased. /// erased.

View File

@ -11,9 +11,10 @@ pub use relationship_query::*;
pub use relationship_source_collection::*; pub use relationship_source_collection::*;
use crate::{ use crate::{
component::{Component, HookContext, Mutable}, component::{Component, Mutable},
entity::{ComponentCloneCtx, Entity, SourceComponent}, entity::{ComponentCloneCtx, Entity, SourceComponent},
error::{ignore, CommandWithEntity, HandleError}, error::{ignore, CommandWithEntity, HandleError},
lifecycle::HookContext,
world::{DeferredWorld, EntityWorldMut}, world::{DeferredWorld, EntityWorldMut},
}; };
use log::warn; use log::warn;

View File

@ -1,268 +0,0 @@
//! Alerting events when a component is removed from an entity.
use crate::{
component::{Component, ComponentId, ComponentIdFor, Tick},
entity::Entity,
event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events},
prelude::Local,
storage::SparseSet,
system::{ReadOnlySystemParam, SystemMeta, SystemParam},
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
use derive_more::derive::Into;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use core::{
fmt::Debug,
iter,
marker::PhantomData,
ops::{Deref, DerefMut},
option,
};
/// Wrapper around [`Entity`] for [`RemovedComponents`].
/// Internally, `RemovedComponents` uses these as an `Events<RemovedComponentEntity>`.
#[derive(Event, Debug, Clone, Into)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))]
pub struct RemovedComponentEntity(Entity);
/// Wrapper around a [`EventCursor<RemovedComponentEntity>`] so that we
/// can differentiate events between components.
#[derive(Debug)]
pub struct RemovedComponentReader<T>
where
T: Component,
{
reader: EventCursor<RemovedComponentEntity>,
marker: PhantomData<T>,
}
impl<T: Component> Default for RemovedComponentReader<T> {
fn default() -> Self {
Self {
reader: Default::default(),
marker: PhantomData,
}
}
}
impl<T: Component> Deref for RemovedComponentReader<T> {
type Target = EventCursor<RemovedComponentEntity>;
fn deref(&self) -> &Self::Target {
&self.reader
}
}
impl<T: Component> DerefMut for RemovedComponentReader<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.reader
}
}
/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`].
#[derive(Default, Debug)]
pub struct RemovedComponentEvents {
event_sets: SparseSet<ComponentId, Events<RemovedComponentEntity>>,
}
impl RemovedComponentEvents {
/// Creates an empty storage buffer for component removal events.
pub fn new() -> Self {
Self::default()
}
/// For each type of component, swaps the event buffers and clears the oldest event buffer.
/// In general, this should be called once per frame/update.
pub fn update(&mut self) {
for (_component_id, events) in self.event_sets.iter_mut() {
events.update();
}
}
/// Returns an iterator over components and their entity events.
pub fn iter(&self) -> impl Iterator<Item = (&ComponentId, &Events<RemovedComponentEntity>)> {
self.event_sets.iter()
}
/// Gets the event storage for a given component.
pub fn get(
&self,
component_id: impl Into<ComponentId>,
) -> Option<&Events<RemovedComponentEntity>> {
self.event_sets.get(component_id.into())
}
/// Sends a removal event for the specified component.
pub fn send(&mut self, component_id: impl Into<ComponentId>, entity: Entity) {
self.event_sets
.get_or_insert_with(component_id.into(), Default::default)
.send(RemovedComponentEntity(entity));
}
}
/// A [`SystemParam`] that yields entities that had their `T` [`Component`]
/// removed or have been despawned with it.
///
/// This acts effectively the same as an [`EventReader`](crate::event::EventReader).
///
/// Note that this does not allow you to see which data existed before removal.
/// If you need this, you will need to track the component data value on your own,
/// using a regularly scheduled system that requests `Query<(Entity, &T), Changed<T>>`
/// and stores the data somewhere safe to later cross-reference.
///
/// If you are using `bevy_ecs` as a standalone crate,
/// note that the `RemovedComponents` list will not be automatically cleared for you,
/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers).
///
/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is
/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`.
/// For the main world, this is delayed until after all `SubApp`s have run.
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// # use bevy_ecs::component::Component;
/// # use bevy_ecs::system::IntoSystem;
/// # use bevy_ecs::removal_detection::RemovedComponents;
/// #
/// # #[derive(Component)]
/// # struct MyComponent;
/// fn react_on_removal(mut removed: RemovedComponents<MyComponent>) {
/// removed.read().for_each(|removed_entity| println!("{}", removed_entity));
/// }
/// # bevy_ecs::system::assert_is_system(react_on_removal);
/// ```
#[derive(SystemParam)]
pub struct RemovedComponents<'w, 's, T: Component> {
component_id: ComponentIdFor<'s, T>,
reader: Local<'s, RemovedComponentReader<T>>,
event_sets: &'w RemovedComponentEvents,
}
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIter<'a> = iter::Map<
iter::Flatten<option::IntoIter<iter::Cloned<EventIterator<'a, RemovedComponentEntity>>>>,
fn(RemovedComponentEntity) -> Entity,
>;
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIterWithId<'a> = iter::Map<
iter::Flatten<option::IntoIter<EventIteratorWithId<'a, RemovedComponentEntity>>>,
fn(
(&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>),
>;
fn map_id_events(
(entity, id): (&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>) {
(entity.clone().into(), id)
}
// For all practical purposes, the api surface of `RemovedComponents<T>`
// should be similar to `EventReader<T>` to reduce confusion.
impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> {
/// Fetch underlying [`EventCursor`].
pub fn reader(&self) -> &EventCursor<RemovedComponentEntity> {
&self.reader
}
/// Fetch underlying [`EventCursor`] mutably.
pub fn reader_mut(&mut self) -> &mut EventCursor<RemovedComponentEntity> {
&mut self.reader
}
/// Fetch underlying [`Events`].
pub fn events(&self) -> Option<&Events<RemovedComponentEntity>> {
self.event_sets.get(self.component_id.get())
}
/// Destructures to get a mutable reference to the `EventCursor`
/// and a reference to `Events`.
///
/// This is necessary since Rust can't detect destructuring through methods and most
/// usecases of the reader uses the `Events` as well.
pub fn reader_mut_with_events(
&mut self,
) -> Option<(
&mut RemovedComponentReader<T>,
&Events<RemovedComponentEntity>,
)> {
self.event_sets
.get(self.component_id.get())
.map(|events| (&mut *self.reader, events))
}
/// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the
/// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events
/// that happened before now.
pub fn read(&mut self) -> RemovedIter<'_> {
self.reader_mut_with_events()
.map(|(reader, events)| reader.read(events).cloned())
.into_iter()
.flatten()
.map(RemovedComponentEntity::into)
}
/// Like [`read`](Self::read), except also returning the [`EventId`] of the events.
pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> {
self.reader_mut_with_events()
.map(|(reader, events)| reader.read_with_id(events))
.into_iter()
.flatten()
.map(map_id_events)
}
/// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any.
pub fn len(&self) -> usize {
self.events()
.map(|events| self.reader.len(events))
.unwrap_or(0)
}
/// Returns `true` if there are no events available to read.
pub fn is_empty(&self) -> bool {
self.events()
.is_none_or(|events| self.reader.is_empty(events))
}
/// Consumes all available events.
///
/// This means these events will not appear in calls to [`RemovedComponents::read()`] or
/// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`.
pub fn clear(&mut self) {
if let Some((reader, events)) = self.reader_mut_with_events() {
reader.clear(events);
}
}
}
// SAFETY: Only reads World removed component events
unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {}
// SAFETY: no component value access.
unsafe impl<'a> SystemParam for &'a RemovedComponentEvents {
type State = ();
type Item<'w, 's> = &'w RemovedComponentEvents;
fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {}
#[inline]
unsafe fn get_param<'w, 's>(
_state: &'s mut Self::State,
_system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
_change_tick: Tick,
) -> Self::Item<'w, 's> {
world.removed_components()
}
}

View File

@ -467,9 +467,9 @@ pub mod common_conditions {
use crate::{ use crate::{
change_detection::DetectChanges, change_detection::DetectChanges,
event::{Event, EventReader}, event::{Event, EventReader},
lifecycle::RemovedComponents,
prelude::{Component, Query, With}, prelude::{Component, Query, With},
query::QueryFilter, query::QueryFilter,
removal_detection::RemovedComponents,
resource::Resource, resource::Resource,
system::{In, IntoSystem, Local, Res, System, SystemInput}, system::{In, IntoSystem, Local, Res, System, SystemInput},
}; };

View File

@ -97,7 +97,7 @@
//! - [`EventWriter`](crate::event::EventWriter) //! - [`EventWriter`](crate::event::EventWriter)
//! - [`NonSend`] and `Option<NonSend>` //! - [`NonSend`] and `Option<NonSend>`
//! - [`NonSendMut`] and `Option<NonSendMut>` //! - [`NonSendMut`] and `Option<NonSendMut>`
//! - [`RemovedComponents`](crate::removal_detection::RemovedComponents) //! - [`RemovedComponents`](crate::lifecycle::RemovedComponents)
//! - [`SystemName`] //! - [`SystemName`]
//! - [`SystemChangeTick`] //! - [`SystemChangeTick`]
//! - [`Archetypes`](crate::archetype::Archetypes) (Provides Archetype metadata) //! - [`Archetypes`](crate::archetype::Archetypes) (Provides Archetype metadata)
@ -408,10 +408,10 @@ mod tests {
component::{Component, Components}, component::{Component, Components},
entity::{Entities, Entity}, entity::{Entities, Entity},
error::Result, error::Result,
lifecycle::RemovedComponents,
name::Name, name::Name,
prelude::{AnyOf, EntityRef, Trigger}, prelude::{AnyOf, EntityRef, OnAdd, Trigger},
query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without},
removal_detection::RemovedComponents,
resource::Resource, resource::Resource,
schedule::{ schedule::{
common_conditions::resource_exists, ApplyDeferred, IntoScheduleConfigs, Schedule, common_conditions::resource_exists, ApplyDeferred, IntoScheduleConfigs, Schedule,
@ -421,7 +421,7 @@ mod tests {
Commands, In, InMut, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, Commands, In, InMut, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res,
ResMut, Single, StaticSystemParam, System, SystemState, ResMut, Single, StaticSystemParam, System, SystemState,
}, },
world::{DeferredWorld, EntityMut, FromWorld, OnAdd, World}, world::{DeferredWorld, EntityMut, FromWorld, World},
}; };
use super::ScheduleSystem; use super::ScheduleSystem;

View File

@ -1,55 +0,0 @@
//! Internal components used by bevy with a fixed component id.
//! Constants are used to skip [`TypeId`] lookups in hot paths.
use super::*;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
/// [`ComponentId`] for [`OnAdd`]
pub const ON_ADD: ComponentId = ComponentId::new(0);
/// [`ComponentId`] for [`OnInsert`]
pub const ON_INSERT: ComponentId = ComponentId::new(1);
/// [`ComponentId`] for [`OnReplace`]
pub const ON_REPLACE: ComponentId = ComponentId::new(2);
/// [`ComponentId`] for [`OnRemove`]
pub const ON_REMOVE: ComponentId = ComponentId::new(3);
/// [`ComponentId`] for [`OnDespawn`]
pub const ON_DESPAWN: ComponentId = ComponentId::new(4);
/// Trigger emitted when a component is inserted onto an entity that does not already have that
/// component. Runs before `OnInsert`.
/// See [`crate::component::ComponentHooks::on_add`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnAdd;
/// Trigger emitted when a component is inserted, regardless of whether or not the entity already
/// had that component. Runs after `OnAdd`, if it ran.
/// See [`crate::component::ComponentHooks::on_insert`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnInsert;
/// Trigger emitted when a component is inserted onto an entity that already has that component.
/// Runs before the value is replaced, so you can still access the original component data.
/// See [`crate::component::ComponentHooks::on_replace`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnReplace;
/// Trigger emitted when a component is removed from an entity, and runs before the component is
/// removed, so you can still access the component data.
/// See [`crate::component::ComponentHooks::on_remove`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnRemove;
/// Trigger emitted for each component on an entity when it is despawned.
/// See [`crate::component::ComponentHooks::on_despawn`] for more information.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnDespawn;

View File

@ -3,9 +3,10 @@ use core::ops::Deref;
use crate::{ use crate::{
archetype::Archetype, archetype::Archetype,
change_detection::{MaybeLocation, MutUntyped}, change_detection::{MaybeLocation, MutUntyped},
component::{ComponentId, HookContext, Mutable}, component::{ComponentId, Mutable},
entity::Entity, entity::Entity,
event::{Event, EventId, Events, SendBatchIds}, event::{Event, EventId, Events, SendBatchIds},
lifecycle::{HookContext, ON_INSERT, ON_REPLACE},
observer::{Observers, TriggerTargets}, observer::{Observers, TriggerTargets},
prelude::{Component, QueryState}, prelude::{Component, QueryState},
query::{QueryData, QueryFilter}, query::{QueryData, QueryFilter},
@ -16,7 +17,7 @@ use crate::{
world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch},
}; };
use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE}; use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World};
/// A [`World`] reference that disallows structural ECS changes. /// A [`World`] reference that disallows structural ECS changes.
/// This includes initializing resources, registering components or spawning entities. /// This includes initializing resources, registering components or spawning entities.

View File

@ -14,15 +14,13 @@ use crate::{
EntityIdLocation, EntityLocation, EntityIdLocation, EntityLocation,
}, },
event::Event, event::Event,
lifecycle::{ON_DESPAWN, ON_REMOVE, ON_REPLACE},
observer::Observer, observer::Observer,
query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData},
relationship::RelationshipHookMode, relationship::RelationshipHookMode,
resource::Resource, resource::Resource,
system::IntoObserverSystem, system::IntoObserverSystem,
world::{ world::{error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World},
error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World,
ON_DESPAWN, ON_REMOVE, ON_REPLACE,
},
}; };
use alloc::vec::Vec; use alloc::vec::Vec;
use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::collections::{HashMap, HashSet};
@ -4756,7 +4754,8 @@ mod tests {
use core::panic::AssertUnwindSafe; use core::panic::AssertUnwindSafe;
use std::sync::OnceLock; use std::sync::OnceLock;
use crate::component::{HookContext, Tick}; use crate::component::Tick;
use crate::lifecycle::HookContext;
use crate::{ use crate::{
change_detection::{MaybeLocation, MutUntyped}, change_detection::{MaybeLocation, MutUntyped},
component::ComponentId, component::ComponentId,

View File

@ -1,7 +1,6 @@
//! Defines the [`World`] and APIs for accessing it directly. //! Defines the [`World`] and APIs for accessing it directly.
pub(crate) mod command_queue; pub(crate) mod command_queue;
mod component_constants;
mod deferred_world; mod deferred_world;
mod entity_fetch; mod entity_fetch;
mod entity_ref; mod entity_ref;
@ -14,13 +13,16 @@ pub mod unsafe_world_cell;
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
pub mod reflect; pub mod reflect;
use crate::error::{DefaultErrorHandler, ErrorHandler};
pub use crate::{ pub use crate::{
change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD},
world::command_queue::CommandQueue, world::command_queue::CommandQueue,
}; };
use crate::{
error::{DefaultErrorHandler, ErrorHandler},
lifecycle::{ComponentHooks, ON_ADD, ON_DESPAWN, ON_INSERT, ON_REMOVE, ON_REPLACE},
prelude::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace},
};
pub use bevy_ecs_macros::FromWorld; pub use bevy_ecs_macros::FromWorld;
pub use component_constants::*;
pub use deferred_world::DeferredWorld; pub use deferred_world::DeferredWorld;
pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_fetch::{EntityFetcher, WorldEntityFetch};
pub use entity_ref::{ pub use entity_ref::{
@ -39,17 +41,17 @@ use crate::{
}, },
change_detection::{MaybeLocation, MutUntyped, TicksMut}, change_detection::{MaybeLocation, MutUntyped, TicksMut},
component::{ component::{
CheckChangeTicks, Component, ComponentDescriptor, ComponentHooks, ComponentId, CheckChangeTicks, Component, ComponentDescriptor, ComponentId, ComponentIds, ComponentInfo,
ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable,
ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, RequiredComponents, RequiredComponentsError, Tick,
}, },
entity::{Entities, Entity, EntityDoesNotExistError}, entity::{Entities, Entity, EntityDoesNotExistError},
entity_disabling::DefaultQueryFilters, entity_disabling::DefaultQueryFilters,
event::{Event, EventId, Events, SendBatchIds}, event::{Event, EventId, Events, SendBatchIds},
lifecycle::RemovedComponentEvents,
observer::Observers, observer::Observers,
query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState},
relationship::RelationshipHookMode, relationship::RelationshipHookMode,
removal_detection::RemovedComponentEvents,
resource::Resource, resource::Resource,
schedule::{Schedule, ScheduleLabel, Schedules}, schedule::{Schedule, ScheduleLabel, Schedules},
storage::{ResourceData, Storages}, storage::{ResourceData, Storages},
@ -1444,7 +1446,7 @@ impl World {
/// assert!(!transform.is_changed()); /// assert!(!transform.is_changed());
/// ``` /// ```
/// ///
/// [`RemovedComponents`]: crate::removal_detection::RemovedComponents /// [`RemovedComponents`]: crate::lifecycle::RemovedComponents
pub fn clear_trackers(&mut self) { pub fn clear_trackers(&mut self) {
self.removed_components.update(); self.removed_components.update();
self.last_change_tick = self.increment_change_tick(); self.last_change_tick = self.increment_change_tick();

View File

@ -8,10 +8,10 @@ use crate::{
component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells}, component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells},
entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation}, entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation},
error::{DefaultErrorHandler, ErrorHandler}, error::{DefaultErrorHandler, ErrorHandler},
lifecycle::RemovedComponentEvents,
observer::Observers, observer::Observers,
prelude::Component, prelude::Component,
query::{DebugCheckedUnwrap, ReadOnlyQueryData}, query::{DebugCheckedUnwrap, ReadOnlyQueryData},
removal_detection::RemovedComponentEvents,
resource::Resource, resource::Resource,
storage::{ComponentSparseSet, Storages, Table}, storage::{ComponentSparseSet, Storages, Table},
world::RawCommandQueue, world::RawCommandQueue,

View File

@ -1,6 +1,6 @@
//! Contains the [`AutoFocus`] component and related machinery. //! Contains the [`AutoFocus`] component and related machinery.
use bevy_ecs::{component::HookContext, prelude::*, world::DeferredWorld}; use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld};
use crate::InputFocus; use crate::InputFocus;

View File

@ -369,7 +369,7 @@ mod tests {
use alloc::string::String; use alloc::string::String;
use bevy_ecs::{ use bevy_ecs::{
component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, lifecycle::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld,
}; };
use bevy_input::{ use bevy_input::{
keyboard::{Key, KeyCode}, keyboard::{Key, KeyCode},

View File

@ -37,9 +37,9 @@ use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
component::Component, component::Component,
entity::Entity, entity::Entity,
lifecycle::RemovedComponents,
query::{Changed, Or}, query::{Changed, Or},
reflect::ReflectComponent, reflect::ReflectComponent,
removal_detection::RemovedComponents,
resource::Resource, resource::Resource,
schedule::IntoScheduleConfigs, schedule::IntoScheduleConfigs,
system::{Query, Res, ResMut}, system::{Query, Res, ResMut},

View File

@ -8,9 +8,9 @@ use bevy_ecs::{
entity::Entity, entity::Entity,
event::EventCursor, event::EventCursor,
hierarchy::ChildOf, hierarchy::ChildOf,
lifecycle::RemovedComponentEntity,
query::QueryBuilder, query::QueryBuilder,
reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource},
removal_detection::RemovedComponentEntity,
system::{In, Local}, system::{In, Local},
world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, world::{EntityRef, EntityWorldMut, FilteredEntityRef, World},
}; };

View File

@ -22,9 +22,10 @@ use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
change_detection::DetectChanges, change_detection::DetectChanges,
component::{Component, HookContext}, component::Component,
entity::{ContainsEntity, Entity}, entity::{ContainsEntity, Entity},
event::EventReader, event::EventReader,
lifecycle::HookContext,
prelude::With, prelude::With,
query::Has, query::Has,
reflect::ReflectComponent, reflect::ReflectComponent,

View File

@ -1,6 +1,7 @@
use bevy_app::Plugin; use bevy_app::Plugin;
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::entity::EntityHash; use bevy_ecs::entity::EntityHash;
use bevy_ecs::lifecycle::{OnAdd, OnRemove};
use bevy_ecs::{ use bevy_ecs::{
component::Component, component::Component,
entity::{ContainsEntity, Entity, EntityEquivalent}, entity::{ContainsEntity, Entity, EntityEquivalent},
@ -9,7 +10,7 @@ use bevy_ecs::{
reflect::ReflectComponent, reflect::ReflectComponent,
resource::Resource, resource::Resource,
system::{Local, Query, ResMut, SystemState}, system::{Local, Query, ResMut, SystemState},
world::{Mut, OnAdd, OnRemove, World}, world::{Mut, World},
}; };
use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::collections::{HashMap, HashSet};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
@ -490,10 +491,11 @@ mod tests {
use bevy_ecs::{ use bevy_ecs::{
component::Component, component::Component,
entity::Entity, entity::Entity,
lifecycle::{OnAdd, OnRemove},
observer::Trigger, observer::Trigger,
query::With, query::With,
system::{Query, ResMut}, system::{Query, ResMut},
world::{OnAdd, OnRemove, World}, world::World,
}; };
use super::{ use super::{

View File

@ -3,8 +3,8 @@ mod render_layers;
use core::any::TypeId; use core::any::TypeId;
use bevy_ecs::component::HookContext;
use bevy_ecs::entity::EntityHashSet; use bevy_ecs::entity::EntityHashSet;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::world::DeferredWorld; use bevy_ecs::world::DeferredWorld;
use derive_more::derive::{Deref, DerefMut}; use derive_more::derive::{Deref, DerefMut};
pub use range::*; pub use range::*;

View File

@ -10,9 +10,9 @@ use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{ use bevy_ecs::{
component::Component, component::Component,
entity::{Entity, EntityHashMap}, entity::{Entity, EntityHashMap},
lifecycle::RemovedComponents,
query::{Changed, With}, query::{Changed, With},
reflect::ReflectComponent, reflect::ReflectComponent,
removal_detection::RemovedComponents,
resource::Resource, resource::Resource,
schedule::IntoScheduleConfigs as _, schedule::IntoScheduleConfigs as _,
system::{Local, Query, Res, ResMut}, system::{Local, Query, Res, ResMut},

View File

@ -8,8 +8,8 @@ use bevy_ecs::{
change_detection::{DetectChanges, DetectChangesMut}, change_detection::{DetectChanges, DetectChangesMut},
entity::Entity, entity::Entity,
hierarchy::{ChildOf, Children}, hierarchy::{ChildOf, Children},
lifecycle::RemovedComponents,
query::With, query::With,
removal_detection::RemovedComponents,
system::{Commands, Query, ResMut}, system::{Commands, Query, ResMut},
world::Ref, world::Ref,
}; };

View File

@ -22,11 +22,12 @@ use bevy_ecs::{
change_detection::DetectChanges, change_detection::DetectChanges,
component::Component, component::Component,
entity::Entity, entity::Entity,
lifecycle::OnRemove,
observer::Trigger, observer::Trigger,
query::With, query::With,
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Commands, Local, Query}, system::{Commands, Local, Query},
world::{OnRemove, Ref}, world::Ref,
}; };
#[cfg(feature = "custom_cursor")] #[cfg(feature = "custom_cursor")]
use bevy_image::{Image, TextureAtlasLayout}; use bevy_image::{Image, TextureAtlasLayout};

View File

@ -3,9 +3,9 @@ use std::collections::HashMap;
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, entity::Entity,
event::EventWriter, event::EventWriter,
lifecycle::RemovedComponents,
prelude::{Changed, Component}, prelude::{Changed, Component},
query::QueryFilter, query::QueryFilter,
removal_detection::RemovedComponents,
system::{Local, NonSendMarker, Query, SystemParamItem}, system::{Local, NonSendMarker, Query, SystemParamItem},
}; };
use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput};

View File

@ -14,7 +14,8 @@
//! between components (like hierarchies or parent-child links) and need to maintain correctness. //! between components (like hierarchies or parent-child links) and need to maintain correctness.
use bevy::{ use bevy::{
ecs::component::{ComponentHook, HookContext, Mutable, StorageType}, ecs::component::{Mutable, StorageType},
ecs::lifecycle::{ComponentHook, HookContext},
prelude::*, prelude::*,
}; };
use std::collections::HashMap; use std::collections::HashMap;

View File

@ -2,9 +2,8 @@
use bevy::{ use bevy::{
ecs::{ ecs::{
component::{ component::{ComponentCloneBehavior, ComponentDescriptor, ComponentId, StorageType},
ComponentCloneBehavior, ComponentDescriptor, ComponentId, HookContext, StorageType, lifecycle::HookContext,
},
world::DeferredWorld, world::DeferredWorld,
}, },
platform::collections::HashMap, platform::collections::HashMap,

View File

@ -0,0 +1,14 @@
---
title: Component lifecycle reorganization
pull_requests: [19543]
---
To improve documentation, discoverability and internal organization, we've gathered all of the component lifecycle-related code we could and moved it into a dedicated `lifecycle` module.
The lifecycle / observer types (`OnAdd`, `OnInsert`, `OnRemove`, `OnReplace`, `OnDespawn`) have been moved from the `bevy_ecs::world` to `bevy_ecs::lifecycle`.
The same move has been done for the more internal (but public) `ComponentId` constants: `ON_ADD`, `ON_INSERT`, `ON_REMOVE`, `ON_REPLACE`, `ON_DESPAWN`.
The code for hooks (`HookContext`, `ComponentHook`, `ComponentHooks`) has been extracted from the very long `bevy_ecs::components` module, and now lives in the `bevy_ecs::lifecycle` module.
The `RemovedComponents` `SystemParam`, along with the public `RemovedIter`, `RemovedIterWithId` and `RemovedComponentEvents` have also been moved into this module as they serve a similar role. All references to `bevy_ecs::removal_detection` can be replaced with `bevy_ecs::lifecycle`.