Home

Awesome

ECS

A small archetype based entity component system. I wrote this out of my own curiosity. This is not meant to be used in a "real" project.

Usage

Copy ecs.c and ecs.h to your project.

#include "ecs.h"
#include <stdio.h>

typedef struct {
  float x;
  float y;
} Position;

typedef struct {
  float x;
  float y;
} Velocity;

void Move(ecs_view_t view, unsigned int row) {
  Position *p = ecs_view(view, row, 0);
  Velocity *v = ecs_view(view, row, 1);
  p->x += v->x;
  p->y += v->y;
  printf("Entity moved to (%f, %f)\n", p->x, p->y);
}

int main(int argc, char *argv[]) {
  ecs_registry_t *registry = ecs_init();

  const ecs_entity_t pos_component = ECS_COMPONENT(registry, Position);
  const ecs_entity_t vel_component = ECS_COMPONENT(registry, Velocity);

  for (int i = 0; i < 10; i++) {
    ecs_entity_t e = ecs_entity(registry);
    ecs_attach(registry, e, pos_component);
    ecs_attach(registry, e, vel_component);
    ecs_set(registry, e, pos_component, &(Position){0.0f, 0.0f});
    ecs_set(registry, e, vel_component, &(Velocity){1.0f, 1.0f});
  }

  ECS_SYSTEM(registry, Move, 2, pos_component, vel_component);

  while (true) {
    ecs_step(registry);
  }

  ecs_destroy(registry);

  return 0;
}

How it works

Entity component systems lets you address performance and maintenance problems that typically arise in traditional object-oriented programming (OOP). Heavily nested hierarchies seen in OOP causes memory to be distributed all over the heap, forcing the CPU to twiddle its thumbs as it waits for data to be fetched from main memory. Object oriented inheritance hierarchies are also difficult to change as a project gets bigger and bigger.

Entities are simply represented as 64 bit integers. They are stored in an archetype, a table where entity component data can be accessed.

Components are just POD (Plain old data). They can be structs, or even integers. You can attach components to an entity. If you used Unity before, this kind of architecture may seem familiar to you.

Systems are pieces of code that updates a given set of components. For example, a system that runs on Position and Velocity components will update all entities with at least those two components.

Archetypes are a huge part of making this entity component system work as fast as it can. Each archetype stores a unique set of component types and component data. Archetypes are also vertices in a graph, with left and right edges. Traversing left in the graph leads to archetypes with one less component type, while traversing right finds archetypes with one additional component type.

archetype graph

Storing archetypes in a graph is a fast approch to search for an archetype. Sander Mertens, the author of Flecs, explains this in depth in his blog post.

More Details

Benchmarks

The graph below shows the time it takes to update a given number of entities, 3600 times. Every entity has two components, and as a result, a single system is performing the operations.

I benchmarked the ECS in this repository, Flecs, and EnTT.

ecs benchmark graph

EntitiesecsFlecsEnTT
101 ms22 ms07 ms
201 ms13 ms09 ms
401 ms34 ms09 ms
801 ms27 ms25 ms
1602 ms35 ms46 ms
3203 ms45 ms84 ms
6406 ms56 ms101 ms
12809 ms75 ms134 ms
25617 ms118 ms284 ms
51234 ms173 ms561 ms
1,02471 ms210 ms1,124 ms
2,048133 ms438 ms2,181 ms
4,096296 ms864 ms4,289 ms
8,192586 ms1,633 ms8,513 ms
16,3841,152 ms3,279 ms17,194 ms
32,7682,357 ms6,427 ms34,019 ms
65,5364,691 ms12,836 ms68,607 ms
131,0729,990 ms26,230 ms137,298 ms
262,14417,728 ms51,824 ms273,707 ms
524,28837,713 ms103,477 ms556,364 ms

I couldn't get to benchmark over 1 million entities as EnTT crashed with the following message:

entt.hpp:8033: entt::basic_registry< <template-parameter-1-1> >::entity_type entt::basic_registry< <template-parameter-1-1> >::create() [with Entity = entt::entity; entt::basic_registry< <template-parameter-1-1> >::entity_type = entt::entity]: Assertion `to_integral(entt) < traits_type::entity_mask' failed.