3
\$\begingroup\$

Correct me if I'm wrong, but having read a lot about the ECS-Pattern in general and in the context of current game engines I am beginning to wonder if the ECS-Pattern in it's core is hardly more than an object-oriented design in disguise with added reusability?

To clarify my point, first of all there are good answers here describing the general function how an ECS approach works (e.g. here, though I would argue about the HashMap approach). Furthermore Unity added alot of functionality in this regard in its 2019 DOTS.

In this unity blog describing the changes and differences to traditional "gameobjects" and the rough implementation Unity uses, one paragraph raised some questions:

[...] In an earlier draft of this post I wrote “we store entities in chunks”, and later changed it to “we store component data for entities in chunks”. It’s an important distinction to make, to realize that an Entity is just a 32-bit integer. There is nothing to store or allocate for it, other than the data of its components. Because they’re so cheap, you can use them for scenarios that game objects weren’t suitable for. Like using an entity for each individual particle in a particle system.

There is - what I'd call - a hype about the ECS-Pattern and why it is so much better than a traditional approach using objects as entities and components, and their class as the systems part. Having done my fair share of projects in different topics and different programming languages, an instance of a class without any variables or functions is, on an abstract level, nothing more than a statement of it's existence, most likely an address in memory.

So it would also satisfy this part

to realize that an Entity is just a 32-bit integer. There is nothing to store or allocate for it

of the quote (apart from the name).

Moreover a class is a collection of variables (or fields/members or any other word for it) which could also be called data components. Together with the class's functions .. is it an ECS based design if you group the objects based on their class when processing multiple objects?

If you compare the ECS-Pattern to a traditional particle system, which usually consists of arrays of variables so it can store different data for each of it's particles, the similarities are even more obvious.

public class ParticleSystem{
    int numParticles;
    float[] x;
    float[] y;
    float[] size;

    public void update() {
        move();
        resize();
    }

    public void move() {
        for (int i = 0; i < numParticles; ++i) {
            x[i] += 0.2f;
            y[i] -= 0.1f;
        }
    }

    public void resize() {
        for (int i = 0; i < numParticles; ++i) {
            size[i] *= 0.95f;
        }
    }
}

Take this over-simplified particle system as an example. It would be treated as a traditional "gameobject". Now you could write it differently ..

public class Position {
    float x, y;
}

public class Size {
    float size;
}

public class ParticleSystem{
    int numParticles;
    Position[] positions;
    Size[] sizes;

    MoveSystem moveSystem;
    SizeSystem sizeSystem;

    public void update() {
        moveSystem.execute();
        sizeSystem.execute();
    }
}

public class MoveSystem {
    Position[] positions;

    public MoveSystem (Position[] pos) { positions = pos; }

    public execute() {
        for (int i = 0; i < positions.length; ++i) {
            positions[i].x += 0.2f;
            positions[i].y -= 0.1f;
        }
    }
}

public class ResizeSystem {
    Size[] sizes;

    public SizeSystem(Size[] s) { sizes = s; }

    public execute() {
        for (int i = 0; i < sizes.length; ++i) {
            sizes[i].size *= 0.95f;
        }
    }
}

