Bevy Events
Events let us communicate between systems. We can write events in one system and then read them in another to trigger our game logic.
They are our main way to decouple what happened from what should happen so our games can be more extensible.
These events can be sent to one (or both) of these places:
- The event stream
- Observers
Each event type we define and add to our App
is added to the EventRegistry
.
Every event automatically implements Component
. Although we don't usually add them to any of our entities. Instead, Bevy uses the ComponentId
to identify each event type.
Each Event
is tracked individually using its EventId
. These IDs auto increment based on the order the events were sent.
The event stream
When we send events to the event stream its stored inside an Events<T>
resource.
This resource is a collection that acts as a double buffered queue. Double buffering is done to ensure a system will get every event, even if they are fired towards the end of a frame.
The EventReader
will track which systems have read which events so that each event can be consumed by all systems that are interested.
To illustrate, imagine you have a game with a PlayerDetected
event that just got written to the stream. We would have two buffers, with the event placed in the current buffer:
Buffer A (current): [PlayerDetected]
Buffer B (previous): []
At the end of this game tick the oldest buffer (Buffer B) will be cleared and made to be our primary buffer.
When our detect_player
system runs again on the next frame our buffers will look like this:
Buffer A (previous): [PlayerDetected]
Buffer B (current): [PlayerDetected]
Then at the end of that tick the oldest buffer (Buffer A) is cleared and set to be our current buffer to write events to, leaving our buffer looking like:
Buffer A (current): []
Buffer B (previous): [PlayerDetected]
In this way our systems have access to both this frame and the last frames events. So it shouldn't matter how we order our systems to be before or after we fire our events, it will have access to both.
Adding events
Events are defined just like our resources and components.
We define events as a type that derives Event
:
#[derive(Event)]
struct PlayerKilled;
#[derive(Event)]
struct PlayerDetected(Entity);
#[derive(Event)]
struct PlayerDamaged {
entity: Entity,
damage: f32,
}
Then we add our event to our App
, similar to how we manage our assets:
fn main() {
App::new()
.add_event::<PlayerKilled>();
}
When we add_event
, Bevy adds a system for handling that specific type: Events<T>::event_update_system
.
This system runs each frame, cleaning up any unconsumed events by calling Events<T>::update
. If this function were not called, then our events will grow unbounded eventually exhausting the queue.
This also means that if your events are not consumed by the next frame then they will be cleaned up and dropped silently.
Writing events to the stream
Events are written to a double buffered queue. This just means that events produced are stored for two frames.
This prevents a situation where a system that was called earlier will miss the event, which is very common because Bevy is working hard to run our systems in parallel.
For example, if you had defined a system to only run conditionally, then its possible to miss events during the frames it was not called.
To write events to the stream we use an EventWriter
. Any two systems that use the same event writer type will not be run in parallel as they both use mutable access to Events<T>
.
fn detect_player(
mut events: EventWriter<PlayerDetected>,
players: Query<(Entity, &Transform), With<Player>>
) {
for (entity, transform) in players.iter() {
// ...
events.send(PlayerDetected(entity));
}
}
Each EventWriter
can only write events for one type that is known during compile time. There may be times where you don't know this type and as a work around you can send type erased events through your Commands
commands.add(|w: &mut World| {
w.send_event(MyEvent);
});
Reading events from the stream
This double buffering strategy means that we must consume our events steadily each frame or risk losing them. We can read events from our systems with an EventReader
that consumes events from our buffers:
fn react_to_detection(
mut events: EventReader<PlayerDetected>
) {
for event in events.read() {
// Do something with each event here
}
}
If you have many different types of events you want handled the same way you can use a generic system and an Events
resource:
fn handle_event<T: Event>(
mut events: ResMut<Events<T>>
) {
// We can clear events this frame
events.clear();
// Or clear events next frame (bevy default)
events.update();
// Or consume our events right here and now
for event in events.drain() {
// ...
}
}
fn main() {
App::new()
.init_resource::<Events<PlayerKilled>>()
.add_systems(Update, handle_event::<PlayerKilled>)
.run();
}
So we can say that the Events<T>
resource represents a collection of all events of the type that occurred in the last two update calls.
Observers
An Observer
is a system that listens for a Trigger
. Each trigger is for a specific event type.
It is important to note that when you send events using an EventWriter
, they do not automatically trigger our observers. We have to trigger them manually, usually using Commands
:
commands.trigger(SomeEvent)
This is not the same as writing an event to an event stream like with EventWriter
. Instead, these events are sent directly to the observer and handled immediately. Not sent to your Events<T>
collection.
Bevy has some built in triggers that we can use to hook into:
Type | Description |
---|---|
OnAdd | Triggers when an entity is added |
OnInsert | Triggers when an entity is inserted |
OnRemove | Triggers when an entity is despawned |
To create an observer, we can add it to our App
definition:
#[derive(Component, Debug)]
struct Position(Vec2);
#[derive(Component)]
struct Enemy;
fn on_respawn(
trigger: Trigger<OnAdd, Enemy>,
query: Query<(&Enemy, &Position)>,
) {
let (enemy, position) = query.get(trigger.entity()).unwrap();
println!("Enemy was respawned at {:?}", position);
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.observe(on_respawn);
}
The first generic argument of Trigger
is the event, the second is optional and you can think of like an argument to the event type. Most observers you create will only take one.
If we want more control, we can choose to react to a type of event and only call our system with a specific entity:
#[derive(Component)]
struct Boss;
#[derive(Event)]
struct BossSpawned;
fn on_boss_spawned(
trigger: Trigger<BossSpawned>,
query: Query<(&Enemy, &Position)>,
) {
let (enemy, position) = query.get(trigger.entity()).unwrap();
println!("Boss was spawned at {:?}", position);
}
fn spawn_boss(
mut commands: Commands,
) {
commands.spawn((Enemy, Boss)).observe(on_boss_spawned);
commands.trigger(BossSpawned);
}
Observers added this way are actually created as an EntityObserver
which will use component hooks to only send our system specific entities.