Files
dusk/.claude/ecs.md
T
2026-06-16 10:15:59 -05:00

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_NAME enum value
  • Union field fieldName in componentdata_t
  • Entry in COMPONENT_DEFINITIONS[] with init / dispose / render function pointers (any may be NULL)

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

  1. 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)
  2. #include the new header in the header block of componentlist.h.
  3. Add an X(...) row in componentlist.h.
  4. 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.