ecspresso 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -959
- package/dist/asset-manager.d.ts +1 -1
- package/dist/asset-types.d.ts +2 -2
- package/dist/command-buffer.d.ts +34 -24
- package/dist/ecspresso-builder.d.ts +100 -72
- package/dist/ecspresso.d.ts +257 -123
- package/dist/entity-manager.d.ts +57 -47
- package/dist/index.d.ts +5 -4
- package/dist/plugin.d.ts +61 -0
- package/dist/{bundles → plugins}/audio.d.ts +27 -47
- package/dist/{bundles → plugins}/bounds.d.ts +17 -25
- package/dist/{bundles → plugins}/camera.d.ts +8 -9
- package/dist/{bundles → plugins}/collision.d.ts +22 -26
- package/dist/plugins/coroutine.d.ts +126 -0
- package/dist/{bundles → plugins}/diagnostics.d.ts +5 -4
- package/dist/{bundles → plugins}/input.d.ts +9 -15
- package/dist/plugins/particles.d.ts +225 -0
- package/dist/{bundles → plugins}/physics2D.d.ts +27 -23
- package/dist/{bundles → plugins}/renderers/renderer2D.d.ts +40 -39
- package/dist/{bundles → plugins}/spatial-index.d.ts +11 -10
- package/dist/plugins/sprite-animation.d.ts +150 -0
- package/dist/{bundles → plugins}/state-machine.d.ts +50 -104
- package/dist/plugins/timers.d.ts +151 -0
- package/dist/{bundles → plugins}/transform.d.ts +18 -19
- package/dist/{bundles → plugins}/tween.d.ts +36 -71
- package/dist/resource-manager.d.ts +32 -7
- package/dist/screen-manager.d.ts +26 -14
- package/dist/screen-types.d.ts +5 -2
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +17 -17
- package/dist/src/plugins/audio.js +4 -0
- package/dist/src/plugins/audio.js.map +10 -0
- package/dist/src/plugins/bounds.js +4 -0
- package/dist/src/plugins/bounds.js.map +10 -0
- package/dist/src/plugins/camera.js +4 -0
- package/dist/src/plugins/camera.js.map +10 -0
- package/dist/src/plugins/collision.js +4 -0
- package/dist/src/plugins/collision.js.map +11 -0
- package/dist/src/plugins/coroutine.js +4 -0
- package/dist/src/plugins/coroutine.js.map +10 -0
- package/dist/src/plugins/diagnostics.js +5 -0
- package/dist/src/plugins/diagnostics.js.map +10 -0
- package/dist/src/plugins/input.js +4 -0
- package/dist/src/plugins/input.js.map +10 -0
- package/dist/src/plugins/particles.js +4 -0
- package/dist/src/plugins/particles.js.map +10 -0
- package/dist/src/plugins/physics2D.js +4 -0
- package/dist/src/plugins/physics2D.js.map +11 -0
- package/dist/src/plugins/renderers/renderer2D.js +4 -0
- package/dist/src/plugins/renderers/renderer2D.js.map +10 -0
- package/dist/src/plugins/spatial-index.js +4 -0
- package/dist/src/plugins/spatial-index.js.map +11 -0
- package/dist/src/plugins/sprite-animation.js +4 -0
- package/dist/src/plugins/sprite-animation.js.map +10 -0
- package/dist/src/plugins/state-machine.js +4 -0
- package/dist/src/plugins/state-machine.js.map +10 -0
- package/dist/src/plugins/timers.js +4 -0
- package/dist/src/plugins/timers.js.map +10 -0
- package/dist/src/plugins/transform.js +4 -0
- package/dist/src/plugins/transform.js.map +10 -0
- package/dist/src/plugins/tween.js +4 -0
- package/dist/src/plugins/tween.js.map +11 -0
- package/dist/system-builder.d.ts +66 -97
- package/dist/type-utils.d.ts +218 -27
- package/dist/types.d.ts +52 -24
- package/dist/utils/check-required-cycle.d.ts +1 -1
- package/dist/utils/narrowphase.d.ts +7 -7
- package/package.json +61 -45
- package/dist/bundle.d.ts +0 -173
- package/dist/bundles/timers.d.ts +0 -173
- package/dist/src/bundles/audio.js +0 -4
- package/dist/src/bundles/audio.js.map +0 -10
- package/dist/src/bundles/bounds.js +0 -4
- package/dist/src/bundles/bounds.js.map +0 -10
- package/dist/src/bundles/camera.js +0 -4
- package/dist/src/bundles/camera.js.map +0 -10
- package/dist/src/bundles/collision.js +0 -4
- package/dist/src/bundles/collision.js.map +0 -11
- package/dist/src/bundles/diagnostics.js +0 -5
- package/dist/src/bundles/diagnostics.js.map +0 -10
- package/dist/src/bundles/input.js +0 -4
- package/dist/src/bundles/input.js.map +0 -10
- package/dist/src/bundles/physics2D.js +0 -4
- package/dist/src/bundles/physics2D.js.map +0 -11
- package/dist/src/bundles/renderers/renderer2D.js +0 -4
- package/dist/src/bundles/renderers/renderer2D.js.map +0 -10
- package/dist/src/bundles/spatial-index.js +0 -4
- package/dist/src/bundles/spatial-index.js.map +0 -11
- package/dist/src/bundles/state-machine.js +0 -4
- package/dist/src/bundles/state-machine.js.map +0 -10
- package/dist/src/bundles/timers.js +0 -4
- package/dist/src/bundles/timers.js.map +0 -10
- package/dist/src/bundles/transform.js +0 -4
- package/dist/src/bundles/transform.js.map +0 -10
- package/dist/src/bundles/tween.js +0 -4
- package/dist/src/bundles/tween.js.map +0 -11
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Type-Safe**: Full TypeScript support with component, event, and resource type inference
|
|
10
|
-
- **Modular**:
|
|
10
|
+
- **Modular**: Plugin-based architecture for organizing features
|
|
11
11
|
- **Developer-Friendly**: Clean, fluent API with method chaining
|
|
12
12
|
- **Event-Driven**: Integrated event system for decoupled communication
|
|
13
13
|
- **Resource Management**: Global state management with lazy loading
|
|
@@ -15,14 +15,10 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
15
15
|
- **Screen Management**: Game state/screen transitions with overlay support
|
|
16
16
|
- **Entity Hierarchy**: Parent-child relationships with traversal and cascade deletion
|
|
17
17
|
- **Query System**: Powerful entity filtering with helper type utilities
|
|
18
|
-
- **System Phases**: Named execution phases
|
|
18
|
+
- **System Phases**: Named execution phases with fixed-timestep simulation
|
|
19
19
|
- **Change Detection**: Per-system monotonic sequence change tracking with `changed` query filters
|
|
20
20
|
- **Reactive Queries**: Enter/exit callbacks when entities match or unmatch queries
|
|
21
|
-
- **System Groups**: Enable/disable groups of systems at runtime
|
|
22
|
-
- **Component Lifecycle**: Callbacks for component add/remove with unsubscribe support
|
|
23
|
-
- **Required Components**: Auto-add dependent components on spawn/addComponent (e.g. `localTransform` implies `worldTransform`)
|
|
24
21
|
- **Command Buffer**: Deferred structural changes for safe entity/component operations during systems
|
|
25
|
-
- **Timer Bundle**: ECS-native timers with event-based completion notifications
|
|
26
22
|
|
|
27
23
|
## Installation
|
|
28
24
|
|
|
@@ -30,31 +26,6 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
30
26
|
npm install ecspresso
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
## Table of Contents
|
|
34
|
-
|
|
35
|
-
- [Quick Start](#quick-start)
|
|
36
|
-
- [Core Concepts](#core-concepts)
|
|
37
|
-
- [Entities and Components](#entities-and-components) -- [Component Callbacks](#component-callbacks), [Reactive Queries](#reactive-queries)
|
|
38
|
-
- [Systems and Queries](#systems-and-queries)
|
|
39
|
-
- [Resources](#resources)
|
|
40
|
-
- [Systems in Depth](#systems-in-depth)
|
|
41
|
-
- [Method Chaining](#method-chaining)
|
|
42
|
-
- [Query Type Utilities](#query-type-utilities)
|
|
43
|
-
- [System Phases](#system-phases)
|
|
44
|
-
- [System Priority](#system-priority)
|
|
45
|
-
- [System Groups](#system-groups)
|
|
46
|
-
- [System Lifecycle](#system-lifecycle)
|
|
47
|
-
- [Events](#events)
|
|
48
|
-
- [Entity Hierarchy](#entity-hierarchy) -- [Traversal](#traversal), [Parent-First Traversal](#parent-first-traversal), [Cascade Deletion](#cascade-deletion)
|
|
49
|
-
- [Change Detection](#change-detection) -- [Marking Changes](#marking-changes), [Changed Query Filter](#changed-query-filter), [Sequence Timing](#sequence-timing)
|
|
50
|
-
- [Command Buffer](#command-buffer) -- [Available Commands](#available-commands)
|
|
51
|
-
- [Bundles](#bundles) -- [Required Components](#required-components), [Built-in Bundles](#built-in-bundles), [Timer Bundle](#timer-bundle)
|
|
52
|
-
- [Asset Management](#asset-management)
|
|
53
|
-
- [Screen Management](#screen-management) -- [Screen-Scoped Systems](#screen-scoped-systems), [Screen Resource](#screen-resource)
|
|
54
|
-
- [Type Safety](#type-safety)
|
|
55
|
-
- [Error Handling](#error-handling)
|
|
56
|
-
- [Performance Tips](#performance-tips)
|
|
57
|
-
|
|
58
29
|
## Quick Start
|
|
59
30
|
|
|
60
31
|
```typescript
|
|
@@ -67,8 +38,10 @@ interface Components {
|
|
|
67
38
|
health: { value: number };
|
|
68
39
|
}
|
|
69
40
|
|
|
70
|
-
// 2. Create a world
|
|
71
|
-
const world =
|
|
41
|
+
// 2. Create a world using the builder — types are inferred automatically
|
|
42
|
+
const world = ECSpresso.create()
|
|
43
|
+
.withComponentTypes<Components>()
|
|
44
|
+
.build();
|
|
72
45
|
|
|
73
46
|
// 3. Add a movement system
|
|
74
47
|
world.addSystem('movement')
|
|
@@ -78,8 +51,7 @@ world.addSystem('movement')
|
|
|
78
51
|
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
79
52
|
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
80
53
|
}
|
|
81
|
-
})
|
|
82
|
-
.build();
|
|
54
|
+
});
|
|
83
55
|
|
|
84
56
|
// 4. Create entities
|
|
85
57
|
const player = world.spawn({
|
|
@@ -92,930 +64,23 @@ const player = world.spawn({
|
|
|
92
64
|
world.update(1/60);
|
|
93
65
|
```
|
|
94
66
|
|
|
95
|
-
##
|
|
96
|
-
|
|
97
|
-
### Entities and Components
|
|
98
|
-
|
|
99
|
-
Entities are containers for components. Use `spawn()` to create entities with initial components:
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
// Create entity with components
|
|
103
|
-
const entity = world.spawn({
|
|
104
|
-
position: { x: 10, y: 20 },
|
|
105
|
-
health: { value: 100 }
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Add components later
|
|
109
|
-
world.entityManager.addComponent(entity.id, 'velocity', { x: 5, y: 0 });
|
|
110
|
-
|
|
111
|
-
// Get component data (returns undefined if not found)
|
|
112
|
-
const position = world.entityManager.getComponent(entity.id, 'position');
|
|
113
|
-
|
|
114
|
-
// Remove components or entities
|
|
115
|
-
world.entityManager.removeComponent(entity.id, 'velocity');
|
|
116
|
-
world.entityManager.removeEntity(entity.id);
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
#### Component Callbacks
|
|
120
|
-
|
|
121
|
-
React to component additions and removals. Both methods return an unsubscribe function:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
const unsubAdd = world.onComponentAdded('health', (value, entity) => {
|
|
125
|
-
console.log(`Health added to entity ${entity.id}:`, value);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const unsubRemove = world.onComponentRemoved('health', (oldValue, entity) => {
|
|
129
|
-
console.log(`Health removed from entity ${entity.id}:`, oldValue);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Unsubscribe when done
|
|
133
|
-
unsubAdd();
|
|
134
|
-
unsubRemove();
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Also available on `world.entityManager.onComponentAdded()` / `onComponentRemoved()`.
|
|
138
|
-
|
|
139
|
-
#### Reactive Queries
|
|
140
|
-
|
|
141
|
-
Get callbacks when entities enter or exit a query match. Unlike regular queries that you poll during `update()`, reactive queries push notifications when the entity's components change:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
world.addReactiveQuery('enemies', {
|
|
145
|
-
with: ['position', 'enemy'],
|
|
146
|
-
without: ['dead'],
|
|
147
|
-
onEnter: (entity) => {
|
|
148
|
-
console.log(`Enemy ${entity.id} appeared at`, entity.components.position);
|
|
149
|
-
spawnHealthBar(entity.id);
|
|
150
|
-
},
|
|
151
|
-
onExit: (entityId) => {
|
|
152
|
-
// Receives ID since entity may already be removed
|
|
153
|
-
console.log(`Enemy ${entityId} gone`);
|
|
154
|
-
removeHealthBar(entityId);
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Triggers onEnter: spawning matching entity, adding required component, removing excluded component
|
|
159
|
-
const enemy = world.spawn({ position: { x: 0, y: 0 }, enemy: true }); // onEnter fires
|
|
160
|
-
|
|
161
|
-
// Triggers onExit: removing required component, adding excluded component, removing entity
|
|
162
|
-
world.entityManager.addComponent(enemy.id, 'dead', true); // onExit fires
|
|
163
|
-
|
|
164
|
-
// Existing matching entities trigger onEnter when query is added
|
|
165
|
-
// Component replacement does NOT trigger enter/exit (match status unchanged)
|
|
166
|
-
|
|
167
|
-
// Remove reactive query when no longer needed
|
|
168
|
-
world.removeReactiveQuery('enemies'); // returns true if existed
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Systems and Queries
|
|
172
|
-
|
|
173
|
-
Systems process entities that match specific component patterns:
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
world.addSystem('combat')
|
|
177
|
-
.addQuery('fighters', {
|
|
178
|
-
with: ['position', 'health'],
|
|
179
|
-
without: ['dead']
|
|
180
|
-
})
|
|
181
|
-
.addQuery('projectiles', {
|
|
182
|
-
with: ['position', 'damage']
|
|
183
|
-
})
|
|
184
|
-
.setProcess((queries, deltaTime) => {
|
|
185
|
-
for (const fighter of queries.fighters) {
|
|
186
|
-
for (const projectile of queries.projectiles) {
|
|
187
|
-
// Combat logic here
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
.build();
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Resources
|
|
195
|
-
|
|
196
|
-
Resources provide global state accessible to all systems.
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
interface Resources {
|
|
200
|
-
score: { value: number };
|
|
201
|
-
settings: { difficulty: 'easy' | 'hard' };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const world = new ECSpresso<Components, {}, Resources>();
|
|
205
|
-
|
|
206
|
-
// Direct values
|
|
207
|
-
world.addResource('score', { value: 0 });
|
|
208
|
-
|
|
209
|
-
// Sync or async factories (lazy initialization)
|
|
210
|
-
world.addResource('config', () => ({ difficulty: 'normal', soundEnabled: true }));
|
|
211
|
-
world.addResource('database', async () => await connectToDatabase());
|
|
212
|
-
|
|
213
|
-
// Factory with dependencies (initialized after dependencies are ready)
|
|
214
|
-
world.addResource('cache', {
|
|
215
|
-
dependsOn: ['database'],
|
|
216
|
-
factory: (ecs) => ({ db: ecs.getResource('database') })
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Initialize all resources (respects dependency order, detects circular deps)
|
|
220
|
-
await world.initializeResources();
|
|
221
|
-
|
|
222
|
-
// Use in systems
|
|
223
|
-
world.addSystem('scoring')
|
|
224
|
-
.setProcess((queries, deltaTime, ecs) => {
|
|
225
|
-
const score = ecs.getResource('score');
|
|
226
|
-
score.value += 10;
|
|
227
|
-
})
|
|
228
|
-
.build();
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
**Builder pattern** -- resources chain naturally with other builder methods:
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
const world = ECSpresso
|
|
235
|
-
.create<Components, Events, Resources>()
|
|
236
|
-
.withBundle(physicsBundle)
|
|
237
|
-
.withResource('config', { debug: true, maxEntities: 1000 })
|
|
238
|
-
.withResource('score', () => ({ value: 0 }))
|
|
239
|
-
.withResource('cache', {
|
|
240
|
-
dependsOn: ['database'],
|
|
241
|
-
factory: (ecs) => createCache(ecs.getResource('database'))
|
|
242
|
-
})
|
|
243
|
-
.build();
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Disposal** -- resources can define cleanup logic with `onDispose` callbacks:
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
world.addResource('keyboard', {
|
|
250
|
-
factory: () => {
|
|
251
|
-
const handler = (e: KeyboardEvent) => { /* ... */ };
|
|
252
|
-
window.addEventListener('keydown', handler);
|
|
253
|
-
return { handler };
|
|
254
|
-
},
|
|
255
|
-
onDispose: (resource) => {
|
|
256
|
-
window.removeEventListener('keydown', resource.handler);
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
await world.disposeResource('keyboard'); // Dispose a single resource
|
|
261
|
-
await world.disposeResources(); // All, in reverse dependency order
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
`onDispose` receives the resource value and the ECSpresso instance. Supports sync and async callbacks. Only initialized resources have their `onDispose` called. `removeResource()` still exists for removal without disposal.
|
|
265
|
-
|
|
266
|
-
## Systems in Depth
|
|
267
|
-
|
|
268
|
-
### Method Chaining
|
|
269
|
-
|
|
270
|
-
Chain multiple systems using `.and()`. The `.and()` method returns the parent container (ECSpresso or Bundle), enabling fluent chaining:
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
world.addSystem('physics')
|
|
274
|
-
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
275
|
-
.setProcess((queries, deltaTime) => {
|
|
276
|
-
// Physics logic
|
|
277
|
-
})
|
|
278
|
-
.and() // Returns ECSpresso for continued chaining
|
|
279
|
-
.addSystem('rendering')
|
|
280
|
-
.addQuery('visible', { with: ['position', 'sprite'] })
|
|
281
|
-
.setProcess((queries) => {
|
|
282
|
-
// Rendering logic
|
|
283
|
-
})
|
|
284
|
-
.build();
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### Query Type Utilities
|
|
288
|
-
|
|
289
|
-
Extract entity types from queries to create reusable helper functions:
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
import { createQueryDefinition, QueryResultEntity } from 'ecspresso';
|
|
293
|
-
|
|
294
|
-
// Create reusable query definitions
|
|
295
|
-
const movingQuery = createQueryDefinition<Components>({
|
|
296
|
-
with: ['position', 'velocity'],
|
|
297
|
-
without: ['frozen']
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// Extract entity type for helper functions
|
|
301
|
-
type MovingEntity = QueryResultEntity<Components, typeof movingQuery>;
|
|
302
|
-
|
|
303
|
-
function updatePosition(entity: MovingEntity, deltaTime: number) {
|
|
304
|
-
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
305
|
-
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Use in systems
|
|
309
|
-
world.addSystem('movement')
|
|
310
|
-
.addQuery('entities', movingQuery)
|
|
311
|
-
.setProcess((queries, deltaTime) => {
|
|
312
|
-
for (const entity of queries.entities) {
|
|
313
|
-
updatePosition(entity, deltaTime);
|
|
314
|
-
}
|
|
315
|
-
})
|
|
316
|
-
.build();
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### System Phases
|
|
320
|
-
|
|
321
|
-
Systems are organized into named execution phases that run in a fixed order:
|
|
322
|
-
|
|
323
|
-
```
|
|
324
|
-
preUpdate → fixedUpdate → update → postUpdate → render
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
Each phase's command buffer is played back before the next phase begins, so entities spawned in `preUpdate` are visible to `fixedUpdate`, and so on. Systems without `.inPhase()` default to `update`.
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
world.addSystem('input')
|
|
331
|
-
.inPhase('preUpdate')
|
|
332
|
-
.setProcess((queries, dt, ecs) => { /* Read input, update timers */ })
|
|
333
|
-
.and()
|
|
334
|
-
.addSystem('physics')
|
|
335
|
-
.inPhase('fixedUpdate')
|
|
336
|
-
.setProcess((queries, dt, ecs) => {
|
|
337
|
-
// dt is always fixedDt here (e.g. 1/60)
|
|
338
|
-
// Runs 0..N times per frame based on accumulated time
|
|
339
|
-
})
|
|
340
|
-
.and()
|
|
341
|
-
.addSystem('gameplay')
|
|
342
|
-
.inPhase('update') // default phase
|
|
343
|
-
.setProcess((queries, dt, ecs) => { /* Game logic, AI */ })
|
|
344
|
-
.and()
|
|
345
|
-
.addSystem('transform-sync')
|
|
346
|
-
.inPhase('postUpdate')
|
|
347
|
-
.setProcess((queries, dt, ecs) => { /* Transform propagation */ })
|
|
348
|
-
.and()
|
|
349
|
-
.addSystem('renderer')
|
|
350
|
-
.inPhase('render')
|
|
351
|
-
.setProcess((queries, dt, ecs) => { /* Visual output */ })
|
|
352
|
-
.build();
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
**Fixed Timestep** -- The `fixedUpdate` phase uses a time accumulator for deterministic simulation. A spiral-of-death cap (8 steps) prevents runaway accumulation.
|
|
356
|
-
|
|
357
|
-
```typescript
|
|
358
|
-
const world = ECSpresso.create<Components, Events, Resources>()
|
|
359
|
-
.withFixedTimestep(1 / 60) // 60Hz physics (default)
|
|
360
|
-
.build();
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
**Interpolation** -- Use `ecs.interpolationAlpha` (0..1) in the render phase to smooth between fixed steps.
|
|
364
|
-
|
|
365
|
-
**Runtime Phase Changes** -- Move systems between phases at runtime with `world.updateSystemPhase('debug-overlay', 'render')`.
|
|
366
|
-
|
|
367
|
-
### System Priority
|
|
368
|
-
|
|
369
|
-
Within each phase, systems execute in priority order (higher numbers first). Systems with the same priority execute in registration order:
|
|
370
|
-
|
|
371
|
-
```typescript
|
|
372
|
-
world.addSystem('physics')
|
|
373
|
-
.inPhase('fixedUpdate')
|
|
374
|
-
.setPriority(100) // Runs first within fixedUpdate
|
|
375
|
-
.setProcess(() => { /* physics */ })
|
|
376
|
-
.and()
|
|
377
|
-
.addSystem('constraints')
|
|
378
|
-
.inPhase('fixedUpdate')
|
|
379
|
-
.setPriority(50) // Runs second within fixedUpdate
|
|
380
|
-
.setProcess(() => { /* constraints */ })
|
|
381
|
-
.build();
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### System Groups
|
|
385
|
-
|
|
386
|
-
Organize systems into groups that can be enabled/disabled at runtime:
|
|
387
|
-
|
|
388
|
-
```typescript
|
|
389
|
-
world.addSystem('renderSprites')
|
|
390
|
-
.inGroup('rendering')
|
|
391
|
-
.addQuery('sprites', { with: ['position', 'sprite'] })
|
|
392
|
-
.setProcess((queries) => { /* ... */ })
|
|
393
|
-
.and()
|
|
394
|
-
.addSystem('renderParticles')
|
|
395
|
-
.inGroup('rendering')
|
|
396
|
-
.inGroup('effects') // Systems can belong to multiple groups
|
|
397
|
-
.setProcess(() => { /* ... */ })
|
|
398
|
-
.build();
|
|
399
|
-
|
|
400
|
-
world.disableSystemGroup('rendering'); // All rendering systems skip
|
|
401
|
-
world.enableSystemGroup('rendering'); // Resume rendering
|
|
402
|
-
world.isSystemGroupEnabled('rendering'); // true/false
|
|
403
|
-
world.getSystemsInGroup('rendering'); // ['renderSprites', 'renderParticles']
|
|
404
|
-
|
|
405
|
-
// If a system belongs to multiple groups, disabling ANY group skips the system
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
### System Lifecycle
|
|
409
|
-
|
|
410
|
-
Systems can have initialization, cleanup, and post-update hooks:
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
world.addSystem('gameSystem')
|
|
414
|
-
.setOnInitialize(async (ecs) => {
|
|
415
|
-
console.log('System starting...');
|
|
416
|
-
})
|
|
417
|
-
.setOnDetach((ecs) => {
|
|
418
|
-
console.log('System shutting down...');
|
|
419
|
-
})
|
|
420
|
-
.build();
|
|
421
|
-
|
|
422
|
-
await world.initialize();
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
**Post-Update Hooks** -- Register callbacks that run between the `postUpdate` and `render` phases:
|
|
426
|
-
|
|
427
|
-
```typescript
|
|
428
|
-
// Returns unsubscribe function; multiple hooks run in registration order
|
|
429
|
-
const unsubscribe = world.onPostUpdate((ecs, deltaTime) => {
|
|
430
|
-
console.log(`Frame completed in ${deltaTime}s`);
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
unsubscribe();
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
## Events
|
|
437
|
-
|
|
438
|
-
Use events for decoupled system communication. Events work across all features -- hierarchy changes, asset loading, timer completion, and custom game events all use the same system.
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
interface Events {
|
|
442
|
-
playerDied: { playerId: number };
|
|
443
|
-
levelComplete: { score: number };
|
|
444
|
-
// Hierarchy events (if using entity hierarchy)
|
|
445
|
-
hierarchyChanged: {
|
|
446
|
-
entityId: number;
|
|
447
|
-
oldParent: number | null;
|
|
448
|
-
newParent: number | null;
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const world = new ECSpresso<Components, Events>();
|
|
453
|
-
|
|
454
|
-
// Subscribe - returns unsubscribe function
|
|
455
|
-
const unsubscribe = world.on('playerDied', (data) => {
|
|
456
|
-
console.log(`Player ${data.playerId} died`);
|
|
457
|
-
});
|
|
458
|
-
unsubscribe();
|
|
459
|
-
|
|
460
|
-
// Or unsubscribe by callback reference
|
|
461
|
-
const handler = (data) => console.log(`Score: ${data.score}`);
|
|
462
|
-
world.on('levelComplete', handler);
|
|
463
|
-
world.off('levelComplete', handler);
|
|
464
|
-
|
|
465
|
-
// Handle events in systems
|
|
466
|
-
world.addSystem('gameLogic')
|
|
467
|
-
.setEventHandlers({
|
|
468
|
-
playerDied: {
|
|
469
|
-
handler: (data, ecs) => {
|
|
470
|
-
// Respawn logic
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
})
|
|
474
|
-
.build();
|
|
475
|
-
|
|
476
|
-
// Publish events from anywhere
|
|
477
|
-
world.eventBus.publish('playerDied', { playerId: 1 });
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**Built-in events**: `hierarchyChanged` (entity parent changes), `assetLoaded` / `assetFailed` / `assetGroupProgress` / `assetGroupLoaded` (asset loading), and timer `onComplete` events (see [Bundles](#bundles)).
|
|
481
|
-
|
|
482
|
-
## Entity Hierarchy
|
|
483
|
-
|
|
484
|
-
Create parent-child relationships between entities for scene graphs, UI trees, or skeletal hierarchies:
|
|
485
|
-
|
|
486
|
-
```typescript
|
|
487
|
-
const player = world.spawn({ position: { x: 0, y: 0 } });
|
|
488
|
-
|
|
489
|
-
// Create child entity
|
|
490
|
-
const weapon = world.spawnChild(player.id, { position: { x: 10, y: 0 } });
|
|
491
|
-
|
|
492
|
-
// Or set parent on existing entity
|
|
493
|
-
const shield = world.spawn({ position: { x: -10, y: 0 } });
|
|
494
|
-
world.setParent(shield.id, player.id);
|
|
495
|
-
|
|
496
|
-
// Orphan an entity
|
|
497
|
-
world.removeParent(shield.id);
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### Traversal
|
|
501
|
-
|
|
502
|
-
| Method | Returns | Description |
|
|
503
|
-
|--------|---------|-------------|
|
|
504
|
-
| `getParent(id)` | `number \| null` | Parent entity ID |
|
|
505
|
-
| `getChildren(id)` | `number[]` | Direct children |
|
|
506
|
-
| `getAncestors(id)` | `number[]` | Entity up to root |
|
|
507
|
-
| `getDescendants(id)` | `number[]` | Depth-first order |
|
|
508
|
-
| `getRoot(id)` | `number` | Root of the hierarchy |
|
|
509
|
-
| `getSiblings(id)` | `number[]` | Other children of same parent |
|
|
510
|
-
| `getRootEntities()` | `number[]` | All root entities |
|
|
511
|
-
| `getChildAt(id, index)` | `number` | Child at index |
|
|
512
|
-
| `getChildIndex(parentId, childId)` | `number` | Index of child |
|
|
513
|
-
| `isDescendantOf(id, ancestorId)` | `boolean` | Relationship check |
|
|
514
|
-
| `isAncestorOf(id, descendantId)` | `boolean` | Relationship check |
|
|
515
|
-
|
|
516
|
-
### Parent-First Traversal
|
|
517
|
-
|
|
518
|
-
Iterate the hierarchy with guaranteed parent-first order (useful for transform propagation):
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
// Callback-based traversal
|
|
522
|
-
world.forEachInHierarchy((entityId, parentId, depth) => {
|
|
523
|
-
// Parents are always visited before their children
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// Filter to specific subtrees
|
|
527
|
-
world.forEachInHierarchy(callback, { roots: [root.id] });
|
|
528
|
-
|
|
529
|
-
// Generator-based (supports early termination)
|
|
530
|
-
for (const { entityId, parentId, depth } of world.hierarchyIterator()) {
|
|
531
|
-
if (depth > 2) break;
|
|
532
|
-
}
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
### Cascade Deletion
|
|
536
|
-
|
|
537
|
-
When removing entities, descendants are automatically removed by default:
|
|
538
|
-
|
|
539
|
-
```typescript
|
|
540
|
-
world.removeEntity(parent.id);
|
|
541
|
-
// All descendants are removed
|
|
542
|
-
|
|
543
|
-
// To orphan children instead:
|
|
544
|
-
world.removeEntity(parent.id, { cascade: false });
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
Hierarchy changes emit the `hierarchyChanged` event (see [Events](#events)).
|
|
548
|
-
|
|
549
|
-
**World position pattern**: `worldPos = localPos + parent.worldPos`. A parent's world position already includes all grandparents, so each entity only needs to combine its local position with its immediate parent's world position. The Transform bundle implements this automatically.
|
|
550
|
-
|
|
551
|
-
## Change Detection
|
|
552
|
-
|
|
553
|
-
ECSpresso tracks component changes using a per-system monotonic sequence. Each `markChanged` call increments a global counter and stamps the component with a unique sequence number. Each system tracks the highest sequence it has seen; on its next execution, it only processes marks with a sequence greater than its last-seen value. This means each mark is processed exactly once per system, and marks expire after a single update cycle.
|
|
554
|
-
|
|
555
|
-
### Marking Changes
|
|
556
|
-
|
|
557
|
-
Components are automatically marked as changed when added via `spawn()`, `addComponent()`, or `addComponents()`. For in-place mutations, call `markChanged` explicitly:
|
|
558
|
-
|
|
559
|
-
```typescript
|
|
560
|
-
const position = world.entityManager.getComponent(entity.id, 'position');
|
|
561
|
-
if (position) {
|
|
562
|
-
position.x += 10;
|
|
563
|
-
world.markChanged(entity.id, 'position');
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### Changed Query Filter
|
|
568
|
-
|
|
569
|
-
Add `changed` to a query definition to filter entities to only those whose specified components changed since the system last ran:
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
world.addSystem('render-sync')
|
|
573
|
-
.addQuery('moved', {
|
|
574
|
-
with: ['position', 'sprite'],
|
|
575
|
-
changed: ['position'], // Only entities whose position changed this tick
|
|
576
|
-
})
|
|
577
|
-
.setProcess((queries) => {
|
|
578
|
-
for (const entity of queries.moved) {
|
|
579
|
-
syncSpritePosition(entity);
|
|
580
|
-
}
|
|
581
|
-
})
|
|
582
|
-
.build();
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
When multiple components are listed in `changed`, entities matching **any** of them are included (OR semantics).
|
|
586
|
-
|
|
587
|
-
### Sequence Timing
|
|
588
|
-
|
|
589
|
-
- Marks made between updates are visible to all systems on the next update
|
|
590
|
-
- Spawn auto-marks are visible on the first update
|
|
591
|
-
- Marks from earlier phases are visible to later phases within the same frame
|
|
592
|
-
- Within a phase, a higher-priority system's marks are visible to lower-priority systems
|
|
593
|
-
- Each mark is processed exactly once per system (single-update expiry)
|
|
594
|
-
|
|
595
|
-
For manual change detection outside of system queries:
|
|
596
|
-
|
|
597
|
-
```typescript
|
|
598
|
-
const em = ecs.entityManager;
|
|
599
|
-
if (em.getChangeSeq(entity.id, 'localTransform') > ecs.changeThreshold) {
|
|
600
|
-
// Component changed since last system execution (or since last update if between updates)
|
|
601
|
-
}
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
**Deferred marking**: `ecs.commands.markChanged(entity.id, 'position')` queues a mark for command buffer playback.
|
|
605
|
-
|
|
606
|
-
**Built-in bundle usage**: Movement marks `localTransform` (fixedUpdate) → Transform propagation reads `localTransform` changed, writes+marks `worldTransform` (postUpdate) → Renderer reads `worldTransform` changed (render).
|
|
607
|
-
|
|
608
|
-
## Command Buffer
|
|
609
|
-
|
|
610
|
-
Queue structural changes during system execution that execute between phases. This prevents issues when modifying entities during iteration.
|
|
611
|
-
|
|
612
|
-
```typescript
|
|
613
|
-
world.addSystem('combat')
|
|
614
|
-
.addQuery('enemies', { with: ['enemy', 'health'] })
|
|
615
|
-
.setProcess((queries, dt, ecs) => {
|
|
616
|
-
for (const entity of queries.enemies) {
|
|
617
|
-
if (entity.components.health.value <= 0) {
|
|
618
|
-
ecs.commands.removeEntity(entity.id);
|
|
619
|
-
ecs.commands.spawn({
|
|
620
|
-
position: entity.components.position,
|
|
621
|
-
explosion: true,
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
})
|
|
626
|
-
.build();
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
### Available Commands
|
|
630
|
-
|
|
631
|
-
```typescript
|
|
632
|
-
// Entity operations
|
|
633
|
-
ecs.commands.spawn({ position: { x: 0, y: 0 } });
|
|
634
|
-
ecs.commands.spawnChild(parentId, { position: { x: 10, y: 0 } });
|
|
635
|
-
ecs.commands.removeEntity(entityId);
|
|
636
|
-
ecs.commands.removeEntity(entityId, { cascade: false });
|
|
637
|
-
|
|
638
|
-
// Component operations
|
|
639
|
-
ecs.commands.addComponent(entityId, 'velocity', { x: 5, y: 0 });
|
|
640
|
-
ecs.commands.addComponents(entityId, { velocity: { x: 5, y: 0 }, health: { value: 100 } });
|
|
641
|
-
ecs.commands.removeComponent(entityId, 'velocity');
|
|
642
|
-
|
|
643
|
-
// Hierarchy operations
|
|
644
|
-
ecs.commands.setParent(childId, parentId);
|
|
645
|
-
ecs.commands.removeParent(childId);
|
|
646
|
-
|
|
647
|
-
// Change detection
|
|
648
|
-
ecs.commands.markChanged(entityId, 'position');
|
|
649
|
-
|
|
650
|
-
// Utility
|
|
651
|
-
ecs.commands.length; // Number of queued commands
|
|
652
|
-
ecs.commands.clear(); // Discard all queued commands
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
Commands execute in FIFO order. If a command fails (e.g., entity doesn't exist), it logs a warning and continues with remaining commands.
|
|
656
|
-
|
|
657
|
-
## Bundles
|
|
658
|
-
|
|
659
|
-
Organize related systems and resources into reusable bundles:
|
|
660
|
-
|
|
661
|
-
```typescript
|
|
662
|
-
import { Bundle } from 'ecspresso';
|
|
663
|
-
|
|
664
|
-
const physicsBundle = new Bundle<GameComponents, {}, GameResources>('physics')
|
|
665
|
-
.addSystem('applyVelocity')
|
|
666
|
-
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
667
|
-
.setProcess((queries, deltaTime) => {
|
|
668
|
-
for (const entity of queries.moving) {
|
|
669
|
-
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
670
|
-
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
671
|
-
}
|
|
672
|
-
})
|
|
673
|
-
.and()
|
|
674
|
-
.addSystem('applyGravity')
|
|
675
|
-
.addQuery('falling', { with: ['velocity'] })
|
|
676
|
-
.setProcess((queries, deltaTime, ecs) => {
|
|
677
|
-
const gravity = ecs.getResource('gravity');
|
|
678
|
-
for (const entity of queries.falling) {
|
|
679
|
-
entity.components.velocity.y += gravity.value * deltaTime;
|
|
680
|
-
}
|
|
681
|
-
})
|
|
682
|
-
.and()
|
|
683
|
-
.addResource('gravity', { value: 9.8 });
|
|
684
|
-
|
|
685
|
-
// Register bundles with the world
|
|
686
|
-
const game = ECSpresso.create<GameComponents, {}, GameResources>()
|
|
687
|
-
.withBundle(physicsBundle)
|
|
688
|
-
.build();
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
### Required Components
|
|
692
|
-
|
|
693
|
-
Bundles can declare that certain components depend on others. When an entity gains a trigger component, any required components that aren't already present are auto-added with default values:
|
|
694
|
-
|
|
695
|
-
```typescript
|
|
696
|
-
const transformBundle = new Bundle<TransformComponents>('transform')
|
|
697
|
-
.registerRequired('localTransform', 'worldTransform', () => ({
|
|
698
|
-
x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
|
|
699
|
-
}));
|
|
700
|
-
|
|
701
|
-
const world = ECSpresso.create()
|
|
702
|
-
.withBundle(transformBundle)
|
|
703
|
-
.build();
|
|
704
|
-
|
|
705
|
-
// worldTransform is auto-added with defaults
|
|
706
|
-
const entity = world.spawn({
|
|
707
|
-
localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
// Explicit values always win — no auto-add if already provided
|
|
711
|
-
const entity2 = world.spawn({
|
|
712
|
-
localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
|
|
713
|
-
worldTransform: { x: 50, y: 50, rotation: 0, scaleX: 2, scaleY: 2 }, // used as-is
|
|
714
|
-
});
|
|
715
|
-
```
|
|
716
|
-
|
|
717
|
-
Requirements can also be registered via the builder or at runtime:
|
|
718
|
-
|
|
719
|
-
```typescript
|
|
720
|
-
// Builder
|
|
721
|
-
const world = ECSpresso.create()
|
|
722
|
-
.withComponentTypes<Components>()
|
|
723
|
-
.withRequired('rigidBody', 'velocity', () => ({ x: 0, y: 0 }))
|
|
724
|
-
.withRequired('rigidBody', 'force', () => ({ x: 0, y: 0 }))
|
|
725
|
-
.build();
|
|
726
|
-
|
|
727
|
-
// Runtime
|
|
728
|
-
world.registerRequired('position', 'velocity', () => ({ x: 0, y: 0 }));
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
**Behavior:**
|
|
732
|
-
- Enforced at insertion time (`spawn`, `addComponent`, `addComponents`, `spawnChild`, command buffer)
|
|
733
|
-
- Removal is unrestricted — removing a required component does not cascade
|
|
734
|
-
- Transitive requirements resolve automatically (A requires B, B requires C → all three added)
|
|
735
|
-
- Circular dependencies are detected and rejected at registration time
|
|
736
|
-
- Auto-added components are marked as changed and trigger reactive queries
|
|
737
|
-
- Component names and factory return types are fully type-checked
|
|
738
|
-
|
|
739
|
-
**Built-in requirements:** The Transform bundle registers `localTransform` → `worldTransform`. The Physics 2D bundle registers `rigidBody` → `velocity` and `rigidBody` → `force`.
|
|
740
|
-
|
|
741
|
-
### Built-in Bundles
|
|
742
|
-
|
|
743
|
-
| Bundle | Import | Default Phase | Description |
|
|
744
|
-
|--------|--------|---------------|-------------|
|
|
745
|
-
| **Input** | `ecspresso/bundles/input` | `preUpdate` | Frame-accurate keyboard/pointer input with action mapping |
|
|
746
|
-
| **Timers** | `ecspresso/bundles/timers` | `preUpdate` | ECS-native timers with event-based completion |
|
|
747
|
-
| **Movement** | `ecspresso/bundles/movement` | `fixedUpdate` | Velocity-based movement integration |
|
|
748
|
-
| **Transform** | `ecspresso/bundles/transform` | `postUpdate` | Hierarchical transform propagation (local/world transforms) |
|
|
749
|
-
| **Bounds** | `ecspresso/bundles/bounds` | `postUpdate` | Screen bounds enforcement (destroy, clamp, wrap) |
|
|
750
|
-
| **Collision** | `ecspresso/bundles/collision` | `postUpdate` | Layer-based AABB/circle collision detection with events |
|
|
751
|
-
| **2D Renderer** | `ecspresso/bundles/renderers/renderer2D` | `render` | Automated PixiJS scene graph wiring |
|
|
752
|
-
|
|
753
|
-
Each bundle accepts a `phase` option to override its default.
|
|
754
|
-
|
|
755
|
-
### Input Bundle
|
|
756
|
-
|
|
757
|
-
The input bundle provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent), and named action mapping. It's a resource-only bundle — input is polled via the `inputState` resource. DOM events are accumulated between frames and snapshotted once per frame, so all systems see consistent state.
|
|
758
|
-
|
|
759
|
-
```typescript
|
|
760
|
-
import {
|
|
761
|
-
createInputBundle,
|
|
762
|
-
type InputResourceTypes, type KeyCode
|
|
763
|
-
} from 'ecspresso/bundles/input';
|
|
764
|
-
|
|
765
|
-
const world = ECSpresso.create()
|
|
766
|
-
.withBundle(createInputBundle({
|
|
767
|
-
actions: {
|
|
768
|
-
jump: { keys: [' ', 'ArrowUp'] },
|
|
769
|
-
shoot: { keys: ['z'], buttons: [0] },
|
|
770
|
-
moveLeft: { keys: ['a', 'ArrowLeft'] },
|
|
771
|
-
moveRight: { keys: ['d', 'ArrowRight'] },
|
|
772
|
-
},
|
|
773
|
-
}))
|
|
774
|
-
.build();
|
|
775
|
-
|
|
776
|
-
// In a system:
|
|
777
|
-
const input = ecs.getResource('inputState');
|
|
778
|
-
if (input.actions.justActivated('jump')) { /* ... */ }
|
|
779
|
-
if (input.keyboard.isDown('ArrowRight')) { /* ... */ }
|
|
780
|
-
if (input.pointer.justPressed(0)) { /* ... */ }
|
|
781
|
-
|
|
782
|
-
// Runtime remapping — must include all configured actions
|
|
783
|
-
input.setActionMap({
|
|
784
|
-
jump: { keys: ['w'] },
|
|
785
|
-
shoot: { keys: ['z'], buttons: [0] },
|
|
786
|
-
moveLeft: { keys: ['a'] },
|
|
787
|
-
moveRight: { keys: ['d'] },
|
|
788
|
-
});
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
Action names are type-safe — `isActive`, `justActivated`, `justDeactivated`, `setActionMap`, and `getActionMap` only accept action names from the config. The type parameter `A` is inferred from the `actions` object keys passed to `createInputBundle`. Defaults to `string` when no actions are configured.
|
|
792
|
-
|
|
793
|
-
Key values use the `KeyCode` type — a union of all standard `KeyboardEvent.key` values — providing autocomplete and compile-time validation. Note that the space bar key is `' '` (a space character), not `'Space'`.
|
|
794
|
-
|
|
795
|
-
### Timer Bundle
|
|
796
|
-
|
|
797
|
-
The timer bundle provides ECS-native timers that follow the "data, not callbacks" philosophy. Timers are components processed each frame, with optional event-based completion notifications.
|
|
798
|
-
|
|
799
|
-
```typescript
|
|
800
|
-
import {
|
|
801
|
-
createTimerBundle, createTimer, createRepeatingTimer,
|
|
802
|
-
type TimerComponentTypes, type TimerEventData
|
|
803
|
-
} from 'ecspresso/bundles/timers';
|
|
804
|
-
|
|
805
|
-
// Events used with onComplete must have TimerEventData payload
|
|
806
|
-
interface Events {
|
|
807
|
-
hideMessage: TimerEventData; // { entityId, duration, elapsed }
|
|
808
|
-
spawnWave: TimerEventData;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
interface Components extends TimerComponentTypes<Events> {
|
|
812
|
-
position: { x: number; y: number };
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const world = ECSpresso
|
|
816
|
-
.create<Components, Events>()
|
|
817
|
-
.withBundle(createTimerBundle<Events>())
|
|
818
|
-
.build();
|
|
819
|
-
|
|
820
|
-
// One-shot timer (poll justFinished or use onComplete event)
|
|
821
|
-
world.spawn({ ...createTimer<Events>(2.0), position: { x: 0, y: 0 } });
|
|
822
|
-
world.spawn({ ...createTimer<Events>(1.5, { onComplete: 'hideMessage' }) });
|
|
823
|
-
|
|
824
|
-
// Repeating timer
|
|
825
|
-
world.spawn({ ...createRepeatingTimer<Events>(5.0, { onComplete: 'spawnWave' }) });
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
Timer components expose `elapsed`, `duration`, `repeat`, `active`, `justFinished`, and optional `onComplete` for runtime control.
|
|
829
|
-
|
|
830
|
-
## Asset Management
|
|
831
|
-
|
|
832
|
-
Manage game assets with eager/lazy loading, groups, and progress tracking:
|
|
833
|
-
|
|
834
|
-
```typescript
|
|
835
|
-
type Assets = {
|
|
836
|
-
playerTexture: { data: ImageBitmap };
|
|
837
|
-
level1Music: { buffer: AudioBuffer };
|
|
838
|
-
level1Background: { data: ImageBitmap };
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
const game = ECSpresso.create<Components, Events, Resources, Assets>()
|
|
842
|
-
.withAssets(assets => assets
|
|
843
|
-
// Eager assets - loaded automatically during initialize()
|
|
844
|
-
.add('playerTexture', async () => {
|
|
845
|
-
const img = await loadImage('player.png');
|
|
846
|
-
return { data: img };
|
|
847
|
-
})
|
|
848
|
-
// Lazy asset group - loaded on demand
|
|
849
|
-
.addGroup('level1', {
|
|
850
|
-
level1Music: async () => ({ buffer: await loadAudio('level1.mp3') }),
|
|
851
|
-
level1Background: async () => ({ data: await loadImage('level1-bg.png') }),
|
|
852
|
-
})
|
|
853
|
-
)
|
|
854
|
-
.build();
|
|
855
|
-
|
|
856
|
-
await game.initialize(); // Loads eager assets
|
|
857
|
-
const player = game.getAsset('playerTexture'); // Access loaded asset
|
|
858
|
-
game.isAssetLoaded('playerTexture'); // Check if loaded
|
|
859
|
-
|
|
860
|
-
await game.loadAssetGroup('level1'); // Load group on demand
|
|
861
|
-
game.getAssetGroupProgress('level1'); // 0-1 progress
|
|
862
|
-
game.isAssetGroupLoaded('level1'); // Check if group is ready
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
Systems can declare required assets and will only run when those assets are loaded:
|
|
866
|
-
|
|
867
|
-
```typescript
|
|
868
|
-
game.addSystem('gameplay')
|
|
869
|
-
.requiresAssets(['playerTexture'])
|
|
870
|
-
.setProcess((queries, dt, ecs) => {
|
|
871
|
-
const player = ecs.getAsset('playerTexture');
|
|
872
|
-
})
|
|
873
|
-
.build();
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
Asset events (`assetLoaded`, `assetFailed`, `assetGroupProgress`, `assetGroupLoaded`) are available through the event system -- see [Events](#events).
|
|
877
|
-
|
|
878
|
-
## Screen Management
|
|
67
|
+
## Documentation
|
|
879
68
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
pause: ScreenDefinition<Record<string, never>, Record<string, never>>;
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
const game = ECSpresso.create<Components, Events, Resources, {}, Screens>()
|
|
898
|
-
.withScreens(screens => screens
|
|
899
|
-
.add('menu', {
|
|
900
|
-
initialState: () => ({ selectedOption: 0 }),
|
|
901
|
-
onEnter: () => console.log('Entered menu'),
|
|
902
|
-
onExit: () => console.log('Left menu'),
|
|
903
|
-
})
|
|
904
|
-
.add('gameplay', {
|
|
905
|
-
initialState: () => ({ score: 0, isPaused: false }),
|
|
906
|
-
onEnter: (config) => console.log(`Starting level ${config.level}`),
|
|
907
|
-
onExit: () => console.log('Gameplay ended'),
|
|
908
|
-
requiredAssetGroups: ['level1'],
|
|
909
|
-
})
|
|
910
|
-
.add('pause', {
|
|
911
|
-
initialState: () => ({}),
|
|
912
|
-
})
|
|
913
|
-
)
|
|
914
|
-
.build();
|
|
915
|
-
|
|
916
|
-
await game.initialize();
|
|
917
|
-
await game.setScreen('menu', {}); // Set initial screen
|
|
918
|
-
await game.setScreen('gameplay', { difficulty: 'hard', level: 1 }); // Transition
|
|
919
|
-
await game.pushScreen('pause', {}); // Push overlay
|
|
920
|
-
await game.popScreen(); // Pop overlay
|
|
921
|
-
|
|
922
|
-
const current = game.getCurrentScreen(); // 'gameplay'
|
|
923
|
-
const config = game.getScreenConfig(); // { difficulty: 'hard', level: 1 }
|
|
924
|
-
const state = game.getScreenState(); // { score: 0, isPaused: false }
|
|
925
|
-
game.updateScreenState({ score: 100 });
|
|
926
|
-
```
|
|
927
|
-
|
|
928
|
-
### Screen-Scoped Systems
|
|
929
|
-
|
|
930
|
-
```typescript
|
|
931
|
-
game.addSystem('menuUI')
|
|
932
|
-
.inScreens(['menu']) // Only runs in 'menu'
|
|
933
|
-
.setProcess((queries, dt, ecs) => {
|
|
934
|
-
renderMenu(ecs.getScreenState().selectedOption);
|
|
935
|
-
})
|
|
936
|
-
.build();
|
|
937
|
-
|
|
938
|
-
game.addSystem('animations')
|
|
939
|
-
.excludeScreens(['pause']) // Runs in all screens except 'pause'
|
|
940
|
-
.setProcess(() => { /* ... */ })
|
|
941
|
-
.build();
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
### Screen Resource
|
|
945
|
-
|
|
946
|
-
Access screen state through the `$screen` resource:
|
|
947
|
-
|
|
948
|
-
```typescript
|
|
949
|
-
game.addSystem('ui')
|
|
950
|
-
.setProcess((queries, dt, ecs) => {
|
|
951
|
-
const screen = ecs.getResource('$screen');
|
|
952
|
-
screen.current; // Current screen name
|
|
953
|
-
screen.config; // Current screen config
|
|
954
|
-
screen.state; // Current screen state (mutable)
|
|
955
|
-
screen.isOverlay; // true if screen was pushed
|
|
956
|
-
screen.stackDepth; // Number of screens in stack
|
|
957
|
-
screen.isCurrent('gameplay'); // Check current screen
|
|
958
|
-
screen.isActive('menu'); // true if in current or stack
|
|
959
|
-
})
|
|
960
|
-
.build();
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
## Type Safety
|
|
964
|
-
|
|
965
|
-
ECSpresso provides comprehensive TypeScript support:
|
|
966
|
-
|
|
967
|
-
```typescript
|
|
968
|
-
// ✅ Valid
|
|
969
|
-
world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
|
|
970
|
-
|
|
971
|
-
// ❌ TypeScript error - invalid component name
|
|
972
|
-
world.entityManager.addComponent(entity.id, 'invalid', { data: 'bad' });
|
|
973
|
-
|
|
974
|
-
// ❌ TypeScript error - wrong component shape
|
|
975
|
-
world.entityManager.addComponent(entity.id, 'position', { x: 0 }); // missing y
|
|
976
|
-
|
|
977
|
-
// Query type safety - TypeScript knows which components exist
|
|
978
|
-
world.addSystem('example')
|
|
979
|
-
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
980
|
-
.setProcess((queries) => {
|
|
981
|
-
for (const entity of queries.moving) {
|
|
982
|
-
entity.components.position.x; // ✅ guaranteed
|
|
983
|
-
entity.components.health.value; // ❌ not in query
|
|
984
|
-
}
|
|
985
|
-
})
|
|
986
|
-
.build();
|
|
987
|
-
|
|
988
|
-
// Bundle type compatibility - conflicting types error at compile time
|
|
989
|
-
const bundle1 = new Bundle<{position: {x: number, y: number}}>('b1');
|
|
990
|
-
const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('b2');
|
|
991
|
-
const world = ECSpresso.create()
|
|
992
|
-
.withBundle(bundle1)
|
|
993
|
-
.withBundle(bundle2) // Types merge successfully
|
|
994
|
-
.build();
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
## Error Handling
|
|
998
|
-
|
|
999
|
-
ECSpresso provides clear, contextual error messages:
|
|
1000
|
-
|
|
1001
|
-
```typescript
|
|
1002
|
-
world.getResource('nonexistent');
|
|
1003
|
-
// → "Resource 'nonexistent' not found. Available resources: [config, score, settings]"
|
|
1004
|
-
|
|
1005
|
-
world.entityManager.addComponent(999, 'position', { x: 0, y: 0 });
|
|
1006
|
-
// → "Cannot add component 'position': Entity with ID 999 does not exist"
|
|
1007
|
-
|
|
1008
|
-
// Component not found returns undefined (no throw)
|
|
1009
|
-
world.entityManager.getComponent(123, 'position'); // undefined
|
|
1010
|
-
```
|
|
69
|
+
- [Getting Started](./docs/getting-started.md)
|
|
70
|
+
- [Core Concepts](./docs/core-concepts.md) — entities, components, systems, resources
|
|
71
|
+
- [Systems](./docs/systems.md) — phases, priority, groups, lifecycle
|
|
72
|
+
- [Queries](./docs/queries.md) — type utilities, reactive queries
|
|
73
|
+
- [Events](./docs/events.md) — pub/sub, built-in events
|
|
74
|
+
- [Entity Hierarchy](./docs/hierarchy.md) — parent-child, traversal, cascade deletion
|
|
75
|
+
- [Change Detection](./docs/change-detection.md) — marking, sequence timing
|
|
76
|
+
- [Command Buffer](./docs/command-buffer.md) — deferred structural changes
|
|
77
|
+
- [Plugins](./docs/plugins.md) — definePlugin, pluginFactory, required components
|
|
78
|
+
- [Asset Management](./docs/assets.md) — loading, groups, progress
|
|
79
|
+
- [Screen Management](./docs/screens.md) — transitions, scoped systems, overlays
|
|
80
|
+
- [Built-in Plugins](./docs/built-in-plugins.md) — input, timers, physics, rendering
|
|
81
|
+
- [Type Safety](./docs/type-safety.md) — type threading, error handling
|
|
82
|
+
- [Performance](./docs/performance.md) — optimization tips
|
|
1011
83
|
|
|
1012
|
-
##
|
|
84
|
+
## License
|
|
1013
85
|
|
|
1014
|
-
|
|
1015
|
-
- Call `markChanged` after in-place mutations so downstream systems can detect the change
|
|
1016
|
-
- Extract business logic into testable helper functions using query type utilities
|
|
1017
|
-
- Bundle related systems for better organization and reusability
|
|
1018
|
-
- Use system phases to separate concerns (physics in `fixedUpdate`, rendering in `render`) and priorities for ordering within a phase
|
|
1019
|
-
- Use resource factories for expensive initialization (textures, audio, etc.)
|
|
1020
|
-
- Consider component callbacks for immediate reactions to state changes
|
|
1021
|
-
- Minimize the number of components in queries when possible to leverage indexing
|
|
86
|
+
MIT
|