The General Idea of ECS

ECS, Entity Component System, divides code between two segments: data and systems. There are a couple specific implementation details, however it breaks down to some system that iterates over a subset of the data and performs some sort of logical operations on it.

The ECS needs to solve generally these problems

  1. entity creation
  2. entity deletion (either by ID or by Index)
  3. entity indexing (either by ID or by Index)
  4. entity iteration

There are at least two types of referencing you care about as the programmer.

  1. Iterating through a list, where index matters more.
  2. Referencing one specific entity, where ID matters more.

Referencing by index comes from the want of these system functions that just care about iterating over a set of data to do something. Like moving entities that have a position and velocity. There you couldn't care less what ID or what kind of entity it is, probably, and you just want to move things that have those components.

Referencing by ID comes from wanting specific relationships between entities or between an entity and some specific functionality that no other entity has. Take the player for example. The player will have things every other entity has-like position, maybe velocity, etc. On the other hand, The player has very specific functionality that a lot of other entities don't have, or maybe none at all. The player entity is controlled by the user so in the main code you will want to reference the player no matter where it is in the Entity Component System.

Some Implementations

The most basic implementation I saw was having multiple arrays, possibly in a struct, and every entity has a slot. You just pre-allocate as many slots as you need with each attribute, whether it's used or not. Here the flags represent what components an entity has. It isn't the only way to represent what components an entity has, but it's a simple approach to quickly get started.

component slot1 slot2 slot3
FLAGS p v - p
ID ID - ID
position xy - xy
velocity xv yv - -

You can then solve the three problems in a simple manner.

  1. Creation is by iterating through the slots looking for a slot with no flags set, meaning it has no components, and then you set the flags and fill out the attributes with your data.
  2. Deletion is also just as simple. Pick the slot, either by finding the ID or by specifying index, then set the flags and ID to something that represents empty in your system, maybe 0 or -1.
  3. Indexing is just iterating through the IDs to find the slot, or just directly specifying the slot.
  4. Iteration inside of system functions is just iterating through all entities looking for valid IDs and flags. The system then can decide if the entity slot matches its criteria of being whatever it's working on. Maybe it's the update position system, and all it cares about is if it has a position flag and a velocity flag.

Now, obviously this implementation isn't very efficient. It loops through empty or irrelevant slots a lot whether it's for a deletion, a creation, or iteration itself. This is where a naive ECS can become a fully fledged ECS, and it's also where you can decide if you even need 10,000 entities. Even though, this is inefficient it's also dead simple. If you build the right abstraction, you can later decide to optimize your implementation while leaving the codebase non-the-wiser. It becomes reminiscent of databases, in that you don't care how the ECS gets your data just that it does. And you can query, filter, or modify whatever data you want.

An implementation of a system is simple, though knowing how to break up systems can require some thought. Systems will query some set of data then perform operations on that data. In this simplest model that means a system queries all entities, as that's all this particular ECS offers, then the system will iterate over all those queried entities. In order to ensure that it doesn't iterate over the wrong data in this simple model that means it checks the flags and potentially other descriptive data depending on how complex your behavior is. Maybe you have multiple entity types with different behavior yet the same components. The only way to distinguish between them is then to have an attribute potentially called entity type, or something equivalent. The alternative is to make the way you query entities more specific. The implementation of filtering out the entities you want to iterate over can differ, but you may just hide this simple loop of checking flags type attributes to return the entities you want to iterate over. However, you may also improve it further, you could perform optimizations on your data layout so that your data is grouped by some subset, or maybe you can keep arrays that index into specific parts of the arrays, say an array specifying every enemy.

If this all sounds familiar, you may be thinking of databases. SQL for example, minus all the guarantees of safe and persistent access to data, offers a similar model to querying and modifying data in a uniform way. Though, I wouldn't recommend using SQL for an ECS.

This is all just my take on ECS after learning about it for a while. OOP isn't worse than ECS, and ECS isn't better than OOP. The context of what you are working on and what you want to do matters. In games, you typically have many objects interacting with many other objects, you can't redesign or rethink that for the most part. So using an ECS is more natural. However, that isn't to say that games can't use OOP. OOP is best fit for the cases where abstraction matters more, such as networking or accessing devices, etc. A printer actually doesn't care how what your GUI application is doing, and your GUI application doesn't care what is happening in the printer specifically, just that a printer can print things, for the most part.