Created
May 27, 2025 01:07
-
-
Save mikesmullin/02413bffb02759581e34ae32e7602fac to your computer and use it in GitHub Desktop.
Mike's Fast ECS v1.0.0-alpha (static C impl)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// this is the ECS lib | |
// Generational Index | |
// assemble an entity id | |
inline GID GI_eid(bool pair, CID gen, CID arch, CID arch_idx) { | |
return // | |
(pair ? PAIR_BIT : 0U) | // | |
((u32)gen << GEN_SHIFT) | // | |
((u32)arch << ARCH_SHIFT) | // | |
((u32)arch_idx & ARCH_IDX_MASK); | |
} | |
inline CID GI_isPair(GID gid) { | |
return 0 != (gid & PAIR_BIT); | |
} | |
inline CID GI_gen(GID gid) { | |
return (gid & GEN_MASK) >> GEN_SHIFT; | |
} | |
inline CID GI_arch(GID gid) { | |
return (gid & ARCH_MASK) >> ARCH_SHIFT; | |
} | |
inline CID GI_arch_idx(GID gid) { | |
return (gid & ARCH_IDX_MASK); | |
} | |
#ifdef DEBUG_SLOW | |
DebugGenId db_gid(GID gid) { | |
return (DebugGenId){ | |
.pair = GI_isPair(gid), | |
.gen = GI_gen(gid), | |
.arch = GI_arch(gid), | |
.arch_idx = GI_arch_idx(gid), | |
}; | |
} | |
void GI_validate(GenIdx* gi, GID gid, CID slot, CID gen, CID arch, CID arch_idx) { | |
if (NULL != gi) { | |
ASSERT(NULL != gi->a); | |
ASSERT(gi->alive_ct <= ENT_MAX); | |
ASSERT(gi->dead_ct <= ENT_MAX); | |
ASSERT(gi->dead_head_idx <= ENT_MAX); | |
} | |
if (0 != gid) { | |
gen = GI_gen(gid); | |
arch = GI_arch(gid); | |
arch_idx = GI_arch_idx(gid); | |
} | |
ASSERT(slot <= ENT_MAX); | |
ASSERT(gen <= ENT_MAX); | |
ASSERT(arch <= ENT_MAX); | |
ASSERT(arch_idx <= ENT_MAX); | |
} | |
#else | |
DebugGenId db_gid(GID gid) { | |
} | |
void GI_validate(GenIdx* gi, GID gid, CID slot, CID gen, CID arch, CID arch_idx) { | |
} | |
#endif | |
// initialize the GenIdx struct | |
void GI_init() { | |
GenIdx* gi = &_G->world.gi; | |
// build a linked list of dead nodes from back to front | |
for (CID i = 0; i < ENT_MAX; i++) { | |
gi->a[i] = GI_eid(false, 0, 0, i + 1); | |
} | |
gi->dead_head_idx = 0; | |
gi->dead_ct = ENT_MAX; | |
gi->alive_ct = 0; | |
GI_validate(gi, 0, 0, 0, 0, 0); | |
} | |
// next available (recycling dead) | |
GID GI_get_next_id(CArch arch) { | |
GenIdx* gi = &_G->world.gi; | |
GI_validate(gi, 0, 0, 0, 0, 0); | |
CID slot = gi->dead_head_idx; | |
GID _gid = gi->a[slot]; | |
CID gen = GI_gen(_gid); // keep generation | |
gi->dead_head_idx = GI_arch_idx(_gid); // next_dead_idx | |
gi->dead_ct--; | |
GID next_eid = GI_eid(false, gen, arch, slot); | |
gi->alive_ct++; | |
return next_eid; | |
} | |
// update/override a gen slot. (does not increment alive counter) | |
// could also be used to add arbitrary (out-of-order) new alive id to the index (ie. remote/server entities) | |
inline void GI_override(bool pair, CID slot, CID gen, CID arch, CID arch_idx) { | |
GenIdx* gi = &_G->world.gi; | |
gi->a[slot] = GI_eid(pair, gen, arch, arch_idx); | |
} | |
// check alive | |
bool GI_is_alive(GID gid) { | |
GenIdx* gi = &_G->world.gi; | |
GI_validate(gi, gid, 0, 0, 0, 0); | |
CID slot = GI_arch_idx(gid); | |
CID gen_a = GI_gen(gid); | |
GID b = gi->a[slot]; | |
CID gen_b = GI_gen(b); | |
return gen_a == gen_b; | |
} | |
// apply death | |
void GI_set_dead(GID gid) { | |
GenIdx* gi = &_G->world.gi; | |
GI_validate(gi, gid, 0, 0, 0, 0); | |
// inc gen (only death can do this) | |
// point the idx to the old dead head | |
gi->a[GI_arch_idx(gid)] = GI_eid(false, GI_gen(gid) + 1, 0, gi->dead_head_idx); | |
// update head | |
gi->a[gi->dead_head_idx] = GI_eid(false, GI_gen(gid), 0, GI_arch_idx(gid)); | |
gi->dead_head_idx = GI_arch_idx(gid); | |
gi->alive_ct--; | |
gi->dead_ct++; | |
} | |
// Paged Arrays | |
// allocate new page | |
void ECS__page_alloc(Paged** p, CID rec_cap, CID rec_sz) { | |
(*p) = (Paged*)Arena__Push(_G->arena, sizeof(Paged)); | |
(*p)->records = Arena__Push /*Zero*/ (_G->arena, rec_sz * rec_cap); | |
(*p)->count = 0; | |
} | |
// append new record to existing page (or if page is full, create & link to new page) | |
void* ECS__page_record_add(List* list, CID rec_cap, CID rec_sz, void* record) { | |
Paged* p = NULL; | |
if (0 == list->len) { | |
ECS__page_alloc(&p, rec_cap, rec_sz); | |
List__append(_G->arena, list, p); | |
} | |
p = (Paged*)list->tail->data; | |
if (p->count >= rec_cap) { | |
ECS__page_alloc(&p, rec_cap, rec_sz); | |
List__append(_G->arena, list, p); | |
} | |
void* dst = p->records + (rec_sz * p->count++); | |
memcpy(dst, record, rec_sz); // copy | |
return dst; | |
} | |
// ECS | |
// allocate a new archetype | |
inline Archetype* ECS__arch_alloc(CArch arch, CID cmp_ct) { | |
Archetype* a = &_G->world.archs[arch]; | |
a->type = arch; | |
a->cmp_ct = 0; // NOTE: assumes you will add the given number of components later | |
a->en_ct = 0; | |
List__init((List*)&a->entities); | |
a->cpools = Arena__Push(_G->arena, sizeof(ComponentPool) * cmp_ct); | |
a->dirty = OBS_NONE; | |
return a; | |
} | |
// add existing entity to existing archetype | |
CID ECS__arch_ent_add(GID entity) { | |
CArch arch = GI_arch(entity); | |
Archetype* a = &_G->world.archs[arch]; | |
CID en_idx = a->en_ct++; | |
ECS__page_record_add((List*)&a->entities, CPOOL_PAGE_COUNT, sizeof(GID), &entity); | |
CID gen = GI_gen(entity); | |
CID arch_idx = GI_arch_idx(entity); | |
GI_override(false, arch_idx, gen, arch, en_idx); | |
a->dirty |= OBS_ADD; | |
return en_idx; | |
} | |
// mark existing entity dead, and set dirty flag on its archetype | |
void ECS__arch_ent_del(GID entity) { | |
CArch arch = GI_arch(entity); | |
Archetype* a = &_G->world.archs[arch]; | |
GI_set_dead(entity); | |
a->dirty |= OBS_REMOVE; | |
} | |
// allocate new component pool | |
inline void ECS__arch_cpool_alloc(CArch arch, CCmp cmp, u16 sz) { | |
Archetype* a = &_G->world.archs[arch]; | |
ComponentPool* c = &a->cpools[a->cmp_ct++]; | |
c->type = cmp; | |
c->stride = sz; | |
List__init((List*)&c->cmps); | |
} | |
// add new component data to existing component pool | |
void* ECS__arch_cpool_add_cmp_data(CArch arch, u16 cmp_idx, CCmp cmp, void* data) { | |
Archetype* a = &_G->world.archs[arch]; | |
ComponentPool* c = &a->cpools[cmp_idx]; | |
a->dirty |= OBS_SET; | |
return ECS__page_record_add((List*)&c->cmps, CPOOL_PAGE_COUNT, c->stride, data); | |
} | |
// set dirty flag (when existing component data was updated) | |
void ECS__arch_cmp_updated(CArch arch) { | |
Archetype* a = &_G->world.archs[arch]; | |
a->dirty |= OBS_SET; | |
} | |
// check if dirty flag is set on existing archetype | |
inline bool ECS__is_dirty(CArch arch, CObs event) { | |
Archetype* a = &_G->world.archs[arch]; | |
return 0 != (a->dirty & event); | |
} | |
// clear dirty flags on all archetypes (to be called at end of frame) | |
void ECS__clean() { | |
Archetype* archs = _G->world.archs; | |
for (u16 i = 0; i < ARCH_COUNT; i++) { | |
archs[i].dirty = OBS_NONE; | |
} | |
} | |
// TODO: make possible to identify which component in a pool was ADD|REMOVE|SET | |
// Queries | |
// iterator initialization before for..loop | |
inline ArchIt* ECS__query_init(ArchIt* it) { | |
Archetype* a = &_G->world.archs[it->arch]; | |
it->a = a; | |
it->len = a->en_ct; | |
return it; | |
} | |
// iterator updates inside for..loop | |
inline void ECS__query_iter(u16 i, ArchIt* it, ...) { | |
// TODO: eventually need to be able to walk multiple archetypes in one loop, or multiple calls to one static system_cb fn | |
// TODO: query will need to skip/purge dead entity cmp data (and eventually force rebuild of archetype) (ie. call ECS__defrag/sweep() fn to centralize/control/debug-count it) | |
if (0 != i % CPOOL_PAGE_COUNT) { | |
return; | |
} | |
// TODO: optimize this asm to be as few ops as possible! | |
// event: page roll-over | |
it->page_idx = i / CPOOL_PAGE_COUNT; | |
it->rec_idx = i % CPOOL_PAGE_COUNT; | |
va_list args; | |
va_start(args, it); | |
PagedEnt* p1; | |
PagedCmp* p2; | |
p1 = List__get((List*)&it->a->entities, it->page_idx); | |
it->eid = p1->records; | |
for (u8 i = 0; i < it->a->cmp_ct; i++) { | |
void** ptr = va_arg(args, void**); | |
p2 = List__get((List*)&it->a->cpools[i].cmps, it->page_idx); | |
*ptr = p2->records; | |
} | |
va_end(args); | |
} | |
// Convenience Wrappers | |
// allocate a new archetype table | |
CArch ECS__arch(CID cmp_ct, CArch arch, ...) { | |
ECS__arch_alloc(arch, cmp_ct); | |
va_list args; | |
va_start(args, arch); | |
for (u8 i = 0; i < cmp_ct * 2; i += 2) { | |
CCmp cls = va_arg(args, CCmp); | |
u32 sz = va_arg(args, u32); | |
ECS__arch_cpool_alloc(arch, cls, sz); | |
} | |
va_end(args); | |
return arch; | |
} | |
// allocate a new entity into the world, w/ all its component data | |
GID ECS__entity(CArch arch, ...) { | |
Archetype* a = &_G->world.archs[arch]; | |
GID r = GI_get_next_id(arch); | |
ECS__arch_ent_add(r); | |
va_list args; | |
va_start(args, arch); | |
for (u8 i = 0; i < a->cmp_ct; i++) { | |
void* pCmp = va_arg(args, void*); | |
ECS__arch_cpool_add_cmp_data(arch, i, 0, pCmp); | |
} | |
va_end(args); | |
return r; | |
} | |
// return pointer to component data array for given entity | |
void* ECS__field(GID entity, u8 cmp_idx) { | |
CArch arch = GI_arch(entity); | |
Archetype* a = &_G->world.archs[arch]; | |
CID arch_idx = GI_arch_idx(entity); | |
u8 page_idx = arch_idx / CPOOL_PAGE_COUNT; | |
u8 rec_idx = arch_idx % CPOOL_PAGE_COUNT; | |
ComponentPool* cpool = &a->cpools[cmp_idx]; | |
PagedCmp* p2 = List__get((List*)&cpool->cmps, page_idx); | |
void* r = p2->records + (cpool->stride * rec_idx); | |
return r; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// this is the actual in-game usage example | |
// clone a new cat entity instance from this prefab | |
GID CatEntity__prefab(f32 x, f32 y, f32 z, u16 r) { | |
// construct entity | |
CArch a = A_CTRANSFORM3D_CRIGIDBODY2D_CREPLICA_CBRAIN_CRENDER; | |
GID cat = GI_get_next_id(a); | |
CmpTransform3D tf = {x, y, z, r}; | |
CmpRigidbody2D rb = {0.0, 0.0, 0U}; | |
CmpReplica replica = { | |
.net_eid = _G->isMaster ? cat : 0, | |
.world_static = false, | |
}; | |
CmpBrain brain = { | |
.bb = NULL, | |
}; | |
CmpRender render = {0}; | |
render.rg = WORLD_ZSORT_RG; | |
render.billboard = true; | |
render.material = Material__alloc(&_G->materials.sprite); | |
Preload(&_G->models.plane2D, &(Str8){"../assets/models/plane2D.obj"}); | |
render.material->mesh = _G->models.plane2D; | |
Preload(&_G->images.atlas, &(Str8){"../assets/textures/atlas.bmp"}); | |
render.material->shader = &_G->shaders.atlas; | |
render.material->texture0 = _G->images.atlas; | |
render.tw = render.th = 8; | |
render.aw = render.ah = 64; | |
render.pi = 0; | |
render.po = Math__randomu(0, 7, &_G->seeds.sg); | |
render.ti = 4 + 2 * 8; | |
render.useMask = true; | |
render.mask = COLOR_BLACK; | |
render.color = COLOR_TRANSPARENT; | |
ECS__arch_ent_add(cat); | |
ECS__arch_cpool_add_cmp_data(a, 0, CTRANSFORM3D, &tf); | |
ECS__arch_cpool_add_cmp_data(a, 1, CRIGIDBODY2D, &rb); | |
ECS__arch_cpool_add_cmp_data(a, 2, CREPLICA, &replica); | |
ECS__arch_cpool_add_cmp_data(a, 3, CBRAIN, &brain); | |
ECS__arch_cpool_add_cmp_data(a, 4, CRENDER, &render); | |
Preload(&_G->audio.meow, &(Str8){"../assets/audio/sfx/meow.wav"}); | |
return cat; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// this is the unit test | |
// @describe ECS | |
// @tag common | |
int main() { | |
_G->arena = Arena__Alloc(50 * 1024 * 1024); // MB | |
//--- | |
// Scenario: Generational Index | |
GI_init(); | |
// clang-format off | |
const CArch a1 = ECS__arch(2, | |
A_CTRANSFORM3D_CRIGIDBODY2D, | |
CTRANSFORM3D, sizeof(CmpTransform3D), | |
CRIGIDBODY2D, sizeof(CmpRigidbody2D)); | |
// clang-format on | |
GID eid0 = GI_eid(false, 1, 2, 3); | |
ASSERT(!GI_isPair(eid0)); | |
ASSERT(1 == GI_gen(eid0)); | |
ASSERT(2 == GI_arch(eid0)); | |
ASSERT(3 == GI_arch_idx(eid0)); | |
eid0 = GI_eid(true, 1, 2, 3); | |
ASSERT(GI_isPair(eid0)); | |
eid0 = GI_get_next_id(a1); | |
ASSERT(0 == GI_gen(eid0)); | |
ASSERT(a1 == GI_arch(eid0)); | |
ASSERT(0 == GI_arch_idx(eid0)); | |
GID eid1 = GI_get_next_id(a1); | |
ASSERT(0 == GI_gen(eid1)); | |
ASSERT(a1 == GI_arch(eid1)); | |
ASSERT(1 == GI_arch_idx(eid1)); | |
GID eid2 = GI_get_next_id(a1); | |
ASSERT(0 == GI_gen(eid2)); | |
ASSERT(a1 == GI_arch(eid2)); | |
ASSERT(2 == GI_arch_idx(eid2)); | |
GI_set_dead(eid1); | |
ASSERT(false == GI_is_alive(eid1)); | |
GID eid3 = GI_get_next_id(a1); | |
ASSERT(1 == GI_gen(eid3)); | |
ASSERT(a1 == GI_arch(eid3)); | |
ASSERT(1 == GI_arch_idx(eid3)); | |
GID eid4 = GI_get_next_id(a1); | |
ASSERT(0 == GI_gen(eid4)); | |
ASSERT(a1 == GI_arch(eid4)); | |
ASSERT(3 == GI_arch_idx(eid4)); | |
ASSERT(false == GI_is_alive(eid1)); | |
ASSERT(true == GI_is_alive(eid2)); | |
ASSERT(true == GI_is_alive(eid3)); | |
ASSERT(true == GI_is_alive(eid4)); | |
// --- | |
// Scenario: Entity + Component creation | |
// LOG_DEBUGF("Tag (PREFABS) %u", TPREFABS); | |
// LOG_DEBUGF("Tag (PLAYERS) %u", TPLAYERS); | |
// LOG_DEBUGF("Tag (APPLES) %u", TAPPLES); | |
// LOG_DEBUGF("Tag (LIKES) %u", TLIKES); | |
// LOG_DEBUGF("Tag (EATS) %u", TEATS); | |
GI_init(); // reset world generations | |
ASSERT(!ECS__is_dirty(a1, OBS_ADD)); | |
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE)); | |
ASSERT(!ECS__is_dirty(a1, OBS_SET)); | |
GID alice = GI_get_next_id(a1); | |
ECS__arch_ent_add(alice); | |
ECS__arch_cpool_add_cmp_data(a1, 0, CTRANSFORM3D, &(CmpTransform3D){1.0f, 1.0f, 1.0f, 45U}); | |
ECS__arch_cpool_add_cmp_data(a1, 1, CRIGIDBODY2D, &(CmpRigidbody2D){0.0f, 0.0f, 1.0f, 0U}); | |
ASSERT(ECS__is_dirty(a1, OBS_ADD)); | |
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE)); | |
ASSERT(ECS__is_dirty(a1, OBS_SET)); | |
ECS__clean(); | |
ASSERT(!ECS__is_dirty(a1, OBS_ADD)); | |
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE)); | |
ASSERT(!ECS__is_dirty(a1, OBS_SET)); | |
ECS__arch_cmp_updated(a1); | |
ASSERT(!ECS__is_dirty(a1, OBS_ADD)); | |
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE)); | |
ASSERT(ECS__is_dirty(a1, OBS_SET)); | |
ECS__clean(); | |
{ // System | |
ArchIt* it = ECS__query_init(&(ArchIt){a1}); | |
CmpTransform3D* tf; | |
CmpRigidbody2D* rb; | |
for (u16 i = 0; i < it->len; i++) { | |
ECS__query_iter(i, it, &tf, &rb); | |
LOG_DEBUGF( | |
"arch %u en %u gen %u tf %f %f %f %u rb %f %f %u", | |
GI_arch(it->eid[i]), | |
GI_arch_idx(it->eid[i]), | |
GI_gen(it->eid[i]), | |
tf[i].x, | |
tf[i].y, | |
tf[i].z, | |
tf[i].rot, | |
rb[i].vx, | |
rb[i].vy, | |
rb[i].avel); | |
} | |
} | |
ECS__arch_ent_del(alice); | |
ASSERT(!ECS__is_dirty(a1, OBS_ADD)); | |
ASSERT(ECS__is_dirty(a1, OBS_REMOVE)); | |
ASSERT(!ECS__is_dirty(a1, OBS_SET)); | |
ECS__clean(); | |
// --- | |
// Scenario: Application to Meow Game | |
// clang-format off | |
const CArch a2 = ECS__arch(3, | |
A_CTRANSFORM3D_CRIGIDBODY2D_CCOOLDOWN, | |
CTRANSFORM3D, sizeof(CmpTransform3D), | |
CRIGIDBODY2D, sizeof(CmpRigidbody2D), | |
CCOOLDOWN, sizeof(CmpCooldown)); | |
// clang-format on | |
GID cat1 = ECS__entity( | |
a2, | |
&(CmpTransform3D){1.0f, 1.0f, 1.0f, 0U}, | |
&(CmpRigidbody2D){0.0f, 0.0f, 0U}, | |
CD_cmp_alloc(&(CmpCooldown){0}, CD_CAT_COUNT)); | |
// read cmp | |
CmpTransform3D* tf = ECS__field(cat1, 0); | |
CmpRigidbody2D* rb = ECS__field(cat1, 1); | |
CmpCooldown* cd = ECS__field(cat1, 2); | |
GID cat2 = ECS__entity( | |
a2, | |
&(CmpTransform3D){2.0f, 2.0f, 2.0f, 0U}, | |
&(CmpRigidbody2D){0.0f, 0.0f, 0U}, | |
CD_cmp_alloc(&(CmpCooldown){0}, CD_CAT_COUNT)); | |
return 0; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// these are the relevant structs / headers | |
// Single Compilation Unit ------------- | |
#define GENERIC_LIST(N, T) \ | |
typedef struct N##__Node { \ | |
struct N##__Node* next; \ | |
T data; \ | |
} N##__Node; \ | |
typedef struct { \ | |
u32 len; \ | |
N##__Node* head; \ | |
N##__Node* tail; \ | |
} N; | |
GENERIC_LIST(List, void*) | |
// BEGIN ECS -------------------------------------------- | |
#define L1_CACHE_LINE (64) // bytes (per variable-read) | |
// (4B) GID x (1024) live-entities = 4KB GenIdx | |
#define ENT_MAX (1024) // live entity limit (up to arch_idx cap) | |
typedef u32 GID; // Generational Index Id | |
typedef u16 CID; // Live Entity Counter (up to ENT_MAX) | |
// Entity Id case | |
// (1) pair = 0 | |
// (1) alive | |
// (10) gen 1024 generations | |
// (10) arch 1024 archetypes | |
// (10) arch_idx 1024 archetype entity offset | |
// (gen x arch x arch_idx = 1 bil dead, 1 mil live) | |
// | |
// Pair Id case | |
// (1) pair = 1 | |
// (1) data (included w/ relationship) | |
// (10) src 1024 tag sources | |
// (10) rel 1024 tag relationships | |
// (10) dst 1024 tag destinations | |
#define PAIR_BIT (0x80000000) // 10000000 00000000 00000000 00000000 | |
// #define ALIVE_BIT (0x40000000) // 01000000 00000000 00000000 00000000 | |
#define GEN_SHIFT (20) // bits | |
#define GEN_MASK (0x3ff00000) // 00111111 11110000 00000000 00000000 | |
#define ARCH_SHIFT (10) // bits | |
#define ARCH_MASK (0x000ffc00) // 00000000 00001111 11111100 00000000 | |
#define ARCH_IDX_MASK (0x000003ff) // 00000000 00000000 00000011 11111111 | |
// magic number for system queries of pairs; | |
// (one unlikely to be used naturally, | |
// but still fits in EID type) | |
#define ANY (1024) // ENT_IDX_BITS + 1 | |
#define CPOOL_PAGE_COUNT (64) // number of records per page of a component pool | |
typedef struct { | |
GID a[ENT_MAX]; // array | |
CID alive_ct; // count | |
CID dead_ct; // count | |
CID dead_head_idx; // index | |
} GenIdx; // Generational Index for ECS | |
// #ifdef DEBUG_SLOW | |
typedef struct { | |
bool pair; | |
// bool unused; | |
CID gen; | |
CID arch; | |
CID arch_idx; | |
} DebugGenId; | |
// #endif | |
#define GENERIC_PAGED(N, T) \ | |
typedef struct { \ | |
T records; \ | |
CID count; \ | |
} N; | |
GENERIC_PAGED(Paged, void*) | |
GENERIC_PAGED(PagedEnt, GID*) | |
GENERIC_LIST(ListPagedEnt, PagedEnt*) | |
typedef struct { | |
List__Node* node; | |
CID row_idx; | |
void* record; | |
} PageIt; // Page Iterator | |
typedef enum { | |
A_CTRANSFORM3D_CRIGIDBODY2D, // used by unit test only | |
A_CTRANSFORM3D_CRIGIDBODY2D_CCOOLDOWN, // used by unit test only | |
A_CTRANSFORM3D_CRIGIDBODY2D_CREPLICA_CBRAIN_CRENDER, // used in-game | |
// ... // (so far; expecting more) | |
ARCH_COUNT, | |
} CArch; | |
typedef enum { | |
CDEBUG, | |
CTRANSFORM3D, | |
CRIGIDBODY2D, | |
CCOOLDOWN, | |
CREPLICA, | |
CBRAIN, | |
CRENDER, | |
CMP_COUNT, | |
} CCmp; | |
typedef enum { | |
OBS_NONE = 0, | |
// at least one entity was added to the archetype | |
OBS_ADD = 1 << 1, | |
// at least one entity was removed from the archetype | |
OBS_REMOVE = 1 << 2, | |
// at least one entity's component data was added/updated in the archetype (first ADD will exec, then SET) | |
OBS_SET = 1 << 3, | |
OBS_COUNT, | |
} CObs; | |
GENERIC_PAGED(PagedCmp, void*) | |
GENERIC_LIST(ListPagedCmp, PagedCmp*) | |
typedef struct { | |
CCmp type; | |
u8 stride; // size of one component (should be small!) | |
ListPagedCmp cmps; // list of Pages of component structs | |
} ComponentPool; | |
typedef struct { | |
CArch type; | |
CID en_ct; | |
ListPagedEnt entities; // list of Pages of EntityIDs | |
u8 cmp_ct; | |
ComponentPool* cpools; // array of ptr to ComponentPools (SoA) | |
CObs dirty; // observer change flags | |
} Archetype; | |
typedef struct { | |
CArch arch; | |
Archetype* a; | |
GID* eid; | |
CID len; | |
u8 page_idx; // shouldn't be many | |
u8 rec_idx; // shouldn't be many | |
} ArchIt; | |
typedef struct { | |
GenIdx gi; | |
Archetype archs[ARCH_COUNT]; // array of Archetype | |
} World; | |
// Components -------------------------------------------- | |
typedef struct { | |
Cooldown* set; | |
} CmpCooldown; | |
typedef struct { | |
f32 x, y, z; | |
u16 rot; // yaw, Euler | |
} CmpTransform3D; | |
typedef struct { | |
f32 mass; // kg (mass x velocity = momentum) | |
// delta momentum, represents the transfer/conservation of momentum | |
f32 vx, vy; // Linear velocity (Force x deltaTime = impulse) | |
u8 avel; // Angular velocity (radians/s) | |
} CmpRigidbody2D; | |
// END ECS ---------------------------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Copyright 2025 Mike Smullin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.