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