Tainted\\Coders

Bevy Events

Bevy version: 0.19Last updated:

Events and messages let us decouple what happened from what should happen by allowing communication between systems.

There are two kinds of events in Bevy:

  1. Message for communication between systems
  2. Event and EntityEvent for observers that react when events are triggered

Messages

A Message is the core part of Bevy's buffered queue style event system. These messages live for two update cycles, so a MessageReader can read them in the frame after they're sent.

Systems can use the MessageReader system parameter to read messages from the queue. Each reader contains a Local<MessageCursor> that holds the system's progress in reading from the Messages<T> resource. This is how each system ensures that it is not reading the same message twice.

MessageWriter is the system parameter we use to write messages to a queue. These are stored inside a Messages<T> resource, which manages two message buffers plus some convenient accessors.

Messages are preferable for one-to-many systems that need to know something has happened.

Messages are also mutable using a MessageMutator system parameter. This mutation can be useful for chains of systems that need to change the message before sending it on.

A system cannot have both a MessageReader<T> and a MessageWriter<T> for the same message type. Use MessageMutator<T> if you need to read and write the same messages in one system.

Each message is tracked individually using its MessageId. These IDs auto increment based on the order the messages were sent.

Messages are double buffered

Messages<T> is a collection that acts as a double buffered queue.

pub struct Messages<M: Message> {
    /// Holds the oldest still active messages.
    pub(crate) messages_a: MessageSequence<M>,
    /// Holds the newer messages.
    pub(crate) messages_b: MessageSequence<M>,
    pub(crate) message_count: usize,
}

Double buffering is done to ensure each system has an opportunity to see each message. It helps systems avoid having to care about the exact ordering within a frame.

To illustrate this double buffering, imagine you have a game with a system that publishes messages when the player gets detected with a PlayerDetected message.

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_systems(
      Update,
      (
        on_player_detected,
        detect_player.after(on_player_detected)
      )
    )
    .run()
}

When you start your game both buffers start out empty:

messages_a: []
messages_b: []

Then we publish a message from detect_player using a MessageWriter. Writes always go to messages_b:

messages_a: []
messages_b: [PlayerDetected]

Unfortunately detect_player runs after our reading system on_player_detected. If we only had one buffer and we wiped it then the on_player_detected system would never get to read the published message.

Instead, at the end of this game tick message_update_system calls Messages::update, which swaps the two buffers and then clears the new messages_b (the buffer that was messages_a before the swap). Our message moves into messages_a and is kept for one more frame:

messages_a: [PlayerDetected]
messages_b: []

Now on_player_detected can read the message on the next frame, even though it ran before the writer.

If more messages are published while that next frame runs they fill messages_b, so during a frame readers see both the previous frame's messages (in messages_a) and the current frame's messages (in messages_b):

messages_a: [PlayerDetected]
messages_b: [PlayerDetected]

This lets the systems earlier in the frame catch up by reading the last frame's messages.

At the end of the following tick Messages::update swaps and clears again. The message from the first frame is now more than one frame old and is dropped silently, while the message written this frame moves into messages_a:

messages_a: [PlayerDetected]
messages_b: []

In this way our systems have access to both this frame and the last frame's messages. So it shouldn't matter how we order our systems to be before or after we fire our messages, it will have access to both.

Adding messages

Messages are defined just like our resources and components where we define a type that derives Message:

// With a marker message
#[derive(Message)]
struct PlayerKilled;

// With a unit type
#[derive(Message)]
struct PlayerDetected(Entity);

// With fields
#[derive(Message)]
struct PlayerDamaged {
  entity: Entity,
  damage: f32,
}

Then we add our message to our App, similar to how we manage our assets:

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_message::<PlayerKilled>()
    .add_message::<PlayerDetected>()
    .run();
}

When we add_message, Bevy registers the Messages<T> resource with the MessageRegistry.

The message_update_system, scheduled by Bevy in First, then updates every registered message type each frame by calling Messages<T>::update. This prevents our messages from growing unbounded, eventually exhausting the queue.

message_update_system is gated by a run condition, so when a fixed timestep is in use messages are only updated after a fixed update has run instead of every render frame.

This also means that if your messages are not consumed within two updates then they will be cleaned up and dropped silently.

Writing messages

Messages are written to a double buffered queue. This just means that messages produced are stored for two frames.

This prevents a situation where a system that ran earlier in the frame misses a message produced later in the same frame. That is very common because Bevy runs systems in parallel and does not guarantee their order.

For example, if you had defined a system to only run conditionally, then its possible to miss messages during the frames it was not called.

