Provide target-relative entity refs for observers
What problem does this solve or what need does it fill?
When writing observers, it is frequently desirable to be able to access a component on the listener entity, that is, the entity in the trigger event.
Currently there are two ways to do this: either inject a query for all components of that type and pick the one you want; or inject a DeferredWorld, and then look up the component you need on the event's entity.
bevy_mod_picking had convenience methods for injecting components directly. I'd like to see something similar for Bevy.
What solution would you like?
I'm not sure about naming, but I was thinking of something like:
entity.observe(trigger: Trigger<Pointer<Down>>, component: ListenerRef<MyComponent>);
There would be a ListenerMut version as well. Alternate names might be TargetRef, TriggerRef, or ObserverEntityRef.
What alternative(s) have you considered?
For now, I'm using DeferredWorld, as I am unsure about the overhead of injecting a Query.
Additional context
bevy_mod_picking also provided a means to access the "target" entity and it's components, which was the entity upon which the event was triggered before bubbling, not the entity which handled the event. However, this feature was used rarely (at least by me anyway).
This is also somewhat related to a previous ticket filed by me a long time ago (#11048) - that was rejected (for good reason), but this ticket provides a much more narrowly-scoped variation of the same idea.
Is this what you're looking for https://github.com/bevyengine/bevy/pull/15066?
Is this what you're looking for #15066?
Not as I understand it. That returns the hidden entity; what I'm looking for is components, not entities.
We are seeing a pattern emerge with global observers, which generally looks something like this
world.observe(|target: Trigger<Pointer<Click>>, buttons: Query<&mut Button>| {
if let Some(button) = buttons.get(trigger.entity()) {
/// add behavior common to all button components
}
});
This is a bit verbose, and it means we end up invoking the observer system often when we don't need to. The ECS knows if the target has a button component, so ideally we could just not run this if it dosn't have one. I think the way to pave this cow-path might look something like the following
world.observe(|target: Trigger<Pointer<Click>>, button: Target<&mut Button>| {
/// add behavior common to all button components
});
where Trigger<T> is a system-param specific to observers (much like Trigger<T>). Ideally the observer dispatch system would check the list of params against the archetype of the target, and only run matching observer systems.
Making this work may require a change to how system input works. Alternatively, we could perhaps add a generic parameter Trigger<T, QueryData> and make this change entirely within the observer dispatcher. This might look something like the following
world.observe(|target: Trigger<Pointer<Click>, &mut Button>| {
let button = target.data();
/// add behavior common to all button components
});
I would prefer the ergonomics of the former, but I'd settle for the latter and it's probably easier.
Another approach would be to have something like a fallible single-entity query:
world.observe(|target: Trigger<Pointer<Click>>, query: QueryListener<(&mut Button, &Hovered, Option<&Disabled>)>| {
let (button, hovered, disabled) = query.get();
/// add behavior common to all button components
});
This would only call the observer function if the query could be satisfied.
Alternatively, we could make it non-fallible if that's easier:
world.observe(|target: Trigger<Pointer<Click>>, query: QueryListener<(&mut Button, &Hovered, Option<&Disabled>)>| {
let Some((button, hovered, disabled)) = query.try_get() else {
return;
}
/// add behavior common to all button components
});
If we want to generalize this further, the second argument to the query could indicate which single entity we are querying: the listener, the trigger, etc. This allows the idea to be generalized to use cases other than observers:
world.observe(|target: Trigger<Pointer<Click>>, query: QuerySingle<(&mut Button, &Hovered, Option<&Disabled>), Listener>| {
let (button, hovered, disabled) = query.get();
/// add behavior common to all button components
});
The downside of this approach is that it's a bit more complex and introduces extra boilerplate in the handler.
From @viridia on Discord.
I was thinking something different: something like Single but which targets the target: entity.observe( |trigger: On<Add, MyComponent>, listener: Listener<Comp, Comp2, Option<Comp3>>|) { ... }
Where Listener is a Single query that implicitly matches the target entity.
Extra credit: Along with Listener: Emitter which binds to the original entity upon which the event was triggered.
I think maybe just having the option to get an EntityMut might be worth it's weight in gold here. In my experience you basically wanna just get different components at different times one at a time, not query them. As a first step at least it might also be easier?
Here's a code pattern that occurs many times in my code:
fn checkbox_on_pointer_click(
mut ev: On<Pointer<Click>>,
q_checkbox: Query<(&CoreCheckbox, Has<Checked>, Has<InteractionDisabled>)>,
mut commands: Commands,
) {
if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) {
// do something with the components
}
}
This is a global observer, not a per-entity one. This avoids the need to call .observe() five times for every checkbox, button, slider and so on (which also spawns five hidden entities). But being global means that it's going to get triggered on every ancestor of the clicked element because of bubbling. However, I only want to process entities that have a particular component (CoreCheckbox in this case), so I inject a query and then call .get() with the event target. This intentionally fails for every ancestor except the one I want.
What I'd like to do is get rid of the if. Of course, with what I am proposing, the if is going to happen anyway, it's just happening inside the observer framework instead of in my handler. And possibly saving a clock cycle or two - I know that injecting queries is cheap, but failing might allow the framework to bail before it's done the work of injecting the other parameters.
Thinking about this some more on my walk today.
This idea overlaps with @cart's Com proposal in #17917 . However, I think that solution may be too restrictive: it assumes that there's only one "special entity" whose components we are attempting to access in our observer / callback / one-shot. And while this will be true most of the time, it won't always be true. In fact, with EntityEvent we now already have two special entities: the target and the original_target (called Listener and Target in the legacy bevy_eventlistener crate).
So I'm going to try and generalize this idea in a somewhat rigorous way:
What is a "special entity"?
This refers to an entity that is of special interest to the closure function of a one-shot system or observer, and which we are highly likely to require access to specific components thereupon.
Where do special entities come from?
There are two possible sources of special entities:
- Entities contained in the arguments passed to the function.
- Entities in variables captured by the closure body.
For the former, there are a couple of possibilities:
- For observers listening to
EntityEvent, there are two possible special entities:- The current target
- The original target
- For callbacks, there is the originator - the entity that invoked the callback
- For other kinds of one-shot systems, it's undefined.
For the first two cases, the special entity is stored in the In argument (or the observer's On argument, which is equivalent). So you could imagine an injectable query-like trait with an associated type that tells you how to extract the entity from the first argument. You can implement one of these query-like types for each type of input event.
Here's an example: Feathers buttons and radio buttons invoke callbacks with an Activate(Entity) input argument. So let's imagine an Activator query type which works much like Single. The implementation of Activator includes a method which can take the Activate event and return an entity id.
So you could then write something like:
fn button_on_activate(
mut ev: On<Activate>,
activator: Activator<(&Pressed, Has<InteractionDisabled>)>,
mut commands: Commands,
) {
let (pressed, disabled) = activator.get();
if pressed {
}
}
Activator acts like Single in that its fallible: if the special entity doesn't have those components, the call is skipped. Like Single, you can use it to access as many components as you want. However, unlike Single, Activator isn't built in to bevy_ecs: it's a specialization of a more general query mechanism (let's call it Inquiry for lack of a better term), and defined in the same module where the Activate event is defined.
Similarly, we can implement Target and OriginalTarget as specializations of Inquiry.
What about captured entity ids?
So the problem here is that we can't get these from the function input parameters. What we could do is supply them at the point where the one-shot or observer is defined (note: defined, not called). That is, we could have a version of register_system which takes an extra parameter that is a tuple of data, and (a) provide a means for the closure to inject that data, and (b) provide a means for Inquiry to access fields of the tuple so that we can define specialized queries that work on that data too.
More thinking along these lines. Let's define some terms:
- An
Inquiryis a special type ofQuerythat targets a single entity, which is the subject of the inquiry. - The
Subjectis a trait that understands how to extract the entity id of the subject from the parameters of the call.
trait Subject {
// This will be the `In<T>` or `On<T>` type.
type Context;
// Returns the entity identified by this `Subject`.
fn identify(context: &Context) -> Entity;
}
Usage:
fn on_activate(
ev: On<MenuEvent>,
target: Inquiry<(&BackgroundColor, &Disabled), EventTarget>,
original_target: Inquiry<(&BackgroundColor, &Disabled), EventOriginalTarget>
) {
let (bg_color, disabled) = target.get();
let (original_bg_color, original_disabled) = original_target.get();
}
Where:
EventTargetis an impl ofSubjectthat knows how to extract thetargetfrom anEntityEvent.EventOriginalTargetis an impl ofSubjectthat knows how to extract theoriginal_targetfromEntityEvent.Inquiryis a fallible query that only returns information about a single entity, the one identified by the subject.
Note: in order for this to be useful, we would need to be able to do blanket impls of Subject for all EntityEvent types. This might cause trait impl conflicts, I don't know.