Skip to content

Commit 0f7c548

Browse files
authored
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()`
1 parent e79f91f commit 0f7c548

File tree

11 files changed

+238
-44
lines changed

11 files changed

+238
-44
lines changed

crates/bevy_ecs/macros/src/component.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
5858

5959
let on_add = hook_register_function_call(quote! {on_add}, attrs.on_add);
6060
let on_insert = hook_register_function_call(quote! {on_insert}, attrs.on_insert);
61+
let on_replace = hook_register_function_call(quote! {on_replace}, attrs.on_replace);
6162
let on_remove = hook_register_function_call(quote! {on_remove}, attrs.on_remove);
6263

6364
ast.generics
@@ -76,6 +77,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
7677
fn register_component_hooks(hooks: &mut #bevy_ecs_path::component::ComponentHooks) {
7778
#on_add
7879
#on_insert
80+
#on_replace
7981
#on_remove
8082
}
8183
}
@@ -86,12 +88,14 @@ pub const COMPONENT: &str = "component";
8688
pub const STORAGE: &str = "storage";
8789
pub const ON_ADD: &str = "on_add";
8890
pub const ON_INSERT: &str = "on_insert";
91+
pub const ON_REPLACE: &str = "on_replace";
8992
pub const ON_REMOVE: &str = "on_remove";
9093

9194
struct Attrs {
9295
storage: StorageTy,
9396
on_add: Option<ExprPath>,
9497
on_insert: Option<ExprPath>,
98+
on_replace: Option<ExprPath>,
9599
on_remove: Option<ExprPath>,
96100
}
97101

@@ -110,6 +114,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
110114
storage: StorageTy::Table,
111115
on_add: None,
112116
on_insert: None,
117+
on_replace: None,
113118
on_remove: None,
114119
};
115120

@@ -132,6 +137,9 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
132137
} else if nested.path.is_ident(ON_INSERT) {
133138
attrs.on_insert = Some(nested.value()?.parse::<ExprPath>()?);
134139
Ok(())
140+
} else if nested.path.is_ident(ON_REPLACE) {
141+
attrs.on_replace = Some(nested.value()?.parse::<ExprPath>()?);
142+
Ok(())
135143
} else if nested.path.is_ident(ON_REMOVE) {
136144
attrs.on_remove = Some(nested.value()?.parse::<ExprPath>()?);
137145
Ok(())

crates/bevy_ecs/src/archetype.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ pub(crate) struct AddBundle {
121121
/// indicate if the component is newly added to the target archetype or if it already existed
122122
pub bundle_status: Vec<ComponentStatus>,
123123
pub added: Vec<ComponentId>,
124+
pub mutated: Vec<ComponentId>,
124125
}
125126

126127
/// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components
@@ -205,13 +206,15 @@ impl Edges {
205206
archetype_id: ArchetypeId,
206207
bundle_status: Vec<ComponentStatus>,
207208
added: Vec<ComponentId>,
209+
mutated: Vec<ComponentId>,
208210
) {
209211
self.add_bundle.insert(
210212
bundle_id,
211213
AddBundle {
212214
archetype_id,
213215
bundle_status,
214216
added,
217+
mutated,
215218
},
216219
);
217220
}
@@ -317,10 +320,12 @@ bitflags::bitflags! {
317320
pub(crate) struct ArchetypeFlags: u32 {
318321
const ON_ADD_HOOK = (1 << 0);
319322
const ON_INSERT_HOOK = (1 << 1);
320-
const ON_REMOVE_HOOK = (1 << 2);
321-
const ON_ADD_OBSERVER = (1 << 3);
322-
const ON_INSERT_OBSERVER = (1 << 4);
323-
const ON_REMOVE_OBSERVER = (1 << 5);
323+
const ON_REPLACE_HOOK = (1 << 2);
324+
const ON_REMOVE_HOOK = (1 << 3);
325+
const ON_ADD_OBSERVER = (1 << 4);
326+
const ON_INSERT_OBSERVER = (1 << 5);
327+
const ON_REPLACE_OBSERVER = (1 << 6);
328+
const ON_REMOVE_OBSERVER = (1 << 7);
324329
}
325330
}
326331

@@ -600,6 +605,12 @@ impl Archetype {
600605
self.flags().contains(ArchetypeFlags::ON_INSERT_HOOK)
601606
}
602607

608+
/// Returns true if any of the components in this archetype have `on_replace` hooks
609+
#[inline]
610+
pub fn has_replace_hook(&self) -> bool {
611+
self.flags().contains(ArchetypeFlags::ON_REPLACE_HOOK)
612+
}
613+
603614
/// Returns true if any of the components in this archetype have `on_remove` hooks
604615
#[inline]
605616
pub fn has_remove_hook(&self) -> bool {
@@ -622,6 +633,14 @@ impl Archetype {
622633
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
623634
}
624635

636+
/// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer
637+
///
638+
/// [`OnReplace`]: crate::world::OnReplace
639+
#[inline]
640+
pub fn has_replace_observer(&self) -> bool {
641+
self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER)
642+
}
643+
625644
/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
626645
///
627646
/// [`OnRemove`]: crate::world::OnRemove

crates/bevy_ecs/src/bundle.rs

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
prelude::World,
1818
query::DebugCheckedUnwrap,
1919
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
20-
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT},
20+
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_REPLACE},
2121
};
2222

2323
use bevy_ptr::{ConstNonNull, OwningPtr};
@@ -456,11 +456,13 @@ impl BundleInfo {
456456
let mut new_sparse_set_components = Vec::new();
457457
let mut bundle_status = Vec::with_capacity(self.component_ids.len());
458458
let mut added = Vec::new();
459+
let mut mutated = Vec::new();
459460

460461
let current_archetype = &mut archetypes[archetype_id];
461462
for component_id in self.component_ids.iter().cloned() {
462463
if current_archetype.contains(component_id) {
463464
bundle_status.push(ComponentStatus::Mutated);
465+
mutated.push(component_id);
464466
} else {
465467
bundle_status.push(ComponentStatus::Added);
466468
added.push(component_id);
@@ -476,7 +478,7 @@ impl BundleInfo {
476478
if new_table_components.is_empty() && new_sparse_set_components.is_empty() {
477479
let edges = current_archetype.edges_mut();
478480
// the archetype does not change when we add this bundle
479-
edges.insert_add_bundle(self.id, archetype_id, bundle_status, added);
481+
edges.insert_add_bundle(self.id, archetype_id, bundle_status, added, mutated);
480482
archetype_id
481483
} else {
482484
let table_id;
@@ -526,6 +528,7 @@ impl BundleInfo {
526528
new_archetype_id,
527529
bundle_status,
528530
added,
531+
mutated,
529532
);
530533
new_archetype_id
531534
}
@@ -665,6 +668,30 @@ impl<'w> BundleInserter<'w> {
665668
let bundle_info = self.bundle_info.as_ref();
666669
let add_bundle = self.add_bundle.as_ref();
667670
let table = self.table.as_mut();
671+
let archetype = self.archetype.as_ref();
672+
673+
// SAFETY: All components in the bundle are guaranteed to exist in the World
674+
// as they must be initialized before creating the BundleInfo.
675+
unsafe {
676+
// SAFETY: Mutable references do not alias and will be dropped after this block
677+
let mut deferred_world = self.world.into_deferred();
678+
679+
deferred_world.trigger_on_replace(
680+
archetype,
681+
entity,
682+
add_bundle.mutated.iter().copied(),
683+
);
684+
if archetype.has_replace_observer() {
685+
deferred_world.trigger_observers(
686+
ON_REPLACE,
687+
entity,
688+
add_bundle.mutated.iter().copied(),
689+
);
690+
}
691+
}
692+
693+
// SAFETY: Archetype gets borrowed when running the on_replace observers above,
694+
// so this reference can only be promoted from shared to &mut down here, after they have been ran
668695
let archetype = self.archetype.as_mut();
669696

670697
let (new_archetype, new_location) = match &mut self.result {
@@ -1132,7 +1159,7 @@ mod tests {
11321159
struct A;
11331160

11341161
#[derive(Component)]
1135-
#[component(on_add = a_on_add, on_insert = a_on_insert, on_remove = a_on_remove)]
1162+
#[component(on_add = a_on_add, on_insert = a_on_insert, on_replace = a_on_replace, on_remove = a_on_remove)]
11361163
struct AMacroHooks;
11371164

11381165
fn a_on_add(mut world: DeferredWorld, _: Entity, _: ComponentId) {
@@ -1143,10 +1170,14 @@ mod tests {
11431170
world.resource_mut::<R>().assert_order(1);
11441171
}
11451172

1146-
fn a_on_remove<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
1173+
fn a_on_replace<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
11471174
world.resource_mut::<R>().assert_order(2);
11481175
}
11491176

1177+
fn a_on_remove<T1, T2>(mut world: DeferredWorld, _: T1, _: T2) {
1178+
world.resource_mut::<R>().assert_order(3);
1179+
}
1180+
11501181
#[derive(Component)]
11511182
struct B;
11521183

@@ -1173,15 +1204,14 @@ mod tests {
11731204
world.init_resource::<R>();
11741205
world
11751206
.register_component_hooks::<A>()
1176-
.on_add(|mut world, _, _| {
1177-
world.resource_mut::<R>().assert_order(0);
1178-
})
1207+
.on_add(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
11791208
.on_insert(|mut world, _, _| world.resource_mut::<R>().assert_order(1))
1180-
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(2));
1209+
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(2))
1210+
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(3));
11811211

11821212
let entity = world.spawn(A).id();
11831213
world.despawn(entity);
1184-
assert_eq!(3, world.resource::<R>().0);
1214+
assert_eq!(4, world.resource::<R>().0);
11851215
}
11861216

11871217
#[test]
@@ -1192,7 +1222,7 @@ mod tests {
11921222
let entity = world.spawn(AMacroHooks).id();
11931223
world.despawn(entity);
11941224

1195-
assert_eq!(3, world.resource::<R>().0);
1225+
assert_eq!(4, world.resource::<R>().0);
11961226
}
11971227

11981228
#[test]
@@ -1201,21 +1231,36 @@ mod tests {
12011231
world.init_resource::<R>();
12021232
world
12031233
.register_component_hooks::<A>()
1204-
.on_add(|mut world, _, _| {
1205-
world.resource_mut::<R>().assert_order(0);
1206-
})
1207-
.on_insert(|mut world, _, _| {
1208-
world.resource_mut::<R>().assert_order(1);
1209-
})
1210-
.on_remove(|mut world, _, _| {
1211-
world.resource_mut::<R>().assert_order(2);
1212-
});
1234+
.on_add(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
1235+
.on_insert(|mut world, _, _| world.resource_mut::<R>().assert_order(1))
1236+
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(2))
1237+
.on_remove(|mut world, _, _| world.resource_mut::<R>().assert_order(3));
12131238

12141239
let mut entity = world.spawn_empty();
12151240
entity.insert(A);
12161241
entity.remove::<A>();
12171242
entity.flush();
1218-
assert_eq!(3, world.resource::<R>().0);
1243+
assert_eq!(4, world.resource::<R>().0);
1244+
}
1245+
1246+
#[test]
1247+
fn component_hook_order_replace() {
1248+
let mut world = World::new();
1249+
world
1250+
.register_component_hooks::<A>()
1251+
.on_replace(|mut world, _, _| world.resource_mut::<R>().assert_order(0))
1252+
.on_insert(|mut world, _, _| {
1253+
if let Some(mut r) = world.get_resource_mut::<R>() {
1254+
r.assert_order(1);
1255+
}
1256+
});
1257+
1258+
let entity = world.spawn(A).id();
1259+
world.init_resource::<R>();
1260+
let mut entity = world.entity_mut(entity);
1261+
entity.insert(A);
1262+
entity.flush();
1263+
assert_eq!(2, world.resource::<R>().0);
12191264
}
12201265

12211266
#[test]

crates/bevy_ecs/src/component.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ use std::{
100100
/// Alternatively to the example shown in [`ComponentHooks`]' documentation, hooks can be configured using following attributes:
101101
/// - `#[component(on_add = on_add_function)]`
102102
/// - `#[component(on_insert = on_insert_function)]`
103+
/// - `#[component(on_replace = on_replace_function)]`
103104
/// - `#[component(on_remove = on_remove_function)]`
104105
///
105106
/// ```
@@ -114,8 +115,8 @@ use std::{
114115
/// // Another possible way of configuring hooks:
115116
/// // #[component(on_add = my_on_add_hook, on_insert = my_on_insert_hook)]
116117
/// //
117-
/// // We don't have a remove hook, so we can leave it out:
118-
/// // #[component(on_remove = my_on_remove_hook)]
118+
/// // We don't have a replace or remove hook, so we can leave them out:
119+
/// // #[component(on_replace = my_on_replace_hook, on_remove = my_on_remove_hook)]
119120
/// struct ComponentA;
120121
///
121122
/// 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);
280281
pub struct ComponentHooks {
281282
pub(crate) on_add: Option<ComponentHook>,
282283
pub(crate) on_insert: Option<ComponentHook>,
284+
pub(crate) on_replace: Option<ComponentHook>,
283285
pub(crate) on_remove: Option<ComponentHook>,
284286
}
285287

@@ -314,6 +316,28 @@ impl ComponentHooks {
314316
.expect("Component id: {:?}, already has an on_insert hook")
315317
}
316318

319+
/// Register a [`ComponentHook`] that will be run when this component is about to be dropped,
320+
/// such as being replaced (with `.insert`) or removed.
321+
///
322+
/// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced,
323+
/// allowing access to the previous data just before it is dropped.
324+
/// This hook does *not* run if the entity did not already have this component.
325+
///
326+
/// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity).
327+
///
328+
/// # Warning
329+
///
330+
/// The hook won't run if the component is already present and is only mutated, such as in a system via a query.
331+
/// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches.
332+
///
333+
/// # Panics
334+
///
335+
/// Will panic if the component already has an `on_replace` hook
336+
pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self {
337+
self.try_on_replace(hook)
338+
.expect("Component id: {:?}, already has an on_replace hook")
339+
}
340+
317341
/// Register a [`ComponentHook`] that will be run when this component is removed from an entity.
318342
/// Despawning an entity counts as removing all of its components.
319343
///
@@ -351,6 +375,19 @@ impl ComponentHooks {
351375
Some(self)
352376
}
353377

378+
/// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed
379+
///
380+
/// This is a fallible version of [`Self::on_replace`].
381+
///
382+
/// Returns `None` if the component already has an `on_replace` hook.
383+
pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> {
384+
if self.on_replace.is_some() {
385+
return None;
386+
}
387+
self.on_replace = Some(hook);
388+
Some(self)
389+
}
390+
354391
/// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity.
355392
///
356393
/// This is a fallible version of [`Self::on_remove`].
@@ -442,6 +479,9 @@ impl ComponentInfo {
442479
if self.hooks().on_insert.is_some() {
443480
flags.insert(ArchetypeFlags::ON_INSERT_HOOK);
444481
}
482+
if self.hooks().on_replace.is_some() {
483+
flags.insert(ArchetypeFlags::ON_REPLACE_HOOK);
484+
}
445485
if self.hooks().on_remove.is_some() {
446486
flags.insert(ArchetypeFlags::ON_REMOVE_HOOK);
447487
}

crates/bevy_ecs/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ pub mod prelude {
6161
SystemParamFunction,
6262
},
6363
world::{
64-
EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, World,
64+
EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, OnReplace,
65+
World,
6566
},
6667
};
6768
}

0 commit comments

Comments
 (0)