No history yet

Integrating Event Streams

The Event Bridge

In a standard ECS architecture, systems often poll for changes. A HealthSystem might check every entity with a HealthComponent each frame to see if its value dropped. This works, but it's not efficient. Systems end up doing a lot of work just to find out that nothing has changed.

By introducing events, we shift from a polling model to a reactive one. Instead of constantly asking "Has anything changed?", systems are simply told when something important happens. This creates the ENCES pattern: Entity, Component, Event, System. Events become the primary communication bridge, decoupling the cause of a state change from the reaction to it.

This decouples the 'what happened' (the Event) from the 'how to react' (the System). A single PlayerTookDamage event can trigger a sound effect, a screen flash, and a log entry, all without the damage-inflicting system knowing anything about audio or UI.

The Event Bus

The central nervous system of this reactive architecture is the Event Bus. Think of it as a post office. Any part of the code can send a letter (an event) to the post office. The post office doesn't read the letter; it just looks at the address (the event type) and delivers it to everyone who has subscribed to that address (the listener systems).

This publish-subscribe (pub-sub) model is incredibly powerful. A PhysicsSystem can publish a CollisionEvent without needing to know that an AudioSystem, a ScoreSystem, and a ParticleSystem are all listening for it. The systems are completely independent of each other, yet they can coordinate their behaviour through the central bus.

This design keeps systems focused on their single responsibility. The PhysicsSystem handles physics. The AudioSystem handles audio. Neither needs dependencies on the other, which makes the codebase easier to manage, test, and extend.

Sync vs. Async Dispatch

When an event is published, when should its listeners react? This choice leads to two dispatch strategies: synchronous and asynchronous.

Synchronous dispatch is immediate and blocking. When an event is fired, the event bus immediately calls all listener functions. The code that fired the event pauses and waits for every single listener to finish its work before it continues. This is useful for events that require an immediate response, where the outcome of one system's reaction might affect the next step in the logic.

For example, if a PlayerDiesEvent is fired, you want to stop player input and show a game-over screen immediately, not a few milliseconds later. The blocking nature guarantees that by the time the publish() call returns, all death-related logic has been executed.

Use synchronous dispatch when you need a guaranteed order of operations and immediate consequences.

Asynchronous dispatch, on the other hand, is non-blocking. When an event is fired, it gets added to a queue. The code that fired the event continues its execution immediately, without waiting. A separate process, or a designated point later in the main loop, will work through the queue and notify the listeners.

This is ideal for performance-critical situations where the main thread cannot afford to wait. Firing a particle effect, logging a statistic to a file, or starting a sound effect are all things that don't need to happen instantly. Asynchronous dispatch avoids stuttering or frame drops by deferring less critical work.

A robust event system often supports both. Events can be flagged as immediate (sync) or deferred (async), giving developers fine-grained control over the application's flow and performance.

Optimising Payloads

In a high-frequency simulation, thousands of events might be created and destroyed every second. If each event's payload (the data it carries) is large, this can cause significant memory allocation overhead and pressure on the garbage collector. The key is to keep event payloads lean.

Instead of passing entire components or complex objects, an event should carry the minimum data required for listeners to act. Often, this is just the entity ID that the event pertains to, along with a few primitive values.

Bad Payload (CollisionEvent)Good Payload (CollisionEvent)
Entity objectAint entityIdA
Entity objectBint entityIdB
Vector3 contactPointfloat contactPointX
float impactForcefloat contactPointY
float contactPointZ
float impactForce

In the 'Good' example, the listeners receive the entity IDs. If they need more information, they can use those IDs to look up the required components themselves. This approach, known as the 'flyweight' or 'handle' pattern, keeps the event objects small and cheap to create. It shifts the work from the event creation step to the event handling step, which is a good trade-off in systems where many events are created but perhaps only a few are actually listened to.

Let's test your understanding of how event streams integrate with an ECS architecture.

Quiz Questions 1/5

What is the primary advantage of using an event-based approach (ENCES) over a traditional polling approach in an ECS architecture?

Quiz Questions 2/5

In the context of an Event Bus using a publish-subscribe model, a PhysicsSystem publishes a CollisionEvent. Which of the following statements is true?

By adding a robust event layer to an ECS, you create a more flexible, scalable, and performant architecture. Systems become truly independent modules that react to a shared stream of facts about the world, rather than being tangled in a web of direct dependencies.