Component Lifecycle Hook & Observer Trigger for replaced values (#14212)

# Objective

Fixes #14202

## Solution

Add `on_replaced` component hook and `OnReplaced` observer trigger

## Testing

- Did you test these changes? If so, how?
  - Updated & added unit tests

---

## Changelog

- Added new `on_replaced` component hook and `OnReplaced` observer
trigger for performing cleanup on component values when they are
overwritten with `.insert()`
This commit is contained in:
Pixelstorm 2024-07-15 16:24:15 +01:00 committed by GitHub
parent e79f91fc45
commit 0f7c548a4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 238 additions and 44 deletions

View file

@ -58,6 +58,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
let on_add = hook_register_function_call(quote! {on_add}, attrs.on_add);
let on_insert = hook_register_function_call(quote! {on_insert}, attrs.on_insert);
let on_replace = hook_register_function_call(quote! {on_replace}, attrs.on_replace);
let on_remove = hook_register_function_call(quote! {on_remove}, attrs.on_remove);
ast.generics
@ -76,6 +77,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
fn register_component_hooks(hooks: &mut #bevy_ecs_path::component::ComponentHooks) {
#on_add
#on_insert
#on_replace
#on_remove
}
}
@ -86,12 +88,14 @@ pub const COMPONENT: &str = "component";
pub const STORAGE: &str = "storage";
pub const ON_ADD: &str = "on_add";
pub const ON_INSERT: &str = "on_insert";
pub const ON_REPLACE: &str = "on_replace";
pub const ON_REMOVE: &str = "on_remove";
struct Attrs {
storage: StorageTy,
on_add: Option<ExprPath>,
on_insert: Option<ExprPath>,
on_replace: Option<ExprPath>,
on_remove: Option<ExprPath>,
}
@ -110,6 +114,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
storage: StorageTy::Table,
on_add: None,
on_insert: None,
on_replace: None,
on_remove: None,
};
@ -132,6 +137,9 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
} else if nested.path.is_ident(ON_INSERT) {
attrs.on_insert = Some(nested.value()?.parse::<ExprPath>()?);
Ok(())
} else if nested.path.is_ident(ON_REPLACE) {
attrs.on_replace = Some(nested.value()?.parse::<ExprPath>()?);
Ok(())
} else if nested.path.is_ident(ON_REMOVE) {
attrs.on_remove = Some(nested.value()?.parse::<ExprPath>()?);
Ok(())

View file

@ -121,6 +121,7 @@ pub(crate) struct AddBundle {
/// indicate if the component is newly added to the target archetype or if it already existed
pub bundle_status: Vec<ComponentStatus>,
pub added: Vec<ComponentId>,
pub mutated: Vec<ComponentId>,
}
/// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components
@ -205,6 +206,7 @@ impl Edges {
archetype_id: ArchetypeId,
bundle_status: Vec<ComponentStatus>,
added: Vec<ComponentId>,
mutated: Vec<ComponentId>,
) {
self.add_bundle.insert(
bundle_id,
@ -212,6 +214,7 @@ impl Edges {
archetype_id,
bundle_status,
added,
mutated,
},
);
}
@ -317,10 +320,12 @@ bitflags::bitflags! {
pub(crate) struct ArchetypeFlags: u32 {
const ON_ADD_HOOK = (1 << 0);
const ON_INSERT_HOOK = (1 << 1);
const ON_REMOVE_HOOK = (1 << 2);
const ON_ADD_OBSERVER = (1 << 3);
const ON_INSERT_OBSERVER = (1 << 4);
const ON_REMOVE_OBSERVER = (1 << 5);
const ON_REPLACE_HOOK = (1 << 2);
const ON_REMOVE_HOOK = (1 << 3);
const ON_ADD_OBSERVER = (1 << 4);
const ON_INSERT_OBSERVER = (1 << 5);
const ON_REPLACE_OBSERVER = (1 << 6);
const ON_REMOVE_OBSERVER = (1 << 7);
}
}
@ -600,6 +605,12 @@ impl Archetype {
self.flags().contains(ArchetypeFlags::ON_INSERT_HOOK)
}
/// Returns true if any of the components in this archetype have `on_replace` hooks
#[inline]
pub fn has_replace_hook(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REPLACE_HOOK)
}
/// Returns true if any of the components in this archetype have `on_remove` hooks
#[inline]
pub fn has_remove_hook(&self) -> bool {
@ -622,6 +633,14 @@ impl Archetype {
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
}
/// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer
///
/// [`OnReplace`]: crate::world::OnReplace
#[inline]
pub fn has_replace_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER)
}
/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
///
/// [`OnRemove`]: crate::world::OnRemove