.. and it looks like an ECS-based particle system with the main class as the "manager" of the entities. Every particle(entity) is just a 32-bit integer (it's index). There is nothing to store or allocate for it, other than the data of its [two] components.

(I do know this is neither elegant nor complete code)

You could write every class inheriting from a basic "gameobject" as a collection of instances. Imagine you have a virtual meadow with sheep roaming around. They need various different components for logic, physics, rendering and ai but you could easily design it like a classic particle system.

And if you read the whole post it describes a "archetype" class with up to 16k per "chunk" holding only entities of the same kind and running systems in a linear loop over all the chunks of all archetypes which feature the same components and all entities in those chunks. Which sounds a lot like "particle systems" for each archetype of entities. It just hides the archetype class from the user ..?

Edit1:

I am not struggling to understand the specifics of how to implements an ECS-Pattern or how it works on a functional level, rather the aspects an reasons why it is considered by many to be vastly superior to an OOP design. This requires a view of the entire system, from the software layer right down to the hardware considerations.

One common argument in favor of ECS is the RAM allocation and the improvements on cache-miss-rates - and therefore performance - for both data and instructions, as a component-based grouping of data and system-based processing of the components allows for more efficient access to consecutive data.

Example: x Entities with components A through Z

Why is an ECS design like

A[x]; B[x]; ... Z[x];

considered more efficient than

(AB..Z)[x];

..?

In the first case you could cache a single component per read for (potentially) multiple entities at once. But depending on your system you need multiple reads from different locations on the RAM. The second case could (potentially) cache a single object per read (more likely a single object with multiple consecutive reads). And although RAM is designed for random access, different sources claim consecutive access is still a lot faster.

I do get the fact a system doesn't need all the data of a single object. Which, as I see it, is the strongest argument on this level. This article from Intel's developer zone includes an illustration of the problem with fragmented RAM on a class-based view of the system. If you change the view from a class-based to a per-byte view, ECS would have the same problem. The components a systems requires to operate are stored in seperate arrays so the system regularly needs to access non-consecutive memory. Which is the flaw of an OOP based memory design that is pointed out in the article, as the prefetching needs to learn what to load next?

Edit2: addressing the comment from Quentin

Again, I am not concerned on the functional design of how a system discovers all relevant entities, but on the "decision-making" process which objects are relevant.

Imagine you want to model two kinds of entities. Returning to the example from this Q&A the first kind of entity is the ball which just falls in the direction of the global gravity.

spd += global.gravity * global.deltaTime;
pos += spd * global.deltaTime;

The second kind of entity shall be an alien ball, which is negatively affected by gravity.

spd += -global.gravity * global.deltaTime;
pos += spd * global.deltaTime;

Notice the - global.gravity this time. Disregarding how the example's system discovers all eligible objects, as long as the systems operates on all objects that feature a speed and a position component - that are eligible - , the questions boils down to what decides if an object is eligible?

As systems are not exclusive, i.e. a system that operates on components A and B, and a system that operate on components A, B and C both process the same object (if the object in question does contain all three components) how would you differentiate different between different behaviours without adding additional components to all entities?

If you were to add a gravity component to the alien ball entities to address the different gravity, the "Fall" system would still process the alien balls as they still have the speed and position components. An "AlienFall" system, which takes speed, position and gravity components would then only process the alien balls with the correct gravity and both systems would negate each other (in terms of the speed calculation) and the result would vary based on the order in which the systems are executed.

Unifying both systems by adding a gravity component to every entity would rather contradict the ECS efficiency paradigmas as you effectively add lots of data compared to the information it represents.

\$\endgroup\$
  • 3
    \$\begingroup\$ I think your assumption that a classic particle system is a good comparison point for object-oriented programming might be the source of confusion. We've long written particle systems in an ECS-like fashion, even before ECS took off as a more general purpose strategy for other game behaviours. So much so that when ECS was a novelty, folks would often introduce it as "it's like how you program particle systems, but for everything" ;) \$\endgroup\$ – DMGregory Mar 5 at 21:07
  • 2
    \$\begingroup\$ I think you might be interested in this Q&A. It's about access restriction and encapsulation, but also tries to draw parallels between ECS and OOP. \$\endgroup\$ – Quentin Mar 6 at 10:18
  • \$\begingroup\$ @DMGregory That is actually an interesting insight! Thanks. Though ECS is not fundamentally different than, is it? Physical memory "management" seems to be a common advantage emphasised in favour of ECS, if you were to group your traditional gameobjects on a "per-class" basis instead of storing them as references of their common superclass in a single array, even the memory aspect is fairly similar. Only the order in which the instances' bytes are stored would be different? \$\endgroup\$ – rootmenu Mar 6 at 19:22
  • \$\begingroup\$ @Quentin Thanks, but the post doen't cover any theoretical/techno-philosophical questions. It is a good explanation with well though out examples for the differences between OOP and ECS. It does, however, as well as any post I've come across so far concerning Unity's DOTS, fail to answer an inportant question: how does a system know on which entities to operate? As functions and systems carry inferred information about a process. From the Q&A: "The system says "I know how to make anything with a position and a speed fall". " Imagine another object which is inversely affected by gravity? \$\endgroup\$ – rootmenu Mar 6 at 19:45
  • \$\begingroup\$ That sounds like a different question, one that's been covered here before, conveniently enough. :) \$\endgroup\$ – DMGregory Mar 6 at 21:08
2
\$\begingroup\$

Strong "no" from me. The concept of what constitutes object-oriented design and programming is widely contested in my experience when it comes to the nuanced details among experts and pioneers who introduced object-oriented concepts to languages. Yet if there's anything that seems to be at the heart of the focus behind OOP, it's encapsulation: the bundling of data (state) and logic (functions/methods) to access/mutate that object's internal state. It's not that OOP is the only way to achieve this, but I would say it's the primary and lowest-common-denominator focus.

The ECS immediately goes in the opposite direction here. It also varies in terms of how people design and implement it, but at the heart of it is a separation, not a bundling, of data (in components) from logic (systems). I would tend to characterize most ECS designs I see as procedural programming on steroids. Sometimes I also see functional-style variants that arrange systems in a pipeline with immutable scenes and components. Of course, some people might use objects here and there to implement an ECS, but again the heart of an ECS revolves around separation, not encapsulation, and so I'd say it's very, very far-removed from OOP.

Why is an ECS design like [...] considered more efficient than [...]

Sticking to the philosophical and generalized kind of vibe here, it is not necessarily always the case that the ECS is more efficient than an object-oriented design. But OOP encourages bundling of data (and logic) based closer to human intuition, not access patterns friendly to the hardware. Since the ECS immediately abandons encapsulation, it is free to do things like split hot/cold data, even for what might normally and naturally be bundled together into a single object by an object-oriented designer, for more efficient access. Again it is not object-oriented, and breaking that capsule apart as the ECS does means we're free to arrange and rearrange data more freely. Object-oriented designs often trap you inside the capsule when you're staring at profiler hotspots unless the capsule you're inside is very big and offers plenty of room to change data representations.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

ECS is not OOP. It is data-driven. From an OO stand point ECS flips encapsulation on its head. However, you can build OOP on top of it.

You can imagine a solution that lets you write a class - in a conventional fashion - but underneath it would be generating components and systems. The solution would enforce any accessibility rules necessary, and would implement any features you consider to be mandatory for OO, and well, it would be OO, but the values of an entity are not contiguous in memory.

I don't think ECS are inherently OO, but you can use them that way. You can implement an object oriented design on top of systems that do not have object oriented features… Case in point: you can implement an OO design on an structured language by following the pattern that the first parameter a routine is the this pointer that goes to an struct with the fields. Yes, that does not have the right encapsulation rules either, however you can built an OO solution on top of it. And that is how C++ started, the first C++ compiler it would generate C code that worked in the fashion I just described.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

Short Answer

ECS fundamentally takes into consideration how modern processors best manipulate data rather than how people think about data and this forces programmers to write more efficient code just because writing bad (slow) code isn't the path of least resistance anymore. One isn't inherently faster than the other but ECS is harder to make slow and easier to multithread!

Long Answer

ECS-Pattern in it's core is object-oriented design in disguise with added reusability?

In traditional OOP you create an object and have it take core of it's own responsibilities leaving everything else for other classes to handle. This means that it contains all of it's own data and the logic to modify it in a single class. In ECS (as you show with examples) you have the data split out from the logic and in the case of Unity you can further divide the logic into multiple systems in order.

This separation of data and logic is what makes ECS different to OOP since a class is no longer responsible for it's own actions, rather the responsibilities have been (or can be) divided up and moved to multiple classes (some of which can be reused if you design it that way), this approach of simply passing data into a thing that knows how to modify the data is what makes it data-driven because you always have to think about the data first.

Can you do this with OOP? Yes. but you then have to keep the data and the logic together whereas ECS keeps separation between it and allows for more flexibility when trying to modify data.

Why is an ECS design like A[x]; B[x]; ... Z[x]; considered more efficient than (AB..Z)[x];..?

It's not. It's implantation that matters. I could write a slow ECS-System and a really fast gameobject-System.

Basically if you can group data together in memory (as you mention) the CPU can make a good guess of what data a system is going to want to access next and it'll load it into the CPU cache before the program ever asks for it. This makes a big difference because the cache is much faster than accessing from memory in comparison and the amount of data that the cache can store is relatively small (MB in Cache vs GB in memory).

If you were using traditional OOP you would have an object with a lot of data which needs to be pulled into the cache in order to start working on the data. Now this could potentially hit multiple bottlenecks:

  • If you have slow memory it's going to take a while to make get the data to the CPU lowering performance
  • If you have a lot of data you won't be able to fit it all into the cache and you'll have to make multiple trips or load bits of it in and out lowering performance
  • If there is other stuff going on which needs to be processed that will also be using a part of the total memory bandwidth which lowers performance.

This is where ECS shines (again, assuming a good implementation) because the data is in small chunks you load only the amount of data that you actually need. Looking at Unity's gameobjects (because that's where your examples are coming from) a gameobject has a name and position and rotation and a parent and children and other components etc. this is all bloat that has to be loaded... why? we don't need it! someone might need it but only is specific circumstances. With entities you have it's components and we don't really care about the entity itself only the components of an entity which we can feed into a system and by extension only the data that we need! see the bullet list why this is important!

