You may not need ECS

Hello hub.

Lately I have often heard in discussions something reminiscent of ECS hype. In articles about organizing code in games, about optimization, about new indies.

Opinions vary greatly, but the main idea is simple – you should try, or rather NO, you should use ECS in your game!
And if I can agree with the first, then I propose to discuss the second.

What is ECS?

ECS stands for Entity Component System and is a specialized high-performance architecture used in games. The name describes the main components:

Details are important here. In the classic approach in Unity, there are also entities (GameObjects), components and, according to the principles of encapsulation, methods for processing them. If you wish, you can move them into separate static methods and separate the data from the logic, but this is usually not recommended, and there are good reasons for this. So what's the difference?

Why ECS?

Performance.

One of the most important problems that ECS is designed to solve is performance, and not just any performance, but memory access performance.
The fact is that reading data from RAM is a radical operation, orders of magnitude more expensive than any arithmetic or logical operation in the processor.

Comparison of the speed of different computer operations

To solve this problem, they use the processor cache – very fast (and expensive) memory, usually located directly on the processor chip. Working with the cache is comparable in speed to other processor operations. Thus, when you, say, want to add two numbers, an entire block of memory of several kilobytes is loaded into the cache. Data in a program is usually located in a fairly localized manner – when working with an object, you use its fields, when working with a function – a piece of the stack, arrays are located in memory in order, that is, this approach significantly speeds up calculations.
But at some point you may need data from a block that is not in the cache. This is called a “Cache Miss”. At this moment, the processor will begin to idle, waiting for the necessary data from regular memory. Of course, this is a simplified diagram, there is no magic inside modern processors, but in general, I think it’s clear. You can even easily test my statements using the classic example of “Array of Structures” vs “Structure of Arrays”. You can make an array of 100,000 instances of a structure of 10 float fields and 10 arrays of 100,000 float numbers, and then square the numbers in the first field / first array. There is a high probability that the array will be much faster.

So here it is. Classic OOP is the worst enemy of cache locality.

Objects are created in random places in memory, which causes constant access to different memory blocks and is very suboptimal.

By constantly using OOP with its polymorphism, large number of objects in memory, virtual functions and other amenities, we pay a fairly high price in terms of performance.

How does it work?

ECS is designed to solve this, and this is what systems, components, and a different way of accessing objects are needed for.

Now ECS monitors memory allocation; it places all components in chunks – arrays with pre-allocated memory and one type of component.

For addressing, instead of references in memory, we use entities. Essentially, this is an identifier attached to a component in memory in order to understand what it logically refers to.
And finally, we use systems for group processing of components. The important thing is that the system is not just a static function for a specific component. These are static functions for arrays that perform not just one operation, but one operation on each element in the array.
By grouping data close together and processing it in a single pass, we make maximum use of the processor cache, and can also effectively use the processor's vector instructions.
Hooray!

It's actually not that simple. More precisely, just for simple cases. The point is that multiplying 2 matrix arrays is really simple and fast, but usually this is not what we need. But we need to multiply them only for even objects. Or for objects with a specific component. Or if the component has less than 0 lives. And make another 38 components and 80 systems for various aspects of their processing and game logic.
Therefore, systems provide APIs for filtering and arranging components.

But this does not solve all problems. For some processing cases, it will be optimal to use a component in the form of a combination of position and direction (movement operations affect both variables), for others it is worth separating them into different ones. Optima will be different for different games and components, moreover, they may be different in different game modes.
And in any case, it depends quite heavily on your specific logic, and accordingly on your understanding of what and how to organize.

ECS has other benefits as well.

The most important thing is the presentation of the data. All your data is guaranteed to be collected into uniform blocks-arrays, uniform and identical. You can easily iterate through them, copy them with a simple byte copy, easily serialize and deserialize them, send them over the network, save any state frame, organize match recording and rewind.
We can say that this is a pleasant and very useful side effect.
It should be noted that an analogue of a “serialized state container” can be organized using classic OOP, you just have to tinker more.
It is impossible to save just a piece of memory, links will have to be converted to another format during serialization and restored during deserialization, there may be restrictions on structures, the binary representation may be complex, etc. Some objects may require separate serialization.

And if you didn't think about it at the start, the implementation can be quite complex, while ECS has it out of the box.

