bevy/examples/ecs/observers.rs

210 lines
6.6 KiB
Rust
Raw Normal View History

Generalised ECS reactivity with Observers (#10839) # Objective - Provide an expressive way to register dynamic behavior in response to ECS changes that is consistent with existing bevy types and traits as to provide a smooth user experience. - Provide a mechanism for immediate changes in response to events during command application in order to facilitate improved query caching on the path to relations. ## Solution - A new fundamental ECS construct, the `Observer`; inspired by flec's observers but adapted to better fit bevy's access patterns and rust's type system. --- ## Examples There are 3 main ways to register observers. The first is a "component observer" that looks like this: ```rust world.observe(|trigger: Trigger<OnAdd, Transform>, query: Query<&Transform>| { let transform = query.get(trigger.entity()).unwrap(); }); ``` The above code will spawn a new entity representing the observer that will run it's callback whenever the `Transform` component is added to an entity. This is a system-like function that supports dependency injection for all the standard bevy types: `Query`, `Res`, `Commands` etc. It also has a `Trigger` parameter that provides information about the trigger such as the target entity, and the event being triggered. Importantly these systems run during command application which is key for their future use to keep ECS internals up to date. There are similar events for `OnInsert` and `OnRemove`, and this will be expanded with things such as `ArchetypeCreated`, `TableEmpty` etc. in follow up PRs. Another way to register an observer is an "entity observer" that looks like this: ```rust world.entity_mut(entity).observe(|trigger: Trigger<Resize>| { // ... }); ``` Entity observers run whenever an event of their type is triggered targeting that specific entity. This type of observer will de-spawn itself if the entity (or entities) it is observing is ever de-spawned so as to not leave dangling observers. Entity observers can also be spawned from deferred contexts such as other observers, systems, or hooks using commands: ```rust commands.entity(entity).observe(|trigger: Trigger<Resize>| { // ... }); ``` Observers are not limited to in built event types, they can be used with any type that implements `Event` (which has been extended to implement Component). This means events can also carry data: ```rust #[derive(Event)] struct Resize { x: u32, y: u32 } commands.entity(entity).observe(|trigger: Trigger<Resize>, query: Query<&mut Size>| { let event = trigger.event(); // ... }); // Will trigger the observer when commands are applied. commands.trigger_targets(Resize { x: 10, y: 10 }, entity); ``` You can also trigger events that target more than one entity at a time: ```rust commands.trigger_targets(Resize { x: 10, y: 10 }, [e1, e2]); ``` Additionally, Observers don't _need_ entity targets: ```rust app.observe(|trigger: Trigger<Quit>| { }) commands.trigger(Quit); ``` In these cases, `trigger.entity()` will be a placeholder. Observers are actually just normal entities with an `ObserverState` and `Observer` component! The `observe()` functions above are just shorthand for: ```rust world.spawn(Observer::new(|trigger: Trigger<Resize>| {}); ``` This will spawn the `Observer` system and use an `on_add` hook to add the `ObserverState` component. Dynamic components and trigger types are also fully supported allowing for runtime defined trigger types. ## Possible Follow-ups 1. Deprecate `RemovedComponents`, observers should fulfill all use cases while being more flexible and performant. 2. Queries as entities: Swap queries to entities and begin using observers listening to archetype creation triggers to keep their caches in sync, this allows unification of `ObserverState` and `QueryState` as well as unlocking several API improvements for `Query` and the management of `QueryState`. 3. Trigger bubbling: For some UI use cases in particular users are likely to want some form of bubbling for entity observers, this is trivial to implement naively but ideally this includes an acceleration structure to cache hierarchy traversals. 4. All kinds of other in-built trigger types. 5. Optimization; in order to not bloat the complexity of the PR I have kept the implementation straightforward, there are several areas where performance can be improved. The focus for this PR is to get the behavior implemented and not incur a performance cost for users who don't use observers. I am leaving each of these to follow up PR's in order to keep each of them reviewable as this already includes significant changes. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: MiniaczQ <xnetroidpl@gmail.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
2024-06-15 01:33:26 +00:00
//! Demonstrates how to observe life-cycle triggers as well as define custom ones.
use bevy::{
prelude::*,
utils::{HashMap, HashSet},
};
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.
.observe(
|trigger: Trigger<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.
.observe(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.
.observe(on_remove_mine)
.run();
}
#[derive(Component)]
struct Mine {
pos: Vec2,
size: f32,
}
impl Mine {
fn random() -> Self {
Mine {
pos: Vec2::new(
(rand::random::<f32>() - 0.5) * 1200.0,
(rand::random::<f32>() - 0.5) * 600.0,
),
size: 4.0 + rand::random::<f32>() * 16.0,
}
}
}
#[derive(Event)]
struct ExplodeMines {
pos: Vec2,
radius: f32,
}
#[derive(Event)]
struct Explode;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
commands.spawn(TextBundle::from_section(
"Click on a \"Mine\" to trigger it.\n\
When it explodes it will trigger all overlapping mines.",
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24.,
color: Color::WHITE,
},
));
commands
.spawn(Mine::random())
// 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()).id();
observer.watch_entity(entity);
}
// By spawning the Observer component, it becomes active!
commands.spawn(observer);
}
fn on_add_mine(
trigger: Trigger<OnAdd, Mine>,
query: Query<&Mine>,
mut index: ResMut<SpatialIndex>,
) {
let mine = query.get(trigger.entity()).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.entity());
}
// Remove despawned mines from our index
fn on_remove_mine(
trigger: Trigger<OnRemove, Mine>,
query: Query<&Mine>,
mut index: ResMut<SpatialIndex>,
) {
let mine = query.get(trigger.entity()).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.entity());
});
}
fn explode_mine(trigger: Trigger<Explode>, query: Query<&Mine>, mut commands: Commands) {
// If a triggered event is targeting a specific entity you can access it with `.entity()`
let id = trigger.entity();
let Some(mut entity) = commands.get_entity(id) else {
return;
};
println!("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: Query<(&Camera, &GlobalTransform)>,
windows: Query<&Window>,
mut commands: Commands,
) {
let (camera, camera_transform) = camera.single();
if let Some(pos) = windows
.single()
.cursor_position()
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
.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
}
}