In the
last article, I described the technologies and approaches that we use when developing a new mobile fast paced shooter. Since it was a review and even a superficial article - today I’ll dig deeper and explain in detail why we decided to write our own ECS framework and did not use the existing ones. There will be code samples and a small bonus at the end.

What is ECS for example
Briefly, I already described what an Entity Component System is, and on Habré there are articles about ECS (mostly, however, translations of articles - see my review of the most interesting of them at the end of the article, as a bonus). And today I will tell you how we use ECS - using our code as an example.
The diagram above describes the essence of the
Player , its components and their data, and the systems that work with the player and its components. The key object on the diagram is the player:
- can move in space - Transform and Movement components, MoveSystem system;
- has a certain amount of health and may die - component Health , Damage , system DamageSystem ;
- after death appears on the spawn point (respawn) - Transform component for the position, the RespawnSystem system;
- may be invulnerable - component Invincible .
We describe this code. For a start we will get interfaces for components and systems. Components can have general auxiliary methods, the system has only one
Execute method, which receives the state of the world at the input to processing:
public interface IComponent {
For components, we create procurement classes that are used by our code generator to convert them into actually used component code. We will get blanks for
Health ,
Damage and
Invincible (for the other components it will be similar).
[Component] public class Health { [Max(1000)]
Components determine the state of the world, therefore they contain only data, without methods. In this case, there is no data in
Invincible , it is used in logic as a sign of invulnerability - if the player’s essence has this component, then the player is now invulnerable.
The
Component attribute is used by the generator to find the procurement classes for the components. The attributes
Max and
DontSend are needed as hints when serializing and reducing the size of the state of the world transmitted over the network or saved to disk. In this case, the server will not serialize the
Amount field and send it over the network (because clients do not use this parameter, it is needed only on the server). And the
Hp field can be well packaged in several bits, given the maximum value of health.
We also have a blank
Entity class, where we add information about all possible components of any entity, and the generator will already create a real class from it:
public class Entity { public Health Health; public Damage Damage; public Invincible Invincible;
After that, our generator will create a code for the
Health ,
Damage, and
Invincible classes of components that will already be used in the game logic:
public sealed class Health : IComponent { public int Hp; public void Reset() { Hp = default(int); }
As you can see, data remained in the classes and methods were added, for example,
Reset . It is needed to optimize and reuse components in pools. Other methods are auxiliary, do not contain business logic - I will not give them for short code.
A class will also be generated for the state of the world, which contains a list of all components and entities:
public sealed class GameState {
And finally, the generated code for
Entity :
public sealed class Entity { public uint Id;
The
Entity class is essentially just a component identifier. References to objects of the
GameState world are used only in helper methods for the convenience of writing business logic code. Knowing the identifier of the component, we can use it to serialize the relationships between entities, implement links in components to other entities. For example, the
Damage component contains a reference to the
Victim entity to determine who caused the damage.
This is where the generated code ends. In general, we need a generator in order not to write auxiliary methods every time. We only describe the components as data, then all the work is done by the generator. Examples of helper methods:
- create / delete entities;
- add / delete / copy a component, access it, if it exists;
- compare two states of the world;
- serialize the state of the world;
- delta compression;
- the code of a web page or Unity window to display the state of the world, entities, components (see details below);
- and etc.
Let us turn to the code systems. They define business logic. For example, we write the code of the system that charges the player:
public sealed class DamageSystem : ISystem { void ISystem.Execute(GameState gs) { foreach (var damage in gs.Damages) { var invincible = damage.Victim.Invincible; if (invincible != null) continue; var health = damage.Victim.Health; if (health == null) continue; health.Hp -= damage.Amount; } } }
The system goes through all
Damage components in the world and looks at whether there is an
Invincible component on a potentially damaged player (
Victim ). If he is - the player is invulnerable, no damage is calculated. Next, we obtain the victim's
Health component and reduce the player’s health by the amount of damage.
Consider the key features of the systems:
- The system is usually a stateless class, it does not contain any internal data, it does not try to save it somewhere, except for world data transmitted from outside.
- Systems usually run through all components of a particular type and work with them. They are usually called by the type of component ( Damage → DamageSystem ) or by the actions they perform ( RespawnSystem ).
- The system implements the minimum functionality. For example, if you go further, after the execution of the DamageSystem, another RemoveDamageSystem will remove all Damage components. On the next tick, another ApplyDamageSystem based on the player’s shooting can again hang the Damage component with new damage. And then the PlayerDeathSystem system will check the player’s health ( Health.Hp ) and, if it is less than or equal to 0, will destroy all the player’s components except Transform , and add the Dead flag component.
So, we get the following classes and relations between them:

Some facts about ECS
ECS has its pros and cons, as an approach to development and a way of representing the world of the game, so everyone decides for himself whether to use it or not. Let's start with the pros:
- Composition against multiple inheritance. In the case of multiple inheritance can be inherited a bunch of unnecessary functionality. In the case of ECS, the functionality appears / disappears when a component is added / removed.
- Separation of logic and data. Ability to change the logic (change systems, delete / add components), without breaking the data. Those. You can at any time disable the group of systems responsible for a certain functionality, everything else will continue to work and this will not affect the data.
- Simplified game cycle. One Update appears, and the whole cycle is divided into systems. Data is processed by the “stream” in the system, regardless of the engine (there are no millions of Update calls, as in Unity).
- An entity does not know which classes affect it (and should not know).
- Efficient use of memory . It depends on the implementation of ECS. You can reuse created entity objects and components using pools; you can use value types for data and store them in memory next ( Data locality ).
- It is easier to test when data is separated from logic. Especially when you consider that logic is a small system with a few lines of code.
- View and edit the state of the world in real time . Since the state of the world is just data; we wrote a tool, which on the web page displays all the state of the world in a match on a server (as well as a scene of a match in 3D). Any component of any entity can be viewed, modified, deleted. The same can be done in the Unity editor for the client.

And now the cons:
- You need to learn to think, design and write code differently . Think within entities, components and systems. Many design patterns in ECS are implemented quite differently (see the example implementation of the State pattern in one of the review articles at the end).
- More code . Controversial. On the one hand, due to the fact that we divide the logic into smaller systems, instead of describing all the functionality in one class, there are more classes, but the code is not much more.
- The order of calling systems affects the operation of the entire game . Usually, the systems are dependent on each other, the order of their execution is given by the list and they are executed in this order. For example, first DamageSystem takes damage, then RemoveDamageSystem removes the Damage component. If you accidentally change the order, then everything will work differently. In general, this is also true for the usual OOP case, if you change the order of method calls, but it is easier to make a mistake in ECS. For example, if part of the logic runs on the client for prediction, then the order should be the same as on the server.
- It is necessary to somehow link the data and events of logic with the view . In the case of Unity, we have MVP:
- Model - GameState from ECS;
- View - we have exclusively standard MonoBehaviour -Unity classes ( Renderer , Text , etc.) and prefabs;
- Presenter uses GameState to determine events of appearance / disappearance of entities, components, etc., creates Unity objects from prefabs and changes them in accordance with changes in the state of the world.
Did you know that:- ECS is not just about data locality . For me, this is more a programming paradigm, a pattern, another way of designing the game world - name it as you please. Data locality is just an optimization.
- Unity has no ECS! Often, during a team interview you ask candidates - and what do you know about ECS? If you have not heard, you tell them, and they reply: “Oh, so this is like in Unity, then I know!”. But no, it's not like in the Unity engine. There, data and logic are combined in the MonoBehaviour component, and GameObject (when compared with the entity in ECS) has additional data - name, place in the hierarchy, etc. Unity developers are now working on a normal ECS implementation in the engine and as long as it seems to be good. They hired specialists in this area - I hope it will be cool.
Our selection criteria for ECS framework
When we decided to make a game on ECS, we started looking for a ready-made solution and wrote out requirements for it based on the experience of one of the developers. And painted, as far as existing solutions meet our requirements. It was a year ago, at the moment something might have changed. As solutions we considered:
- Entitas
- Artemis C #
- Ash.NET
- ECS is our own solution at the time we intended it. Those. Our assumptions and Wishlist that we can do ourselves.
We compiled a table for comparison, where I also included our current solution (labeled it as
ECS (now) ):
Red color - the decision does not support our requirement, orange - partially supports, green - supports completely.For us, an analogy to the operations of accessing components, searching for entities in ECS were operations in the sql database. Therefore, we used concepts like table (table), join (join operation), indices (indices), etc.
We will write out our requirements and how relevant third-party libraries and frameworks are to them:
- separate data sets (history, current, visual, static) - the ability to separately receive and store the states of the world (for example, the current state for processing, for drawing, history of states, etc.). All of the pending decisions supported this requirement .
- entity ID as integer - support for the presentation of an entity by its id-number. It is necessary for transmission over the network and the ability to link entities in the history of states. None of the solutions considered was supported. For example, in Entitas, an entity is represented as a full-fledged object (like a GameObject in Unity).
- join by ID O (N + M) - support for relatively fast sampling of two types of components. For example, when you need to get all the entities with components of type Damage (say, their N pieces) and Health (M pieces) for calculating and causing damage. There was full support at Artemis; in Entitas and Ash.NET, it is faster than O (N²), but slower than O (N + M). I don't remember the exact assessment now.
- join by ID reference O (N + M) is the same as above, only when a component of one entity has a link to another, and the latter needs to get another component (in our example, the Damage component on the auxiliary entity refers to the essence of the Victim player and from there you need to get the component Health ). Not supported by any of the solutions reviewed.
- no query alloc - no extra memory allocations when requesting components and entities from the state of the world. In Entitas in certain cases she was, but insignificant for us.
- pool tables - storage of world data in pools, the ability to reuse memory, allocate only when the pool is empty. There was “some kind” support in Entitas and Artemis, the complete absence in Ash.NET.
- compare by ID (add, del) - built-in support for creating / deleting events and components by ID. It is necessary for the level of display (View) to show / hide objects, play animations, effects. Not supported by any of the solutions reviewed.
- Δ serialization (quantisation, skip) - embedded delta compression when serializing the state of the world (for example, to reduce the size of data sent over the network). Out of the box was not supported in any of the solutions.
- Interpolation is a built-in interpolation mechanism between states of the world. None of the solutions supported.
- reuse component type - the ability to use once written component type in different types of entities. Supported only Entitas .
- explicit order of systems - the ability to set your own system call order. All decisions supported.
- editor (unity / server) - support for viewing and editing entities in real time, both for the client and for the server. Entitas supported the ability to view and edit entities and components only in the Unity editor.
- fast copy / replace - the possibility of cheap copying / replacing data. None of the solutions supported.
- component as value type (struct) - components as value types. Basically, I wanted to achieve good performance based on this. Not supported by any of the systems, everywhere there were component classes.
Non-binding requirements (
none of the solutions supported them at that time ):
- indices - index data as in the database.
- composite keys - complex keys for quick access to data (as in the database).
- integrity check - the ability to check the integrity of data in the state of the world. Useful for debugging.
- content-aware compression - better data compression based on knowledge of the nature of the data. For example, if we know the maximum size of the map or the maximum number of objects in the world.
- types / systems limit - a limit on the number of types of components or systems. In Artemis at that time it was impossible to create more than 32 or 64 types of components and systems .
As you can see from the table, on our own we wanted to implement all the requirements, except optional ones. In fact, at the moment we have
not done:
- join by ID O (N + M) and join by ID reference O (N + M) - a sample of two different components is still occupied by O (N²) (in fact, the nested for loop). On the other hand, there are not so many entities and components for a match.
- compare by ID (add, del) - not needed at the framework level. We implemented this at a higher level in MVP.
- fast copy / replace and component as value type (struct) - at some point we realized that working with structures would not be as convenient as with classes, and stopped at classes - we preferred the convenience of development instead of better performance. By the way, Entitas developers have done the same thing in the end .
At the same time, we still implemented one of the initially optional requirements:
- content-aware compression - due to it we managed to significantly (dozens of times) reduce the size of the packet transmitted over the network. For mobile data networks, it is very important to fit the packet size in MTU, so that “on the way” it will not be broken up into small parts that can be lost, walk in a different order, and which will then need to be collected in parts. For example, in Photon, if the data size does not fit in the MTU specified in the library, it breaks the data into packets and sends them as reliable (with guaranteed delivery), even if you send them from above as unreliable. Tested with pain first hand.
Features of our development on ECS
- We at ECS write exclusively business logic . No work with resources, views, etc. Since the ECS-code works simultaneously on the client in Unity and on the server - it should be as independent as possible from other levels and modules.
- We try to minimize components and systems . Usually for each new task we get new components and systems. But sometimes it happens that we modify old data, add new data to the components, and “inflate” systems.
- In our ECS implementation, you cannot add multiple components of the same type to one entity . Therefore, if in one tick the player has inflicted damage several times (for example, several opponents), then we usually create a Damage component for each damage.
- Sometimes, the view is not enough the information that is in GameState . Then you have to add special components or additional data that are not involved in logic, but are needed by the view. For example, an instant shot on a server, one tick lives, and visually it lasts longer on a client. Therefore, for the client, the shot lifetime parameter is added to the shot.
- We implement events / requests by creating special components . For example, if a player is dead, we hang a component on him without the Dead data, which is an event for other systems and View-level that the player has died. Or if we need to re-revive a player at a point, we create a separate entity with the Respawn component with additional information to revive. A separate system RespawnSystem at the very beginning of the game cycle passes through these components and already creates the essence of the player. Those. in fact, the first entity is a request to create a second.
- We have special "singleton" components / entities . For example, we have an entity with ID = 1, on which special components hang - game settings.
Bonus
In the process of deciding whether Habra needs an article on ECS, I did a little research. , , , :
- Unity, ECS -- — ECS . mopsicus , ECS, . : Unity ECS , . . «» ECS Unity. ECS-, : LeoECS , BrokenBricksECS , Svelto.ECS .
- Unity3D ECS Job System — , ECS Unity. fstleo , Unity ECS, , - , JobSystem.
- Entity System Framework ? — Ash- ActionScript. , , OOP- ECS-.
- Ash Entity System — , FSM State ECS — , .
- Entity-Component-System — — ECS C++.