Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ecs/observers.rs
6849 views
1
//! Demonstrates how to observe events: both component lifecycle events and custom events.
2
3
use bevy::{
4
platform::collections::{HashMap, HashSet},
5
prelude::*,
6
};
7
use rand::{Rng, SeedableRng};
8
use rand_chacha::ChaCha8Rng;
9
10
fn main() {
11
App::new()
12
.add_plugins(DefaultPlugins)
13
.init_resource::<SpatialIndex>()
14
.add_systems(Startup, setup)
15
.add_systems(Update, (draw_shapes, handle_click))
16
// Observers are systems that run when an event is "triggered". This observer runs whenever
17
// `ExplodeMines` is triggered.
18
.add_observer(
19
|explode_mines: On<ExplodeMines>,
20
mines: Query<&Mine>,
21
index: Res<SpatialIndex>,
22
mut commands: Commands| {
23
// Access resources
24
for entity in index.get_nearby(explode_mines.pos) {
25
// Run queries
26
let mine = mines.get(entity).unwrap();
27
if mine.pos.distance(explode_mines.pos) < mine.size + explode_mines.radius {
28
// And queue commands, including triggering additional events
29
// Here we trigger the `Explode` event for entity `e`
30
commands.trigger(Explode { entity });
31
}
32
}
33
},
34
)
35
// This observer runs whenever the `Mine` component is added to an entity, and places it in a simple spatial index.
36
.add_observer(on_add_mine)
37
// This observer runs whenever the `Mine` component is removed from an entity (including despawning it)
38
// and removes it from the spatial index.
39
.add_observer(on_remove_mine)
40
.run();
41
}
42
43
#[derive(Component)]
44
struct Mine {
45
pos: Vec2,
46
size: f32,
47
}
48
49
impl Mine {
50
fn random(rand: &mut ChaCha8Rng) -> Self {
51
Mine {
52
pos: Vec2::new(
53
(rand.random::<f32>() - 0.5) * 1200.0,
54
(rand.random::<f32>() - 0.5) * 600.0,
55
),
56
size: 4.0 + rand.random::<f32>() * 16.0,
57
}
58
}
59
}
60
61
/// This is a normal [`Event`]. Any observer that watches for it will run when it is triggered.
62
#[derive(Event)]
63
struct ExplodeMines {
64
pos: Vec2,
65
radius: f32,
66
}
67
68
/// An [`EntityEvent`] is a specialized type of [`Event`] that can target a specific entity. In addition to
69
/// running normal "top level" observers when it is triggered (which target _any_ entity that Explodes), it will
70
/// also run any observers that target the _specific_ entity for that event.
71
#[derive(EntityEvent)]
72
struct Explode {
73
entity: Entity,
74
}
75
76
fn setup(mut commands: Commands) {
77
commands.spawn(Camera2d);
78
commands.spawn((
79
Text::new(
80
"Click on a \"Mine\" to trigger it.\n\
81
When it explodes it will trigger all overlapping mines.",
82
),
83
Node {
84
position_type: PositionType::Absolute,
85
top: px(12),
86
left: px(12),
87
..default()
88
},
89
));
90
91
let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
92
93
commands
94
.spawn(Mine::random(&mut rng))
95
// Observers can watch for events targeting a specific entity.
96
// This will create a new observer that runs whenever the Explode event
97
// is triggered for this spawned entity.
98
.observe(explode_mine);
99
100
// We want to spawn a bunch of mines. We could just call the code above for each of them.
101
// That would create a new observer instance for every Mine entity. Having duplicate observers
102
// generally isn't worth worrying about as the overhead is low. But if you want to be maximally efficient,
103
// you can reuse observers across entities.
104
//
105
// First, observers are actually just entities with the Observer component! The `observe()` functions
106
// you've seen so far in this example are just shorthand for manually spawning an observer.
107
let mut observer = Observer::new(explode_mine);
108
109
// As we spawn entities, we can make this observer watch each of them:
110
for _ in 0..1000 {
111
let entity = commands.spawn(Mine::random(&mut rng)).id();
112
observer.watch_entity(entity);
113
}
114
115
// By spawning the Observer component, it becomes active!
116
commands.spawn(observer);
117
}
118
119
fn on_add_mine(add: On<Add, Mine>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
120
let mine = query.get(add.entity).unwrap();
121
let tile = (
122
(mine.pos.x / CELL_SIZE).floor() as i32,
123
(mine.pos.y / CELL_SIZE).floor() as i32,
124
);
125
index.map.entry(tile).or_default().insert(add.entity);
126
}
127
128
// Remove despawned mines from our index
129
fn on_remove_mine(remove: On<Remove, Mine>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
130
let mine = query.get(remove.entity).unwrap();
131
let tile = (
132
(mine.pos.x / CELL_SIZE).floor() as i32,
133
(mine.pos.y / CELL_SIZE).floor() as i32,
134
);
135
index.map.entry(tile).and_modify(|set| {
136
set.remove(&remove.entity);
137
});
138
}
139
140
fn explode_mine(explode: On<Explode>, query: Query<&Mine>, mut commands: Commands) {
141
// Explode is an EntityEvent. `explode.entity` is the entity that Explode was triggered for.
142
let Ok(mut entity) = commands.get_entity(explode.entity) else {
143
return;
144
};
145
info!("Boom! {} exploded.", explode.entity);
146
entity.despawn();
147
let mine = query.get(explode.entity).unwrap();
148
// Trigger another explosion cascade.
149
commands.trigger(ExplodeMines {
150
pos: mine.pos,
151
radius: mine.size,
152
});
153
}
154
155
// Draw a circle for each mine using `Gizmos`
156
fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) {
157
for mine in &mines {
158
gizmos.circle_2d(
159
mine.pos,
160
mine.size,
161
Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8),
162
);
163
}
164
}
165
166
// Trigger `ExplodeMines` at the position of a given click
167
fn handle_click(
168
mouse_button_input: Res<ButtonInput<MouseButton>>,
169
camera: Single<(&Camera, &GlobalTransform)>,
170
windows: Query<&Window>,
171
mut commands: Commands,
172
) {
173
let Ok(windows) = windows.single() else {
174
return;
175
};
176
177
let (camera, camera_transform) = *camera;
178
if let Some(pos) = windows
179
.cursor_position()
180
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
181
.map(|ray| ray.origin.truncate())
182
&& mouse_button_input.just_pressed(MouseButton::Left)
183
{
184
commands.trigger(ExplodeMines { pos, radius: 1.0 });
185
}
186
}
187
188
#[derive(Resource, Default)]
189
struct SpatialIndex {
190
map: HashMap<(i32, i32), HashSet<Entity>>,
191
}
192
193
/// Cell size has to be bigger than any `TriggerMine::radius`
194
const CELL_SIZE: f32 = 64.0;
195
196
impl SpatialIndex {
197
// Lookup all entities within adjacent cells of our spatial index
198
fn get_nearby(&self, pos: Vec2) -> Vec<Entity> {
199
let tile = (
200
(pos.x / CELL_SIZE).floor() as i32,
201
(pos.y / CELL_SIZE).floor() as i32,
202
);
203
let mut nearby = Vec::new();
204
for x in -1..2 {
205
for y in -1..2 {
206
if let Some(mines) = self.map.get(&(tile.0 + x, tile.1 + y)) {
207
nearby.extend(mines.iter());
208
}
209
}
210
}
211
nearby
212
}
213
}
214
215