Since all components are open and systems have equal access to them, you can easily organize any type of game logic, combining any components and actions on them.
At the same time, your system is structurally very simple – you only have a list of components and a list of systems, what could be simpler?

Why ECS might not be right for you?

So why might ECS not be right for you?
It's simple – it probably won't solve YOUR performance problems, but it will make your code more difficult to maintain.

Non-game logic performance.

ECS is only effective with what it works with, that is, game logic entities. That being said, the essence of game logic is actually the most complex, but not the largest part of the game.
In the context of a game, a logical object structure typically weighs tens of bytes, while a texture can be hundreds of kilobytes to megabytes, and each mesh contains thousands of points.
The example with textures and meshes is crafty – they are loaded into video memory and spin there, optimal and efficient through the efforts of NVidia engineers. But each super efficient mesh rendering requires a large amount of calculations – preparing materials, calculating scene parameters, sorting (for transparent) and grouping (for batching) objects, switching contexts, etc.

And this is just a render. Most of the heterogeneous operations for which we actually use engines work on the processor – composing and playing music and sounds, simulating particle systems and physics, heterogeneous logical components, loading resources, maintaining an object tree, drawing interfaces. Making an optimal render from a million triangles is not a problem, the problem is making a game with it.

Most of these components are well written, probably by good developers, and optimized in key places. Developers of audio libraries probably know about the efficient use of arrays and processor caches. But the question is quantity. Music will still take up more kilobytes and spend more clock cycles than typical game logic for monster movement. An array with font character parameters will contain more elements than the average array with game objects.

You should start optimizing your game by controlling these values, and no amount of ECS will help you.
If you're an indie, you need to keep an eye on this and try to figure out how it works. If you are on a team, you should explain this to the artists, because no amount of optimization of an object up to 18 bytes will help if the artist hangs 15 particle systems on the object and does magical ray tracing for your mobile game.
Unity is not perfect, but it is very good, and some of its shortcomings are extensions of its advantages.
It’s very easy to make a braking object without a single line of code!

The most effective strategy is to test, look for, and treat these problems.

There is another way – you can rewrite any component, system or function in a cache-friendly manner, specializing them for your game. This way you can optimize and improve any aspect of the game.
You just have to rewrite a bunch of ready-made code. Need I say that as long as it doesn’t slow you down, you don’t need to do this?

Game logic performance

A separate issue is that many games, in principle, do not need ECS in game logic. Look at popular games:
Shooters? – Simulating hundreds of players and thousands of bullets for ECS is not even a warm-up, regular code can easily handle this, you may not even get an advantage.

RPG? – Hundreds of monsters are more a matter of rendering than a matter of game logic of the monster.

Strategies? – even here this is not always justified.
And we're not even talking about pixelated indie roguelikes and match 3 games.
Individual parts of such games can be optimized separately using the usual OOP approach, but most of them generally do not require optimization. It is much more efficient to organize this code into well-decomposed objects and systems, with normal connections, understandable methods and a good hierarchy.
For online games, structure optimizations and binary serialization will be useful, but the optimization is in what data to transfer, not in the speed of serialization. If you do not transfer unnecessary data, they are optimized perfectly.

Performance of alternatives

It is important to note that regular OOP is not exactly slow either. The compiler can optimize.

Random objects in memory are not located randomly – allocators place objects of the same size in the same memory blocks (this makes it easier to delete and replace them later, avoiding fragmentation), thus objects of the same type naturally end up in one memory location.

If we create many objects at a time, they are likely to be created in memory in order. Thus, by creating an array with objects, we get a localized array with links and, with a high probability, localized objects in this array.

Some objects are not created on the heap at all, because the compiler understands that they are short-lived. At the same time, ECS essentially handles memory management itself, and it’s not a fact that it does it more efficiently in all scenarios.

It may sound sad that what you usually need to do is not create cool architectures, but remove redraws in interfaces and look for fat particle systems to scold technical artists. It's healthier.

To summarize: it’s probably not the game logic that’s slowing you down in your game, so it’s not the game logic that needs to be optimized.

Complexity

Many people promote ECS as the default solution because “just use it and get good performance for free.”
The problem is that this silver bullet is not free – ECS requires you to organize logic in a very specific and organizationally complex form, which makes the code difficult to read and write.
In appearance, everything seems simple – 2 layers, you can reach any component, what's so complicated about that?
But now you have architecture in your project!
But the project architecture is not only layers and patterns, it is your entities and game logic. Your units, bullets, effects, and how they interact is the real architecture of your game.
Creating efficient, usable, and understandable entities is critical to your game, and ECS tends to get in the way of that.

