In an ECS architecture, components steal all the spotlight. But components aren’t the only type of data we care about. For Blizzard’s Overwatch, the engineers quickly learned that you often want “components of one”, that is basically a singleton. I think they quoted something around half of their components were “singletons”.

In Mana, we support singletons as a first class feature, with even an added ability to consider a type a “Mutual Singleton” where it can be written to from multiple systems at the same time.

But one type of data that is rarely discussed in ECS architectures are Events, often also called messages.

In Mana, we called them Events because Messages gives the wrong idea. A Message implies the creator of the message is sending it to a known destination. That analogy doesn’t fit well to the design of Events. Instead, a system emits an event and has no idea (nor cares) who is going to use the event data.

Events are important in game development. We use them all the time. In the Mana Engine, like Components and Singletons, they are first-class features. Moreover, events are also mutual, which means the same thing as it does for Singletons. Multiple systems can emit events of the same type at the same time, safely from multiple threads.

To declare an event, it’s as easy as declaring any data type in Mana. Just a struct with a tag.

Events have some unique data storage properties that sets them apart.

  1. They are not bound by EntityId to any other types, the way Components are. They are simply a pool of the Event structure.
  2. Events will be automatically cleared at the end of the frame, which means you cannot get events from the previous frame.

And one important one that is just like everything else:

  1. Events are accessed by AuthorizedSystems just like Components and Singletons, where readers go after writers.

As with all major features in Mana, we write “Stress Tests” that basically exercise the feature with a reasonably large set of data. Here is the stress test we wrote for Events.

The idea is that we have X number of particles falling from the sky. When they reach the bottom, they will emit an event, and then move to the top of the scene. A “listening” system will do something as a result of reading the event.

First, let’s define our data.

Some notes:

  1. We will have a particle that is affected by gravity (and other forces in theory, but the test only applies gravity).
  2. The Event indicates that the particle has hit the floor of the scene.
  3. The is just a way for a system to make sure it runs after updating has occurred. This isn’t required but it makes it so that system execution order is not dependent on initialization order. Just adds a bit of safety.

To plan out the things we’re going to actually do, this is the general outline of the systems I want:

  1. A creation system, that creates the particles on start up.
  2. A system that applies forces (gravity) to our particles. This will run before the .
  3. A system that will check if any particle has hit the floor. If so, it will emit an event and reset the particle’s position. We want to make sure this runs after the .
  4. For the sake of seeing the particles, a system that updates object transforms based on particle positions.
  5. A system to react to the particles. For my case, I’m just going to draw a single debug box as my “do something”.

Let’s stub out the systems.

Using these, this is the graph I get:

Everything happens in the order I want, and it automatically detected that and can be run in concurrently. Mana Engine flexing that muscle for us.

Now here’s filling out the behavior.

Almost painfully straightforward. WorldProxy for the components we want. We call which you can think of as on a container. Loop the number of times we want to create, call Create, set to a random position in a 500 unit cube space, and assign the model name.

Sidebar: is basically a string_view that also has a hash value in it. The macro guarantees that the is evaluated at compile time.

Ok, next.

For each particle, apply gravity, considering delta time. Real simulation would want a fixed time step, but this is just stress testing.

Finally some actual event code. Iterate over all particles. If they’ve hit the floor (or, are under it) then we will call , fill out the position and id, then reset the particle.

Notice that without the , this system and would have the “same order” and fallback to “initialization order”. The semaphore here lets me be explicit.

Painfully straightforward. Just updating the model’s transform.

Ok, last one:

Before we saw , and here is the other side of it: . returns a forward iterable view of the internal pool. For each one, we create a box.

And here’s the end result, when processing 10,000 “particles”. A bit hard to appreciate it from a still image rather than a video. For what it’s worth, this is all running on a single core in 1.3ms.

Just waiting for someone to criticize the random distribution.

This post is already getting long enough, so I’ll wrap it up here. In a future post I hope to show how events can be used in a full physics simulation, making the process easy to implement, performant, and thread-safe by default.