in the past few weeks I had to deal intensively with an Entity Component System for one of my projects. There are some articles in the internet, that describe this topic well, but all these articles are focused on the definition of an Entity Component System and don’t tell you how they work. I couldn’t find a detailed description how to implement such a system. In my search on the net, I have of course found some implementations for the most common languages, but unfortunately not for the one I wanted to use (FreePascal). So if you want to implement an Entity Component System yourself, or just want to know how to implement something like this, then you are exactly right here.
So what will you expect in this article? I will, like all others, first explain what an Entity Component System is and what advantages or disadvantages it entails. Then I will explain how to implement such a system. This will mainly be done in text form. Some small code examples will round up everything. I will deal with problems that I have encountered in my implementation. At the end of the article, you should have enough knowledge to implement your own Entity Component System.
Before we start, a small hint: What I have summarized here are just my personal experiences and ideas. This does not mean that you have to do it the same way, or that what I have written, is the exact way how to implement such a system. I just want to share my knowledge with you.
Table of Contents
Let’s suppose you want to develop a cool space game and now you’re thinking about what you want to do in this game. In addition to fancy spaceships and powerful space stations, there are countless planets that want to be explored and populated. So you start to think of a corresponding class design. You distribute the properties of the individual objects so that they can be easily integrated into your design.
Now you have forgotten that you want to implement a big war station and find no place in your class hierarchy, where the new class is in good hands. On the one hand you should derive from the class SpaceStation and on the other hand from the class WarShip. So you have the classic Diamond of Death problem.
How can you solve this problem? If you have the good fortune and your programming language supports multiple inheritance, then you could simply derive from two classes. But even if your language supports this feature, you should agree that this is a bad solution. Another solution would be to move the relevant code along the class hierarchy to one of the base classes. For a very large project, however, the base classes would be unnecessarily complicated. You also could simply copy the required code, but then there is again the risk that you copied a possible bug. The resulting code would be impossible to maintain.
Another disadvantage of the object-oriented approach is that the training of new team members takes a very long time. You must usually be able to see a large part of the classes in order to get a picture of how they work. If you do not consider all eventualities from a small change, the application does not work as expected.
The last problem I would like to address here is in wich order are virtual methods are called. What would happen if our SpaceObject had a virtual method Render and our newly created class WarStation first want to call the Render method of SpaceObject, then want to execute their own code and at last the Render method of SpaceStation? This would not be possible in most programming languages.
In short: A game design, which is based on inheritance is difficult to maintain and it’s not trivial to extend it. To implement new large features usually requires on a complete redesign of the application.
Now you ask yourself: How should I manage my data and the code otherwise, if not with an inheritance? The answer is simple: Aggregation! If you see the game objects no longer as part of a hierarchy, but as an aggregation of different components, you could solve all the problems that were revealed in the last section. For example, in our game, there is a component Station that contains all the information you need for a space station, and a component Combat that has information about how much damage a game object could cause. If you want to implement your war station, you just make the WarStation an aggregation of the required components.
The individual components are merged together in a so called entity. An entity can have several components, each component type can be represented at most once. Important in this approach is that you implement any kind of data in the components and do not hold any data in the entity itself. This is the only way to use the full potential of this system. Usually, the entity is only an ID that you use to get access to the components of the entity.
If the data is now completely in the components, and the entity is only an ID, where is the actual code going? For this we use systems. A system is a piece of code that works with some selected components. For example, you could write a system that uses the Position and the Velocity component to cyclically update the position of an entity. The complete game logic is implemented in these systems. The nice thing about this approach is that it could be parallelized in several threads without problems. The only interfaces that must be synchronized are the accesses to the components (via the entity ID) and the distribution of the events.
The systems are supported by an event manager that fires events as soon as data in the components have changed or entities and components have been created or deleted. These events can be seen as a kind of method call in the class hierarchy. If a system has detected that it is causing damage to one of our WarShip entities, it generates a TakeDamage event. The event manager then distributes the event to all other systems that are interested in the event. For example, the sound system can play a sound, the particle system takes care of nice explosions, the combat system reduces the ship’s shields and hull points, and the network system makes sure that the server is informed about the attack. You could even write a system that stores all the events and then plays it as a kind of replay. The possibilities are manifold.
As explained above, a component is nothing but data needed by one of our game objects. The difficulty is to determine how the data should be distributed to the components. I made a lot of mistakes in the beginning. First, my components were too small and the system became too slow and confusing. Then they were too big and the flexibility of the system was lost.
I advise you to sit down with a pen and paper (or just the editor of your choice) and write down what objects your game should have and what properties these objects have. Then, consider what systems and game logics you have in your game and write down which data of which object your systems must operate on. Now you can see which objects and systems need the same data. Then you merge this data to a component. You should ensure that the component is always related to a object and a system. For example, it does not make sense to define a color component just because your ship and your space station have a color, when this component is not used in a separate system. This makes your design unnecessarily complicated. You can move such properties to a component that the ship or the station already has.
Rule: Move properties only to a new components when they are used by different entities and simultaneously by different systems!
By definition, components are only data, but this does not mean, that the object that implement such a component could not contain any code. You just have to be careful that this code does not contain any game logic. I have written an abstract base class for all of my components in my system. In this base class I have defined some virtual methods which allow me to address all properties of the component via an index or a name. This makes storing and loading the component easier.
TecsComponent = class protected function GetPropertyCount(): Integer; virtual; function GetPropertyName (const aIndex: Integer): String; virtual; function GetPropertyValue(const aIndex: Integer): Variant; virtual; procedure SetPropertyValue(const aIndex: Integer; aValue: Variant); virtual; public property PropertyCount: Integer read GetPropertyCount; property PropertyNames[const aIndex: Integer]: String read GetpropertyName; property Properties [const aIndex: Integer]: Variant read GetPropertyValue write SetPropertyValue; function IndexOfPropertyName(const aName: String): Integer; constructor Create; destructor Destroy; override; end;
Whether the component is implemented as an abstract base class or as an interface does not matter. What is important is that the components have a commonality that you can use to access the components data. In C ++ you could implement the whole thing with themplates. However, finally you have to decide by yourself, how you want to implement it.
Another type of code that I would allow in the components is the dispatching of events. If you changed the property of a component, it can automatically send an event to the event manager, which informs other systems. Here it is important that its implemented only for properties that are rarely changed. For example, an update event for the position would trigger a flood of events. If you are not sure how often a property is changed, or if you need an event, just leave it away.
A special type of components are so-called tag components. Tag components are simple components without data that you can use to tag an entity. For example, you could implement a tag component named PlayerControlledTag. This component does not need any data, and is only used to recognize all the entities that are controlled by the player.
You already know that a entity in the broadest sense is only a collection of components that are addressed by an ID and represent an object in your game world. This sounds relatively easy on the whole, but there is still the one or the other thing you have to consider.
The entity ID is an important part of your entity component system. It must be ensured that an entity can be unambiguously identified with its ID. The ID must be easily transferred over the network, stored in a file, or loaded from a file. And as if that were not enough, it must also be performant.
Looking for a solution to all these problems, I’ve come across with a very interesting article, which solves exactly this problems. Basically, it is a handler manager that can access data via a handle (or even our entity ID) and still makes sure that you can serialize the handles. This works as follows. The handle is 64bit.
- 32bit Index – is used to find the data in the array
- 16bit Counter – is used to reuse a index and ensure it’s not the same data
- 16bit Type – the type of the stored data (could be used to prioritize our entities if needed))
In the array of the handle manager, not only the actually data is stored, but also a few meta data to manage the array.
- Next Free Index – index if the next free element in the array
- Counter – the counter value of the current stored data (used to compare with the handles counter value)
- Active – bool to check if this element is used
- End Of List – bool to check if this is the last element in the list
- Entry – the actually stored data
Using these meta data, a linked list is created inside of the array. This ensures that you can find your elements with a runtime of O(1) and can also add or delete elements with the runtime of O(1). This should be sufficient as a rough guide. You can find an exact implementation in the linked article.
Managenent of the entities
The most important thing in your Entity Component System is accessing your entities. The entity must be able to be addressed with its ID, and it must be ensured that only one thread works simultaneously with the entity, this means the entity must be synchronized. It is a good idea to write a global entity manager that manages the entities and automatically does the synchronization. If you only work with the entity ID, or you create an entity object is left to you.
I have decided to not access the entity exclusively by their ID. The entity manager in my system can be asked for an entity if you know their ID. If the manager has found the entity, you get an interface from the manager you could use to access the entity.
IecsEntity = interface function GetID: TecsEntityID; function GetComponents: TecsComponents; procedure Kill; property ID: TecsEntityID read GetID; property Components: TecsComponents read GetComponents; end;
The interface has three methods:
- GetID – returns the ID of an entity
- GetComponents – returns the component object of an entity
- Kill – destroys the entity
The background is as follows. In FreePascal (and also in Delphi), interfaces have an automatic reference counter. That means, if there is no longer a variable holding the interface, the underlying object is automatically released. I have made use of this mechanic. The entity manager has a internal class representing an entity. Among other things, this class contains the components of the entity. If I ask the manager for an entity, he looks for it in the handle manager, blocks the entity for further accesses and returns the interface. Now I can work with the entity without having to worry about another thread having access to the entity. As soon as the local variable storing the interface is released the entity will be unblocked an other threads can use it. The interface can of course only be used in the method in which I get the entity from the manager, and must not be stored otherwise the entity would be blocked forever.
A decent event system belongs to any good Entity Component System. Events are used to exchange data between different systems. The systems do not have to run on the same computer. In a client server environment it is quite possible that a system on the client creates an event, which is then processed by a system on the server. There are two basic types of events.
- Change event – a property of a component has changed
- Execute event – something should be done with an entitiy
The first type of events is simply a notification that something has changed. These events are not acknowledged by the recipient in the normal case. The second type of events is the more interesting and can be compared with methods in an object-oriented system. These events say what should happen with an entity or what has already happened. For example, if you want to create an entity, you don’t easily create it, you fire an EntityCreate event. This event is then received by a corresponding system, the entity is created and the event is acknowledged with an EntityCreated event.
The event can go various ways from trigger to its target system. For example, the event could be delivered from the client to the server, which then checks whether the client has the permission to create this entity at all, and then executes the operation.
Events can be implemented with simple callbacks. Data which must be shipped with an event, I have implemented in my system via an interface again (you remember, in FPC, interfaces are released automatically). Depending on what event I want to dispatch there is a corresponding implementation with different data. It is recommended to implement an event ID for each event in order to uniquely identify the event. An event that was triggered due to another event should use the same ID. So you can easily see if the EntityKilled event is part of the EntityKill event you have triggered. If you are developing a client server system, you must ensure that the event ID is not only unique on your computer, but on all computers connected to the system.
Now let’s talk about the actual game logic, the systems. Systems are small separate tasks that your game must perform. The systems should be as small as possible in order to minimize the effort of the system. There are also two different types of systems.
- Event bases systems
- Cyclic systems
Event based systems react to events from the event manager and then execute a specific task. The systems decide on which events they have to react to. Cyclical systems are always repeated in your game, like the render loop. If you implement new systems, the rest of the game logic remains unaffected.
In systems that work with entities and presume that the entity has certain components, you should implement a cache if possible (and if it makes sense). If an entity is created or deleted, or a component is added to or removed from an entity, then this cache should be updated. The cache should then only contain the components that meet the requirements of your system. This makes sense especially in the case of cyclic system, since it would be very inperformant to examine all the entities and their components in each cycle. So the system must only process the entities that it already knows ans that provide the required components.
Another great feature of Entity Components Systems is that you can easily create blueprints or templates for an entity. This template contains all the components and values that a particular object should have in your game world. For example, you could define a template for an asteroid. If you need a new asteroid object in the game, just load the template and than you only have to adjust a few properties of the entity (like the correct position).
These templates can also be easily transferred to a file and loaded into the program. A tree-like file structure such as XML is a good choice.
<Blueprint Name="asteroid"> <Components> <Component Type="TPositionComponent"> <Properties> <Property Name="Position" Value="0,000; 0,000"/> <Property Name="Size" Value="100"/> </Properties> </Component> <Component Type="TAsteroidComponent"> <Properties> <Property Name="ResourceType" Value="Metal"/> </Properties> </Component> </Components> </Blueprint>
Nested templates are also conceivable. The basic template defines the properties of the asteroid. As an extended template, you could create a habitable asteroid by simply adding another component. The possibilities are almost limitless.
The good old object-oriented approach does not provide enough flexibility for complex games. An Entity Component System, on the other hand, offers exactly this by its strict separation of data and logic. The data stored in the system can be serialized and deserialised without problems. Whether in a file or over the network. The logic in the systems could be very easy parallelized on several threads and thus it is very performant. With a simple but powerful event system, the game can be easily controlled, be it through the network, from an AI or from the player itself. Only the corresponding event has to be dispatched. In addition, by recording events, a replay function can be implemented without problems. And I’m sure there are a lot more advantages I’m not thinking about right now.
I hope you could learn something from this article. I would be very grateful about a some feedback. Maybe you have already developed your own Entity Component System and see something different than me, then let me know. I also like to learn something new 😉