"The way you organize data directly impacts performance"
- Robert Nystrom
The article turned out to be quite large, so here is the content:
Intro
Game entities (game objects) are the basic building blocks of a game. There are a lot of different approaches and techniques to build a system behind game entities. The game development community has increasingly adopted the Entity-Component System (ECS) architecture for building modern game engines and games.
But before we dive into ECS architecture I would like to mention other approaches as well.
What could we do?
Jason Gregory (lead game engine developer from Naughty Dog) has an excellent introduction to an entity component system in his book “Game Engine Architecture”. He uses the term “runtime object model” to describe a game entity. I like how he highlights two basic architectural styles:
- Object-centric
- Property-centric
Let’s take a closer look at them.
Object-centric
In this architecture, each logical game object is implemented as an instance of a class or interconnected class instances. Many different designs are possible using this approach. I will show you two of them just to get the main idea.
"Is-A" design. Unreal Engine's Actor
The first one uses well-known “is-a” type of a class hierarchy that is based on inheritance and polymorphism. In this type of hierarchy, a derived class is a more specific version of its base class and can be used anywhere the base class is expected. It’s the most intuitive and straightforward way to represent a collection of interrelated game object types. Using this design our game class hierarchy may look like this:
A game object class hierarchy usually begins small and simple, and in that form, it can be a powerful and intuitive way to describe a collection of game object types. However, as class hierarchies grow, they have a complex structure. Let’s see just a little part of the AActor base class hierarchy used in Unreal Engine (actually it has more than 300 inherited classes):
Problems with "is-a" design
This so-called monolithic class hierarchy tends to cause problems for the game development team for a wide range of reasons:
- The deeper a class lies within a class hierarchy, the harder it is to understand, maintain and modify. This is because to understand a class, you need to understand all of its parent classes as well.
- One of the biggest problems with any hierarchy is that it can only classify objects along a single “axis” — according to one particular set of criteria — at each tree level. Once the criteria have been chosen for a particular hierarchy, it becomes difficult or impossible to classify along an entirely different set of “axes”.
- Multiple inheritance can be the key and solution to resolving logic problems in such a tree hierarchy. However, multiple inheritance in C++ poses several practical problems. For example, it can lead to an object that contains multiple copies of its base class members — a condition known as the “deadly diamond” or “diamond of death”. So, game developers usually limit the use of multiple inheritance in their class hierarchy.
Deadly diamond
- Dynamic binding (virtual function calls) leads to a slight overhead in terms of performance, as the compiler needs to generate extra code to support dynamic binding. Moreover, it affects memory too because an instance of an inherited class also contains a pointer (vPointer) to the virtual table (vTable) associated with it in its memory layout, as you can see in the diagram below. Of course, you could say “This overhead is often outweighed by the advantages of code flexibility and maintainability that come with using virtual functions” but it’s definitely a tradeoff when developing a complex architecture.
Memory layout of the class instance
"Has-A" design
Converting the “is-a” relationship into the “has-a” one can be a useful technique for reducing the width, depth, and complexity of a game’s class hierarchy. Let’s imagine we have a game object that should have render, animation, ai, and physics logic. Using the “has-a” technique we could make classes relationships looks like this:
In code, it may look like this:
Another flexible approach is to implement GameObject with a generic component array:
The code may look like this:
The benefit of this technique is that we can create different game object types using the same class.
Problems with "has-a" design
Let’s imagine we used the “has-a” design for our entity component system and our main game loop may look like so:
Nothing wrong at first glance. But let’s talk about cache. A cache is a small chunk of contiguous memory that is built into the CPU chip. Data is transferred between main memory and cache in blocks of fixed size, called cache lines. If the next byte of data you need happens to be in that chunk, the CPU reads it straight from the cache, which is much faster than hitting RAM. Successfully finding a piece of data in the cache is called a cache hit. If it can’t find it in there and has to go to main memory, that’s a cache miss.
When we use the “has-a” design, finding memory locations the CPU needs to process may look like so:
I took this image from Robert Nystrom’s great book “Game Programming Patterns”. He described entity component system and problems with CPU caching in the Data Locality pattern chapter.
Briefly speaking, we have no idea how components of every type are lying in the memory. They are definitely not placed сontiguously in memory and so we will have cache misses and the performance of our game will be affected.
What we can do to improve our performance and make our system more cache-friendly? Let’s see a property-centric approach.
Property-centric approach and data locality
Jason Gregory’s Property-centric approach and Robert Nystrom’s Data locality pattern are all about the same way to organize memory for game entities.
Instead of having game objects as holders for components, we’ll have a big array for each type of component: a flat array of AI components, another for physics, another for animation, and another for rendering.
Like this:
Now our game loop may look like so:
Instead of skipping around in memory, we’re doing a straight crawl through four contiguous arrays. Next simplified illustration of the memory layout of the game components from the “Game programming patterns” book:
ECS introduction
Now let’s talk about ECS.
Actually, the terms Entity-Component System and ECS are often used interchangeably, but the term ECS has become more commonly used in recent years to refer specifically to a variant of the Entity-Component System architecture that is based on a data-oriented design. Briefly speaking, data-oriented design and ECS particularly are all about property-centric approach described above. I will use the ECS term from now on to describe a system that I will be implementing.
Typical ECS architecture consists of the following parts:
- Entity. Usually, it’s represented by an integer number and is used as a unique game object id.
- Component. Game object data represented by so-called POD (plain old data) C++ structure/class without behavior.
- System. A system is a process that acts on all entities with the desired components. It contains game object behavior.
Architecture based on ECS principle is more multithreading-friendly as we have logically separate subsystems that can be processed in parallel, and it’s another benefit.
My implementation
I will follow the ECS architecture style described above, and I want to point out a few things my ECS to follow:
- Not overcomplicated. I don’t want a system that is as huge as the entire engine.
- Data-oriented design in the core.
- Usability and extensibility. It should be easy to work with new entities and components.
- Convenient interface to use.
- A “System” can be represented by a class or single function. It will depend on the logic we want to have.
- Practical-oriented. It should be usable in the real game/engine and not be just code snippets.
First iteration: MVP
In the first iteration of my ECS, I will implement Entity and Component logic and show you how it can be used. Then I will improve the system for better usability and extensibility point. Also, I will add an optional “System” part of the architecture.
You can find the final version of the ECS integrated into the game engine in my GitHub repository.
Let’s start coding.
Entity
As described above Entity is represented as an integer number (unique identifier). Also, I would add a constant to indicate an invalid entity with a value of 0. Of course, we need a function to generate an Entity id. We could use a random generator for this but I would better use monotonic increment because it can be more appropriate if we will decide to use archetypes in our architecture (see potential improvements part).
By the way, I use atomic type to synchronize access to the counter when multiple threads try to get access to it. Our code will be the next:
Component
Firstly, we need to distinguish component types. I’ve decided to make ComponentTypeId, helper constants, and logic to get the next component type id:
I think there is nothing difficult to understand. We have a static variable that holds the used component type count. Also, we have a global function to get the following component type id using this variable.
Now, let’s see how we want to work with our components. We want to create a pool of components with a particular component type. Since we may have a lot of components of one type, the std::vector container will be an appropriate choice for our pool. Also, we may want to have generic add/get/remove possibilities for that. So, I will create a skeleton for our generic ComponentPool class like this:
How will we distinguish components per particular entity?
We need to hold information about an entity-component connection.
For that, I add a separate pool of entities that contains this type of component using vector, where an index is a component index that is the same for index from m_components pool and value - an entity id associated with this component.
And additional std::unordered_map where the key is an Entity id and value - component index from the m_components pool associated with this entity.
Here is a visualization of these relationships:
You may ask, “Why don’t you just use vectors of components, where an index is an Entity id?”. Well, you can, but then component arrays will have a memory layout like this:
There is a problem when a particular component is used infrequently, and the array stores enough memory for all active entities. It might not be the problem for small games where component arrays contain dozens or hundreds of objects per component type. But when there are thousands of objects per type and we have entities that don’t have all of the component types, I think it’s definitely not good.
It is a cause why I use an additional vector and unordered map to support entity-component connection. It will have a performance hit while processing components of different types at one for loop, but let’s talk about it later.
Now let’s fill our functions with all the logic we need.
Actually, MVP is ready to use in a simple manner.
Such an entity-component system may be enough for developing small games or when developers can extend engine code to add custom component types (ComponentPool objects).
But I would try to make it more friendly for the engine users, so the developer won’t need to think about where and how he should add new components and how to interact with them.
Second iteration
MVP has a simple interface of using. But as I mentioned above, I would like to make a more extensibility-friendly system.
Scene
Simple games may have only one scene, but complex big games have a lot of them. A scene has its unique entities with particular components.
Firstly, we need to hold all Component Pools our scene has. I think an unordered_map container where a key is a ComponentType, and a value corresponds ComponentPool is an appropriate choice.
Secondly, we need to hold all active entities of the scene. We could keep them in the vector container. But how could we know what components are connected to a particular entity? We may use ComponentPool to check whether it has a component that is connected to the entity. But when we need to destroy an entity, we should iterate all ComponentPool instances and call the RemoveComponent function to check whether we have the entity in this ComponentPool. So, I’ve added an unordered_map container (m_entities) in the Scene class for this purpose. A key will be an Entity id and a value will be a bitmask. Every bit in bitmask corresponds to a particular ComponentType. Also, this container will help implement SceneView logic and Scene Serialization feature when we need to filter out entities with a specific signature (see potential improvements part).
Since the Scene class will contain ComponentPools as pointers in one container, we need to support it. I’ve made an interface class for this purpose. ComponentPool class inherits this interface.
The ComponentPool class functions code is identical to the previous ComponentPool code snippet.
Now we can keep the components pool in the Scene class.
Let’s think about the Scene interface and how we want to interact with our scene and ECS. The main points are:
- create/destroy entity; get all entities;
- register new component type;
- add component with or without arguments. We need to support both default constructor and constructor with arguments for our components structures;
- remove component;
- check whether we have a component for the entity;
- get component by entity id;
- get all components of a particular type;
- check whether component type is registered;
- get component pool by type;
- get entity by component index.
Now, you can easily interact with ECS through the Scene interface like this:
Systems
As you saw in the example above you need no “System” to interact with the Entity-Component system consistently. But “System” could be useful for complex game/game engine architecture structures. It’s just another abstractions layer.
There are different approaches to implementing the System part of ECS. You may register a system with particular component types that will be used there, filter out entities on the pre-update stage, for example, etc.
But I still adhere to minimalistic ECS architecture to avoid too much abstraction.
There are next points our “System” part should follow:
- System interface class with OnInit, OnShutdown, OnUpdate, and OnRender functions which correspond to different runtime stages.
- Systems objects will be contained inside the Scene class in the std::vector container.
- It should be registered manually using the Scene class.
- It should have a pointer to the Scene instance for convenient access to the entities and components.
Let’s start with System interface class.
Now, let’s add a container that will hold our system objects inside the Scene class. And implement the RegisterSystem function to register our systems.
Besides, we need to support system stages inside our Scene class.
Now we can use it like so:
Behavior components / scripts
Another great feature is the behavior component or just a script. It’s more similar to a script that can be attached to entities in such engines as Unity, Unreal Engine, etc.
Let’s think about how we want to use this feature. I want to add custom behavior to entities in a consistent manner, for instance.
- It could be inherited from some class that can be passed to BehaviorComponent and processed automatically on the different runtime stages of the game.
- I don’t want this Behavior is being executed not in runtime (e.g. Editor logic, menu, etc.).
- I want to have a pointer to the Scene object to access ECS.
- I want some function like GetComponent to get any component I need for an entity.
Behavior class may look like this:
Now, we need to create BehaviorComponent that will contain a Behavior object.
As you may notice, I don’t create a Behavior object during component construction. I use lambda to hold behavior instantiation before we need it. Also, we need to call the OnDestroy function of the behavior object in the destructor. As we explicitly declared destructor we have to explicitly declare the move constructor and move assignment operator too because they are used in ECS components movement.
Also, I need a system that will process the functions of my Behavior class.
This feature can be used like so:
Game demo
There are a lot of articles about theoretical approaches to building ECS. But practically, there are a lot of pitfalls when implementing it for the game/game engine. As I mentioned above, ECS should be more practical-oriented in this article, therefore I’ve made three classic games using my engine and the ECS system I’ve described here: Invaders, Pong, and Tron. You can find the code of the games in the repo. The invaders game is the most complex and uses the next components:
- in-engine components:
- Trasform
- Sprite
- Camera
- Text
- RectTransform (for the text rendering)
- AABB
- Tag
- game-specific components:
- Player
- Enemy
- Bullet
- PowerUp
I’ve used the Behavior feature to make the PlayerBehavior script. Also, I’ve used “System” part of ECS to make game-specific systems:
- BulletMovementSystem
- EnemyControlSystem
- PowerupsSystem
- CombatSystem
I think it’s turned out to be a fun game (turn on the sound 😀):
Potential improvements
SceneView
There is a bunch of stuff that can be improved and changed for better performance and usability. For instance, we can add so-called SceneView (another abstraction layer) support to replace fetching entities of a particular signature. So we can replace entities iteration like so:
Archetypes
As I mentioned previously, there is a performance hit in our ECS when we process entities with a complex signature (it has more than one component per entity). This problem is caused by using unordered_map when we try to find a component of another type using entity (fetch data from other ComponentPool and memory location). It probably isn’t cached, and we have a cache miss.
To fix this problem, we need to reduce the usage of unordered_map. An alternative is an approach to implementing ECS using so-called Archetypes. Archetype is a unique combination of component types stored in contiguous arrays. We don’t need any unordered_map containers when using it because every archetype contains component pools of particular types with the same sizes, and we can easily fetch a component of another type per iteration using an entity as an index. This type of ECS is used in Unity engine for it’s Data-Oriented-Technology-Stack (DOTS).
There is a visualization of such a system.
But this approach also has a downside when we need to add or remove components from the entities that cause moving all the component data from one pool to another.
Scene serialization
There are a lot of approaches and libraries you can use to implement serialization for the scene. I will not describe the steps and code I’ve wrote here. You can find my implementation in the project repository. I’ve used the nlohmann’s JSON library to implement it. I would make a separate post about Scene serialization after implementing the Scene hierarchy and its serialization.
Final thoughts
I’ve researched different ways of building an entity-component system for a game engine. All approaches described above have their pros and cons. One is more appropriate for small games other is more performance-friendly but bad in usability. There are tradeoffs that engineers have to adhere to use a suitable type of system.
I’ve implemented ECS and used it to create some simple games (Invaders, Pong, and Tron games that you can find on my Github) to test my game engine and ECS, in particular. I think I’ve made a system that is more data-oriented and cache-friendly. Also, it follows my points of usability and future extensibility.
References
I was inspired by all of the engineers mentioned above and which will be mentioned below. I think it’s worth checking them all if you are interested in game/game engines, data-oriented programming, or want to make reliable and robust software.
- Jason Gregory. Game Engine Architecture, 3rd Edition.
- Robert Nystrom. Game Programming Patterns.
- Mike Acton. His talks about Data-Oriented design: 1 and 2.
- Johnatan Blow. Data-Oriented Demo: SOA, composition. New (jai) programming language for game developing.
- Turánszki János. Entity-component system in Wicked Engine.
- List of data-oriented resources that may be interesting.
- Yan Chernikov and his channel The Cherno. He integrated the popular open-source ECS library EnTT into his game engine in the video series.