View file

@ -17,7 +17,7 @@ use crate::{
prelude::World,
query::DebugCheckedUnwrap,
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT},
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_REPLACE},
};
use bevy_ptr::{ConstNonNull, OwningPtr};
@ -456,11 +456,13 @@ impl BundleInfo {
let mut new_sparse_set_components = Vec::new();
let mut bundle_status = Vec::with_capacity(self.component_ids.len());
let mut added = Vec::new();
let mut mutated = Vec::new();
let current_archetype = &mut archetypes[archetype_id];
for component_id in self.component_ids.iter().cloned() {
if current_archetype.contains(component_id) {
bundle_status.push(ComponentStatus::Mutated);
mutated.push(component_id);
} else {
bundle_status.push(ComponentStatus::Added);
added.push(component_id);
@ -476,7 +478,7 @@ impl BundleInfo {
if new_table_components.is_empty() && new_sparse_set_components.is_empty() {
let edges = current_archetype.edges_mut();
// the archetype does not change when we add this bundle
edges.insert_add_bundle(self.id, archetype_id, bundle_status, added);
edges.insert_add_bundle(self.id, archetype_id, bundle_status, added, mutated);
archetype_id
} else {
let table_id;
@ -526,6 +528,7 @@ impl BundleInfo {
new_archetype_id,
bundle_status,
added,
mutated,
);
new_archetype_id
}
@ -665,6 +668,30 @@ impl<'w> BundleInserter<'w> {
let bundle_info = self.bundle_info.as_ref();
let add_bundle = self.add_bundle.as_ref();
let table = self.table.as_mut();
let archetype = self.archetype.as_ref();
// SAFETY: All components in the bundle are guaranteed to exist in the World
// as they must be initialized before creating the BundleInfo.
unsafe {
// SAFETY: Mutable references do not alias and will be dropped after this block
let mut deferred_world = self.world.into_deferred();
deferred_world.trigger_on_replace(
archetype,
entity,
add_bundle.mutated.iter().copied(),
);
if archetype.has_replace_observer() {
deferred_world.trigger_observers(
ON_REPLACE,
entity,
add_bundle.mutated.iter().copied(),
);
}
}
// SAFETY: Archetype gets borrowed when running the on_replace observers above,
// so this reference can only be promoted from shared to &mut down here, after they have been ran
let archetype = self.archetype.as_mut();
let (new_archetype, new_location) = match &mut self.result {
@ -1132,7 +1159,7 @@ mod tests {
struct A;
#[derive(Component)]
#[component(on_add = a_on_add, on_insert = a_on_insert, on_remove = a_on_remove)]
#[component(on_add = a_on_add, on_insert = a_on_insert, on_replace = a_on_replace, on_remove = a_on_remove)]
struct AMacroHooks;
fn a_on_add(mut world: DeferredWorld, _: Entity, _: ComponentId) {
@ -1143,10 +1170,14 @@ mod tests {
world.resource_mut::<R>().assert_order(1);
}
fn a_on_remove<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
fn a_on_replace<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
world.resource_mut::<R>().assert_order(2);
}
fn a_on_remove<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
world.resource_mut::<R>().assert_order(3);
}
#[derive(Component)]
struct B;
@ -1173,15 +1204,14 @@ mod tests {
world.init_resource::<R>();
world
.register_component_hooks::<A>()
.on_add(|mut world, _, _| {
world.resource_mut::<R>().assert_order(0);
})
.on_add(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _| world.resource_mut::<R>().assert_order(1))
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(2));
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(2))
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(3));
let entity = world.spawn(A).id();
world.despawn(entity);
assert_eq!(3, world.resource::<R>().0);
assert_eq!(4, world.resource::<R>().0);
}
#[test]
@ -1192,7 +1222,7 @@ mod tests {
let entity = world.spawn(AMacroHooks).id();
world.despawn(entity);
assert_eq!(3, world.resource::<R>().0);
assert_eq!(4, world.resource::<R>().0);
}
#[test]
@ -1201,21 +1231,36 @@ mod tests {
world.init_resource::<R>();
world
.register_component_hooks::<A>()
.on_add(|mut world, _, _| {
world.resource_mut::<R>().assert_order(0);
})
.on_insert(|mut world, _, _| {
world.resource_mut::<R>().assert_order(1);
})
.on_remove(|mut world, _, _| {
world.resource_mut::<R>().assert_order(2);
});
.on_add(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _| world.resource_mut::<R>().assert_order(1))
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(2))
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(3));
let mut entity = world.spawn_empty();
entity.insert(A);
entity.remove::<A>();
entity.flush();
assert_eq!(3, world.resource::<R>().0);
assert_eq!(4, world.resource::<R>().0);
}
#[test]
fn component_hook_order_replace() {
let mut world = World::new();
world
.register_component_hooks::<A>()
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
.on_insert(|mut world, _, _| {
if let Some(mut r) = world.get_resource_mut::<R>() {
r.assert_order(1);
}
});
let entity = world.spawn(A).id();
world.init_resource::<R>();
let mut entity = world.entity_mut(entity);
entity.insert(A);
entity.flush();
assert_eq!(2, world.resource::<R>().0);
}
#[test]

View file

@ -100,6 +100,7 @@ use std::{
/// Alternatively to the example shown in [`ComponentHooks`]' documentation, hooks can be configured using following attributes:
/// - `#[component(on_add = on_add_function)]`
/// - `#[component(on_insert = on_insert_function)]`
/// - `#[component(on_replace = on_replace_function)]`
/// - `#[component(on_remove = on_remove_function)]`
///
/// ```
@ -114,8 +115,8 @@ use std::{
/// // Another possible way of configuring hooks:
/// // #[component(on_add = my_on_add_hook, on_insert = my_on_insert_hook)]
/// //
/// // We don't have a remove hook, so we can leave it out:
/// // #[component(on_remove = my_on_remove_hook)]
/// // We don't have a replace or remove hook, so we can leave them out:
/// // #[component(on_replace = my_on_replace_hook, on_remove = my_on_remove_hook)]
/// struct ComponentA;
///
/// fn my_on_add_hook(world: DeferredWorld, entity: Entity, id: ComponentId) {
@ -280,6 +281,7 @@ pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, Entity, ComponentId);
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>,
}
@ -314,6 +316,28 @@ impl ComponentHooks {
.expect("Component id: {:?}, 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 id: {:?}, 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.
///
@ -351,6 +375,19 @@ impl ComponentHooks {
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`].
@ -442,6 +479,9 @@ impl ComponentInfo {
if self.hooks().on_insert.is_some() {
flags.insert(ArchetypeFlags::ON_INSERT_HOOK);
}
if self.hooks().on_replace.is_some() {
flags.insert(ArchetypeFlags::ON_REPLACE_HOOK);
}
if self.hooks().on_remove.is_some() {
flags.insert(ArchetypeFlags::ON_REMOVE_HOOK);
}

View file

@ -61,7 +61,8 @@ pub mod prelude {
SystemParamFunction,
},
world::{
EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, World,
EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, OnReplace,
World,
},
};
}

View file

@ -169,6 +169,7 @@ pub struct Observers {
// Cached ECS observers to save a lookup most common triggers.
on_add: CachedObservers,
on_insert: CachedObservers,
on_replace: CachedObservers,
on_remove: CachedObservers,
// Map from trigger type to set of observers
cache: HashMap<ComponentId, CachedObservers>,
@ -179,6 +180,7 @@ impl Observers {
match event_type {
ON_ADD => &mut self.on_add,
ON_INSERT => &mut self.on_insert,
ON_REPLACE => &mut self.on_replace,
ON_REMOVE => &mut self.on_remove,
_ => self.cache.entry(event_type).or_default(),
}
@ -188,6 +190,7 @@ impl Observers {
match event_type {
ON_ADD => Some(&self.on_add),
ON_INSERT => Some(&self.on_insert),
ON_REPLACE => Some(&self.on_replace),
ON_REMOVE => Some(&self.on_remove),
_ => self.cache.get(&event_type),
}
@ -258,6 +261,7 @@ impl Observers {
match event_type {
ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER),
ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),
ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER),
ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER),
_ => None,
}
@ -271,6 +275,7 @@ impl Observers {
if self.on_add.component_observers.contains_key(&component_id) {
flags.insert(ArchetypeFlags::ON_ADD_OBSERVER);
}
if self
.on_insert
.component_observers
@ -278,6 +283,15 @@ impl Observers {
{
flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER);
}
if self
.on_replace
.component_observers
.contains_key(&component_id)
{
flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER);
}
if self
.on_remove
.component_observers
@ -418,7 +432,9 @@ mod tests {
use bevy_ptr::OwningPtr;
use crate as bevy_ecs;
use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState};
use crate::observer::{
EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace,
};
use crate::prelude::*;
use crate::traversal::Traversal;
@ -474,11 +490,12 @@ mod tests {
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.assert_order(0));
world.observe(|_: Trigger<OnInsert, A>, mut res: ResMut<R>| res.assert_order(1));
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(2));
world.observe(|_: Trigger<OnReplace, A>, mut res: ResMut<R>| res.assert_order(2));
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(3));
let entity = world.spawn(A).id();
world.despawn(entity);
assert_eq!(3, world.resource::<R>().0);
assert_eq!(4, world.resource::<R>().0);
}
#[test]
@ -488,13 +505,14 @@ mod tests {
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.assert_order(0));
world.observe(|_: Trigger<OnInsert, A>, mut res: ResMut<R>| res.assert_order(1));
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(2));
world.observe(|_: Trigger<OnReplace, A>, mut res: ResMut<R>| res.assert_order(2));
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(3));
let mut entity = world.spawn_empty();
entity.insert(A);
entity.remove::<A>();
entity.flush();
assert_eq!(3, world.resource::<R>().0);
assert_eq!(4, world.resource::<R>().0);
}
#[test]
@ -504,13 +522,34 @@ mod tests {
world.observe(|_: Trigger<OnAdd, S>, mut res: ResMut<R>| res.assert_order(0));
world.observe(|_: Trigger<OnInsert, S>, mut res: ResMut<R>| res.assert_order(1));
world.observe(|_: Trigger<OnRemove, S>, mut res: ResMut<R>| res.assert_order(2));
world.observe(|_: Trigger<OnReplace, S>, mut res: ResMut<R>| res.assert_order(2));
world.observe(|_: Trigger<OnRemove, S>, mut res: ResMut<R>| res.assert_order(3));
let mut entity = world.spawn_empty();
entity.insert(S);
entity.remove::<S>();
entity.flush();
assert_eq!(3, world.resource::<R>().0);
assert_eq!(4, world.resource::<R>().0);
}
#[test]
fn observer_order_replace() {
let mut world = World::new();
world.init_resource::<R>();
let entity = world.spawn(A).id();
world.observe(|_: Trigger<OnReplace, A>, mut res: ResMut<R>| res.assert_order(0));
world.observe(|_: Trigger<OnInsert, A>, mut res: ResMut<R>| res.assert_order(1));
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
// and therefore does not automatically flush.
world.flush();
let mut entity = world.entity_mut(entity);
entity.insert(A);
entity.flush();
assert_eq!(2, world.resource::<R>().0);
}
#[test]

View file

@ -7,8 +7,10 @@ use crate::{self as bevy_ecs};
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(2);
pub const ON_REMOVE: ComponentId = ComponentId::new(3);
/// Trigger emitted when a component is added to an entity.
#[derive(Event)]
@ -18,6 +20,10 @@ pub struct OnAdd;
#[derive(Event)]
pub struct OnInsert;
/// Trigger emitted when a component is replaced on an entity.
#[derive(Event)]
pub struct OnReplace;
/// Trigger emitted when a component is removed from an entity.
#[derive(Event)]
pub struct OnRemove;

View file

@ -317,6 +317,28 @@ impl<'w> DeferredWorld<'w> {
}
}
/// Triggers all `on_replace` hooks for [`ComponentId`] in target.
///
/// # Safety
/// Caller must ensure [`ComponentId`] in target exist in self.
#[inline]
pub(crate) unsafe fn trigger_on_replace(
&mut self,
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
) {
if archetype.has_replace_hook() {
for component_id in targets {
// SAFETY: Caller ensures that these components exist
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_replace {
hook(DeferredWorld { world: self.world }, entity, component_id);
}
}
}
}
/// Triggers all `on_remove` hooks for [`ComponentId`] in target.
///
/// # Safety
@ -330,9 +352,8 @@ impl<'w> DeferredWorld<'w> {
) {
if archetype.has_remove_hook() {
for component_id in targets {
let hooks =
// SAFETY: Caller ensures that these components exist
unsafe { self.world.components().get_info_unchecked(component_id) }.hooks();
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_remove {
hook(DeferredWorld { world: self.world }, entity, component_id);
}

View file

@ -16,7 +16,7 @@ use bevy_ptr::{OwningPtr, Ptr};
use std::{any::TypeId, marker::PhantomData};
use thiserror::Error;
use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE};
use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE, ON_REPLACE};
/// A read-only reference to a particular [`Entity`] and all of its components.
///
@ -904,7 +904,7 @@ impl<'w> EntityWorldMut<'w> {
// SAFETY: all bundle components exist in World
unsafe {
trigger_on_remove_hooks_and_observers(
trigger_on_replace_and_on_remove_hooks_and_observers(
&mut deferred_world,
old_archetype,
entity,
@ -1085,7 +1085,7 @@ impl<'w> EntityWorldMut<'w> {
// SAFETY: all bundle components exist in World
unsafe {
trigger_on_remove_hooks_and_observers(
trigger_on_replace_and_on_remove_hooks_and_observers(
&mut deferred_world,
old_archetype,
entity,
@ -1222,6 +1222,10 @@ impl<'w> EntityWorldMut<'w> {
// SAFETY: All components in the archetype exist in world
unsafe {
deferred_world.trigger_on_replace(archetype, self.entity, archetype.components());
if archetype.has_replace_observer() {
deferred_world.trigger_observers(ON_REPLACE, self.entity, archetype.components());
}
deferred_world.trigger_on_remove(archetype, self.entity, archetype.components());
if archetype.has_remove_observer() {
deferred_world.trigger_observers(ON_REMOVE, self.entity, archetype.components());
@ -1423,12 +1427,16 @@ impl<'w> EntityWorldMut<'w> {
}
/// SAFETY: all components in the archetype must exist in world
unsafe fn trigger_on_remove_hooks_and_observers(
unsafe fn trigger_on_replace_and_on_remove_hooks_and_observers(
deferred_world: &mut DeferredWorld,
archetype: &Archetype,
entity: Entity,
bundle_info: &BundleInfo,
) {
deferred_world.trigger_on_replace(archetype, entity, bundle_info.iter_components());
if archetype.has_replace_observer() {
deferred_world.trigger_observers(ON_REPLACE, entity, bundle_info.iter_components());
}
deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_components());
if archetype.has_remove_observer() {
deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.iter_components());

View file

@ -164,6 +164,7 @@ impl World {
fn bootstrap(&mut self) {
assert_eq!(ON_ADD, self.init_component::<OnAdd>());
assert_eq!(ON_INSERT, self.init_component::<OnInsert>());
assert_eq!(ON_REPLACE, self.init_component::<OnReplace>());
assert_eq!(ON_REMOVE, self.init_component::<OnRemove>());
}
/// Creates a new empty [`World`].

View file

@ -22,7 +22,7 @@ use std::collections::HashMap;
/// using [`Component`] derive macro:
/// ```no_run
/// #[derive(Component)]
/// #[component(on_add = ..., on_insert = ..., on_remove = ...)]
/// #[component(on_add = ..., on_insert = ..., on_replace = ..., on_remove = ...)]
/// ```
struct MyComponent(KeyCode);
@ -59,7 +59,7 @@ fn setup(world: &mut World) {
// This is to prevent overriding hooks defined in plugins and other crates as well as keeping things fast
world
.register_component_hooks::<MyComponent>()
// There are 3 component lifecycle hooks: `on_add`, `on_insert` and `on_remove`
// There are 4 component lifecycle hooks: `on_add`, `on_insert`, `on_replace` and `on_remove`
// A hook has 3 arguments:
// - a `DeferredWorld`, this allows access to resource and component data as well as `Commands`
// - the entity that triggered the hook
@ -85,6 +85,13 @@ fn setup(world: &mut World) {
.on_insert(|world, _, _| {
println!("Current Index: {:?}", world.resource::<MyComponentIndex>());
})
// `on_replace` will trigger when a component is inserted onto an entity that already had it,
// and runs before the value is replaced.
// Also triggers when a component is removed from an entity, and runs before `on_remove`
.on_replace(|mut world, entity, _| {
let value = world.get::<MyComponent>(entity).unwrap().0;
world.resource_mut::<MyComponentIndex>().remove(&value);
})
// `on_remove` will trigger when a component is removed from an entity,
// since it runs before the component is removed you can still access the component data
.on_remove(|mut world, entity, component_id| {
@ -93,7 +100,6 @@ fn setup(world: &mut World) {
"Component: {:?} removed from: {:?} with value {:?}",
component_id, entity, value
);
world.resource_mut::<MyComponentIndex>().remove(&value);
// You can also issue commands through `.commands()`
world.commands().entity(entity).despawn();
});