If there is a good implementation of ECS that data will be tightly packed together, no gaps in memory that will cause a CPU cache miss (which is more expensive than just getting the data for reasons that are beyond this question) and no unused data that is either repeated (and therefore only read once) or not used at all (cause you can just delete it or put it into another component if a different system uses it).

If you compare this to gameobjects you have loose data, a component might be in one place in memory that might contain a list which links to 200 other objects which are also spread throughout memory and then we might need another piece of data which is is inside another component on the gameobject which is somewhere else in memory (and I hope we have already cached the location of that component otherwise we get to search for it every game loop!). How is the CPU supposed to guess where this stuff is and what your going to need next? It won't and you performance suffers. but it doesn't really matter because CPUs are fast so you keep going till you want to optimise and then it's too late cause you can't pull that data out anymore, too much stuff needs it and you have to ship... Again it's not impossible to get good performance from OOP (gameobjects), just much easier to get slow performance.

Another aspect of ECS that people don't consider as much is that since the system that you build inside ECS only has to calculate/retrieve frame dependent data once and it can then use it for all components. In most cases this means a system grabs delta time at the start and has it ready to go as it processes the components rather than grabbing it from a random memory location each time a gameobject component goes through the game loop. In other cases a lot of stuff might only really need to be calculated once then mixed in with component data and ECS makes this really easy because all of the data is at an ECS systems finger tips and harder to make the mistake of recalculating each frame because you know that a system is called once were as a gameobject might be called many times but which one takes the hit in working out this data once? if you mess it up can you live with last frames data or will that cause bugs? Most people either don't want to take the risk or don't realise this costs potential performance.

| improve this answer | |
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.