Keel’s entire data model is an archetype-based Entity Component System (ECS). Every piece of game state — a player’s position, a bullet’s velocity, a score counter — lives in a component attached to an entity. The ECS groups entities by their exact component types into archetypes, which store data in numpy structured arrays laid out column-by-column. This means hot loops over thousands of entities become vectorized numpy operations, not PythonDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/VKSFY/keel/llms.txt
Use this file to discover all available pages before exploring further.
for loops over individual objects.
Components
A component is a plain Python class annotated with@keel.component. The decorator inspects the class fields and, when every field maps to a numpy-compatible type (float, int, or bool), builds a numpy dtype for the component’s structured array column.
| Python annotation | numpy dtype |
|---|---|
float | float64 |
int | int64 |
bool | bool_ |
np.float32 | float32 |
np.int32 | int32 |
str, tuple, or dict, for example), the entire component falls back to a plain Python list column. The component is still valid; it just loses the vectorized-mutation benefit.
Archetypes
An archetype is the set of component types shared by a group of entities. Two entities that both havePosition and Velocity live in the same archetype. An entity that also adds Health moves to a different archetype — {Position, Velocity, Health}.
Each archetype owns one column per component type: a numpy structured array (or Python list) sized to the number of entities currently in that archetype. Rows are added on spawn and removed via swap-remove on despawn, so the array stays compact and cache-friendly.
ArchetypeRegistry inside World creates them automatically the first time a new component-type combination appears.
Spawning Entities
world.spawn(*components) allocates a fresh entity ID and queues a deferred spawn command. It returns the integer entity ID immediately — but the entity does not appear in queries until world.flush() runs.
The main loop calls
world.flush() automatically after every simulation tick (after POST_UPDATE completes). Inside systems you can safely call world.spawn() without an immediate flush — the entity appears on the next simulation tick. If you need it to appear within the same code path (for example, during startup), call world.flush() explicitly.spawn() raises ValueError immediately:
Querying
world.query(*args) returns a QueryResult that yields one tuple of column views per matching archetype. Iteration is once per archetype, not once per entity.
Position and Velocity. Each pos and vel slice covers every entity in that archetype simultaneously, so the += is a vectorized numpy operation rather than a Python loop.
Query modifiers
Without[C]
Exclude archetypes that contain component
C. Use this to skip entities with a particular tag or flag.Optional[D]
Include the column if the archetype has
D, otherwise yield None in that position.In-Place Mutation
Column views fromworld.query() are numpy array slices. Writing to them mutates ECS storage directly — no copy, no write-back step needed.
Singleton Reads: query_one
For components where exactly one entity holds that component (a GameState, a Camera2D, a level config), use world.query_one(ComponentType). It returns the first matching entity’s fields as a plain Python dict with scalar values — no [0] indexing, no numpy type coercions.
query_one is read-only. The returned dict is a snapshot; mutating it does not write back to the ECS. To update fields, use world.set(entity_id, GameState, score=42) or iterate via world.query(GameState) for in-place bulk mutation.Per-Entity Reads and Writes
For one-off access to a specific entity (a player paddle, a named UI label), useworld.get and world.set.
world.set raises ValueError if a keyword argument names a field that does not exist on the component.
Checking Entity State
Deferred Structural Changes
All operations that change an entity’s component set are deferred — they queue a command and apply it only whenworld.flush() runs.
| Method | What it defers |
|---|---|
world.spawn(*components) | Allocate ID + insert into archetype |
world.despawn(entity) | Remove from archetype + free ID |
world.add_component(entity, component) | Migrate entity to new archetype |
world.remove_component(entity, type) | Migrate entity to smaller archetype |
