Skip to content
Lesson 7 of 14

Step 1 of 5 · Reading · ~3 min

Read

Entity-Component System

Events & Messaging

Systems should not call each other. If the collision system had to know about the sound system, the score system and the particle system, adding a fourth would mean editing collision. Events break that link. After this lesson your program will run an event bus: producers send typed events, any number of consumers read them at their own pace, and old events expire on schedule.

Facts, not calls

A collision system that finds a hit sends a Hit event carrying the two entities. It neither knows nor cares who listens. Audio plays a sound, the HUD flashes, the score system adds points, and each of them can be written, tested and removed on its own. Events are plain data, not callbacks: they sit in a buffer until a system asks for them, which keeps execution order explicit and replays possible.

Broadcast with a cursor per reader

Every event of a type gets the next id for that type (1, 2, 3...). A reader remembers the id of the last event it was shown, its cursor. Reading returns the stored events newer than the cursor, oldest first, then moves the cursor up to date.

  • Two readers each see every event once, because each has its own cursor.
  • The same reader reading twice in a row sees nothing the second time.
  • A reader of one type does not disturb its cursor for another type.

The mistake to avoid is a single "delivered" flag on the event: the first reader would consume it for everyone.

How long an event lives

Events cannot live forever, and they cannot vanish the moment they are read, because other readers may not have run yet. Engines such as Bevy keep two buffers and swap them at the frame boundary, so an event survives two boundaries: the frame it was sent in and the whole next frame. After the second boundary it is dropped.

Two frames rather than one matters when a consumer is scheduled before the producer. In frame N it runs, sees nothing, and the producer sends afterwards. At the boundary the event is still alive, so the consumer reads it early in frame N+1. One frame of latency, but never a missed event.

The price: a reader that skips two consecutive frames silently loses events. Systems that read events run every frame.

Draining: events that exactly one handler must see

Broadcast suits facts ("a hit happened"). Commands ("spawn this enemy", "load this level") must be handled once. For those, a drain returns everything stored for a type and removes it for everybody.

Worked trace

SEND hit a1
READ audio hit    audio hit: a1
UPDATE            (a1 is one boundary old)
SEND hit a2
READ ui hit       ui hit: a1 a2        ui had never read, a1 is still alive
UPDATE            (a1 is dropped, a2 is one boundary old)
READ audio hit    audio hit: a2        audio already saw a1
READ ui hit       ui hit: (none)       ui caught up on both
UPDATE            (a2 is dropped)

Common mistakes

  • Removing an event when the first reader reads it. Later readers see nothing.
  • One shared cursor per type. The second reader of the frame is told there is nothing new.
  • Dropping events at the first boundary. A consumer scheduled before its producer never sees anything.
  • Keeping events forever. The buffer grows without bound.
  • Chains of events (A causes B causes C causes D). They work, and they are miserable to debug; prefer a system that reads A and does the whole job.

Your exercise

Build the bus described in the exercise. The starter parses the commands and prints each result; you write send, update, read and drain. The most likely mistake is using one cursor for all readers.

Up nextCollision DetectionEntity-Component System

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…