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

180 lines
5.8 KiB
Markdown

# 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
```c
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
```c
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`:
```c
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
```c
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:
```c
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:**
```c
// 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:
```c
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`.