Nomad Game Engine: Part 7— The Event System
This post is part of a series where I’m documenting my experience building an ECS game engine from scratch. Check out the homepage for this project for more posts, information, and source code.
Up until this point, we’ve almost got a working game engine — we have entities with components and systems acting on those components. This blog post will outline one of the last significant parts of a functional ECS — the event system.
To illustrate the need for an event system, we’re going to construct a super simple game based on space invaders. Here are some quick requirements:
- Spaceship can move side to side using the arrow keys
- Spacebar can be pressed to shoot a laser
- When a laser hits an alien, it disappears
If we build out this game similar to how we’ve done previous examples, we’ll end up with something like this:
I may write a future blog post which actually implements this entire game, but for now we’re just going to deal with these components and systems conceptually. If we walk through our requirements and write pseudocode for each, we can very quickly see a hole in what this blog series has covered so far.
Let’s take a look at the first requirement:
Spaceship can move side to side using the arrow keys
This requirement isn’t too bad — this is what we have the Player system for.
On to our second requirement:
Spacebar can be pressed to shoot a laser
Once again, this will go in our Player system:
None of this so far should be much of a surprise, we’ve talked about all these concepts in the previous blog posts. Now we get to the important requirement:
When a laser hits an alien, it dies
The previous two requirements had functionality that nicely fit into an existing system we had built explicitly for this purpose (the player system). A simple approach to implementing this 3rd requirement might be something like this:
The issue with this implementation is that we’ve now coupled two parts of our game that really have nothing to with each other: Physics (handling collisions), and Combat (gameplay of enemies and players fighting). This might not seem like a problem on the surface, but let’s imagine we end up making our game a bit more complex:
When a laser hits an alien, it deals damage based on the alien’s armor
Now our code would look more like this:
Let’s remember that all this code is still in the Physics system. As our game grows more complex, we end up with more and more ridiculous code shoved into our physics system that has nothing to do with the physics it’s supposed to be handling. Clearly, we need a way for systems to communicate with eachother.
Enter events. Events in Nomad use a pub/sub design pattern. Systems can subscribe to events they care about as well as publish events that are relevant to them.
To decouple our Physics from our Combat, we will move the combat logic into it’s own
CombatSystem, and allow the
PhysicsSystemto publish a
CollisionEvent that the
CombatSystem will be subscribed to and handle. The implementation of this architecture mostly revolves around the
EventBus. Our end goal is to let events be published in one line:
in addition letting subscribers subscribe in one line and use a member function as a handler:
My implementation for the event system is mostly based on this forum post. I spent quite a while experimenting with different implementations but this one ended up being the best mix of performance, readability, and reusability that I could find. All event routing is handled by the event bus, which holds on to a list of methods to call for each event, and calls them all when that event is received.
The primary challenge with this design is safely wrapping member functions so that they can be called at a later time. Enter
While it might seem complex, really all it’s doing is holding on to a reference to the class instance and the offset of the method to be called. What’s nice about this implementation is that it uses
static_cast<>, which adds a layer of safety and helps with performance vs
Now we just need our
EventBus class, which holds on to a bunch of
MemberFunctionHandlers and calls them when an event is published.
As long as each system holds on to a reference to a shared
EventBus instance, published events will be safely and quickly sent to all subscribers.
Fixing what was broken
Going back to our original requirements, we first make an event:
And a new Combat system:
Now instead of having our collision resolution code in physics, we simply fire an event:
Notice how from the physics system’s perspective, we only care about collisions, not dealing with their side effects. This is good design because we’re not coupling our behaviours.
If we wanted to change how combat works (e.g. by adding shields, damage mitigation, etc.) we could now handle all of it in the Combat system as opposed to shoehorning it into the physics system. If we want to be really fancy, we can use a similar approach with the inputs, letting us further abstract away the library we use for input from the actual player movement code.
Further benefits: Achievements
In addition to the obvious communication between gameplay systems, we can actually leverage events for a whole bunch of useful things. For example, let’s say we decide to add achievements to our game, and have an achievement reward for dealing 100,000 damage.
Because we’re using events, we would have a
DamageEvent being published every time the player deals or takes damage. All we’d need to do is have a new
AchievementSystem subscribe to
DamageEvent, and add to a running total when the damage event is from the player. In this way, our code is completely separate from all other game logic and we’re not going to have any bugs due to the achievement system existing or not. This is one example of the many benefits that having an event driven game engine can bring.
Even further benefits: Concurrency
While I haven’t talked about concurrency at all, having a game engine that uses message passing at its core gives many benefits. I’m hopefully going to cover concurrency in the future, but for now check out the Actor Model which actually bears a lot of similarities to the way we’ve been conceptually thinking about our game/engine. In theory, we could have different systems running on completely different threads, communicating and giving the game life solely through events.