First, previously convenient entities such as the bullet are now decoupled into multiple components and multiple systems. Do we remember about productivity? The bullet's position and its attributes must be stored in different components, otherwise you will lose speed. Spawning, moving, hitting are no longer different functions in one method close to each other, but 3 systems where the code is organized not for you, but for optimal traversal of the array.

This architecture imposes excessive decomposition on you and the cognitive complexity of understanding such code is very high.

To understand the full behavior of an entity, you need to find all its components, all the systems that update it, and map them in your head in order of execution (which is configured by the order of the systems). With poor decomposition, it can be quite difficult to understand where the unit ends and the bullet begins.
And to modify it, you need not only to understand how it works, but also to come up with the necessary modifications and, perhaps, create an entire system or a new component for them.

Secondly, you don't have basic primitives. Do you want a delegate? There are none, use the component. Polymorphism? No, it's an enum and multiple systems. Filter by cycle? You need a component + a system. Need a tree? There are no trees, use components with entity references. Oh yes, and it will only work quickly if you organize the tree correctly; arbitrary connections will break your cache locality.
You don't have half the modern programming tools, just imperative programming and basic structures.
Or, you can use whatever you want – then you will have a mutant that does not have the benefits of ECS.

Thirdly, you don’t have the usual development tools. For classical programming there are notations, scopes, link navigation, search for classes, inheritors and interfaces.
In OOP, you have private variables and interfaces to hide details, clear code organization patterns, and hierarchical organization. Of course, classic code can also be made confusing and poorly decomposed, but for ECS it is very easy to make it that way.
Of course, you can come up with some notations, different rules for designing objects. But the likelihood of errors and the complexity of understanding and expanding such code are very high and grow rapidly as the project grows.

When to use ECS

Despite my criticism, there are plenty of arguments for using ECS.
ECS is a wonderful tool. But it's just a tool, you need to use it correctly.
You'll need to think about this carefully, but you may find ECS useful if:

You make Factorio.

Some specific games may indeed require maximum performance of game logic and the interaction of a large number of game objects. On the Factorio authors' blog you can read what hacks they used (they don't use ECS, just their own implementation in C++) to process all these thousands of copper bars on conveyors in real time and over the network. Need I say that these are specific and complex games? Perhaps you don't need to represent tens of thousands of bars?

You are making a complex online game.

The logical part must run efficiently on the servers, supporting hundreds of sessions on each machine; synchronization of server and client data has a high priority. In addition, ECS easily allows you to organize saving and loading sessions, serialization for synchronization. In this case, the complexity may be worth it.

You are learning.

I think it’s clear here, try it yourself, you don’t have to take my word for it. Just please, on something small and cheap.

You know what you're doing.

I think there are enough people who understand the issues described above better than me. Burn it.

You are using ECS ​​for only part of the project.

It is important to understand that the entire game does not have to be written in this style. Perhaps the opposite approach would be justified – you highlight exactly what these super requirements are for. State game, network-synchronized battle session model. The rest of the game code, the interface, the meta part, can be written in the usual OOP style, with an emphasis on the convenience and clarity of the code, or the convenience and clarity of extensions, editing by designers and other requirements.

Alternatives

Also, a very important part to remember is that you can optimize any specialized aspect of the game separately without ECS.
Almost all of its benefits can be used separately from it.

Do you want to generate a 3D voxel landscape and generate 3D meshes for it? Perhaps you don't need ECS, but rather a specialized component. It can work on native arrays and binary conversions and use Jobs for parallel computing.

Want a super fast event system? Perhaps this is the only thing that needs to be written specifically.

Do you want a million bullets? You can make the bullet system very fast by using an attribute array, a position array, and a direction array, and grouping the movements into one method. There will be only one problem left – to draw them, and for this you do not need ECS, but you need DrawInstance.

Want high performance pathfinding? You need to execute tasks in other threads.
By the way, DOTS does not require ECS; its jobs type tools can be used separately.

Instead of a conclusion

Ultimately, I believe the ECS hype is an unhealthy reaction, but a reaction to real and complex problems.

Unity is a great tool. It has many disadvantages, but most of it is a continuation of its advantages:

Don't go after the hype, try to understand what you need.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *