Infinite Limit

Looking between the pixels...

Entity Component Systems From Scratch

Category / Engine Architecture

29 May 2017

(Editor's Note: This article will be published in French on the author's website. The original English text can be found here. This version has been modified for clarity.)

Entity Component System is an interesting pattern. Instead of thinking of a game entity as having data and logic, an ECS pushes you to instead separate logic from data. Think of logic as a factory processing some raw material to craft an object.

There is a lot of writing about ECS theory and concepts, from vague explanations of the reasons behind storing entities in contiguous memory (we'll get to that term later). This is not intented to be a trustable reference but just some notes from someone who tried from vague explanations found on Internet to build a full ECS system in Lua, initially for Love2D.

Why this choice?

The biggest reason behind this move was experimentation. I'm currently facing issues with the structures of the game engine I've built or used and wanted to explore different options, relating with my targeted professional work (I'm aiming to become game engine developer). I heard mostly good feedback about ECS and I wanted to delve into it to understand the advantages and the problems that arise from adopting such an architecture.

Also, I'm still on the game engine structure discovery adventure. I'm still trying to work on my PICO-8 experiment with coroutine-base engine (where update and render of each entity are coroutines, allowing for delayed procedures as if it was a simple list). I liked building this light system (with a sample CPU usage graph entity, it weights around 202 tokens) that allowed me even to break down global game state as entities (the splash screen itself is an entity).

An ECS allows data and logic separation through Components and Systems. A component is just made of data (like "Vector" would only be comprised of its coordinates) where a system is just holding the data (like "VectorRenderer" which would for instance draw a vector on the screen). This allows for components or systems to be reused at multiple places and pushed into a more generic or structure conception process.

Design Choices

I wanted to build a system that:

  • Allowed me to create entities just by listing a few components and/or their arguments
  • Trying to store contiguously components to allow, the best I could on lua, streamlining data to make the most of CPU caches.
  • Light and fast enough to not be limiting an user's choices around design and organisation.

Conception

I rediscovered Love2D by discussing with people about PICO-8. I decided to choose this game engine as it gives a directly usable framework to play with with the classic update/render loop. Plugging the engine on it will be easy. As a part of a bigger engine idea, the ECS will be stored in a `World` (here `ECSWorld`, I might be swapping both names, sorry) that'll be called at every update or render tick next to the Quake-like debug console I also did for this experiment.

Given a class factory by a friend, the ECS system is built over those classes:

  • Component: a simple class designed to only hold a single function "init" defining its own structure.
  • System: the base behind the systems (also called processors in some engines). They're stored and called by System Filters.
  • Renderable and Updatable: two base system classes to determine which components needs to be updated or rendered (called inside the world's "update" or "render").
  • SystemFilter: just a small system container to group them by the list of components they need to avoid filtering the entities per component. Those classes are updated and/or called by the world.
  • ECSWorld: the ECS system container holding the entity array, the system filters and the components.

And that's all. Lua is very flexible so making templates or containers aren't really needed here. That's its biggest advantage for dense code. Let's delve into the code class by class, shall we?

Entity

Entity is just a simple class that holds an UID, a boolean to know if the entity can be recycled (here I have both for another reason) and a reference to its container world for simpler access code. It's as simple as this:

function Entity:init(index, world)
self.__destroyed = false
self.__destroy_required = false
self.__eid = index
self.__world = world
end
-- and later in the code...
local entity = engine.world:createEntity()
entity:addComponent(LifeComponent, 25, 42) -- See Component section

I have two booleans to allow me to remove an entity only once the update loop is done, not on the fly but the logic stays the same, there is just an ID, an alive indicator and let's roll!

Component

The component is just supposed to be a data structure (in C++, you would call them "POD"), with small changes, like giving them meta-methods to return a string from them. Every Component class holds a Unique Identifier to allow sorting them and comparing them more easily, predictably and faster than comparing them with their content or their pointer.

local LifeComponent = Component()
LifeComponent.__name = "LifeComponent" -- This is just to have a readable result when calling __tostring
function LifeComponent:init(life, life_max)
self.life = life
self.life_max = life_max
end
local ManaComponent = Component()
ManaComponent.__name = "ManaComponent"
function ManaComponent:init(mana, mana_max)
self.mana = mana
self.mana_max = mana_max
end

Note that we could make this less repetitive by making a common class like Gauge, or Stat, but that's just an example.

System

"System" is a similar class. Having meta-methods to give them names and an unique identifier, they'll contain instead the logic through an "update" function. In my engine, they're directly given the SystemFilter's registered entities (we'll get to that later) so they'll process them the way they want (processing their logic one by one or globally managing them).

local CreatureSystem = System()
function CreatureSystem.update(dt, entities)
for i,entity in ipairs(entities) do
print(string.format([[HP : %d/%d MP : %d/%d]],
entity:getComponent(LifeComponent).life,
entity:getComponent(LifeComponent).life_max,
entity:getComponent(ManaComponent).mana,
entity:getComponent(ManaComponent).mana_max))
end
end

-- and later in the code
-- Registering CreatureSystem as needing LifeComponent and ManaComponent
world:registerSystem(CreatureSystem, LifeComponent, ManaComponent)

What I'm planning at the time I'm writing this note is adding callbacks to the systems at some events during entity modification: adding or removing a component should trigger related systems' callback for usages like removing a hitbox from a physics engine once its entity is stripped off the Hitbox component.

System Filters

"SystemFilter" is a class that only exists to store "Systems" and to register compatible entities. First, a "SystemFilter" is created by passing a list of "Component" classes to be registered as a filter, entities having an instance of all Components of a class will be registered to the filter.

-- Returns true if the entity has all the components for the current filter.
function SystemFilter:checkEntityCompatibility(entity)
local num_valid_components = 0
for i,component in ipairs(self.required_components) do
if entity:hasComponent(component) then
num_valid_components = num_valid_components + 1
end
end
return num_valid_components == #self.required_components
end

Thus, only entities fully relating to systems are managed. This is not a perfect implementation as there is still need for a local entity list. As said earlier, such class allows some memory sparing and also allows a faster (I suppose) processing as the class registers and unregisters entities based on their component list.

Renderable and Updatable components

Those two components are directy used by SystemFilter to filter out to determine which component will be called at update time or at render time. Nothing really fancy except the fact that every component must be a child class from one of those two (or both) to be acounted by any system.

ECSWorld

"ECSWorld" is the class that holds everything in place and opens function such as to create an entity and call update/render loops.

My Own Caveats

The most important potential issue I'm facing is the impossiblity of an entity having multiple components of the same type, preventing me from having a simple structure like a sprite and multiple hitboxes on the same entities. Instead, I would have to make sub-entities directly linked to the main one.

Another major issue is graphics rendering. Here, Love2D doesn't seem to have Z-ordering, which currently blocks me from agencing the way I like drawable elements. An idea I had is to create a drawable list and sort it by priority. The fact this issue exists raises the global issue of not being able to run the processes in a custom order. This is not supposed to be an issue, but I suppose in this case and in some edge ones, it could be.

Some smaller ones are more the public API, like the system registering which is a bit weird to have as that's the part that declares the component requirement. It would be better if I were to put this in the System declaration for instance.

More to come?

I don't know if I'll push forward with this structure. Creating everything in lua engine-wise sounds pretty silly to me after working in performance critical code, even though I might use this structure as an exercise or with small games. I tried to see where it could be bound (CPU or GPU) and it seems that for a few thousands of simple entities that would draw each one a filled rectangle, it gets pretty GPU-bound, even though I had some floating point operation at every component. I scrapped the source as I didn't realized I could use it as a sample code here. Sorry! CPU might not be a problem with a high-end computer but the thing that worried a bit for me was the memory managment. I'm not really used to see the memory usage grows over time without being able to see correctly the true memory usage without throwing a garbage collection.

By Eiyeron

A gamedev with too many projects and not enough focus. Find them @Eiyeron or on their website.