ECS meets SQL: the world as a relational database

2024-11-01  |  
ECS SQL

Originally published at https://github.com/gilzoide/ecsql/blob/main/articles/01-ecs-databases-en.md

Note: this document assumes you have basic knowledge of SQL

Primer on ECS

Entity Component System, or ECS for short, is a design pattern that provides code reusability by separating data from behaviour logic, often used by video games. ECS models are composed of:

For more detailed explanation of ECS concepts, check out this ECS FAQ.

ECS and databases

An ECS world can be viewed as a database, which stores all living entities and their components data. The world is queried to match systems with the correct component set, using a sort of JOIN operation between different components. Components may be added and removed from their entities in either a one-to-one (1:1) or one-to-many (1:N) relationship.

Thinking about this analogy of ECS worlds as databases, what if we implemented an ECS framework using SQL databases, powered by SQLite?

The pros:

The cons:

Even if it will certainly be slower than specialized ECS implementations, SQLite is very fast in general and it is likely fast enough for lots of game projects, so I want to experiment with this idea and see how far it goes. This experimental ECS framework powered by SQLite will be called ECSQL.

Representing entities in SQL

In ECS, entities are represented by a unique identifier, usually an integer value. To represent entities in SQL, all we need to do is create an entity table that stores the existing entities, represented by their numeric ids:

CREATE TABLE entity (
  id INTEGER PRIMARY KEY
);

Representing components in SQL

Components are data blocks that can be attached to entities. All components must be associated to a living entity. When the entity is deleted, its components should be deleted along with it.

To me, the best representation for ECS components in SQL is creating a table for each component type.

The base for every component could be defined like the following:

CREATE TABLE component (
  -- Owner entity id
  -- "PRIMARY KEY": components are uniquely identified by their entity's id
  -- "REFERENCES entity(id)": foreign key constraint, 1:1 relationship
  -- "ON DELETE CASCADE": delete this component when entity is deleted
  entity_id INTEGER PRIMARY KEY REFERENCES entity(id) ON DELETE CASCADE
);

For example, a 3D "Position" component could be defined like the following table:

CREATE TABLE position (
  entity_id INTEGER PRIMARY KEY REFERENCES entity(id) ON DELETE CASCADE,

  -- 3D position axes, all defaulting to 0
  x DEFAULT 0,
  y DEFAULT 0,
  z DEFAULT 0
);

As another example, a "Velocity" component could be defined like so:

CREATE TABLE velocity (
  entity_id INTEGER PRIMARY KEY REFERENCES entity(id) ON DELETE CASCADE,

  -- Velocity value, in m/s
  value DEFAULT 0
);

Querying components from systems

Systems by themselves will not live in the SQLite database, but they will use SQL for querying components. Let's take the "Move" system example one more time, which is applied to entities with both a "Position" and "Velocity" components as described above. Iterating over all entities that have both components can be accomplished with the following query:

SELECT 
  entity_id,
  position.x, position.y, position.z,
  velocity.value
FROM position
JOIN velocity USING(entity_id);

We could also easily support systems that require some components, but only optionally requires others. This can be done by simply changing the JOIN by a LEFT JOIN or RIGHT JOIN, making SQLite return NULL for data from components that are not present in the entity.

Conclusion

We've seen briefly what ECS is and how ECS worlds can be modeled as SQL databases.

In the next article, we'll start implementing our experimental ECSQL framework. See you there!



Like this content? Sponsor my work using the button below!