|
| 1 | +--- |
| 2 | +name: koota |
| 3 | +description: Real-time ECS state management for TypeScript and React. Use when the user mentions koota, ECS, entities, traits, queries, or building data-oriented applications. |
| 4 | +--- |
| 5 | + |
| 6 | +# Koota ECS |
| 7 | + |
| 8 | +Koota manages state using entities with composable traits. |
| 9 | + |
| 10 | +## Glossary |
| 11 | + |
| 12 | +- **Entity** - A unique identifier pointing to data defined by traits. Spawned from a world. |
| 13 | +- **Trait** - A reusable data definition. Can be schema-based (SoA), callback-based (AoS), or a tag. |
| 14 | +- **Relation** - A directional connection between entities to build graphs. |
| 15 | +- **World** - The context for all entities and their data (traits). |
| 16 | +- **Archetype** - A unique combination of traits that entities share. |
| 17 | +- **Query** - Fetches entities matching an archetype. The primary way to batch update state. |
| 18 | + |
| 19 | +## Design Principles |
| 20 | + |
| 21 | +### Data-oriented |
| 22 | + |
| 23 | +Behavior is separated from data. Data is defined as traits, entities compose traits, and systems mutate data on traits via queries. See [Basic usage](#basic-usage) for a complete example. |
| 24 | + |
| 25 | +### Composable systems |
| 26 | + |
| 27 | +Design systems as small, single-purpose units rather than monolithic functions that do everything in sequence. Each system should handle one concern so that behaviors can be toggled on/off independently. |
| 28 | + |
| 29 | +```typescript |
| 30 | +// Good: Composable systems - each can be enabled/disabled independently |
| 31 | +function applyVelocity(world: World) {} |
| 32 | +function applyGravity(world: World) {} |
| 33 | +function applyFriction(world: World) {} |
| 34 | +function syncToDOM(world: World) {} |
| 35 | + |
| 36 | +// Bad: Monolithic system - can't disable gravity without disabling everything |
| 37 | +function updatePhysicsAndRender(world: World) { |
| 38 | + // velocity, gravity, friction, DOM sync all in one function |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +This enables feature flags, debugging (disable one system to isolate issues), and flexible runtime configurations. |
| 43 | + |
| 44 | +### Decouple view from logic |
| 45 | + |
| 46 | +Separate core state and logic (the "core") from the view ("app"): |
| 47 | + |
| 48 | +- Run logic independent of rendering |
| 49 | +- Swap views while keeping state (2D β 3D) |
| 50 | +- Run logic in a worker or on a server |
| 51 | + |
| 52 | +### Prefer traits + actions over classes |
| 53 | + |
| 54 | +Prefer not to use classes to encapsulate data and behavior. Use traits for data and actions for behavior. Only use classes when required by external libraries (e.g., THREE.js objects) or the user prefers it. |
| 55 | + |
| 56 | +## Directory structure |
| 57 | + |
| 58 | +If the user has a preferred structure, follow it. Otherwise, use this guidance: the directory structure should mirror how the app's data model is organized. Separate core state/logic from the view layer: |
| 59 | + |
| 60 | +- **Core** - Pure TypeScript. Traits, systems, actions, world. No view imports. |
| 61 | +- **View** - Reads from world, mutates via actions. Organized by domain/feature. |
| 62 | + |
| 63 | +``` |
| 64 | +src/ |
| 65 | +βββ core/ # Pure TypeScript, no view imports |
| 66 | +β βββ traits/ |
| 67 | +β βββ systems/ |
| 68 | +β βββ actions/ |
| 69 | +β βββ world.ts |
| 70 | +βββ features/ # View layer, organized by domain |
| 71 | +``` |
| 72 | + |
| 73 | +Files are organized by role, not by feature slice. Traits and systems are composable and don't map cleanly to features. |
| 74 | + |
| 75 | +For detailed patterns and monorepo structures, see [references/architecture.md](references/architecture.md). |
| 76 | + |
| 77 | +## Trait types |
| 78 | + |
| 79 | +| Type | Syntax | Use when | Examples | |
| 80 | +| ------------------ | -------------------------- | ------------------------- | -------------------------------- | |
| 81 | +| **SoA (Schema)** | `trait({ x: 0 })` | Simple primitive data | `Position`, `Velocity`, `Health` | |
| 82 | +| **AoS (Callback)** | `trait(() => new Thing())` | Complex objects/instances | `Ref` (DOM), `Keyboard` (Set) | |
| 83 | +| **Tag** | `trait()` | No data, just a flag | `IsPlayer`, `IsEnemy`, `IsDead` | |
| 84 | + |
| 85 | +## Trait naming conventions |
| 86 | + |
| 87 | +| Type | Pattern | Examples | |
| 88 | +| ------------- | --------------- | -------------------------------- | |
| 89 | +| **Tags** | Start with `Is` | `IsPlayer`, `IsEnemy`, `IsDead` | |
| 90 | +| **Relations** | Prepositional | `ChildOf`, `HeldBy`, `Contains` | |
| 91 | +| **Trait** | Noun | `Position`, `Velocity`, `Health` | |
| 92 | + |
| 93 | +## Relations |
| 94 | + |
| 95 | +Relations build graphs between entities such as hierarchies, inventories, targeting. |
| 96 | + |
| 97 | +```typescript |
| 98 | +import { relation } from 'koota' |
| 99 | + |
| 100 | +const ChildOf = relation({ autoDestroy: 'orphan' }) // Hierarchy |
| 101 | +const Contains = relation({ store: { amount: 0 } }) // With data |
| 102 | +const Targeting = relation({ exclusive: true }) // One target only |
| 103 | + |
| 104 | +// Build graph |
| 105 | +const parent = world.spawn() |
| 106 | +const child = world.spawn(ChildOf(parent)) |
| 107 | + |
| 108 | +// Query children of parent |
| 109 | +const children = world.query(ChildOf(parent)) |
| 110 | + |
| 111 | +// Query all entities with any ChildOf relation |
| 112 | +const allChildren = world.query(ChildOf('*')) |
| 113 | + |
| 114 | +// Get targets from entity |
| 115 | +const items = entity.targetsFor(Contains) // Entity[] |
| 116 | +const target = entity.targetFor(Targeting) // Entity | undefined |
| 117 | +``` |
| 118 | + |
| 119 | +For detailed patterns, traversal, ordered relations, and anti-patterns, see [references/relations.md](references/relations.md). |
| 120 | + |
| 121 | +## Basic usage |
| 122 | + |
| 123 | +```typescript |
| 124 | +import { trait, createWorld } from 'koota' |
| 125 | + |
| 126 | +// 1. Define traits |
| 127 | +const Position = trait({ x: 0, y: 0 }) |
| 128 | +const Velocity = trait({ x: 0, y: 0 }) |
| 129 | +const IsPlayer = trait() |
| 130 | + |
| 131 | +// 2. Create world and spawn entities |
| 132 | +const world = createWorld() |
| 133 | +const player = world.spawn(Position({ x: 100, y: 50 }), Velocity, IsPlayer) |
| 134 | + |
| 135 | +// 3. Query and update |
| 136 | +world.query(Position, Velocity).updateEach(([pos, vel]) => { |
| 137 | + pos.x += vel.x |
| 138 | + pos.y += vel.y |
| 139 | +}) |
| 140 | +``` |
| 141 | + |
| 142 | +## Entities |
| 143 | + |
| 144 | +Entities are unique identifiers that compose traits. Spawned from a world. |
| 145 | + |
| 146 | +```typescript |
| 147 | +// Spawn |
| 148 | +const entity = world.spawn(Position, Velocity) |
| 149 | + |
| 150 | +// Read/write traits |
| 151 | +entity.get(Position) // Read trait data |
| 152 | +entity.set(Position, { x: 10 }) // Write (triggers change events) |
| 153 | +entity.add(IsPlayer) // Add trait |
| 154 | +entity.remove(Velocity) // Remove trait |
| 155 | +entity.has(Position) // Check if has trait |
| 156 | + |
| 157 | +// Destroy |
| 158 | +entity.destroy() |
| 159 | +``` |
| 160 | + |
| 161 | +**Entity IDs** |
| 162 | + |
| 163 | +An entity is internally a number packed with entity ID, generation ID (for recycling), and world ID. Safe to store directly for persistence or networking. |
| 164 | + |
| 165 | +```typescript |
| 166 | +entity.id() // Just the entity ID (reused after destroy) |
| 167 | +entity // Full packed number (unique forever) |
| 168 | +``` |
| 169 | + |
| 170 | +**Typing** |
| 171 | + |
| 172 | +Use `TraitRecord` to get the type that `entity.get()` returns |
| 173 | + |
| 174 | +```typescript |
| 175 | +type PositionRecord = TraitRecord<typeof Position> |
| 176 | +``` |
| 177 | +
|
| 178 | +## Queries |
| 179 | +
|
| 180 | +Queries fetch entities matching an archetype and are the primary way to batch update state. |
| 181 | +
|
| 182 | +```typescript |
| 183 | +// Query and update |
| 184 | +world.query(Position, Velocity).updateEach(([pos, vel]) => { |
| 185 | + pos.x += vel.x |
| 186 | + pos.y += vel.y |
| 187 | +}) |
| 188 | + |
| 189 | +// Read-only iteration (no write-back) |
| 190 | +const data: Array<{ x: number; y: number }> = [] |
| 191 | +world.query(Position, Velocity).readEach(([pos, vel]) => { |
| 192 | + data.push({ x: pos.x, y: pos.y }) |
| 193 | +}) |
| 194 | + |
| 195 | +// Get first match |
| 196 | +const player = world.queryFirst(IsPlayer, Position) |
| 197 | + |
| 198 | +// Filter with modifiers |
| 199 | +world.query(Position, Not(Velocity)) // Has Position but not Velocity |
| 200 | +world.query(Or(IsPlayer, IsEnemy)) // Has either trait |
| 201 | +``` |
| 202 | + |
| 203 | +**Note:** `updateEach`/`readEach` only return data-bearing traits (SoA/AoS). Tags, `Not()`, and relation filters are **excluded**: |
| 204 | + |
| 205 | +```typescript |
| 206 | +world.query(IsPlayer, Position, Velocity).updateEach(([pos, vel]) => { |
| 207 | + // Array has 2 elements - IsPlayer (tag) excluded |
| 208 | +}) |
| 209 | +``` |
| 210 | + |
| 211 | +For tracking changes, caching queries, and advanced patterns, see [references/queries.md](references/queries.md). |
| 212 | + |
| 213 | +## React integration |
| 214 | + |
| 215 | +**Imports:** Core types (`World`, `Entity`) from `'koota'`. React hooks from `'koota/react'`. |
| 216 | + |
| 217 | +**Change detection:** `entity.set()` and `world.set()` trigger change events that cause hooks like `useTrait` to rerender. For AoS traits where you mutate objects directly, manually signal with `entity.changed(Trait)`. |
| 218 | + |
| 219 | +For React hooks and actions, see [references/react-hooks.md](references/react-hooks.md). |
| 220 | + |
| 221 | +For component patterns (App, Startup, Renderer, view sync, input), see [references/react-patterns.md](references/react-patterns.md). |
| 222 | + |
| 223 | +## Runtime |
| 224 | + |
| 225 | +Systems query the world and update entities. Run them via frameloop (continuous) or event handlers (discrete). |
| 226 | + |
| 227 | +For systems, frameloop, event-driven patterns, and time management, see [references/runtime.md](references/runtime.md). |
0 commit comments