
# 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.
216 lines
6.9 KiB
Rust
216 lines
6.9 KiB
Rust
//! Demonstrates how to observe life-cycle triggers as well as define custom ones.
|
|
|
|
use bevy::{
|
|
platform::collections::{HashMap, HashSet},
|
|
prelude::*,
|
|
};
|
|
use rand::{Rng, SeedableRng};
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.init_resource::<SpatialIndex>()
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, (draw_shapes, handle_click))
|
|
// Observers are systems that run when an event is "triggered". This observer runs whenever
|
|
// `ExplodeMines` is triggered.
|
|
.add_observer(
|
|
|trigger: On<ExplodeMines>,
|
|
mines: Query<&Mine>,
|
|
index: Res<SpatialIndex>,
|
|
mut commands: Commands| {
|
|
// You can access the trigger data via the `Observer`
|
|
let event = trigger.event();
|
|
// Access resources
|
|
for e in index.get_nearby(event.pos) {
|
|
// Run queries
|
|
let mine = mines.get(e).unwrap();
|
|
if mine.pos.distance(event.pos) < mine.size + event.radius {
|
|
// And queue commands, including triggering additional events
|
|
// Here we trigger the `Explode` event for entity `e`
|
|
commands.trigger_targets(Explode, e);
|
|
}
|
|
}
|
|
},
|
|
)
|
|
// This observer runs whenever the `Mine` component is added to an entity, and places it in a simple spatial index.
|
|
.add_observer(on_add_mine)
|
|
// This observer runs whenever the `Mine` component is removed from an entity (including despawning it)
|
|
// and removes it from the spatial index.
|
|
.add_observer(on_remove_mine)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Mine {
|
|
pos: Vec2,
|
|
size: f32,
|
|
}
|
|
|
|
impl Mine {
|
|
fn random(rand: &mut ChaCha8Rng) -> Self {
|
|
Mine {
|
|
pos: Vec2::new(
|
|
(rand.r#gen::<f32>() - 0.5) * 1200.0,
|
|
(rand.r#gen::<f32>() - 0.5) * 600.0,
|
|
),
|
|
size: 4.0 + rand.r#gen::<f32>() * 16.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Event)]
|
|
struct ExplodeMines {
|
|
pos: Vec2,
|
|
radius: f32,
|
|
}
|
|
|
|
#[derive(Event)]
|
|
struct Explode;
|
|
|
|
fn setup(mut commands: Commands) {
|
|
commands.spawn(Camera2d);
|
|
commands.spawn((
|
|
Text::new(
|
|
"Click on a \"Mine\" to trigger it.\n\
|
|
When it explodes it will trigger all overlapping mines.",
|
|
),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(12.),
|
|
left: Val::Px(12.),
|
|
..default()
|
|
},
|
|
));
|
|
|
|
let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
|
|
|
|
commands
|
|
.spawn(Mine::random(&mut rng))
|
|
// Observers can watch for events targeting a specific entity.
|
|
// This will create a new observer that runs whenever the Explode event
|
|
// is triggered for this spawned entity.
|
|
.observe(explode_mine);
|
|
|
|
// We want to spawn a bunch of mines. We could just call the code above for each of them.
|
|
// That would create a new observer instance for every Mine entity. Having duplicate observers
|
|
// generally isn't worth worrying about as the overhead is low. But if you want to be maximally efficient,
|
|
// you can reuse observers across entities.
|
|
//
|
|
// First, observers are actually just entities with the Observer component! The `observe()` functions
|
|
// you've seen so far in this example are just shorthand for manually spawning an observer.
|
|
let mut observer = Observer::new(explode_mine);
|
|
|
|
// As we spawn entities, we can make this observer watch each of them:
|
|
for _ in 0..1000 {
|
|
let entity = commands.spawn(Mine::random(&mut rng)).id();
|
|
observer.watch_entity(entity);
|
|
}
|
|
|
|
// By spawning the Observer component, it becomes active!
|
|
commands.spawn(observer);
|
|
}
|
|
|
|
fn on_add_mine(trigger: On<Add, Mine>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
|
|
let mine = query.get(trigger.target().unwrap()).unwrap();
|
|
let tile = (
|
|
(mine.pos.x / CELL_SIZE).floor() as i32,
|
|
(mine.pos.y / CELL_SIZE).floor() as i32,
|
|
);
|
|
index
|
|
.map
|
|
.entry(tile)
|
|
.or_default()
|
|
.insert(trigger.target().unwrap());
|
|
}
|
|
|
|
// Remove despawned mines from our index
|
|
fn on_remove_mine(trigger: On<Remove, Mine>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
|
|
let mine = query.get(trigger.target().unwrap()).unwrap();
|
|
let tile = (
|
|
(mine.pos.x / CELL_SIZE).floor() as i32,
|
|
(mine.pos.y / CELL_SIZE).floor() as i32,
|
|
);
|
|
index.map.entry(tile).and_modify(|set| {
|
|
set.remove(&trigger.target().unwrap());
|
|
});
|
|
}
|
|
|
|
fn explode_mine(trigger: On<Explode>, query: Query<&Mine>, mut commands: Commands) {
|
|
// If a triggered event is targeting a specific entity you can access it with `.target()`
|
|
let id = trigger.target().unwrap();
|
|
let Ok(mut entity) = commands.get_entity(id) else {
|
|
return;
|
|
};
|
|
info!("Boom! {} exploded.", id.index());
|
|
entity.despawn();
|
|
let mine = query.get(id).unwrap();
|
|
// Trigger another explosion cascade.
|
|
commands.trigger(ExplodeMines {
|
|
pos: mine.pos,
|
|
radius: mine.size,
|
|
});
|
|
}
|
|
|
|
// Draw a circle for each mine using `Gizmos`
|
|
fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) {
|
|
for mine in &mines {
|
|
gizmos.circle_2d(
|
|
mine.pos,
|
|
mine.size,
|
|
Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Trigger `ExplodeMines` at the position of a given click
|
|
fn handle_click(
|
|
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
|
camera: Single<(&Camera, &GlobalTransform)>,
|
|
windows: Query<&Window>,
|
|
mut commands: Commands,
|
|
) {
|
|
let Ok(windows) = windows.single() else {
|
|
return;
|
|
};
|
|
|
|
let (camera, camera_transform) = *camera;
|
|
if let Some(pos) = windows
|
|
.cursor_position()
|
|
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
|
|
.map(|ray| ray.origin.truncate())
|
|
{
|
|
if mouse_button_input.just_pressed(MouseButton::Left) {
|
|
commands.trigger(ExplodeMines { pos, radius: 1.0 });
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
struct SpatialIndex {
|
|
map: HashMap<(i32, i32), HashSet<Entity>>,
|
|
}
|
|
|
|
/// Cell size has to be bigger than any `TriggerMine::radius`
|
|
const CELL_SIZE: f32 = 64.0;
|
|
|
|
impl SpatialIndex {
|
|
// Lookup all entities within adjacent cells of our spatial index
|
|
fn get_nearby(&self, pos: Vec2) -> Vec<Entity> {
|
|
let tile = (
|
|
(pos.x / CELL_SIZE).floor() as i32,
|
|
(pos.y / CELL_SIZE).floor() as i32,
|
|
);
|
|
let mut nearby = Vec::new();
|
|
for x in -1..2 {
|
|
for y in -1..2 {
|
|
if let Some(mines) = self.map.get(&(tile.0 + x, tile.1 + y)) {
|
|
nearby.extend(mines.iter());
|
|
}
|
|
}
|
|
}
|
|
nearby
|
|
}
|
|
}
|