Skip to content

Instantly share code, notes, and snippets.

@waldnercharles
Created March 13, 2026 15:42
Show Gist options
  • Select an option

  • Save waldnercharles/6f65c1ff206d769d4647213fcf0d369c to your computer and use it in GitHub Desktop.

Select an option

Save waldnercharles/6f65c1ff206d769d4647213fcf0d369c to your computer and use it in GitHub Desktop.
Just a fat archetypal ecs in ~500 loc
/*
arch_ecs.h — C23 Archetypal ECS, single-header, SoAoS layout
USAGE:
#define ECS_COMPONENTS(X) X(Type, field) ...
#include "arch_ecs.h"
Zero-initialised ecs_world is ready to use immediately.
Components are immutable per entity lifetime (fixed archetype).
SPAWNING
ecs_entity e = ecs_spawn(&world, Position, Velocity, ({
.position = { 1.f, 2.f },
.velocity = { 3.f, 4.f },
}));
// or: spawn then set
ecs_entity e = ecs_spawn(&world, Position, Velocity);
ecs_set_position(&world, e, (Position){ 1.f, 2.f });
ITERATING
ECS_WITH(&world, position, velocity) {
position->x += velocity->vx;
}
ECS_QUERY(&world, Position, pos, Velocity, vel) {
pos->x += vel->vx;
}
ECS_EACH(&world) { ... }
ECS_WITH(&world, position)
ECS_NONE_OF(velocity) { ... }
HANDLES
ecs_entity is a thin (id, gen) pair — 8 bytes.
ecs_view() returns a fat view with T* per component.
ecs_alive() catches stale handles after kill or slot reuse.
Double-kill is safe.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#ifndef ECS_MAX_ENTITIES
#define ECS_MAX_ENTITIES 4096
#endif
#ifndef ECS_MAX_ARCHETYPES
#define ECS_MAX_ARCHETYPES 64
#endif
#ifndef ECS_CHUNK_SIZE
#define ECS_CHUNK_SIZE 256
#endif
#ifdef ECS_COMPONENTS
#ifndef ARCH_ECS_H
#define ARCH_ECS_H
/* --- internal variable prefix --------------------------------- */
#define ECS_P_(name) _ecs_##name
/* --- component IDs and masks (field-name based) --------------- */
enum
{
#define ECS_ENUM_(T, f) ECS_ID_##f,
ECS_COMPONENTS(ECS_ENUM_)
#undef ECS_ENUM_
ECS_COMPONENT_COUNT
};
_Static_assert(ECS_COMPONENT_COUNT <= 64, "max 64 components");
#define ECS_MASK(f) ((uint64_t)1ull << ECS_ID_##f)
/* clang-format off */
/* ECS_MASK_ALL(f, ...) — OR of field masks for any number of components. */
#define ECS_M_EMPTY_()
#define ECS_M_DEFER_(m) m ECS_M_EMPTY_()
#define ECS_EVAL_(...) ECS_E1_(ECS_E1_(ECS_E1_(__VA_ARGS__)))
#define ECS_E1_(...) ECS_E2_(ECS_E2_(ECS_E2_(__VA_ARGS__)))
#define ECS_E2_(...) ECS_E3_(ECS_E3_(ECS_E3_(__VA_ARGS__)))
#define ECS_E3_(...) ECS_E4_(ECS_E4_(ECS_E4_(__VA_ARGS__)))
#define ECS_E4_(...) __VA_ARGS__
#define ECS_M_FOLD_(f, ...) ECS_MASK(f) __VA_OPT__(| ECS_M_DEFER_(ECS_M_FOLD_C_)()(__VA_ARGS__))
#define ECS_M_FOLD_C_() ECS_M_FOLD_
#define ECS_MASK_ALL(...) (ECS_EVAL_(ECS_M_FOLD_(__VA_ARGS__)))
/* ECS_TMASK_TYPE_ — map a type name to its component mask via _Generic.
ECS_TMASK_CASE_ must stay defined (expanded at each call site). */
#define ECS_TMASK_CASE_(T, f) T: ECS_MASK(f),
#define ECS_TMASK_TYPE_(TypeName) \
_Generic((TypeName){0}, ECS_COMPONENTS(ECS_TMASK_CASE_) default: (uint64_t)0)
/* clang-format on */
/* --- entity handle (thin) ------------------------------------- */
typedef struct
{
int id;
uint16_t gen;
} ecs_entity;
#define ECS_NULL ((ecs_entity){ 0 })
/* --- entity map ----------------------------------------------- */
typedef struct
{
int archetype; /* index into world archetypes, -1 = dead */
int row;
uint16_t gen;
} ecs_entity_info;
/* --- archetype ------------------------------------------------ */
typedef struct
{
uint64_t mask;
int count, capacity;
int *entity_ids;
#define ECS_ARCH_COL_(T, f) T *f;
ECS_COMPONENTS(ECS_ARCH_COL_)
#undef ECS_ARCH_COL_
} ecs_archetype;
/* --- spawn descriptor ----------------------------------------- */
typedef struct
{
#define ECS_DESC_(T, f) T f;
ECS_COMPONENTS(ECS_DESC_)
#undef ECS_DESC_
} ecs_spawn_desc;
/* --- view (fat handle) ---------------------------------------- */
typedef struct
{
int id;
uint16_t gen;
#define ECS_VIEW_PTR_(T, f) T *f;
ECS_COMPONENTS(ECS_VIEW_PTR_)
#undef ECS_VIEW_PTR_
} ecs_view_t;
/* --- world ---------------------------------------------------- */
typedef struct
{
ecs_entity_info entities[ECS_MAX_ENTITIES];
int free_list[ECS_MAX_ENTITIES];
int free_count, hwm, alive_count;
ecs_archetype archetypes[ECS_MAX_ARCHETYPES];
int archetype_count;
} ecs_world;
/* --- alive check ---------------------------------------------- */
static inline bool ecs_alive(ecs_world *w, ecs_entity e)
{
return e.id > 0 && e.id < ECS_MAX_ENTITIES && w->entities[e.id].gen == e.gen;
}
/* --- archetype helpers ---------------------------------------- */
static inline int ecs_find_or_create_archetype_(ecs_world *w, uint64_t mask)
{
for (int a = 0; a < w->archetype_count; a++)
if (w->archetypes[a].mask == mask) return a;
assert(w->archetype_count < ECS_MAX_ARCHETYPES);
int a = w->archetype_count++;
w->archetypes[a].mask = mask;
return a;
}
static inline void ecs_archetype_grow_(ecs_archetype *arch)
{
int new_cap = arch->capacity + ECS_CHUNK_SIZE;
arch->entity_ids = realloc(arch->entity_ids, (size_t)new_cap * sizeof(int));
#define ECS_GROW_(T, f) \
if (arch->mask & ECS_MASK(f)) \
arch->f = realloc(arch->f, (size_t)new_cap * sizeof(T));
ECS_COMPONENTS(ECS_GROW_)
#undef ECS_GROW_
arch->capacity = new_cap;
}
/* --- spawn ---------------------------------------------------- */
static inline ecs_entity ecs_spawn_(ecs_world *w, uint64_t mask, ecs_spawn_desc *desc)
{
int id;
if (w->free_count > 0) {
id = w->free_list[--w->free_count];
} else {
assert(w->hwm + 1 < ECS_MAX_ENTITIES);
id = ++w->hwm;
}
w->entities[id].gen++;
int ai = ecs_find_or_create_archetype_(w, mask);
ecs_archetype *arch = &w->archetypes[ai];
if (arch->count == arch->capacity) ecs_archetype_grow_(arch);
int row = arch->count++;
arch->entity_ids[row] = id;
if (desc) {
#define ECS_COPY_(T, f) \
if (mask & ECS_MASK(f)) arch->f[row] = desc->f;
ECS_COMPONENTS(ECS_COPY_)
#undef ECS_COPY_
}
w->entities[id].archetype = ai;
w->entities[id].row = row;
w->alive_count++;
return (ecs_entity){ .id = id, .gen = w->entities[id].gen };
}
/* --- ecs_spawn: type-folding macro with deferred recursion --- */
// clang-format off
#define ECS_EAT_(...)
#define ECS_UNWRAP_(...) __VA_ARGS__
/* is-parenthesized detection */
#define ECS_IS_PAREN_P_(...) ~, 1
#define ECS_IS_PAREN_CHK_(...) ECS_IS_PAREN_N_(__VA_ARGS__, 0)
#define ECS_IS_PAREN_N_(a, n, ...) n
/* dispatch: parenthesized → spawn with init, else → spawn mask only */
#define ECS_SPAWN_LAST_(w, mask, x) ECS_SPAWN_SEL_(ECS_IS_PAREN_P_ x)(w, mask, x)
#define ECS_SPAWN_SEL_(...) ECS_SPAWN_SEL2_(__VA_ARGS__, ECS_SPAWN_INIT_, ECS_SPAWN_NOINIT_, ~)
#define ECS_SPAWN_SEL2_(a, b, sel, ...) sel
#define ECS_SPAWN_INIT_(w, mask, init) ecs_spawn_((w), (mask), &(ecs_spawn_desc)ECS_UNWRAP_ init)
#define ECS_SPAWN_NOINIT_(w, mask, x) ecs_spawn_((w), (mask) | ECS_TMASK_TYPE_(x), NULL)
/* recursive fold: accumulate type masks until last arg */
#define ECS_SPAWN_FOLD_(w, mask, x, ...) \
__VA_OPT__(ECS_M_DEFER_(ECS_SPAWN_FOLD_C_)()((w), (mask) | ECS_TMASK_TYPE_(x), __VA_ARGS__) ECS_EAT_) \
(ECS_SPAWN_LAST_((w), (mask), x))
#define ECS_SPAWN_FOLD_C_() ECS_SPAWN_FOLD_
/* ecs_spawn: unified macro — with or without init block.
With init: ecs_spawn(&world, Position, Velocity, ({ ... }))
Without init: ecs_spawn(&world, Position, Velocity) */
#define ecs_spawn(w, ...) ECS_EVAL_(ECS_SPAWN_FOLD_((w), (uint64_t)0, __VA_ARGS__))
// clang-format on
/* --- kill (swap-remove) --------------------------------------- */
static inline void ecs_kill(ecs_world *w, ecs_entity e)
{
if (!ecs_alive(w, e)) return;
ecs_entity_info *info = &w->entities[e.id];
ecs_archetype *arch = &w->archetypes[info->archetype];
int row = info->row;
int last = arch->count - 1;
if (row != last) {
int swapped_id = arch->entity_ids[last];
arch->entity_ids[row] = swapped_id;
#define ECS_SWAP_(T, f) \
if (arch->mask & ECS_MASK(f)) arch->f[row] = arch->f[last];
ECS_COMPONENTS(ECS_SWAP_)
#undef ECS_SWAP_
w->entities[swapped_id].row = row;
}
arch->count--;
info->gen++;
info->archetype = -1;
w->free_list[w->free_count++] = e.id;
w->alive_count--;
}
/* --- view ----------------------------------------------------- */
static inline ecs_view_t ecs_view_at_(ecs_world *w, ecs_archetype *arch, int row)
{
int id = arch->entity_ids[row];
return (ecs_view_t){ .id = id,
.gen = w->entities[id].gen,
#define ECS_VA_(T, f) .f = (arch->mask & ECS_MASK(f)) ? &arch->f[row] : NULL,
ECS_COMPONENTS(ECS_VA_)
#undef ECS_VA_
};
}
static inline ecs_view_t ecs_view(ecs_world *w, ecs_entity e)
{
assert(ecs_alive(w, e));
ecs_entity_info *info = &w->entities[e.id];
return ecs_view_at_(w, &w->archetypes[info->archetype], info->row);
}
/* --- per-component accessors ---------------------------------- */
#define ECS_GET_IMPL_(T, f) \
static inline T *ecs_get_##f(ecs_world *w, ecs_entity e) \
{ \
assert(ecs_alive(w, e)); \
ecs_entity_info *info = &w->entities[e.id]; \
ecs_archetype *arch = &w->archetypes[info->archetype]; \
assert(arch->mask & ECS_MASK(f)); \
return &arch->f[info->row]; \
}
ECS_COMPONENTS(ECS_GET_IMPL_)
#undef ECS_GET_IMPL_
#define ECS_SET_IMPL_(T, f) \
static inline void ecs_set_##f(ecs_world *w, ecs_entity e, T val) \
{ \
assert(ecs_alive(w, e)); \
ecs_entity_info *info = &w->entities[e.id]; \
ecs_archetype *arch = &w->archetypes[info->archetype]; \
assert(arch->mask & ECS_MASK(f)); \
arch->f[info->row] = val; \
}
ECS_COMPONENTS(ECS_SET_IMPL_)
#undef ECS_SET_IMPL_
/* clang-format off */
/* ecs_set(world, entity, value) — type-dispatched setter via _Generic.
ECS_GENERIC_SET_ must stay defined; ecs_set expands it at each call site. */
#define ECS_GENERIC_SET_(T, f) T: ecs_set_##f,
#define ecs_set(w, e, ...) \
_Generic((__VA_ARGS__), \
ECS_COMPONENTS(ECS_GENERIC_SET_) \
default: (void)0 \
)((w), (e), __VA_ARGS__)
/* clang-format on */
/* --- queries -------------------------------------------------- */
#define ecs_has(w, e, f) \
(ecs_alive((w), (e)) && \
((w)->archetypes[(w)->entities[(e).id].archetype].mask & ECS_MASK(f)))
#define ecs_has_all(w, e, ...) \
(ecs_alive((w), (e)) && \
(((w)->archetypes[(w)->entities[(e).id].archetype].mask & \
ECS_MASK_ALL(__VA_ARGS__)) == ECS_MASK_ALL(__VA_ARGS__)))
static inline ecs_entity ecs_first_(ecs_world *w, uint64_t mask)
{
for (int a = 0; a < w->archetype_count; a++) {
ecs_archetype *arch = &w->archetypes[a];
if ((arch->mask & mask) == mask && arch->count > 0) {
int id = arch->entity_ids[0];
return (ecs_entity){ .id = id, .gen = w->entities[id].gen };
}
}
return ECS_NULL;
}
// clang-format off
#define ecs_first(w, ...) ecs_first_((w), ECS_MASK_ALL(__VA_ARGS__))
/* --- ECS_WITH / ECS_EACH / ECS_NONE_OF ----------------------- */
/*
ECS_WITH: iterate entities owning ALL listed components.
Injects auto *restrict pointers per queried field.
ECS_EACH: iterate all live entities. Injects ecs_view_t e.
ECS_NONE_OF: chain after ECS_WITH/ECS_EACH to exclude entities
that own ANY of the listed components.
ECS_WITH(&world, position, velocity) {
position->x += velocity->vx;
}
ECS_EACH(&world) { e.position->x = 0; }
ECS_WITH(&world, position)
ECS_NONE_OF(velocity) { ... }
break and continue work correctly in all three.
*/
/* Variable injection: recursive fold generates one-shot for-loops
with *restrict pointers into archetype SoA arrays. */
#define ECS_IV_ENTRY_(f, ...) \
for (typeof(ECS_P_(arch)->f[0]) *restrict f = &ECS_P_(arch)->f[ECS_P_(i)]; f; f = NULL) \
__VA_OPT__(ECS_M_DEFER_(ECS_IV_NEXT_C_)()(__VA_ARGS__))
#define ECS_IV_NEXT_(f, ...) \
for (typeof(ECS_P_(arch)->f[0]) *restrict f = &ECS_P_(arch)->f[ECS_P_(i)]; f; f = NULL) \
__VA_OPT__(ECS_M_DEFER_(ECS_IV_NEXT_C_)()(__VA_ARGS__))
#define ECS_IV_NEXT_C_() ECS_IV_NEXT_
#define ECS_INJECT_VARS_(...) ECS_EVAL_(ECS_IV_ENTRY_(__VA_ARGS__))
#define ECS_WITH(world, ...) \
for (ecs_world * ECS_P_(w) = (world), *ECS_P_(once) = ECS_P_(w); ECS_P_(once); ECS_P_(once) = NULL) \
for (int ECS_P_(a) = 0, ECS_P_(brk) = 0; ECS_P_(a) < ECS_P_(w)->archetype_count && !ECS_P_(brk); ECS_P_(a)++) \
if ((ECS_P_(w)->archetypes[ECS_P_(a)].mask & ECS_MASK_ALL(__VA_ARGS__)) == ECS_MASK_ALL(__VA_ARGS__)) \
for (ecs_archetype * ECS_P_(arch) = &ECS_P_(w)->archetypes[ECS_P_(a)], *ECS_P_(a_once) = ECS_P_(arch); ECS_P_(a_once); ECS_P_(a_once) = NULL) \
for (int ECS_P_(i) = 0; ECS_P_(i) < ECS_P_(arch)->count && !ECS_P_(brk); ECS_P_(i)++) \
ECS_INJECT_VARS_(__VA_ARGS__) \
for (int ECS_P_(done) = (ECS_P_(brk) = 1, 0); !ECS_P_(done); ECS_P_(done) = 1, ECS_P_(brk) = 0)
#define ECS_EACH(world) \
for (ecs_world *ECS_P_(w) = (world), *ECS_P_(once) = ECS_P_(w); ECS_P_(once); ECS_P_(once) = NULL) \
for (int ECS_P_(a) = 0, ECS_P_(brk) = 0; ECS_P_(a) < ECS_P_(w)->archetype_count && !ECS_P_(brk); ECS_P_(a)++) \
if (ECS_P_(w)->archetypes[ECS_P_(a)].count > 0) \
for (ecs_archetype *ECS_P_(arch) = &ECS_P_(w)->archetypes[ECS_P_(a)], *ECS_P_(a_once) = ECS_P_(arch); ECS_P_(a_once); ECS_P_(a_once) = NULL) \
for (int ECS_P_(i) = 0; ECS_P_(i) < ECS_P_(arch)->count && !ECS_P_(brk); ECS_P_(i)++) \
for (ecs_view_t e = (ECS_P_(brk) = 1, ecs_view_at_(ECS_P_(w), ECS_P_(arch), ECS_P_(i))); e.id; e.id = 0, ECS_P_(brk) = 0)
#define ECS_NONE_OF(...) if (!(ECS_P_(arch)->mask & ECS_MASK_ALL(__VA_ARGS__)))
/* --- ECS_QUERY (flat-pair: Type, name, Type, name, ...) ------- */
/*
ECS_QUERY(&world, Position, pos, Velocity, vel) {
pos->x += vel->vx;
}
ECS_QUERY(&world, Position, pos)
ECS_QNONE_OF(Velocity) { ... }
break and continue work correctly.
*/
/* Type-to-field-pointer: resolve a type name to its SoA pointer in the
current archetype at the current row.
ECS_TYPE_FIELD_PTR_ must stay defined (expanded at each call site). */
#define ECS_TYPE_FIELD_PTR_(T, f) T: &ECS_P_(arch)->f[ECS_P_(i)],
#define ECS_RESOLVE_(TypeName) _Generic((TypeName){ 0 }, ECS_COMPONENTS(ECS_TYPE_FIELD_PTR_) default: (void *)0)
/* Flat-pair mask fold: consume (Type, name) pairs, ignore name. */
#define ECS_PM_FOLD_(T, name, ...) ECS_TMASK_TYPE_(T) __VA_OPT__(| ECS_M_DEFER_(ECS_PM_FOLD_C_)()(__VA_ARGS__))
#define ECS_PM_FOLD_C_() ECS_PM_FOLD_
#define ECS_PMASK_ALL(...) (ECS_EVAL_(ECS_PM_FOLD_(__VA_ARGS__)))
/* Flat-pair variable injection: one-shot for-loops with *restrict pointers. */
#define ECS_PIV_ENTRY_(T, name, ...) \
for (typeof(*ECS_RESOLVE_(T)) *restrict name = ECS_RESOLVE_(T); name; name = NULL) \
__VA_OPT__(ECS_M_DEFER_(ECS_PIV_NEXT_C_)()(__VA_ARGS__))
#define ECS_PIV_NEXT_(T, name, ...) \
for (typeof(*ECS_RESOLVE_(T)) *restrict name = ECS_RESOLVE_(T); name; name = NULL) \
__VA_OPT__(ECS_M_DEFER_(ECS_PIV_NEXT_C_)()(__VA_ARGS__))
#define ECS_PIV_NEXT_C_() ECS_PIV_NEXT_
#define ECS_PINJECT_VARS_(...) ECS_EVAL_(ECS_PIV_ENTRY_(__VA_ARGS__))
#define ECS_QUERY(world, ...) \
for (ecs_world *ECS_P_(w) = (world), *ECS_P_(once) = ECS_P_(w); ECS_P_(once); ECS_P_(once) = NULL) \
for (int ECS_P_(a) = 0, ECS_P_(brk) = 0; ECS_P_(a) < ECS_P_(w)->archetype_count && !ECS_P_(brk); ECS_P_(a)++) \
if ((ECS_P_(w)->archetypes[ECS_P_(a)].mask & ECS_PMASK_ALL(__VA_ARGS__)) == ECS_PMASK_ALL(__VA_ARGS__)) \
for (ecs_archetype *ECS_P_(arch) = &ECS_P_(w)->archetypes[ECS_P_(a)], *ECS_P_(a_once) = ECS_P_(arch); ECS_P_(a_once); ECS_P_(a_once) = NULL) \
for (int ECS_P_(i) = 0; ECS_P_(i) < ECS_P_(arch)->count && !ECS_P_(brk); ECS_P_(i)++) \
ECS_PINJECT_VARS_(__VA_ARGS__) \
for (int ECS_P_(done) = (ECS_P_(brk) = 1, 0); !ECS_P_(done); ECS_P_(done) = 1, ECS_P_(brk) = 0)
/* ECS_QNONE_OF — exclude by type names (single args, not pairs). */
#define ECS_TMT_FOLD_(T, ...) ECS_TMASK_TYPE_(T) __VA_OPT__(| ECS_M_DEFER_(ECS_TMT_FOLD_C_)()(__VA_ARGS__))
#define ECS_TMT_FOLD_C_() ECS_TMT_FOLD_
#define ECS_TMASK_ALL_TYPES(...) (ECS_EVAL_(ECS_TMT_FOLD_(__VA_ARGS__)))
#define ECS_QNONE_OF(...) if (!(ECS_P_(arch)->mask & ECS_TMASK_ALL_TYPES(__VA_ARGS__)))
//
// clang-format on
/* --- cleanup -------------------------------------------------- */
static inline void ecs_world_destroy(ecs_world *w)
{
for (int a = 0; a < w->archetype_count; a++) {
ecs_archetype *arch = &w->archetypes[a];
free(arch->entity_ids);
#define ECS_FREE_(T, f) free(arch->f);
ECS_COMPONENTS(ECS_FREE_)
#undef ECS_FREE_
}
}
#endif /* ARCH_ECS_H */
#else
#error "Define ECS_COMPONENTS(X) before including arch_ecs.h"
#endif /* ECS_COMPONENTS */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment