Tainted \\ Coders

Bevy Events

Last updated:

In Bevy events let us communicate between systems. We can write events in one system and then read them in another to trigger our game logic.

Events are created by an EventWriter and read by an EventReader which we access in our systems as parameters.

The EventReader will track which systems have read which events so that each event can be consumed by all systems that are interested.

Adding your events

We define events as a type that derives Event:

#[derive(Event)]
struct PlayerKilled(Entity);

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 a system gets added for that type: Events<T>::update_system.

This system runs each frame cleaning up any unconsumed events by calling Events::update.

If Events::update is 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

Events are written to a double buffered queue. Double buffered just means that events produced are stored for two frames.

When you write an event you are adding it to the current buffer, which becomes the previous buffer during the next frame.

To write events from our systems we use an EventWriter:

#[derive(Event)]
struct PlayerDetected(Entity);

#[derive(Component)]
struct Player;

fn detect_player(
    mut events: EventWriter<PlayerDetected>,
    players: Query<(Entity, &Transform), With<Player>>
) {
    for (entity, transform) in players.iter() {
        // ...
        events.send(PlayerDetected(entity));
    }
}

So if we have one player in our game, after the first tick our buffers look like this:

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.

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

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();
}

The Events<T> resource represents a collection of all events of the type that occurred in the last two update calls.

Read more