To write messages to a stream we use a MessageWriter<T>. Any two systems that use the same message writer type will not be run in parallel as they both use mutable access to Messages<T>.

fn detect_player(
  mut messages: MessageWriter<PlayerDetected>,
  players: Query<(Entity, &Transform), With<Player>>,
) {
  for (entity, transform) in players {
    // ...
    messages.write(PlayerDetected(entity));
  }
}

Each MessageWriter<T> can only write messages for one type that is known during compile time. There may be times where you don't know this type and as a workaround you can send type erased messages through your Commands:

commands.queue(|world: &mut World| {
  world.write_message(MyMessage);
});

Reading messages

This double buffering strategy means that we must consume our messages steadily each frame or risk losing them. We can read messages from our systems with a MessageReader<T> that consumes messages from our buffers:

fn react_to_detection(mut messages: MessageReader<PlayerDetected>) {
  for message in messages.read() {
    // Do something with each event here
  }
}

A MessageReader system parameter tracks the consumption of these messages on a per-system basis using a Local<MessageCursor>, which guarantees each system an opportunity to read each message once.

If you have many different types of messages you want handled the same way you can use a generic system and a Messages resource:

fn handle_message<T: Message>(mut messages: ResMut<Messages<T>>) {
  // We can clear messages this frame
  messages.clear();

  // Or clear messages next frame (bevy default)
  messages.update();

  // Or consume our messages right here and now
  for message in messages.drain() {
    // ...
  }
}

fn main() {
  App::new()
    .add_message::<PlayerKilled>()
    .add_systems(Update, handle_message::<PlayerKilled>)
    .run();
}

So we can say that the Messages<T> resource represents a collection of all messages of the type that occurred in the last two update calls.

If you want to skip a system entirely when there are no new messages, use PopulatedMessageReader<T> or the on_message::<T> run condition instead of a plain MessageReader<T>.

Events

Bevy implements Event as a trait with a Trigger type.

trait Event {
  type Trigger<'a>: Trigger<Self>;
}

We derive this trait on our own event structs:

#[derive(Event)]
struct GameStarted;

#[derive(EntityEvent)]
struct PlayerKilled {
  entity: Entity,
}

These events come in two flavors:

  1. Event for global events defined with a GlobalTrigger
  2. EntityEvent for entity specific events defined with an EntityTrigger, or with a PropagateEntityTrigger when propagation is enabled

The Trigger type is what Bevy uses to decide which observers run, the order in which propagation visits targets, and what data the observers are passed. This is defined within the type system which helps to prevent misuse between different event flavors.

GlobalTrigger runs all global observers, the ones defined on the App that match the specific event it is defined on. In fact, the EntityTrigger does the same thing as GlobalTrigger, but additionally runs any entity scoped triggers.

An Observer is the callback system that receives these events. Observers also happen to come in two distinct categories:

  1. Global observers
  2. Entity observers

To call these callbacks, we need to trigger events that they are interested in:

// World::trigger runs the observers immediately
world.trigger(SomeEvent)

// Commands::trigger queues the trigger as a command, so the observers run at
// the next sync point in the schedule instead
commands.trigger(SomeEvent)

// Entity events work the same way: the event carries its own target entity
commands.trigger(SomeEntityEvent { entity })

In addition to our own custom events, Bevy also has built-in events for each part of the component life-cycle:

TypeDescription
AddTriggers when a component is added
InsertTriggers when a component is inserted
DiscardTriggers when a component is discarded
RemoveTriggers when a component is removed
DespawnTriggers for each component on an entity when it is despawned

The difference between Add/Insert and Discard/Remove is about whether or not the component is replaced.

Global observers

If we want a global observer that listens to events globally we can add the observer to our App definition:

#[derive(Component, Debug)]
struct Position(Vec2);

#[derive(Component)]
struct Enemy;

fn on_respawn(event: On<Add, Enemy>, query: Query<(&Enemy, &Position)>) {
  let (enemy, position) = query.get(event.entity).unwrap();
  println!("Enemy was respawned at {:?}", position);
}

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_observer(on_respawn);
}

Any time an Enemy component is added, our on_respawn system will fire. Observer based systems must have On as their first argument.

Entity observers

If we use an entity observer we can choose to react to events that are triggered on a specific entity.

We define EntityEvent and provide an entity field:

#[derive(EntityEvent)]
struct BossSpawned {
  entity: Entity,
}

Then we need to define an observer

fn on_boss_spawned(event: On<BossSpawned>, query: Query<(&Enemy, &Position)>) {
  if let Ok((enemy, position)) = query.get(event.entity) {
    println!("Boss was spawned at {:?}", position);
  }
}

