5.8 KiB
Entity Component System (ECS)
Source: src/dusk/entity/
Core concepts
- Entity (
entityid_t=uint8_t) -- a numeric ID. No data of its own; just an index into the entity manager pool. - Component -- a plain data struct registered in
componentlist.h. Stores state; no behaviour. - System -- functions that query all entities with a given component type and act on them each tick.
Hard limits
| Constant | Value |
|---|---|
ENTITY_COUNT_MAX |
64 |
ENTITY_COMPONENT_COUNT_MAX |
16 per entity |
| Total component slots | 1024 (64 x 16) |
| Update callbacks per entity | 5 |
| Dispose callbacks per entity | 5 |
ENTITY_ID_INVALID = 0xFF, COMPONENT_ID_INVALID = 0xFF.
Global state
extern entitymanager_t ENTITY_MANAGER;
// .entities[64] -- entity structs
// .components[1024] -- all component data (entity * 16 + comp)
// .entitiesWithComponent -- O(1) lookup indexed by [type * 64 + entityId]
Entity lifecycle
entityid_t id = entityManagerAdd(); // reserve first inactive slot
entityInit(id); // zero the entity, mark active
componentid_t posId = entityAddComponent(id, COMPONENT_TYPE_POSITION);
componentid_t rendId = entityAddComponent(id, COMPONENT_TYPE_RENDERABLE);
// Per-frame update (called by entityManagerUpdate):
entityUpdate(id);
// Cleanup:
entityDispose(id); // dispose components, mark inactive
entityDisposeDeep(id); // dispose self + entire position hierarchy
Component registration (X-macro)
All component types are declared in a single table in
src/dusk/entity/componentlist.h:
X(NAME, type_t, fieldName, initFn, disposeFn, renderFn)
This generates:
COMPONENT_TYPE_NAMEenum value- Union field
fieldNameincomponentdata_t - Entry in
COMPONENT_DEFINITIONS[]withinit/dispose/renderfunction pointers (any may beNULL)
Current registered components:
| Enum suffix | Struct | Notes |
|---|---|---|
POSITION |
entityposition_t |
Transform + parent/child hierarchy |
CAMERA |
entitycamera_t |
View matrix setup |
RENDERABLE |
entityrenderable_t |
Sprite batch, shader material, or custom draw |
PHYSICS |
entityphysics_t |
Physics body (see .claude/physics.md) |
TRIGGER |
entitytrigger_t |
Collision trigger zone |
Accessing component data
void *componentGetData(
entityid_t entityId,
componentid_t componentId,
componenttype_t type
);
// Returns pointer into the preallocated components pool.
// Never NULL for a valid (id, type) pair.
Querying all entities with a given type:
entityid_t ids[ENTITY_COUNT_MAX];
componentid_t comps[ENTITY_COUNT_MAX];
entityid_t count = componentGetEntitiesWithComponent(
COMPONENT_TYPE_PHYSICS, ids, comps
);
Adding a new component -- checklist
- Create
src/dusk/entity/component/<category>/entity<Name>.h/.c.- Struct:
entity<Name>_t entity<Name>Init(entityid_t, componentid_t)(required)entity<Name>Dispose(entityid_t, componentid_t)(if needed)
- Struct:
#includethe new header in the header block ofcomponentlist.h.- Add an
X(...)row incomponentlist.h. - If JS-facing, add a script module (see
CLAUDE.md).
Position component (entityposition_t)
The position component implements the transform hierarchy and uses lazy evaluation with dirty flags to avoid redundant matrix rebuilds.
Dirty flags:
| Flag | Meaning |
|---|---|
ENTITY_POSITION_FLAG_PRS_DIRTY |
Cached position/rotation/scale stale vs localTransform |
ENTITY_POSITION_FLAG_ROTATION_DIRTY |
Rotation columns of localTransform stale |
ENTITY_POSITION_FLAG_POSITION_DIRTY |
Position column of localTransform stale |
ENTITY_POSITION_FLAG_WORLD_DIRTY |
World matrix stale |
Hierarchy: up to 8 children per entity. entityPositionSetParent()
reparents and maintains the child list. entityDisposeDeep() /
entityPositionDisposeDeep() recursively disposes the entire subtree.
Key functions:
// Local space getters/setters (mark local dirty):
entityPositionGetLocalPosition / SetLocalPosition
entityPositionGetLocalRotation / SetLocalRotation
entityPositionGetLocalScale / SetLocalScale
// World space getters/setters (ensure world updated):
entityPositionGetWorldPosition / SetWorldPosition
entityPositionGetWorldRotation / SetWorldRotation
entityPositionGetWorldScale / SetWorldScale
entityPositionSetParent(entityId, parentEntityId, parentComponentId);
entityPositionLookAt(entityId, componentId, eye, target, up);
entityPositionRebuild(pos); // force immediate matrix rebuild
Renderable component (entityrenderable_t)
Three rendering modes selected via entityrenderabletype_t:
| Mode | Description |
|---|---|
ENTITY_RENDERABLE_TYPE_CUSTOM |
User-supplied draw callback |
ENTITY_RENDERABLE_TYPE_SPRITEBATCH |
Up to 64 sprites + texture |
ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL |
Up to 8 meshes, shader, material, display state |
Default on init: shader material (white, unlit, depth-tested cube).
priority (int8_t) controls render order; 0 = automatic.
Callback hooks on entities
Entities support up to 5 registered update callbacks and 5 dispose callbacks. These are used by systems that need per-entity ticks without building a full query loop every frame:
entityUpdateAdd(entityId, updateFn, componentId, user);
entityUpdateRemove(entityId, updateFn);
entityDisposeAdd(entityId, disposeFn, componentId, user);
entityDisposeRemove(entityId, disposeFn);
Design rules
- Components store data only. No logic in a component struct or its init beyond setting default values.
- Keep components small and focused.
- Cross-component access is fine from a system, but a component must never hold a pointer to another component -- use entity IDs.
- Systems must not assume component ordering. Use
componentGetEntitiesWithComponent.