close
Skip to content

Observer EventMatchers#24013

Open
ItsDoot wants to merge 7 commits intobevyengine:mainfrom
ItsDoot:ecs/nomoreb
Open

Observer EventMatchers#24013
ItsDoot wants to merge 7 commits intobevyengine:mainfrom
ItsDoot:ecs/nomoreb

Conversation

@ItsDoot
Copy link
Copy Markdown
Contributor

@ItsDoot ItsDoot commented Apr 28, 2026

Objective

On<E, B>'s B type parameter is the single most commonly confusing construct in bevy_ecs. The predominate use case is for filtering lifecycle event observers:

world.add_observer(|on: On<Add, A>| {
    // gets fired whenever component `A` is added to an entity
});

For most other use cases, like picking or custom user events, this type parameter is wholly unnecessary.

Therefore, it should be removed from view for the cases where it is not needed.

Solution

Introduce a tiny layer above Event called EventMatcher, and update On's bounds:

pub trait EventMatcher: 'static {
    type Event: Event;
    type Components: Bundle;
}

pub struct On<'w, 't, E: EventMatcher> {
    observer: Entity,
    event: &'w mut E::Event,
    trigger: &'w mut EventMatcherTrigger<'t, E>,
    trigger_context: &'w TriggerContext,
}

This allows us to lift the B type parameter in On<E, B> into the E type parameter. To ensure other events like picking continue to work, we introduce a blanket implementation for all Events:

// All events are implicitly EventMatchers, with no additional components.
impl<E: Event> EventMatcher for E {
    type Event = Self;
    type Components = ();
}

To facilitate lifecycle events, we first rename them as follows:

  • Add -> AddEvent
  • Insert -> InsertEvent
  • Discard -> DiscardEvent
  • Remove -> RemoveEvent
  • Despawn -> DespawnEvent

Then, we introduce new generic types in place of those old identifiers that are EventMatchers:

pub struct Add<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventMatcher for Add<B> {
    type Event = AddEvent;
    type Components = B;
}

pub struct Insert<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventMatcher for Insert<B> {
    type Event = InsertEvent;
    type Components = B;
}

pub struct Discard<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventMatcher for Discard<B> {
    type Event = DiscardEvent;
    type Components = B;
}

pub struct Remove<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventMatcher for Remove<B> {
    type Event = RemoveEvent;
    type Components = B;
}

pub struct Despawn<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventMatcher for Despawn<B> {
    type Event = DespawnEvent;
    type Components = B;
}

This results in a slightly modified observer setup:

world.add_observer(|on: On<Add<A>>| {
    // gets fired whenever component `A` is added to an entity
});

Testing

No new tests have been added, we have decent coverage of observer tests as is.

@ItsDoot ItsDoot added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 28, 2026
@ItsDoot ItsDoot added this to the 0.20 milestone Apr 28, 2026
@alice-i-cecile alice-i-cecile added M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers X-Needs-SME This type of work requires an SME to approve it. and removed X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers labels Apr 28, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS Apr 28, 2026
Comment thread _release-content/migration-guides/observer_event_matching.md
Copy link
Copy Markdown
Contributor

@james-j-obrien james-j-obrien left a comment

Choose a reason for hiding this comment

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

Cool approach! Looks good.

Comment thread crates/bevy_ecs/src/lifecycle.rs Outdated
Copy link
Copy Markdown
Contributor

@Diddykonga Diddykonga left a comment

Choose a reason for hiding this comment

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

LGTM

Abstracts the E: Event and B: Bundle from On<E, B> into On<E> where E: EventMatcher
This allows the syntax to be like Add<C>, while the actual semantics use the Event and Components associated types on the EventMatcher. Similiar-ish to an custom WorldQuery.

@ItsDoot ItsDoot requested a review from chescock April 28, 2026 07:32
Copy link
Copy Markdown
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

Nice, this is an elegant way to solve this! I like how a bunch of B generics disappeared from places that don't care about them.

> On<'w, 't, E, B>
impl<'w, 't, const AUTO_PROPAGATE: bool, E, T> On<'w, 't, E>
where
E: EntityEvent + for<'a> Event<Trigger<'a> = PropagateEntityTrigger<AUTO_PROPAGATE, E, T>>,
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.

Should E be generalized from Event to EventMatcher here, like the other uses of On? Or is the idea that EntityEvents never use components?

/// **not** an `AND` filter. For example, `Add<(A, B)>` will trigger if either
/// component `A` or component `B` is added to an entity.
#[doc(alias = "OnAdd")]
pub struct Add<B: Bundle>(PhantomData<B>);
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.

Making sure I understand: We never actually create values of these types, and they just exist to be used as type parameters. Right?

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.

Correct.

M: Send + Sync + 'static,
I: IntoObserverSystem<E, B, M> + Send + Sync,
> Bundle for AddObserver<E, B, M, I>
unsafe impl<E: EntityEvent, M: Send + Sync + 'static, I: IntoObserverSystem<E, M> + Send + Sync>
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.

(Same question about EntityEvent for the uses in this file.)

label = "invalid `EventMatcher`",
note = "consider annotating `{Self}` with `#[derive(Event)]` or implementing `EventMatcher` manually"
)]
pub trait EventMatcher: 'static {
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.

Ooh, once we have a separation between the type used to trigger the event and the type used to observe it, I bet we'll be able to use this for other things, like #14649.

```

For lifecycle observers watching dynamic components, you now need to modify
`On<Add>` to `On<Add<()>>`:
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.

It's also possible to use On<AddEvent>, right? But our recommendation is On<Add<()>> for consistency?

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.

Correct.

type Trigger<'a>: Trigger<Self>;
}

/// Trait for types that can be 'matched' on by [`Observer`]s to register additional
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.

Ah, so the idea behind the "matcher" metaphor is that there is a stream of AddEvents going by and the Add<C> matcher will only match ones with a C? I was a little confused by the name at first. Matcher is an uncommon term, and I was thinking of these more like sets of events than like filters.

Bikeshedding a bit: How about something like EventPattern? These types seem to fill the role of the pattern in a match expression. And a "pattern" also sounds like a way to generate a set of events to observe.

/// All components specified in the [`Bundle`] are treated as an `OR` filter
/// **not** an `AND` filter. For example, `Add<(A, B)>` will trigger if either
/// component `A` or component `B` is added to an entity.
#[doc(alias = "OnAdd")]
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.

Not really related to this PR, but: Are we planning to leave these aliases in place forever? I thought they were mostly to help migrate in the release where we renamed them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Normally I would just remove the doc aliases / deprecation after a cycle, but with the rise of the LLM I'm questioning that policy. Lots of reports of "my LLM got confused by a migration" for up to a couple years after it was done.

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.

Do we need to worry about someones tool breaking on migrations, when that tool hallucinates/breaks on its own?

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 29, 2026
@alice-i-cecile
Copy link
Copy Markdown
Member

I'm curious about the answers to @chescock's questions :)

FYI, I'm planning to hold off merging this until after we cut the 0.19-rc, so we can land this together with a F: QueryFilter generic in 0.20.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Needs-SME This type of work requires an SME to approve it.

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

5 participants