When we spawn the entity, we can add this observer to that specific entity. Later, in any other system (or the same one like below) when you trigger your event with that entity, that observer will run.

fn spawn_boss(mut commands: Commands) {
  let entity = commands
    .spawn((Enemy, Boss, Position(Vec2::ZERO)))
    .observe(on_boss_spawned)
    .id();

  commands.trigger(BossSpawned { entity });
}

An EntityEvent that enables propagation will bubble up a hierarchy along a Traversal (by default ChildOf, which goes from child to parent). Propagation is opt-in: it only happens if the event enables it, and (unless the event uses auto_propagate) only after an observer calls On::propagate(true).

When events are propagated they are re-sent to their next target while keeping track of where they started through On::original_event_target(). This propagation continues until the chain reaches a dead-end, or the observer handling the propagation manually stops it with On::propagate(false).

Triggers

The generic arguments for On can be somewhat confusing:

pub struct On<'w, 't, E: Event, B: Bundle = ()> {
  // ...
}

The first generic argument E is the event.

The second generic argument B is optional. A good mental model is to think that the second generic argument is only meaningful for Bevy's built-in lifecycle events. Observers for your own events usually leave it as the default ().

You should be aware that when using Bevy's built in trigger events the second generic B can be a bundle of components, and that it is going to act as a filter of those components using OR not AND logic.

So this observer:

fn on_respawn(
  event: On<Add, (Enemy, Person)>,
  // ...
)

Is going to trigger when an Enemy OR a Person is added.

Event propagation

You can set events to automatically propagate themselves according to a relationship.

#[derive(EntityEvent)]
#[entity_event(auto_propagate, propagate = &'static ChildOf)]
struct LocationTravelled {
  #[event_target]
  entity: Entity,
}

With ChildOf as the traversal, a LocationTravelled event triggered on an entity that has a ChildOf component is automatically triggered again for its parent.

#[derive(Component, Default)]
struct Ship;

#[derive(Component)]
struct Player;

fn spawn_player(mut commands: Commands) {
  // Adding `Player` as a child of `Ship`
  let ship = commands.spawn(Ship).id();
  let player = commands.spawn((Player, ChildOf(ship))).id();

  // Trigger the event on the player. It propagates up the `ChildOf`
  // relationship, so observers on the ship will also run.
  commands.trigger(LocationTravelled { entity: player });
}

In this example, even though we triggered the event on the Player, an additional event for the Ship would also be sent to any relevant observers.

This can be useful in situations like picking where the actual event triggers on something like a Mesh but you want to handle the event on a related component somewhere in its hierarchy.

Run conditions

Observers can be added with run conditions:

#[derive(Resource)]
struct GamePaused(bool);

// Observer only runs when game is not paused
app.add_observer(
  on_damage.run_if(|paused: Res<GamePaused>| !paused.0)
);

// Multiple conditions can be chained (AND semantics)
app.add_observer(
  on_damage
    .run_if(|paused: Res<GamePaused>| !paused.0)
    .run_if(any_with_component::<Player>)
);

These run conditions work the same way as the ones added to your scheduled systems, with one difference: chained .run_if() calls do not short-circuit, so every condition is evaluated on each trigger. If you need short-circuit behavior, combine the conditions with .and(...).

Choosing messages vs events

Events can be easier to reason about if we care about the effects of our events happening within a single frame.

Observers are processed when a key (the On trigger) is used to lookup a set of values (the observing systems) which are iterated in an arbitrary order. Note that only World::trigger runs them immediately. Commands::trigger defers them to the next sync point in the schedule just like our usual commands.

Another good use case for observers is events we want to handle from only certain entities and not others of the same type as they can be triggered per entity.

On the other hand, systems that read Messages<T> can be put in a specific order.

Observers cannot currently be explicitly ordered. This becomes a problem if your observers depend on other observers having run before them.

Messages also help you decouple systems from each other. We can send a message and not have to control who exactly consumes it. For observers, the producers of the event need to explicitly know who to trigger the event for.

This table shows the key differences:

EventsMessages
Optimal event frequencyInfrequentFrequent
HandlerOnly handles a single eventCan handle many messages together
LatencyImmediate with World::trigger; next sync point with Commands::triggerUp to 1 frame
Event propagationBubblingNone
ScopeWorld or EntityWorld
OrderingNo explicit orderOrdered
CouplingHighLow

There can be multiple ways of defining the same behavior between the two so it is more up to your game's specific constraints.

Examples of good use cases for observers:

Examples of good use cases for events: