Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions crates/bevy_ecs/macro_logic/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct DeriveComponent {
pub on_add: Option<HookAttributeKind>,
/// The `on_insert` hook.
pub on_insert: Option<HookAttributeKind>,
/// The `on_mutate` hook.
pub on_mutate: Option<HookAttributeKind>,
/// The `on_discard` hook.
pub on_discard: Option<HookAttributeKind>,
/// The `on_remove` hook.
Expand Down Expand Up @@ -62,6 +64,7 @@ impl DeriveComponent {
storage: None,
on_add: None,
on_insert: None,
on_mutate: None,
on_discard: None,
on_remove: None,
on_despawn: None,
Expand Down Expand Up @@ -102,6 +105,11 @@ impl DeriveComponent {
parse_quote! { Self::on_insert }
})?);
Ok(())
} else if nested.path.is_ident(ON_MUTATE) {
attrs.on_mutate = Some(HookAttributeKind::parse(nested.input, || {
parse_quote! { Self::on_mutate }
})?);
Ok(())
} else if nested.path.is_ident(ON_DISCARD) {
attrs.on_discard = Some(HookAttributeKind::parse(nested.input, || {
parse_quote! { Self::on_discard }
Expand Down Expand Up @@ -240,15 +248,19 @@ impl DeriveComponent {
let storage = storage_path(bevy_ecs, self.storage.unwrap_or(default_storage));

let on_add_path = Vec::from_iter(self.on_add.map(|path| path.to_token_stream(bevy_ecs)));
let on_remove_path =
Vec::from_iter(self.on_remove.map(|path| path.to_token_stream(bevy_ecs)));

let mut on_insert_path =
Vec::from_iter(self.on_insert.map(|path| path.to_token_stream(bevy_ecs)));

let on_mutate_path =
Vec::from_iter(self.on_mutate.map(|path| path.to_token_stream(bevy_ecs)));

let mut on_discard_path =
Vec::from_iter(self.on_discard.map(|path| path.to_token_stream(bevy_ecs)));

let on_remove_path =
Vec::from_iter(self.on_remove.map(|path| path.to_token_stream(bevy_ecs)));

let mut on_despawn_path =
Vec::from_iter(self.on_despawn.map(|path| path.to_token_stream(bevy_ecs)));

Expand All @@ -269,6 +281,7 @@ impl DeriveComponent {

let on_add = hook_register_function_call(bevy_ecs, quote! {on_add}, &on_add_path);
let on_insert = hook_register_function_call(bevy_ecs, quote! {on_insert}, &on_insert_path);
let on_mutate = hook_register_function_call(bevy_ecs, quote! {on_mutate}, &on_mutate_path);
let on_discard =
hook_register_function_call(bevy_ecs, quote! {on_discard}, &on_discard_path);
let on_remove = hook_register_function_call(bevy_ecs, quote! {on_remove}, &on_remove_path);
Expand Down Expand Up @@ -373,6 +386,7 @@ impl DeriveComponent {

#on_add
#on_insert
#on_mutate
#on_discard
#on_remove
#on_despawn
Expand Down Expand Up @@ -529,6 +543,7 @@ const RELATIONSHIP_TARGET: &str = "relationship_target";

const ON_ADD: &str = "on_add";
const ON_INSERT: &str = "on_insert";
const ON_MUTATE: &str = "on_mutate";
const ON_DISCARD: &str = "on_discard";
const ON_REMOVE: &str = "on_remove";
const ON_DESPAWN: &str = "on_despawn";
Expand Down
34 changes: 26 additions & 8 deletions crates/bevy_ecs/src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ impl Edges {
}

/// Metadata about an [`Entity`] in a [`Archetype`].
#[derive(Clone, Copy)]
pub struct ArchetypeEntity {
entity: Entity,
table_row: TableRow,
Expand Down Expand Up @@ -369,14 +370,16 @@ bitflags::bitflags! {
pub(crate) struct ArchetypeFlags: u32 {
const ON_ADD_HOOK = (1 << 0);
const ON_INSERT_HOOK = (1 << 1);
const ON_DISCARD_HOOK = (1 << 2);
const ON_REMOVE_HOOK = (1 << 3);
const ON_DESPAWN_HOOK = (1 << 4);
const ON_ADD_OBSERVER = (1 << 5);
const ON_INSERT_OBSERVER = (1 << 6);
const ON_DISCARD_OBSERVER = (1 << 7);
const ON_REMOVE_OBSERVER = (1 << 8);
const ON_DESPAWN_OBSERVER = (1 << 9);
const ON_MUTATE_HOOK = (1 << 2);
const ON_DISCARD_HOOK = (1 << 3);
const ON_REMOVE_HOOK = (1 << 4);
const ON_DESPAWN_HOOK = (1 << 5);
const ON_ADD_OBSERVER = (1 << 6);
const ON_INSERT_OBSERVER = (1 << 7);
const ON_MUTATE_OBSERVER = (1 << 8);
const ON_DISCARD_OBSERVER = (1 << 9);
const ON_REMOVE_OBSERVER = (1 << 10);
const ON_DESPAWN_OBSERVER = (1 << 11);
}
}

Expand Down Expand Up @@ -686,6 +689,12 @@ impl Archetype {
self.flags().contains(ArchetypeFlags::ON_INSERT_HOOK)
}

/// Returns true if any of the components in this archetype have `on_mutate` hooks
#[inline]
pub fn has_mutate_hook(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_MUTATE_HOOK)
}

/// Returns true if any of the components in this archetype have `on_discard` hooks
#[inline]
pub fn has_discard_hook(&self) -> bool {
Expand Down Expand Up @@ -720,6 +729,14 @@ impl Archetype {
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
}

/// Returns true if any of the components in this archetype have at least one [`Mutate`] observer
///
/// [`Mutate`]: crate::lifecycle::Mutate
#[inline]
pub fn has_mutate_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_MUTATE_OBSERVER)
}

/// Returns true if any of the components in this archetype have at least one [`Discard`] observer
///
/// [`Discard`]: crate::lifecycle::Discard
Expand Down Expand Up @@ -785,6 +802,7 @@ pub struct Archetypes {
}

/// Metadata about how a component is stored in an [`Archetype`].
#[derive(Clone, Copy)]
pub struct ArchetypeRecord {
/// Index of the component in the archetype's [`Table`](crate::storage::Table),
/// or None if the component is a sparse set component.
Expand Down
12 changes: 7 additions & 5 deletions crates/bevy_ecs/src/component/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
pub const ADD: usize = 0;
/// `usize` for the [`Insert`](crate::lifecycle::Insert) component used in lifecycle observers.
pub const INSERT: usize = 1;
/// `usize` for the [`Mutate`](crate::lifecycle::Mutate) component used in lifecycle observers.
pub const MUTATE: usize = 2;
/// `usize` for the [`Discard`](crate::lifecycle::Discard) component used in lifecycle observers.
pub const DISCARD: usize = 2;
pub const DISCARD: usize = 3;
/// `usize` for the [`Remove`](crate::lifecycle::Remove) component used in lifecycle observers.
pub const REMOVE: usize = 3;
pub const REMOVE: usize = 4;
/// `usize` for [`Despawn`](crate::lifecycle::Despawn) component used in lifecycle observers.
pub const DESPAWN: usize = 4;
pub const DESPAWN: usize = 5;
/// `usize` of the [`IsResource`](crate::resource::IsResource) component used to mark entities with resources.
pub const IS_RESOURCE: usize = 5;
pub const IS_RESOURCE: usize = 6;
/// `usize` for [`ArchetypeCreated`](crate::archetype::ArchetypeCreated) component used as an observer event key.
pub(crate) const ARCHETYPE_CREATED: usize = 6;
pub(crate) const ARCHETYPE_CREATED: usize = 7;
3 changes: 3 additions & 0 deletions crates/bevy_ecs/src/component/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ impl ComponentInfo {
if self.hooks().on_insert.is_some() {
flags.insert(ArchetypeFlags::ON_INSERT_HOOK);
}
if self.hooks().on_mutate.is_some() {
flags.insert(ArchetypeFlags::ON_MUTATE_HOOK);
}
if self.hooks().on_discard.is_some() {
flags.insert(ArchetypeFlags::ON_DISCARD_HOOK);
}
Expand Down
11 changes: 11 additions & 0 deletions crates/bevy_ecs/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,11 @@ pub trait Component: Send + Sync + 'static {
None
}

/// Gets the `on_mutate` [`ComponentHook`] for this [`Component`] if one is defined.
fn on_mutate() -> Option<ComponentHook> {
None
}

/// Gets the `on_discard` [`ComponentHook`] for this [`Component`] if one is defined.
fn on_discard() -> Option<ComponentHook> {
None
Expand Down Expand Up @@ -772,6 +777,12 @@ pub enum StorageType {
SparseSet,
}

/// A trait to get the [`ComponentId`]s an type contains.
pub trait ContainsComponents {
/// Gets the [`ComponentId`]s
fn components(&self) -> &[ComponentId];
}

/// A [`SystemParam`] that provides access to the [`ComponentId`] for a specific component type.
///
/// # Example
Expand Down
119 changes: 119 additions & 0 deletions crates/bevy_ecs/src/event/trigger.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::component::ContainsComponents;
use crate::event::{EventPattern, SetEntityEventTarget};
use crate::{
archetype::Archetype,
Expand All @@ -8,6 +9,7 @@ use crate::{
traversal::Traversal,
world::DeferredWorld,
};
use alloc::vec::Vec;
use bevy_ptr::PtrMut;
use core::{fmt, marker::PhantomData};

Expand Down Expand Up @@ -523,3 +525,120 @@ impl<'a> EntityComponentsTrigger<'a> {
}
}
}

/// An [`EntityEvent`] [`Trigger`] that, in addition to behaving like a normal [`EntityTrigger`], _also_ runs observers
/// that watch for components that match the [`impl Iterator<Item=ComponentId>`] returned by [`ContainsComponents::components`]. This includes
/// both _global_ observers of those components and "entity scoped" observers that watch the [`EntityEvent::event_target`].
///
/// This is used by Bevy's built-in [lifecycle events](crate::lifecycle).
#[derive(Default, Debug)]
pub struct EntityMutateTrigger;

// SAFETY:
// - `E`'s [`Event::Trigger`] is constrained to [`EntityComponentsTrigger`]
unsafe impl<E: EntityEvent + for<'a> Event<Trigger<'a> = EntityMutateTrigger> + ContainsComponents>
Trigger<E> for EntityMutateTrigger
{
unsafe fn trigger(
&mut self,
world: DeferredWorld,
observers: &CachedObservers,
trigger_context: &TriggerContext,
event: &mut E,
) {
let entity = event.event_target();
let components: Vec<ComponentId> = event.components().into();
Comment on lines +534 to +550

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why you didn't use EntityComponentsTrigger for MutateEvent like the other lifecycle observers do?

@Diddykonga Diddykonga Aug 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 50/50 on this, but mainly its because a Mutation shouldn't ever be an structural change, so no archetypes will ever change, so passing them is assumed to be useless information. (There is also overhead for Mutate to pass them in the queue path)
The components inside the Event instead of the Trigger, is more of a personal preference, but we already store the target Entity inside the Event, and so having all the data of which to match an Observer, be on the Event seemed fitting.

@SkiFire13 SkiFire13 Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a Mutation shouldn't ever be an structural change, so no archetypes will ever change, so passing them is assumed to be useless information.

Mhm yeah passing archetypes here is not ideal either. Edit: could we maybe move them onto the various lifecycle events as ArchetypeId?

The components inside the Event instead of the Trigger, is more of a personal preference, but we already store the target Entity inside the Event, and so having all the data of which to match an Observer, be on the Event seemed fitting.

Why don't we do this for the other lifecycle events too then?

Storing the components on the Event also forces you to use Vec instead of a borrowed slice since Event must be 'static.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhm yeah passing archetypes here is not ideal either. Edit: could we maybe move them onto the various lifecycle events as ArchetypeId?

I have thought and suggested a thing before, we would need to discuss with Doot as they are working on a Replace Event and are wanting direct access to Archetypes for perf, but perhaps it would still be okay with ArchetypeId's?

Why don't we do this for the other lifecycle events too then?

Storing the components on the Event also forces you to use Vec instead of a borrowed slice since Event must be 'static.

Same as above, I would prefer it, but some Birbs have a differing opinion and so we would need to discuss that.

// SAFETY:
// - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger`
// - the passed in event pointer comes from `event`, which is an `Event`
// - the passed in components comes from 'event', and will live not outlive the event pointer.
// - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger`
unsafe {
self.trigger_internal(
world,
observers,
event.into(),
entity,
&components,
trigger_context,
);
}
}
}

impl EntityMutateTrigger {
/// # Safety
/// - `observers` must come from the `world` [`DeferredWorld`]
/// - `event` must point to an [`Event`] whose [`Event::Trigger`] is [`EntityComponentsTrigger`]
/// - `trigger_context`'s [`TriggerContext::event_key`] must correspond to the `event` type.
#[inline(never)]
unsafe fn trigger_internal(
&mut self,
mut world: DeferredWorld,
observers: &CachedObservers,
mut event: PtrMut,
entity: Entity,
components: &[ComponentId],
trigger_context: &TriggerContext,
) {
// SAFETY:
// - `observers` come from `world` and match the event type `E`, enforced by the call to `trigger`
// - the passed in event pointer comes from `event`, which is an `Event`
// - `trigger` is a matching trigger type, as it comes from `self`, which is the Trigger for `E`
// - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger`
unsafe {
trigger_entity_internal(
world.reborrow(),
observers,
event.reborrow(),
self.into(),
entity,
trigger_context,
);
}

// Trigger observers watching for a specific component
for id in components {
if let Some(component_observers) = observers.component_observers().get(id) {
for (observer, runner) in component_observers.global_observers() {
// SAFETY:
// - `observers` come from `world` and match the `event` type, enforced by the call to `trigger_internal`
// - the passed in event pointer is an `Event`, enforced by the call to `trigger_internal`
// - `trigger` is a matching trigger type, enforced by the call to `trigger_internal`
// - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_internal`
unsafe {
(runner)(
world.reborrow(),
*observer,
trigger_context,
event.reborrow(),
self.into(),
);
}
}

if let Some(map) = component_observers
.entity_component_observers()
.get(&entity)
{
for (observer, runner) in map {
// SAFETY:
// - `observers` come from `world` and match the `event` type, enforced by the call to `trigger_internal`
// - the passed in event pointer is an `Event`, enforced by the call to `trigger_internal`
// - `trigger` is a matching trigger type, enforced by the call to `trigger_internal`
// - `trigger_context`'s event_key matches `E`, enforced by the call to `trigger_internal`
unsafe {
(runner)(
world.reborrow(),
*observer,
trigger_context,
event.reborrow(),
self.into(),
);
}
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub mod prelude {
error::{BevyError, ContextExt, Result, ResultSeverityExt, Severity},
event::{EntityEvent, Event, EventPattern},
hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children},
lifecycle::{Add, Despawn, Discard, Insert, Remove, RemovedComponents},
lifecycle::{Add, Despawn, Discard, Insert, MutateEvent, Remove, RemovedComponents},
message::{
Message, MessageMutator, MessageReader, MessageWriter, Messages, PopulatedMessageReader,
},
Expand Down
Loading