180 lines
5.8 KiB
Markdown
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`.
|