bevy/examples/ecs/entity_disabling.rs
Joona Aalto e5dc177b4b
Rename Trigger to On (#19596)
# Objective

Currently, the observer API looks like this:

```rust
app.add_observer(|trigger: Trigger<Explode>| {
    info!("Entity {} exploded!", trigger.target());
});
```

Future plans for observers also include "multi-event observers" with a
trigger that looks like this (see [Cart's
example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508)):

```rust
trigger: Trigger<(
    OnAdd<Pressed>,
    OnRemove<Pressed>,
    OnAdd<InteractionDisabled>,
    OnRemove<InteractionDisabled>,
    OnInsert<Hovered>,
)>,
```

In scenarios like this, there is a lot of repetition of `On`. These are
expected to be very high-traffic APIs especially in UI contexts, so
ergonomics and readability are critical.

By renaming `Trigger` to `On`, we can make these APIs read more cleanly
and get rid of the repetition:

```rust
app.add_observer(|trigger: On<Explode>| {
    info!("Entity {} exploded!", trigger.target());
});
```

```rust
trigger: On<(
    Add<Pressed>,
    Remove<Pressed>,
    Add<InteractionDisabled>,
    Remove<InteractionDisabled>,
    Insert<Hovered>,
)>,
```

Names like `On<Add<Pressed>>` emphasize the actual event listener nature
more than `Trigger<OnAdd<Pressed>>`, and look cleaner. This *also* frees
up the `Trigger` name if we want to use it for the observer event type,
splitting them out from buffered events (bikeshedding this is out of
scope for this PR though).

For prior art:
[`bevy_eventlistener`](https://github.com/aevyrie/bevy_eventlistener)
used
[`On`](https://docs.rs/bevy_eventlistener/latest/bevy_eventlistener/event_listener/struct.On.html)
for its event listener type. Though in our case, the observer is the
event listener, and `On` is just a type containing information about the
triggered event.

## Solution

Steal from `bevy_event_listener` by @aevyrie and use `On`.

- Rename `Trigger` to `On`
- Rename `OnAdd` to `Add`
- Rename `OnInsert` to `Insert`
- Rename `OnReplace` to `Replace`
- Rename `OnRemove` to `Remove`
- Rename `OnDespawn` to `Despawn`

## Discussion

### Naming Conflicts??

Using a name like `Add` might initially feel like a very bad idea, since
it risks conflict with `core::ops::Add`. However, I don't expect this to
be a big problem in practice.

- You rarely need to actually implement the `Add` trait, especially in
modules that would use the Bevy ECS.
- In the rare cases where you *do* get a conflict, it is very easy to
fix by just disambiguating, for example using `ops::Add`.
- The `Add` event is a struct while the `Add` trait is a trait (duh), so
the compiler error should be very obvious.

For the record, renaming `OnAdd` to `Add`, I got exactly *zero* errors
or conflicts within Bevy itself. But this is of course not entirely
representative of actual projects *using* Bevy.

You might then wonder, why not use `Added`? This would conflict with the
`Added` query filter, so it wouldn't work. Additionally, the current
naming convention for observer events does not use past tense.

### Documentation

This does make documentation slightly more awkward when referring to
`On` or its methods. Previous docs often referred to `Trigger::target`
or "sends a `Trigger`" (which is... a bit strange anyway), which would
now be `On::target` and "sends an observer `Event`".

You can see the diff in this PR to see some of the effects. I think it
should be fine though, we may just need to reword more documentation to
read better.
2025-06-12 18:22:33 +00:00

154 lines
5.0 KiB
Rust

//! Disabling entities is a powerful feature that allows you to hide entities from the ECS without deleting them.
//!
//! This can be useful for implementing features like "sleeping" objects that are offscreen
//! or managing networked entities.
//!
//! While disabling entities *will* make them invisible,
//! that's not its primary purpose!
//! [`Visibility`](bevy::prelude::Visibility) should be used to hide entities;
//! disabled entities are skipped entirely, which can lead to subtle bugs.
//!
//! # Default query filters
//!
//! Under the hood, Bevy uses a "default query filter" that skips entities with the
//! the [`Disabled`] component.
//! These filters act as a by-default exclusion list for all queries,
//! and can be bypassed by explicitly including these components in your queries.
//! For example, `Query<&A, With<Disabled>`, `Query<(Entity, Has<Disabled>>)` or
//! `Query<&A, Or<(With<Disabled>, With<B>)>>` will include disabled entities.
use bevy::ecs::entity_disabling::Disabled;
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, MeshPickingPlugin))
.add_observer(disable_entities_on_click)
.add_systems(
Update,
(list_all_named_entities, reenable_entities_on_space),
)
.add_systems(Startup, (setup_scene, display_instructions))
.run();
}
#[derive(Component)]
struct DisableOnClick;
fn disable_entities_on_click(
trigger: On<Pointer<Click>>,
valid_query: Query<&DisableOnClick>,
mut commands: Commands,
) {
let clicked_entity = trigger.target().unwrap();
// Windows and text are entities and can be clicked!
// We definitely don't want to disable the window itself,
// because that would cause the app to close!
if valid_query.contains(clicked_entity) {
// Just add the `Disabled` component to the entity to disable it.
// Note that the `Disabled` component is *only* added to the entity,
// its children are not affected.
commands.entity(clicked_entity).insert(Disabled);
}
}
#[derive(Component)]
struct EntityNameText;
// The query here will not find entities with the `Disabled` component,
// because it does not explicitly include it.
fn list_all_named_entities(
query: Query<&Name>,
mut name_text_query: Query<&mut Text, With<EntityNameText>>,
mut commands: Commands,
) {
let mut text_string = String::from("Named entities found:\n");
// Query iteration order is not guaranteed, so we sort the names
// to ensure the output is consistent.
for name in query.iter().sort::<&Name>() {
text_string.push_str(&format!("{:?}\n", name));
}
if let Ok(mut text) = name_text_query.single_mut() {
*text = Text::new(text_string);
} else {
commands.spawn((
EntityNameText,
Text::default(),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
right: Val::Px(12.0),
..default()
},
));
}
}
fn reenable_entities_on_space(
mut commands: Commands,
// This query can find disabled entities,
// because it explicitly includes the `Disabled` component.
disabled_entities: Query<Entity, With<Disabled>>,
input: Res<ButtonInput<KeyCode>>,
) {
if input.just_pressed(KeyCode::Space) {
for entity in disabled_entities.iter() {
// To re-enable an entity, just remove the `Disabled` component.
commands.entity(entity).remove::<Disabled>();
}
}
}
const X_EXTENT: f32 = 900.;
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.spawn(Camera2d);
let named_shapes = [
(Name::new("Annulus"), meshes.add(Annulus::new(25.0, 50.0))),
(
Name::new("Bestagon"),
meshes.add(RegularPolygon::new(50.0, 6)),
),
(Name::new("Rhombus"), meshes.add(Rhombus::new(75.0, 100.0))),
];
let num_shapes = named_shapes.len();
for (i, (name, shape)) in named_shapes.into_iter().enumerate() {
// Distribute colors evenly across the rainbow.
let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);
commands.spawn((
name,
DisableOnClick,
Mesh2d(shape),
MeshMaterial2d(materials.add(color)),
Transform::from_xyz(
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
-X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
0.0,
0.0,
),
));
}
}
fn display_instructions(mut commands: Commands) {
commands.spawn((
Text::new(
"Click an entity to disable it.\n\nPress Space to re-enable all disabled entities.",
),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}