With an OOP architecture, you usually end up with code which if unrolled to a purely procedural style would look like this:
for each entity
if entity can move
move(entity)
ifswitch entity canattacking shoottype
when shooter
shoot(entity)
when melee
meleeAttack(entity)
when nothing
doNothing(entity)
if entity has health
checkIfDead(entity)
While with an ECS architecture, the resulting logic would look more like this:
for each entity which can move
move(entity)
for each entity which can shoot
shoot(entity)
for each entity which can meleeAttack
meleeAttack(entity)
for each entity which has health
checkIfDead(entity)
Why is the second usually faster?
- Smaller loops are easier to optimize. This applies to both your compiler and to the CPU microcode.
- ECS allows those loops to only iterate over those objects which actually have those components. OOP needs to check at runtime. Polymorphism isn't magic. When you call a method of a base-class or interface, then it might look like a simple method call in your code. But what actually happens at runtime is a hidden
switch
statement to pick the correct implementing method based on the actual type. ECS, on the other hand, can pre-compute lists of which entities are relevant for each system and only needs to update those lists when the composition of an entity changes. So it no longer needs to check every tick.
- Memory locality. The move-loop only requires the move-component. The heal-loop only requires the health-component, and so on. In a well-designed ECS architecture, the data of all components of the same type will be stored in an array, which will be a contiguous section in memory. Contiguous memory section are very quick to load from RAM into CPU. In an OOP design, on the other hand, you are doing a loop over all the data of all the entities at the same time. Which means they all need to be loaded from RAM into the CPU cache. When you don't have enough CPU cache to hold them all, then this is going to get slow.
- Parallelizability. This is not something you get automatically. But if you want to design an engine which utilizes multiple cores well, then ECS usually makes that easier than OOP. The main problem with parallelization is finding units of processing which don't share any data. The reasons for that would make this answer too long. So just trust me that you don't want two threads to access the same memory, especially not when at least one of them needs to change that data.
The data-oriented design of ECS makes that much easier, because it makes it far more visible which systems use which data. That means it is usually easy to tell which systems can be run in parallel and which systems can use parallelization internally. But with OOP, every object is a black box (intentionally!). You don't know what data an object accesses, and you don't want to (it's called abstraction). That makes it very hard to judge what can be parallelized and what can not.