ecspresso 0.3.6 → 0.4.0
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 +230 -377
- package/dist/ecspresso.d.ts +34 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +7 -6
- package/dist/system-builder.d.ts +28 -0
- package/dist/types.d.ts +74 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ A type-safe, modular, and extensible Entity Component System (ECS) framework for
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- 🔒 **Type-Safe**: Full TypeScript support with
|
|
12
|
-
- 🧩 **Modular**: Bundle-based architecture for
|
|
13
|
-
- 💡 **
|
|
14
|
-
- 🔄 **Event-Driven**: Integrated event
|
|
15
|
-
- 🗄️ **Resource Management**: Global
|
|
16
|
-
-
|
|
11
|
+
- 🔒 **Type-Safe**: Full TypeScript support with component, event, and resource type inference
|
|
12
|
+
- 🧩 **Modular**: Bundle-based architecture for organizing features
|
|
13
|
+
- 💡 **Developer-Friendly**: Clean, fluent API with method chaining
|
|
14
|
+
- 🔄 **Event-Driven**: Integrated event system for decoupled communication
|
|
15
|
+
- 🗄️ **Resource Management**: Global state management with lazy loading
|
|
16
|
+
- ⚡ **Query System**: Powerful entity filtering with helper type utilities
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -24,494 +24,347 @@ npm install ecspresso
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
|
-
import
|
|
27
|
+
import ECSpresso from 'ecspresso';
|
|
28
28
|
|
|
29
29
|
// Define your component types
|
|
30
30
|
interface Components {
|
|
31
31
|
position: { x: number; y: number };
|
|
32
32
|
velocity: { x: number; y: number };
|
|
33
|
-
|
|
33
|
+
health: { value: number };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
collision: { entity1: number; entity2: number };
|
|
39
|
-
scoreChange: { amount: number };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Define your resource types
|
|
43
|
-
interface Resources {
|
|
44
|
-
score: { value: number };
|
|
45
|
-
gameState: 'playing' | 'paused' | 'gameOver';
|
|
46
|
-
assets: { textures: Record<string, any>; sounds: Record<string, any> };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Create a world instance directly
|
|
50
|
-
const world = new ECSpresso<Components, Events, Resources>();
|
|
51
|
-
|
|
52
|
-
// Add resources - can be direct values or factory functions
|
|
53
|
-
world.addResource('score', { value: 0 });
|
|
54
|
-
world.addResource('gameState', 'paused');
|
|
55
|
-
world.addResource('assets', async () => {
|
|
56
|
-
// Simulate loading game assets
|
|
57
|
-
const textures = await loadTextures();
|
|
58
|
-
const sounds = await loadSounds();
|
|
59
|
-
return { textures, sounds };
|
|
60
|
-
});
|
|
36
|
+
// Create a world and add a system
|
|
37
|
+
const world = new ECSpresso<Components>();
|
|
61
38
|
|
|
62
|
-
// Add a movement system directly to the world
|
|
63
39
|
world.addSystem('movement')
|
|
64
|
-
.addQuery('
|
|
65
|
-
with: ['position', 'velocity']
|
|
66
|
-
})
|
|
40
|
+
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
67
41
|
.setProcess((queries, deltaTime) => {
|
|
68
|
-
for (const entity of queries.
|
|
42
|
+
for (const entity of queries.moving) {
|
|
69
43
|
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
70
44
|
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
71
45
|
}
|
|
72
46
|
})
|
|
73
|
-
.setOnInitialize(async (ecs) => {
|
|
74
|
-
// One-time initialization of the system
|
|
75
|
-
console.log('Setting up movement system...');
|
|
76
|
-
const gameState = ecs.getResource('gameState');
|
|
77
|
-
gameState.lastMovementUpdate = Date.now();
|
|
78
|
-
})
|
|
79
|
-
.build(); // Don't forget to call build() to finalize the system
|
|
80
|
-
|
|
81
|
-
// Create an entity with position and velocity components
|
|
82
|
-
const entity = world.entityManager.createEntity();
|
|
83
|
-
world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
|
|
84
|
-
world.entityManager.addComponent(entity.id, 'velocity', { x: 10, y: 5 });
|
|
85
|
-
|
|
86
|
-
// Initialize everything (systems and resources with factory functions) in one call
|
|
87
|
-
await world.initialize();
|
|
88
|
-
|
|
89
|
-
// Run a single update
|
|
90
|
-
world.update(1/60);
|
|
91
|
-
|
|
92
|
-
// Check new position
|
|
93
|
-
const position = world.entityManager.getComponent(entity.id, 'position');
|
|
94
|
-
console.log(position); // { x: 0.16666..., y: 0.08333... }
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Building Modular Systems with Bundles
|
|
98
|
-
|
|
99
|
-
Bundles are a powerful way to organize game features:
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
// Create a player input bundle
|
|
103
|
-
const inputBundle = new Bundle<Components, Events, Resources>('input-bundle')
|
|
104
|
-
.addSystem('playerInput')
|
|
105
|
-
.setProcess((_queries, _deltaTime, ecs) => {
|
|
106
|
-
// Handle keyboard input and modify player velocity
|
|
107
|
-
// ...
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Create a rendering bundle
|
|
111
|
-
const renderBundle = new Bundle<Components, Events, Resources>('render-bundle')
|
|
112
|
-
.addSystem('renderer')
|
|
113
|
-
.addQuery('sprites', { with: ['position', 'sprite'] })
|
|
114
|
-
.setProcess((queries) => {
|
|
115
|
-
// Render all sprites
|
|
116
|
-
for (const entity of queries.sprites) {
|
|
117
|
-
// Draw entities at their positions
|
|
118
|
-
// ...
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Create a scoring bundle that adds a resource and listens for events
|
|
123
|
-
const scoringBundle = new Bundle<Components, Events, Resources>('scoring-bundle')
|
|
124
|
-
.addResource('score', { value: 0 })
|
|
125
|
-
// Resources can also be added using factory functions
|
|
126
|
-
.addResource('gameStats', () => ({
|
|
127
|
-
highScore: 0,
|
|
128
|
-
totalPlayTime: 0,
|
|
129
|
-
sessionStartTime: Date.now()
|
|
130
|
-
}))
|
|
131
|
-
.addSystem('scoreKeeper')
|
|
132
|
-
.setEventHandlers({
|
|
133
|
-
scoreChange: {
|
|
134
|
-
handler: (data, ecs) => {
|
|
135
|
-
const score = ecs.getResource('score');
|
|
136
|
-
score.value += data.amount;
|
|
137
|
-
console.log(`Score: ${score.value}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Create a game initialization bundle with event handlers
|
|
143
|
-
const initBundle = new Bundle<Components, Events, Resources>('init-bundle')
|
|
144
|
-
.addSystem('initialization')
|
|
145
|
-
.setOnInitialize(async (ecs) => {
|
|
146
|
-
console.log('Game systems initializing...');
|
|
147
|
-
// Do one-time system setup here
|
|
148
|
-
})
|
|
149
|
-
.setEventHandlers({
|
|
150
|
-
gameStart: {
|
|
151
|
-
async handler(data, ecs) {
|
|
152
|
-
console.log('Game starting...');
|
|
153
|
-
|
|
154
|
-
// Initialize all resources and systems
|
|
155
|
-
await ecs.initialize();
|
|
156
|
-
|
|
157
|
-
// Resources and systems are now ready to use
|
|
158
|
-
const assets = ecs.getResource('assets');
|
|
159
|
-
console.log(`Loaded ${Object.keys(assets.textures).length} textures`);
|
|
160
|
-
|
|
161
|
-
// Continue with game initialization
|
|
162
|
-
// ...
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Create the game world with all features using the builder pattern
|
|
168
|
-
const game = ECSpresso.create<Components, Events, Resources>()
|
|
169
|
-
.withBundle(initBundle)
|
|
170
|
-
.withBundle(inputBundle)
|
|
171
|
-
.withBundle(renderBundle)
|
|
172
|
-
.withBundle(scoringBundle)
|
|
173
|
-
.build()
|
|
174
|
-
.addResource('assets', async () => {
|
|
175
|
-
// This won't execute until initializeResources is called
|
|
176
|
-
return { textures: await loadTextures(), sounds: await loadSounds() };
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Start the game
|
|
180
|
-
game.eventBus.publish('gameStart', {});
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Type Safety with the Builder Pattern
|
|
184
|
-
|
|
185
|
-
ECSpresso uses a builder pattern to provide strong type checking for bundle compatibility:
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
// These bundles have compatible component types
|
|
189
|
-
const bundle1 = new Bundle<{position: {x: number, y: number}}>('bundle1');
|
|
190
|
-
const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('bundle2');
|
|
191
|
-
|
|
192
|
-
// Create a world with both bundles - TypeScript will allow this
|
|
193
|
-
const world = ECSpresso.create()
|
|
194
|
-
.withBundle(bundle1)
|
|
195
|
-
.withBundle(bundle2)
|
|
196
47
|
.build();
|
|
197
48
|
|
|
198
|
-
//
|
|
199
|
-
const
|
|
200
|
-
|
|
49
|
+
// Create entities and run
|
|
50
|
+
const player = world.spawn({
|
|
51
|
+
position: { x: 0, y: 0 },
|
|
52
|
+
velocity: { x: 10, y: 5 },
|
|
53
|
+
health: { value: 100 }
|
|
54
|
+
});
|
|
201
55
|
|
|
202
|
-
//
|
|
203
|
-
const world2 = ECSpresso.create()
|
|
204
|
-
.withBundle(bundle3)
|
|
205
|
-
// @ts-expect-error - TypeScript will flag this because the position types conflict
|
|
206
|
-
.withBundle(bundle4)
|
|
207
|
-
.build();
|
|
56
|
+
world.update(1/60); // Run one frame
|
|
208
57
|
```
|
|
209
58
|
|
|
210
|
-
##
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
const world = ECSpresso.create<Components, Events, Resources>()
|
|
214
|
-
.withBundle(/* your bundle */)
|
|
215
|
-
.build();
|
|
59
|
+
## Core Concepts
|
|
216
60
|
|
|
217
|
-
|
|
218
|
-
const entity = world.entityManager.createEntity();
|
|
61
|
+
### Entities and Components
|
|
219
62
|
|
|
220
|
-
|
|
221
|
-
world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
|
|
222
|
-
world.entityManager.addComponent(entity.id, 'velocity', { x: 0, y: 0 });
|
|
63
|
+
Entities are containers for components. Use `spawn()` to create entities with initial components:
|
|
223
64
|
|
|
224
|
-
|
|
225
|
-
|
|
65
|
+
```typescript
|
|
66
|
+
// Create entity with components
|
|
67
|
+
const entity = world.spawn({
|
|
226
68
|
position: { x: 10, y: 20 },
|
|
227
|
-
|
|
69
|
+
health: { value: 100 }
|
|
228
70
|
});
|
|
229
71
|
|
|
72
|
+
// Add components later
|
|
73
|
+
world.entityManager.addComponent(entity.id, 'velocity', { x: 5, y: 0 });
|
|
74
|
+
|
|
230
75
|
// Get component data
|
|
231
76
|
const position = world.entityManager.getComponent(entity.id, 'position');
|
|
232
77
|
|
|
233
|
-
//
|
|
234
|
-
const hasPosition = world.entityManager.hasComponent(entity.id, 'position');
|
|
235
|
-
|
|
236
|
-
// Remove a component
|
|
78
|
+
// Remove components or entities
|
|
237
79
|
world.entityManager.removeComponent(entity.id, 'velocity');
|
|
238
|
-
|
|
239
|
-
// Remove an entity (and all its components)
|
|
240
80
|
world.entityManager.removeEntity(entity.id);
|
|
241
81
|
```
|
|
242
82
|
|
|
243
|
-
|
|
83
|
+
### Systems and Queries
|
|
244
84
|
|
|
245
|
-
Systems
|
|
85
|
+
Systems process entities that match specific component patterns:
|
|
246
86
|
|
|
247
87
|
```typescript
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Set system execution priority (higher numbers execute first)
|
|
253
|
-
.setPriority(50)
|
|
254
|
-
// Query entities that have both position and velocity components
|
|
255
|
-
.addQuery('movingEntities', {
|
|
256
|
-
with: ['position', 'velocity']
|
|
88
|
+
world.addSystem('combat')
|
|
89
|
+
.addQuery('fighters', {
|
|
90
|
+
with: ['position', 'health'],
|
|
91
|
+
without: ['dead']
|
|
257
92
|
})
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
with: ['position'],
|
|
261
|
-
without: ['player']
|
|
262
|
-
})
|
|
263
|
-
// Query entities with different component combinations
|
|
264
|
-
.addQuery('flyingNonPlayerEntities', {
|
|
265
|
-
with: ['flying', 'position'],
|
|
266
|
-
without: ['player', 'grounded']
|
|
93
|
+
.addQuery('projectiles', {
|
|
94
|
+
with: ['position', 'damage']
|
|
267
95
|
})
|
|
268
96
|
.setProcess((queries, deltaTime) => {
|
|
269
|
-
// Process
|
|
270
|
-
for (const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// Process non-player objects
|
|
276
|
-
for (const entity of queries.nonPlayerObjects) {
|
|
277
|
-
// Do something with non-player objects
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Process flying non-player entities
|
|
281
|
-
for (const entity of queries.flyingNonPlayerEntities) {
|
|
282
|
-
// Apply flying behavior
|
|
97
|
+
// Process fighters and projectiles
|
|
98
|
+
for (const fighter of queries.fighters) {
|
|
99
|
+
for (const projectile of queries.projectiles) {
|
|
100
|
+
// Combat logic here
|
|
101
|
+
}
|
|
283
102
|
}
|
|
284
103
|
})
|
|
285
|
-
.build();
|
|
104
|
+
.build();
|
|
286
105
|
```
|
|
287
106
|
|
|
288
|
-
|
|
107
|
+
### Resources
|
|
289
108
|
|
|
290
|
-
|
|
109
|
+
Resources provide global state accessible to all systems:
|
|
291
110
|
|
|
292
111
|
```typescript
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
console.log('System initializing');
|
|
298
|
-
// Load assets, configure resources, etc.
|
|
299
|
-
})
|
|
112
|
+
interface Resources {
|
|
113
|
+
score: { value: number };
|
|
114
|
+
settings: { difficulty: 'easy' | 'hard' };
|
|
115
|
+
}
|
|
300
116
|
|
|
301
|
-
|
|
302
|
-
.setOnDetach((ecs) => {
|
|
303
|
-
console.log('System detached');
|
|
304
|
-
// Clean up resources, cancel subscriptions, etc.
|
|
305
|
-
})
|
|
117
|
+
const world = new ECSpresso<Components, {}, Resources>();
|
|
306
118
|
|
|
119
|
+
// Add resources
|
|
120
|
+
world.addResource('score', { value: 0 });
|
|
121
|
+
world.addResource('settings', { difficulty: 'easy' });
|
|
122
|
+
|
|
123
|
+
// Use in systems
|
|
124
|
+
world.addSystem('scoring')
|
|
125
|
+
.setProcess((queries, deltaTime, ecs) => {
|
|
126
|
+
const score = ecs.getResource('score');
|
|
127
|
+
score.value += 10;
|
|
128
|
+
})
|
|
307
129
|
.build();
|
|
308
130
|
```
|
|
309
131
|
|
|
310
|
-
|
|
132
|
+
## Working with Systems
|
|
133
|
+
|
|
134
|
+
### Method Chaining
|
|
135
|
+
|
|
136
|
+
ECSpresso supports fluent method chaining for creating multiple systems:
|
|
311
137
|
|
|
312
138
|
```typescript
|
|
313
|
-
|
|
139
|
+
world.addSystem('physics')
|
|
140
|
+
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
141
|
+
.setProcess((queries, deltaTime) => {
|
|
142
|
+
// Physics logic
|
|
143
|
+
})
|
|
144
|
+
.and() // Complete this system and continue chaining
|
|
145
|
+
.addSystem('rendering')
|
|
146
|
+
.addQuery('visible', { with: ['position', 'sprite'] })
|
|
147
|
+
.setProcess((queries) => {
|
|
148
|
+
// Rendering logic
|
|
149
|
+
})
|
|
150
|
+
.build(); // Only the final system needs .build()
|
|
314
151
|
```
|
|
315
152
|
|
|
316
|
-
|
|
153
|
+
### Query Type Utilities
|
|
317
154
|
|
|
318
|
-
|
|
155
|
+
Extract entity types from queries to create reusable helper functions:
|
|
319
156
|
|
|
320
157
|
```typescript
|
|
321
|
-
|
|
322
|
-
// Default priority is 0 if not specified
|
|
158
|
+
import { createQueryDefinition, QueryResultEntity } from 'ecspresso';
|
|
323
159
|
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
})
|
|
330
|
-
.build();
|
|
160
|
+
// Create reusable query definitions
|
|
161
|
+
const movingQuery = createQueryDefinition({
|
|
162
|
+
with: ['position', 'velocity'] as const,
|
|
163
|
+
without: ['frozen'] as const
|
|
164
|
+
} as const);
|
|
331
165
|
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
.setPriority(50)
|
|
335
|
-
.setProcess(() => {
|
|
336
|
-
// Physics update logic
|
|
337
|
-
})
|
|
338
|
-
.build();
|
|
166
|
+
// Extract entity type for helper functions
|
|
167
|
+
type MovingEntity = QueryResultEntity<Components, typeof movingQuery>;
|
|
339
168
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
.
|
|
343
|
-
.
|
|
344
|
-
|
|
169
|
+
// Create type-safe helper functions
|
|
170
|
+
function updatePosition(entity: MovingEntity, deltaTime: number) {
|
|
171
|
+
entity.components.position.x += entity.components.velocity.x * deltaTime;
|
|
172
|
+
entity.components.position.y += entity.components.velocity.y * deltaTime;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Use in systems
|
|
176
|
+
world.addSystem('movement')
|
|
177
|
+
.addQuery('entities', movingQuery)
|
|
178
|
+
.setProcess((queries, deltaTime) => {
|
|
179
|
+
for (const entity of queries.entities) {
|
|
180
|
+
updatePosition(entity, deltaTime); // Perfect type safety!
|
|
181
|
+
}
|
|
345
182
|
})
|
|
346
183
|
.build();
|
|
347
184
|
```
|
|
348
185
|
|
|
349
|
-
|
|
186
|
+
### System Priority
|
|
350
187
|
|
|
351
|
-
|
|
188
|
+
Control execution order with priorities (higher numbers execute first):
|
|
352
189
|
|
|
353
190
|
```typescript
|
|
354
|
-
|
|
355
|
-
|
|
191
|
+
world.addSystem('physics')
|
|
192
|
+
.setPriority(100) // Runs first
|
|
193
|
+
.setProcess(() => { /* physics */ })
|
|
194
|
+
.and()
|
|
195
|
+
.addSystem('rendering')
|
|
196
|
+
.setPriority(50) // Runs second
|
|
197
|
+
.setProcess(() => { /* rendering */ })
|
|
198
|
+
.build();
|
|
356
199
|
```
|
|
357
200
|
|
|
358
|
-
|
|
201
|
+
## Advanced Features
|
|
202
|
+
|
|
203
|
+
### Bundles
|
|
204
|
+
|
|
205
|
+
Organize related systems and resources into reusable bundles:
|
|
359
206
|
|
|
360
207
|
```typescript
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
.
|
|
365
|
-
|
|
208
|
+
import { Bundle } from 'ecspresso';
|
|
209
|
+
|
|
210
|
+
const inputBundle = new Bundle<Components, Events, Resources>('input')
|
|
211
|
+
.addSystem('playerInput')
|
|
212
|
+
.addQuery('players', { with: ['position', 'velocity', 'player'] })
|
|
213
|
+
.setProcess((queries, deltaTime, ecs) => {
|
|
214
|
+
// Handle input
|
|
366
215
|
});
|
|
367
216
|
|
|
368
|
-
const
|
|
369
|
-
.addSystem('
|
|
370
|
-
.
|
|
371
|
-
.setProcess(() => {
|
|
372
|
-
//
|
|
217
|
+
const renderBundle = new Bundle<Components, Events, Resources>('render')
|
|
218
|
+
.addSystem('renderer')
|
|
219
|
+
.addQuery('sprites', { with: ['position', 'sprite'] })
|
|
220
|
+
.setProcess((queries) => {
|
|
221
|
+
// Render sprites
|
|
373
222
|
});
|
|
374
223
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
.withBundle(
|
|
224
|
+
// Create world with bundles
|
|
225
|
+
const game = ECSpresso.create<Components, Events, Resources>()
|
|
226
|
+
.withBundle(inputBundle)
|
|
227
|
+
.withBundle(renderBundle)
|
|
378
228
|
.build();
|
|
379
229
|
```
|
|
380
230
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
## Event System
|
|
231
|
+
### Events
|
|
384
232
|
|
|
385
|
-
|
|
233
|
+
Use events for decoupled system communication:
|
|
386
234
|
|
|
387
235
|
```typescript
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
236
|
+
interface Events {
|
|
237
|
+
playerDied: { playerId: number };
|
|
238
|
+
levelComplete: { score: number };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const world = new ECSpresso<Components, Events, Resources>();
|
|
242
|
+
|
|
243
|
+
// Handle events in systems
|
|
244
|
+
world.addSystem('gameLogic')
|
|
391
245
|
.setEventHandlers({
|
|
392
|
-
|
|
246
|
+
playerDied: {
|
|
393
247
|
handler: (data, ecs) => {
|
|
394
|
-
|
|
395
|
-
//
|
|
248
|
+
console.log(`Player ${data.playerId} died`);
|
|
249
|
+
// Respawn logic
|
|
396
250
|
}
|
|
397
251
|
}
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
const world = ECSpresso.create<Components, Events, Resources>()
|
|
401
|
-
.withBundle(collisionBundle)
|
|
252
|
+
})
|
|
402
253
|
.build();
|
|
403
254
|
|
|
404
|
-
// Publish
|
|
405
|
-
world.eventBus.publish('
|
|
406
|
-
entity1: 1,
|
|
407
|
-
entity2: 2
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// Subscribe to events manually (outside of systems)
|
|
411
|
-
const unsubscribe = world.eventBus.subscribe('collision', (data) => {
|
|
412
|
-
console.log(`Collision between entities ${data.entity1} and ${data.entity2}`);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// Stop listening
|
|
416
|
-
unsubscribe();
|
|
255
|
+
// Publish events from anywhere
|
|
256
|
+
world.eventBus.publish('playerDied', { playerId: 1 });
|
|
417
257
|
```
|
|
418
258
|
|
|
419
|
-
|
|
259
|
+
### Resource Factories
|
|
420
260
|
|
|
421
|
-
|
|
261
|
+
Create resources lazily with factory functions:
|
|
422
262
|
|
|
423
263
|
```typescript
|
|
424
|
-
//
|
|
425
|
-
world.addResource('
|
|
264
|
+
// Sync factory
|
|
265
|
+
world.addResource('config', () => ({
|
|
266
|
+
difficulty: 'normal',
|
|
267
|
+
soundEnabled: true
|
|
268
|
+
}));
|
|
426
269
|
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
270
|
+
// Async factory
|
|
271
|
+
world.addResource('assets', async () => {
|
|
272
|
+
const textures = await loadTextures();
|
|
273
|
+
return { textures };
|
|
274
|
+
});
|
|
430
275
|
|
|
431
|
-
//
|
|
432
|
-
|
|
276
|
+
// Initialize all resources
|
|
277
|
+
await world.initializeResources();
|
|
433
278
|
```
|
|
434
279
|
|
|
435
|
-
###
|
|
280
|
+
### System Lifecycle
|
|
436
281
|
|
|
437
|
-
|
|
282
|
+
Systems can have initialization and cleanup hooks:
|
|
438
283
|
|
|
439
284
|
```typescript
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
285
|
+
world.addSystem('gameSystem')
|
|
286
|
+
.setOnInitialize(async (ecs) => {
|
|
287
|
+
// One-time setup
|
|
288
|
+
console.log('System starting...');
|
|
289
|
+
})
|
|
290
|
+
.setOnDetach((ecs) => {
|
|
291
|
+
// Cleanup when system is removed
|
|
292
|
+
console.log('System shutting down...');
|
|
293
|
+
})
|
|
294
|
+
.build();
|
|
450
295
|
|
|
451
|
-
//
|
|
452
|
-
world.
|
|
453
|
-
|
|
454
|
-
const gameConfig = ecs.getResource('gameConfig');
|
|
455
|
-
return {
|
|
456
|
-
speed: gameConfig.difficulty === 'hard' ? 200 : 100,
|
|
457
|
-
startingHealth: gameConfig.difficulty === 'hard' ? 50 : 100
|
|
458
|
-
};
|
|
459
|
-
});
|
|
296
|
+
// Initialize all systems
|
|
297
|
+
await world.initialize();
|
|
298
|
+
```
|
|
460
299
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// You can access other resources during async initialization
|
|
465
|
-
const settings = ecs.getResource('settings');
|
|
466
|
-
const assets = await loadAssets(settings.assetQuality);
|
|
467
|
-
return assets;
|
|
468
|
-
});
|
|
300
|
+
## Type Safety
|
|
301
|
+
|
|
302
|
+
ECSpresso provides comprehensive TypeScript support:
|
|
469
303
|
|
|
470
|
-
|
|
471
|
-
|
|
304
|
+
### Component Type Safety
|
|
305
|
+
```typescript
|
|
306
|
+
// ✅ Valid
|
|
307
|
+
world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 });
|
|
472
308
|
|
|
473
|
-
//
|
|
474
|
-
|
|
309
|
+
// ❌ TypeScript error - invalid component
|
|
310
|
+
world.entityManager.addComponent(entity.id, 'invalid', { data: 'bad' });
|
|
475
311
|
|
|
476
|
-
//
|
|
477
|
-
|
|
312
|
+
// ❌ TypeScript error - wrong component shape
|
|
313
|
+
world.entityManager.addComponent(entity.id, 'position', { x: 0 }); // missing y
|
|
478
314
|
```
|
|
479
315
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
316
|
+
### Query Type Safety
|
|
317
|
+
```typescript
|
|
318
|
+
world.addSystem('example')
|
|
319
|
+
.addQuery('moving', { with: ['position', 'velocity'] })
|
|
320
|
+
.setProcess((queries) => {
|
|
321
|
+
for (const entity of queries.moving) {
|
|
322
|
+
// ✅ TypeScript knows these exist
|
|
323
|
+
entity.components.position.x;
|
|
324
|
+
entity.components.velocity.y;
|
|
325
|
+
|
|
326
|
+
// ❌ TypeScript error - not guaranteed to exist
|
|
327
|
+
entity.components.health.value;
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
.build();
|
|
331
|
+
```
|
|
485
332
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
333
|
+
### Bundle Type Compatibility
|
|
334
|
+
```typescript
|
|
335
|
+
// ✅ Compatible bundles merge cleanly
|
|
336
|
+
const bundle1 = new Bundle<{position: {x: number, y: number}}>('bundle1');
|
|
337
|
+
const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('bundle2');
|
|
338
|
+
|
|
339
|
+
const world = ECSpresso.create()
|
|
340
|
+
.withBundle(bundle1)
|
|
341
|
+
.withBundle(bundle2) // ✅ Types merge successfully
|
|
342
|
+
.build();
|
|
343
|
+
|
|
344
|
+
// ❌ Conflicting types cause TypeScript errors
|
|
345
|
+
const conflictingBundle = new Bundle<{position: string}>('conflict');
|
|
346
|
+
// world.withBundle(conflictingBundle); // TypeScript error!
|
|
347
|
+
```
|
|
489
348
|
|
|
490
349
|
## Component Callbacks
|
|
491
350
|
|
|
492
|
-
|
|
351
|
+
React to component changes with callbacks:
|
|
493
352
|
|
|
494
353
|
```typescript
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// Listen for when a "health" component is added
|
|
499
|
-
entityManager.onComponentAdded('health', (value, entity) => {
|
|
354
|
+
// Listen for component additions/removals
|
|
355
|
+
world.entityManager.onComponentAdded('health', (value, entity) => {
|
|
500
356
|
console.log(`Health added to entity ${entity.id}:`, value);
|
|
501
357
|
});
|
|
502
358
|
|
|
503
|
-
|
|
504
|
-
entityManager.onComponentRemoved('health', (oldValue, entity) => {
|
|
359
|
+
world.entityManager.onComponentRemoved('health', (oldValue, entity) => {
|
|
505
360
|
console.log(`Health removed from entity ${entity.id}:`, oldValue);
|
|
506
361
|
});
|
|
507
|
-
|
|
508
|
-
// Create an entity and add/remove components to trigger callbacks
|
|
509
|
-
const e = entityManager.createEntity();
|
|
510
|
-
entityManager.addComponent(e.id, 'health', { value: 100 });
|
|
511
|
-
// => logs: Health added to entity 1: { value: 100 }
|
|
512
|
-
entityManager.removeComponent(e.id, 'health');
|
|
513
|
-
// => logs: Health removed from entity 1: { value: 100 }
|
|
514
362
|
```
|
|
515
363
|
|
|
516
|
-
|
|
364
|
+
## Performance Tips
|
|
517
365
|
|
|
366
|
+
- Use query type utilities to extract business logic into testable helper functions
|
|
367
|
+
- Bundle related systems for better organization
|
|
368
|
+
- Use system priorities to ensure correct execution order
|
|
369
|
+
- Leverage resource factories for expensive initialization
|
|
370
|
+
- Consider component callbacks for immediate reactions to state changes
|