Mana Engine: Events

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.
struct MyEvent
{
EVENT_DEFINITION();
//Your data members here.
};Events have some unique data storage properties that sets them apart.
- They are not bound by EntityId to any other types, the way Components are. They are simply a pool of the Event structure.
- 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:
- 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.
struct FallingParticle
{
COMPONENT_DEFINITION();
Vector3 position;
Vector3 velocity;
};struct HitTheBottom
{
EVENT_DEFINITION();
Vector3 position;
EntityId id;
};DEFINE_TOKEN_COMPONENT(ParticleUpdateSemaphore);
Some notes:
- We will have a particle that is affected by gravity (and other forces in theory, but the test only applies gravity).
- The Event indicates that the particle has hit the floor of the scene.
- The
ParticleUpdateSemaphoreis 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:
- A creation system, that creates the particles on start up.
- A system that applies forces (gravity) to our particles. This will run before the
ParticleUpdateSemaphore. - 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
ParticleUpdateSemaphore. - For the sake of seeing the particles, a system that updates object transforms based on particle positions.
- 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.
struct StressTestEventCreate : public AuthorizedSystem<
Rendering::DynamicModel, FallingParticle>
{
//This will override Initialize(), not Update().
};struct StressTestEventUpdate : public AuthorizedSystem<
const Time, FallingParticle, ParticleUpdateSemaphore>
{
};struct StressTestEventEmitter : public AuthorizedSystem<
HitTheBottom, FallingParticle, const ParticleUpdateSemaphore>
{
};struct StressTestEventSyncModel : public AuthorizedSystem<
Rendering::DynamicModel, const FallingParticle>
{
};struct StressTestEventHandleEvent : public AuthorizedSystem<
Rendering::DebugRendering, const HitTheBottom>
{
};
Using these, this is the graph I get:

Everything happens in the order I want, and it automatically detected that HandleEvent and SyncModel can be run in concurrently. Mana Engine flexing that muscle for us.
Now here’s filling out the behavior.
struct StressTestEventCreate : public AuthorizedSystem<
Rendering::DynamicModel, FallingParticle>
{
constexpr uint32 createCount = 10'000;
void Initialize()
{
auto wp = GetWorldProxyByTypes<Rendering::DynamicModel, FallingParticle>();
wp.HintCreate(createCount);
for (uint32 i = 0; i < createCount; i++)
{
auto[id, model, particle] = wp.Create();
particle->position = (GetRandomVector3() * 500.0f) + (Vector3::Mask010 * 250.0f);
model->modelName = StringHashViewLiteral("WhiteSphere");
}
}
};Almost painfully straightforward. WorldProxy for the components we want. We call HintCreate() which you can think of as reserve() 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: StringHashView is basically a string_view that also has a hash value in it. The macro StringHashViewLiteral() guarantees that the StringHashView is evaluated at compile time.
Ok, next.
struct StressTestEventUpdate : public AuthorizedSystem<
const Time, FallingParticle, ParticleUpdateSemaphore>
{
static constexpr auto gravity = Vector3::Mask010 * -9.8f;
void Update()
{
const auto& time = GetSingleton<const Time>(); for (auto[id, particle] : GetEntities<FallingParticle>())
{
particle->velocity += gravity * time.deltaTime;
particle->position += particle->velocity * time.deltaTime;
}
}
};
For each particle, apply gravity, considering delta time. Real simulation would want a fixed time step, but this is just stress testing.
struct StressTestEventEmitter : public AuthorizedSystem<
HitTheBottom, FallingParticle, const ParticleUpdateSemaphore>
{
void Update()
{
for (auto[id, particle] : GetEntities<FallingParticle>())
{
if (particle->position.y <= 0.0f)
{
auto* pEvent = CreateEvent<HitTheBottom>();
pEvent->id = id;
pEvent->position = particle->position;
//Bring the particle back up to the top.
particle->position = (particle->position * Vector3::Mask101) + (Vector3::Mask010 * 500.0f);
particle->velocity = Vector3::Zero;
}
}
}
};Finally some actual event code. Iterate over all particles. If they’ve hit the floor (or, are under it) then we will call CreateEvent<>(), fill out the position and id, then reset the particle.
Notice that without the ParticleUpdateSemaphore, this system and StressTestEventUpdate would have the “same order” and fallback to “initialization order”. The semaphore here lets me be explicit.
struct StressTestEventSyncModel : public AuthorizedSystem<
Rendering::DynamicModel, const FallingParticle>
{
void Update()
{
for (auto[id, model, particle] : GetEntities<Rendering::DynamicModel, const FallingParticle>())
{
model->transform[3] = particle->position;
}
}
};Painfully straightforward. Just updating the model’s transform.
Ok, last one:
struct StressTestEventHandleEvent : public AuthorizedSystem<
Rendering::DebugRendering, const HitTheBottom>
{
void Update()
{
auto& debugRender = GetMutableSingleton<Rendering::DebugRendering>();
for (auto& evt : GetEvents<const HitTheBottom>())
{
debugRender.DrawBox(m4f::Identity,
Box3(evt.position - 5.0f, evt.position + 5.0f),
Color::Blue, FillMode::Solid);
}
}
};Before we saw CreateEvent<>(), and here is the other side of it: GetEvents<>(). GetEvents<>() 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.

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.





