Nomad Game Engine: Part 3 — The Big Picture

Niko Savas
6 min readJul 8, 2016

--

Game engine architecture

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.

After the last post explained the basic concepts of a component manager and a system, you’re probably wondering how everything fits together. This blog post is the last fully conceptual one, after this one we will start delving into the nitty gritty of implementation.

A quick note before I jump into this is that the engine is still very much a work in progress. I may change some of the implementation in the future but for now the architecture I’m using is working well for all the use cases I can come up with. If anyone reading this has any ideas or constructive criticism I’d love to discuss it!

Alright. Let’s dive in.

The Entity Manager

The entity manager handles which IDs are handed out to new entities. When you create a new entity, the entity manager takes care of making sure that no other living entities share the same ID (as this would cause major problems). For this reason, the entity manager is a singleton.

Example: When the game starts, I need to add the player to the game. To do this, I’d call something like entityManager.create(), which would return me a new Entity (if you remember from last post, an entity is simply an ID).

//Returns {2} — The player's entity ID is 2
Entity player = entityManager.create();

The Component Manager

Note: I covered component managers in some depth in the last blog post. If any of this is confusing go back and read that.
A component is a chunk of data that is associated with an entity. Rather than having entities hold their own components, all components of the same type are handled by a component manager for that type. All interactions with the component data itself goes through the component manager.

Example: My player starts the game with 7 HP out of a maximum of 10. At the beginning of the game, therefore, I would need to tell the “Health” component manager that I need to add a new component associated with my player entity with those values.

//Create the entity
Entity player = entityManager->create();
//Create the component
HealthComponent hp;
hp.currentHp = 7;
hp.maxHp = 10;
//Add the component to the entity
healthComponentManager->addComponent(player, hp);

The System

Note: I covered systems in some depth in the last blog post. If any of this is confusing go back and read that.
A system is a self contained unit of game functionality. Generally systems run their logic on every game update. Each system specifies which components it wants to pay attention to, and is handed entities based on this.

Example: I have a system that needs to heal all entities with “Health” components by 1 every 20 seconds. In this situation, the system would specify that it wants to pay attention to “Health” components. Every time an entity gets a health component, it would be added to the list of entities the system needs to update. Every time an entity loses a health component, the system would stop updating it.

void HealingSystem::update(int dt){
_timeSinceLastHeal += dt;
if(_timeSinceLastHeal > 20000){
//20 seconds have passed
_timeSinceLastHeal = 0;
//Update all entities we’re paying attention to
for(auto& entity : _entities){
healthComponentManager->heal(entity, 1);
}
}
}

You might be thinking that some of this code looks really awkward to write and that you’d never want to develop a game using this syntax. Well, you’re right! That’s why I added handles. Check out one of the Q&A questions near the end of this blog post to see what game code in the engine actually looks like.

The World

Those of you following along very closely will surely realize that there’s a missing piece here. The main question that needs to be answered is how do systems find out that a component has been added or removed? There are too many interconnected pieces here that we’ve already mentioned. To prove the need for the “world”, let’s use the example of a system that removes an entity from the game.

For this example, we will assume we have a bomb in the game that explodes and kills everything in a blast radius — thereby removing all the entities from the game. Here’s what the control flow looks like without the world:

Deleting an entity without the “world”

You’ll notice that the entity manager is doing a ton of heavy lifting here. Not only is it managing entity lifetime and creation (as discussed above) but it’s also essentially running the show. To perform the tasks outlined above, it would need references to all component managers and all systems that exist in the game.

Because it’s already managing a list of all systems and components, it would make sense for the entity manager to also deal with all of the systems at runtime, for example calling update() every game tick. This means that the entity manager would also be handling any parallelization that we wanted to exist in our engine. Suddenly, the scope of the entity manager has gone far beyond what we originally wanted it to be. For this reason, we turn to the “world”. The world acts as the glue between the parts we’ve already covered above. Let’s take a look at the same use case, deleting an entity, but with a world.

Deleting an entity with the “world”

You’ll notice that everything is essentially the same, but instead of the entity manager running the show, it’s the “world” running the show. The world holds a reference to all of the aforementioned parts of the engine: all the systems, all the component managers, and the entity manager. Access to any of these is run through the world, but the world doesn’t actually do any of the work itself, just hands out references and makes calls.

Here are a couple other use cases with the world:

Updating a component with the world
Adding a component with the world

So, the world keeps track of all the systems, and systems have a pointer back to the world they exist in. All calls to the rest of the engine from the systems go through the world.

“This process makes game logic code very difficult to write”

The way I’ve written code samples above are for clarity. In future posts I’ll explain how I abstract away a lot of the implementation details using handles so that writing game logic is very intuitive, and looks more like this:

//Create the player
Entity player = _world->createEntity();
//Give the player a health component, starting with 7/10 HP
HealthHandle hp = player.addComponent(HealthComponent(7,10));
//Heal the player
hp.healFor(3);

“Can I have multiple worlds?”

Yes, there are situations where you might want two worlds, e.g. one for dealing with your inventory (when you open the inventory screen the “inventory world” becomes the one displayed). This allows for more concurrency between parts of the game that won’t affect each other significantly. I’ve only ever played with one world at this point so I haven’t fully fleshed out the concept of multiple worlds.

“How do systems communicate between each other?”

Let’s say we have a bomb in our game that explodes and does damage. We likely have one system dedicated to collision detection already, which would make it foolish to have a second run of the collision checks in the “bomb system”. Instead, we use an event queue to manage communication between different parts of the game. More on this to come, for now we can just imagine a call from a system looking something like this:

//Inside update() for the "Collision System"
Entity e1;
Entity e2;
if(collided(e1,e2)){
eventQueue->fireCollisionEvent(e1,e2);
}

Current Progress

Current progress of Nomad.
  • Better collision detection (covers more edge cases)
  • Layer-based collision (same as the way Unity does it)
  • Custom Script component using lambdas (more on this later)
  • Made AoS/SoA toggles for component storage (more on this later as well :) )
  • Various bug fixes

--

--