archetype-ecs 1.3.1 → 1.3.2

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.
Files changed (47) hide show
  1. package/README.md +104 -34
  2. package/bench/allocations-1m.js +1 -1
  3. package/bench/multi-ecs-bench.js +1 -1
  4. package/bench/typed-vs-bitecs-1m.js +1 -1
  5. package/bench/typed-vs-untyped.js +81 -81
  6. package/bench/vs-bitecs.js +31 -56
  7. package/dist/ComponentRegistry.d.ts +18 -0
  8. package/dist/ComponentRegistry.js +29 -0
  9. package/dist/EntityManager.d.ts +52 -0
  10. package/dist/EntityManager.js +891 -0
  11. package/dist/Profiler.d.ts +12 -0
  12. package/dist/Profiler.js +38 -0
  13. package/dist/System.d.ts +41 -0
  14. package/dist/System.js +159 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +29 -0
  17. package/dist/src/ComponentRegistry.d.ts +18 -0
  18. package/dist/src/ComponentRegistry.js +29 -0
  19. package/dist/src/EntityManager.d.ts +49 -0
  20. package/dist/src/EntityManager.js +853 -0
  21. package/dist/src/Profiler.d.ts +12 -0
  22. package/dist/src/Profiler.js +38 -0
  23. package/dist/src/System.d.ts +37 -0
  24. package/dist/src/System.js +139 -0
  25. package/dist/src/index.d.ts +14 -0
  26. package/dist/src/index.js +29 -0
  27. package/dist/tests/EntityManager.test.d.ts +1 -0
  28. package/dist/tests/EntityManager.test.js +651 -0
  29. package/dist/tests/System.test.d.ts +1 -0
  30. package/dist/tests/System.test.js +630 -0
  31. package/dist/tests/types.d.ts +1 -0
  32. package/dist/tests/types.js +129 -0
  33. package/package.json +9 -7
  34. package/src/ComponentRegistry.ts +49 -0
  35. package/src/EntityManager.ts +1018 -0
  36. package/src/{Profiler.js → Profiler.ts} +18 -5
  37. package/src/System.ts +226 -0
  38. package/src/index.ts +44 -0
  39. package/tests/{EntityManager.test.js → EntityManager.test.ts} +338 -70
  40. package/tests/System.test.ts +730 -0
  41. package/tests/types.ts +67 -66
  42. package/tsconfig.json +8 -5
  43. package/tsconfig.test.json +13 -0
  44. package/src/ComponentRegistry.js +0 -21
  45. package/src/EntityManager.js +0 -578
  46. package/src/index.d.ts +0 -118
  47. package/src/index.js +0 -37
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  ---
15
15
 
16
- An Entity Component System for games and simulations in JavaScript. Entities with the same components are grouped into archetypes, and their fields are stored in TypedArrays — so iterating a million entities is a tight loop over contiguous memory, not a scatter of object lookups.
16
+ An Entity Component System for games and simulations in TypeScript. Entities with the same components are grouped into archetypes, and their fields are stored in TypedArrays — so iterating a million entities is a tight loop over contiguous memory, not a scatter of object lookups.
17
17
 
18
18
  ```
19
19
  npm i archetype-ecs
@@ -55,6 +55,7 @@ em.forEach([Position, Velocity], (arch) => {
55
55
  <tr><td><strong>Low memory</strong></td><td>86 MB for 1M entities. Sparse-array ECS libraries use up to 2.4x more.</td></tr>
56
56
  <tr><td><strong>No allocations</strong></td><td><code>get</code>, <code>set</code>, and <code>forEach</code> don't allocate.</td></tr>
57
57
  <tr><td><strong>Typed</strong></td><td>TypeScript generics throughout. Field names autocomplete, wrong fields don't compile.</td></tr>
58
+ <tr><td><strong>Systems</strong></td><td>Class-based systems with <code>@OnAdded</code> / <code>@OnRemoved</code> decorators. Functional API also available.</td></tr>
58
59
  </table>
59
60
 
60
61
  ---
@@ -83,7 +84,7 @@ const Enemy = component('Enemy')
83
84
 
84
85
  ### Entities
85
86
 
86
- ```js
87
+ ```ts
87
88
  const em = createEntityManager()
88
89
 
89
90
  // One at a time
@@ -109,7 +110,7 @@ em.destroyEntity(player)
109
110
 
110
111
  ### Read & write
111
112
 
112
- ```js
113
+ ```ts
113
114
  // Access a single field (doesn't allocate)
114
115
  em.get(player, Position.x) // 0
115
116
  em.get(player, Name.name) // 'Hero'
@@ -120,7 +121,7 @@ em.getComponent(player, Position) // { x: 0, y: 0 }
120
121
  em.getComponent(player, Name) // { name: 'Hero', title: 'Sir' }
121
122
  ```
122
123
 
123
- ### Systems — `forEach` vs `query`
124
+ ### Queries — `forEach` vs `query`
124
125
 
125
126
  Two ways to work with entities in bulk. Pick the right one for the job:
126
127
 
@@ -128,8 +129,8 @@ Two ways to work with entities in bulk. Pick the right one for the job:
128
129
 
129
130
  Iterates over matching archetypes. You get the backing TypedArrays directly.
130
131
 
131
- ```js
132
- function movementSystem(dt) {
132
+ ```ts
133
+ function movementSystem(dt: number) {
133
134
  em.forEach([Position, Velocity], (arch) => {
134
135
  const px = arch.field(Position.x) // Float32Array
135
136
  const py = arch.field(Position.y)
@@ -147,7 +148,7 @@ function movementSystem(dt) {
147
148
 
148
149
  Returns entity IDs for when you need to target specific entities.
149
150
 
150
- ```js
151
+ ```ts
151
152
  // Find the closest enemy to the player
152
153
  const enemies = em.query([Position, Enemy])
153
154
  let closest = -1, minDist = Infinity
@@ -177,9 +178,67 @@ const total = em.count([Position])
177
178
  | **Allocates** | Nothing | `number[]` of entity IDs |
178
179
  | **Access** | TypedArrays by field | `get` / `set` by entity ID |
179
180
 
181
+ ### Systems
182
+
183
+ Class-based systems with decorators for component lifecycle hooks:
184
+
185
+ ```ts
186
+ import { System, OnAdded, OnRemoved, createSystems, type EntityId } from 'archetype-ecs'
187
+
188
+ class MovementSystem extends System {
189
+ tick() {
190
+ this.forEach([Position, Velocity], (arch) => {
191
+ const px = arch.field(Position.x)
192
+ const py = arch.field(Position.y)
193
+ const vx = arch.field(Velocity.vx)
194
+ const vy = arch.field(Velocity.vy)
195
+ for (let i = 0; i < arch.count; i++) {
196
+ px[i] += vx[i]
197
+ py[i] += vy[i]
198
+ }
199
+ })
200
+ }
201
+ }
202
+
203
+ class DeathSystem extends System {
204
+ @OnAdded(Health)
205
+ onSpawn(id: EntityId) {
206
+ console.log(`Entity ${id} spawned with ${this.em.get(id, Health.hp)} HP`)
207
+ }
208
+
209
+ @OnRemoved(Health)
210
+ onDeath(id: EntityId) {
211
+ this.em.addComponent(id, Dead)
212
+ }
213
+ }
214
+
215
+ const em = createEntityManager()
216
+ const pipeline = createSystems(em, [MovementSystem, DeathSystem])
217
+
218
+ // Game loop
219
+ em.flushHooks()
220
+ pipeline()
221
+ ```
222
+
223
+ `@OnAdded(Health, Position)` fires when an entity has **all** specified components. `@OnRemoved(Health)` fires when any specified component is removed. Hooks are buffered and deduplicated — they fire during `pipeline()` (or `sys.run()`), after `flushHooks()` collects the pending changes.
224
+
225
+ A functional API is also available:
226
+
227
+ ```ts
228
+ import { createSystem } from 'archetype-ecs'
229
+
230
+ const deathSystem = createSystem(em, (sys) => {
231
+ sys.onAdded(Health, (id) => console.log(`${id} spawned`))
232
+ sys.onRemoved(Health, (id) => console.log(`${id} died`))
233
+ })
234
+
235
+ em.flushHooks()
236
+ deathSystem()
237
+ ```
238
+
180
239
  ### Serialize
181
240
 
182
- ```js
241
+ ```ts
183
242
  const symbolToName = new Map([
184
243
  [Position._sym, 'Position'],
185
244
  [Velocity._sym, 'Velocity'],
@@ -202,32 +261,16 @@ Supports stripping components, skipping entities, and custom serializers.
202
261
  Component types are inferred from their definition. Field names autocomplete, wrong fields are compile errors.
203
262
 
204
263
  ```ts
205
- // Schema is inferred — Position becomes ComponentDef<{ x: number; y: number }>
264
+ // Schema is inferred — Position becomes ComponentDef<'x' | 'y'>
206
265
  const Position = component('Position', 'f32', ['x', 'y'])
207
266
 
208
- Position.x // autocompletes to .x and .y
209
- Position.z // Property 'z' does not exist
210
-
211
- em.get(id, Position.x) // number | undefined
212
- em.set(id, Position.z, 5) // Property 'z' does not exist
213
-
214
- em.addComponent(id, Position, { x: 1, y: 2 }) // ok
215
- em.addComponent(id, Position, { x: 1 }) // Property 'y' is missing
216
-
217
- em.getComponent(id, Position) // { x: number; y: number } | undefined
218
- ```
219
-
220
- String fields are fully typed too:
221
-
222
- ```ts
223
- const Name = component('Name', 'string', ['name', 'title'])
267
+ Position.x // FieldRef — autocompletes to .x and .y
268
+ Position.z // compile error: Property 'z' does not exist
224
269
 
225
- em.get(id, Name.name) // string | undefined
226
- em.set(id, Name.name, 'Hero') // ok
227
- em.set(id, Name.name, 42) // number not assignable to string
270
+ em.get(id, Position.x) // zero-alloc field access
271
+ em.set(id, Position.x, 5) // zero-alloc field write
228
272
 
229
- em.addComponent(id, Name, { name: 'Hero', title: 'Sir' }) // ok
230
- em.addComponent(id, Name, { foo: 'bar' }) // type error
273
+ arch.field(Position.x) // Float32Array direct TypedArray access
231
274
  ```
232
275
 
233
276
  ---
@@ -242,7 +285,7 @@ Tag component — no data, used as a marker for queries.
242
285
 
243
286
  Schema component with uniform field type.
244
287
 
245
- ```js
288
+ ```ts
246
289
  const Position = component('Position', 'f32', ['x', 'y'])
247
290
  const Name = component('Name', 'string', ['name', 'title'])
248
291
  ```
@@ -251,7 +294,7 @@ const Name = component('Name', 'string', ['name', 'title'])
251
294
 
252
295
  Schema component with mixed field types.
253
296
 
254
- ```js
297
+ ```ts
255
298
  const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })
256
299
  ```
257
300
 
@@ -273,9 +316,33 @@ Returns an entity manager with the following methods:
273
316
  | `query(include, exclude?)` | Get matching entity IDs |
274
317
  | `count(include, exclude?)` | Count matching entities |
275
318
  | `forEach(include, callback, exclude?)` | Iterate archetypes with TypedArray access |
319
+ | `onAdd(Comp, callback)` | Register callback for component additions *(deferred)* |
320
+ | `onRemove(Comp, callback)` | Register callback for component removals *(deferred)* |
321
+ | `flushHooks()` | Collect pending add/remove events for registered hooks |
276
322
  | `serialize(symbolToName, strip?, skip?, opts?)` | Serialize world to JSON-friendly object |
277
323
  | `deserialize(data, nameToSymbol, opts?)` | Restore world from serialized data |
278
324
 
325
+ ### `System`
326
+
327
+ Base class for decorator-based systems.
328
+
329
+ | | Description |
330
+ |---|---|
331
+ | `@OnAdded(...Comps)` | Decorator — method fires when entity gains **all** specified components |
332
+ | `@OnRemoved(...Comps)` | Decorator — method fires when **any** specified component is removed |
333
+ | `tick()` | Override — called every `run()` after hook callbacks |
334
+ | `forEach(types, callback, exclude?)` | Shorthand for `this.em.forEach(...)` |
335
+ | `run()` | Fire buffered hook callbacks, then `tick()` |
336
+ | `dispose()` | Unsubscribe all hooks |
337
+
338
+ ### `createSystem(em, constructor)`
339
+
340
+ Functional alternative to class-based systems. The constructor receives a context with `onAdded`, `onRemoved`, and `forEach`, and optionally returns a tick function.
341
+
342
+ ### `createSystems(em, entries)`
343
+
344
+ Creates a pipeline from an array of class-based (`System` subclasses) and/or functional system constructors. Returns a callable that runs all systems in order, with a `dispose()` method.
345
+
279
346
  ---
280
347
 
281
348
  ## Benchmarks
@@ -290,7 +357,7 @@ Returns an entity manager with the following methods:
290
357
 
291
358
  Each library runs the same test — iterate 1M entities over 500 frames:
292
359
 
293
- ```js
360
+ ```ts
294
361
  // archetype-ecs
295
362
  em.forEach([Position, Velocity], (arch) => {
296
363
  const px = arch.field(Position.x) // Float32Array, dense
@@ -326,6 +393,7 @@ Compared against other JS ECS libraries:
326
393
  | Mixed string + numeric components | ✓ | — | — | — | — |
327
394
  | `forEach` with dense TypedArray field access | ✓ | — | — | — | — |
328
395
  | Field descriptors for both per-entity and bulk access | ✓ | — | — | — | — |
396
+ | TC39 decorator system (`@OnAdded` / `@OnRemoved`) | ✓ | — | — | — | — |
329
397
  | Built-in profiler | ✓ | — | — | — | — |
330
398
 
331
399
  ### Full comparison
@@ -338,12 +406,14 @@ Compared against other JS ECS libraries:
338
406
  | TypeScript type inference | ✓ | — | ✓ | ✓ | ✓✓ |
339
407
  | Batch entity creation | ✓ | — | — | ✓ | ✓ |
340
408
  | Zero-alloc per-entity access | ✓ | ✓ | ✓ | ✓ | — |
409
+ | System framework (class + functional) | ✓ | — | — | — | — |
410
+ | Component lifecycle hooks | ✓ | — | — | — | ✓ |
341
411
  | Relations / hierarchies | — | ✓ | — | — | — |
342
412
  | React integration | — | — | — | — | ✓ |
343
413
 
344
414
  ✓✓ = notably stronger implementation in that library.
345
415
 
346
- archetype-ecs is the only one that combines fast iteration, string storage, serialization, and type safety.
416
+ archetype-ecs is the only one that combines fast iteration, string storage, serialization, decorator-based systems, and type safety.
347
417
 
348
418
  ---
349
419
 
@@ -4,7 +4,7 @@
4
4
  import {
5
5
  createWorld, addEntity, addComponent, removeEntity, query
6
6
  } from 'bitecs';
7
- import { createEntityManager, component } from '../src/index.js';
7
+ import { createEntityManager, component } from '../dist/index.js';
8
8
 
9
9
  const COUNT = 1_000_000;
10
10
  const RUNS = 5;
@@ -2,7 +2,7 @@
2
2
  // Tests: iteration (Position += Velocity), entity creation, memory usage
3
3
  // Run with: node --expose-gc bench/multi-ecs-bench.js
4
4
 
5
- import { createEntityManager, component } from '../src/index.js';
5
+ import { createEntityManager, component } from '../dist/index.js';
6
6
 
7
7
  const COUNT = 1_000_000;
8
8
  const FRAMES = 500;
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  createWorld, addEntity, addComponent, query
5
5
  } from 'bitecs';
6
- import { createEntityManager, component } from '../src/index.js';
6
+ import { createEntityManager, component } from '../dist/index.js';
7
7
 
8
8
  const COUNT = 1_000_000;
9
9
  const FRAMES = 500;
@@ -1,38 +1,34 @@
1
- // Benchmark: typed (SoA TypedArrays) vs untyped (object arrays) components
1
+ // Benchmark: forEach+field (bulk TypedArray) vs query+get/set (per-entity) vs query+getComponent (allocating)
2
2
 
3
- import { createEntityManager, component } from '../src/index.js';
3
+ import { createEntityManager, component } from '../dist/index.js';
4
4
 
5
5
  const COUNT = 1_000_000;
6
6
  const FRAMES = 200;
7
7
 
8
- // --- Typed: creation ---
9
- function benchTypedCreate() {
10
- const Pos = component('TP', 'f32', ['x', 'y']);
11
- const Vel = component('TV', 'f32', ['vx', 'vy']);
8
+ // --- forEach + field() — bulk dense TypedArray access ---
9
+ function benchForEachField() {
10
+ const Pos = component('FP', 'f32', ['x', 'y']);
11
+ const Vel = component('FV', 'f32', ['vx', 'vy']);
12
12
  const em = createEntityManager();
13
13
 
14
- const t0 = performance.now();
15
14
  for (let i = 0; i < COUNT; i++) {
16
15
  em.createEntityWith(Pos, { x: i, y: i }, Vel, { vx: 1, vy: 1 });
17
16
  }
18
- return { em, Pos, Vel, time: performance.now() - t0 };
19
- }
20
-
21
- // --- Untyped: creation ---
22
- function benchUntypedCreate() {
23
- const Pos = component('UP');
24
- const Vel = component('UV');
25
- const em = createEntityManager();
26
17
 
27
- const t0 = performance.now();
28
- for (let i = 0; i < COUNT; i++) {
29
- em.createEntityWith(Pos, { x: i, y: i }, Vel, { vx: 1, vy: 1 });
18
+ // Warmup
19
+ for (let f = 0; f < 5; f++) {
20
+ em.forEach([Pos, Vel], (arch) => {
21
+ const px = arch.field(Pos.x);
22
+ const py = arch.field(Pos.y);
23
+ const vx = arch.field(Vel.vx);
24
+ const vy = arch.field(Vel.vy);
25
+ for (let i = 0; i < arch.count; i++) {
26
+ px[i] += vx[i];
27
+ py[i] += vy[i];
28
+ }
29
+ });
30
30
  }
31
- return { em, Pos, Vel, time: performance.now() - t0 };
32
- }
33
31
 
34
- // --- Typed: iteration with forEach + field() ---
35
- function benchTypedIterate(em, Pos, Vel) {
36
32
  const t0 = performance.now();
37
33
  for (let f = 0; f < FRAMES; f++) {
38
34
  em.forEach([Pos, Vel], (arch) => {
@@ -49,19 +45,61 @@ function benchTypedIterate(em, Pos, Vel) {
49
45
  return (performance.now() - t0) / FRAMES;
50
46
  }
51
47
 
52
- // --- Untyped: iteration with forEach (object access) ---
53
- function benchUntypedIterate(em, Pos, Vel) {
48
+ // --- query + get/set per-entity field access (zero-alloc per field) ---
49
+ function benchQueryGetSet() {
50
+ const Pos = component('GP', 'f32', ['x', 'y']);
51
+ const Vel = component('GV', 'f32', ['vx', 'vy']);
52
+ const em = createEntityManager();
53
+
54
+ for (let i = 0; i < COUNT; i++) {
55
+ em.createEntityWith(Pos, { x: i, y: i }, Vel, { vx: 1, vy: 1 });
56
+ }
57
+
58
+ const ids = em.query([Pos, Vel]);
59
+
60
+ // Warmup
61
+ for (let f = 0; f < 5; f++) {
62
+ for (let i = 0; i < ids.length; i++) {
63
+ em.set(ids[i], Pos.x, em.get(ids[i], Pos.x) + em.get(ids[i], Vel.vx));
64
+ em.set(ids[i], Pos.y, em.get(ids[i], Pos.y) + em.get(ids[i], Vel.vy));
65
+ }
66
+ }
67
+
54
68
  const t0 = performance.now();
55
69
  for (let f = 0; f < FRAMES; f++) {
56
- const ids = em.query([Pos, Vel]);
70
+ for (let i = 0; i < ids.length; i++) {
71
+ em.set(ids[i], Pos.x, em.get(ids[i], Pos.x) + em.get(ids[i], Vel.vx));
72
+ em.set(ids[i], Pos.y, em.get(ids[i], Pos.y) + em.get(ids[i], Vel.vy));
73
+ }
74
+ }
75
+ return (performance.now() - t0) / FRAMES;
76
+ }
77
+
78
+ // --- query + getComponent — per-entity object allocation ---
79
+ function benchQueryGetComponent() {
80
+ const Pos = component('CP', 'f32', ['x', 'y']);
81
+ const Vel = component('CV', 'f32', ['vx', 'vy']);
82
+ const em = createEntityManager();
83
+
84
+ for (let i = 0; i < COUNT; i++) {
85
+ em.createEntityWith(Pos, { x: i, y: i }, Vel, { vx: 1, vy: 1 });
86
+ }
87
+
88
+ const ids = em.query([Pos, Vel]);
89
+
90
+ // Only 20 frames for this one — it's very slow at 1M
91
+ const frames = 20;
92
+
93
+ const t0 = performance.now();
94
+ for (let f = 0; f < frames; f++) {
57
95
  for (let i = 0; i < ids.length; i++) {
58
96
  const pos = em.getComponent(ids[i], Pos);
59
97
  const vel = em.getComponent(ids[i], Vel);
60
- pos.x += vel.vx;
61
- pos.y += vel.vy;
98
+ em.set(ids[i], Pos.x, pos.x + vel.vx);
99
+ em.set(ids[i], Pos.y, pos.y + vel.vy);
62
100
  }
63
101
  }
64
- return (performance.now() - t0) / FRAMES;
102
+ return (performance.now() - t0) / frames;
65
103
  }
66
104
 
67
105
  // --- String SoA: creation + access ---
@@ -102,65 +140,27 @@ function benchStringSoA() {
102
140
  return { createTime, forEachTime, getTime, count, count2 };
103
141
  }
104
142
 
105
- // --- String untyped: creation + access ---
106
- function benchStringUntyped() {
107
- const Name = component('SUn');
108
- const em = createEntityManager();
109
-
110
- const t0 = performance.now();
111
- for (let i = 0; i < COUNT; i++) {
112
- em.createEntityWith(Name, { name: `entity_${i}`, tag: 'npc' });
113
- }
114
- const createTime = performance.now() - t0;
115
-
116
- // getComponent access
117
- const ids = em.query([Name]);
118
- const t1 = performance.now();
119
- let count = 0;
120
- for (let f = 0; f < 50; f++) {
121
- for (let i = 0; i < ids.length; i++) {
122
- const n = em.getComponent(ids[i], Name);
123
- if (n.name.length > 5) count++;
124
- }
125
- }
126
- const accessTime = (performance.now() - t1) / 50;
127
-
128
- return { createTime, accessTime, count };
129
- }
130
-
131
143
  // --- Run ---
132
- console.log(`\n=== Typed vs Untyped: ${(COUNT / 1e6).toFixed(0)}M entities ===\n`);
133
-
134
- // Warmup
135
- benchTypedCreate();
136
- benchUntypedCreate();
144
+ console.log(`\n=== Access patterns: ${(COUNT / 1e6).toFixed(0)}M entities ===\n`);
137
145
 
138
- // Creation
139
- const typed = benchTypedCreate();
140
- const untyped = benchUntypedCreate();
146
+ console.log(`Iteration (${FRAMES} frames, Position += Velocity):`);
147
+ const forEachField = benchForEachField();
148
+ console.log(` forEach + field(): ${forEachField.toFixed(2)} ms/frame`);
141
149
 
142
- console.log(`Creation (${(COUNT / 1e6).toFixed(0)}M entities, createEntityWith):`);
143
- console.log(` typed: ${typed.time.toFixed(0)} ms`);
144
- console.log(` untyped: ${untyped.time.toFixed(0)} ms`);
145
- console.log(` ratio: ${(untyped.time / typed.time).toFixed(2)}x`);
150
+ const queryGetSet = benchQueryGetSet();
151
+ console.log(` query + get/set: ${queryGetSet.toFixed(2)} ms/frame`);
146
152
 
147
- // Iteration
148
- const typedIter = benchTypedIterate(typed.em, typed.Pos, typed.Vel);
149
- const untypedIter = benchUntypedIterate(untyped.em, untyped.Pos, untyped.Vel);
153
+ const queryGetComp = benchQueryGetComponent();
154
+ console.log(` query + getComponent: ${queryGetComp.toFixed(2)} ms/frame`);
150
155
 
151
- console.log(`\nIteration (${FRAMES} frames, Position += Velocity):`);
152
- console.log(` typed (forEach+field): ${typedIter.toFixed(2)} ms/frame`);
153
- console.log(` untyped (query+getComponent): ${untypedIter.toFixed(2)} ms/frame`);
154
- console.log(` ratio: ${(untypedIter / typedIter).toFixed(1)}x slower`);
156
+ console.log(`\n forEach vs get/set: ${(queryGetSet / forEachField).toFixed(1)}x faster`);
157
+ console.log(` forEach vs getComponent: ${(queryGetComp / forEachField).toFixed(1)}x faster`);
155
158
 
156
159
  // String components
157
160
  console.log(`\nString component (${(COUNT / 1e6).toFixed(0)}M entities, { name, tag }):`);
158
- const strSoA = benchStringSoA();
159
- const strUn = benchStringUntyped();
160
- console.log(` SoA create: ${strSoA.createTime.toFixed(0)} ms`);
161
- console.log(` untyped create: ${strUn.createTime.toFixed(0)} ms`);
162
- console.log(` SoA forEach(field): ${strSoA.forEachTime.toFixed(1)} ms/frame`);
163
- console.log(` SoA get(): ${strSoA.getTime.toFixed(1)} ms/frame`);
164
- console.log(` untyped getComponent(): ${strUn.accessTime.toFixed(1)} ms/frame`);
165
- console.log(` forEach vs getComponent: ${(strUn.accessTime / strSoA.forEachTime).toFixed(1)}x faster`);
161
+ const str = benchStringSoA();
162
+ console.log(` create: ${str.createTime.toFixed(0)} ms`);
163
+ console.log(` forEach(field): ${str.forEachTime.toFixed(1)} ms/frame`);
164
+ console.log(` get() per entity: ${str.getTime.toFixed(1)} ms/frame`);
165
+ console.log(` forEach vs get: ${(str.getTime / str.forEachTime).toFixed(1)}x faster`);
166
166
  console.log();
@@ -1,7 +1,7 @@
1
- // Benchmark: archetype-ecs (current) vs archetype+TypedArrays (hypothetical) vs bitECS
1
+ // Benchmark: archetype-ecs vs bitECS
2
2
  // Tests: system loop, entity creation, component add/remove churn
3
3
 
4
- import { createEntityManager } from '../src/EntityManager.js';
4
+ import { createEntityManager, component } from '../dist/index.js';
5
5
  import {
6
6
  createWorld, addEntity,
7
7
  addComponent, removeComponent,
@@ -19,52 +19,29 @@ const pad = (s, n) => String(s).padStart(n);
19
19
  // =========================================================================
20
20
 
21
21
  function benchArchetypeLoop(entityCount) {
22
+ const Position = component('APos', 'f32', ['x', 'y']);
23
+ const Velocity = component('AVel', 'f32', ['vx', 'vy']);
22
24
  const em = createEntityManager();
23
- const Position = Symbol('Position');
24
- const Velocity = Symbol('Velocity');
25
25
 
26
26
  for (let i = 0; i < entityCount; i++) {
27
- const id = em.createEntity();
28
- em.addComponent(id, Position, { x: Math.random() * 100, y: Math.random() * 100 });
29
- em.addComponent(id, Velocity, { vx: Math.random() * 10, vy: Math.random() * 10 });
30
- }
31
-
32
- const t0 = performance.now();
33
- for (let f = 0; f < FRAMES; f++) {
34
- const entities = em.query([Position, Velocity]);
35
- for (let i = 0; i < entities.length; i++) {
36
- const pos = em.getComponent(entities[i], Position);
37
- const vel = em.getComponent(entities[i], Velocity);
38
- pos.x += vel.vx;
39
- pos.y += vel.vy;
40
- }
41
- }
42
- return (performance.now() - t0) / FRAMES;
43
- }
44
-
45
- // Simulates archetype-ecs with TypedArray backing:
46
- // Dense arrays per archetype, but Float32Arrays instead of object arrays
47
- function benchArchetypeTypedLoop(entityCount) {
48
- // One archetype with all entities (they all have Position+Velocity)
49
- const count = entityCount;
50
- const px = new Float32Array(count);
51
- const py = new Float32Array(count);
52
- const vx = new Float32Array(count);
53
- const vy = new Float32Array(count);
54
- for (let i = 0; i < count; i++) {
55
- px[i] = Math.random() * 100;
56
- py[i] = Math.random() * 100;
57
- vx[i] = Math.random() * 10;
58
- vy[i] = Math.random() * 10;
27
+ em.createEntityWith(
28
+ Position, { x: Math.random() * 100, y: Math.random() * 100 },
29
+ Velocity, { vx: Math.random() * 10, vy: Math.random() * 10 },
30
+ );
59
31
  }
60
32
 
61
33
  const t0 = performance.now();
62
34
  for (let f = 0; f < FRAMES; f++) {
63
- // Dense iteration — no entity ID lookup, no getComponent, no sparse gaps
64
- for (let i = 0; i < count; i++) {
65
- px[i] += vx[i];
66
- py[i] += vy[i];
67
- }
35
+ em.forEach([Position, Velocity], (arch) => {
36
+ const px = arch.field(Position.x);
37
+ const py = arch.field(Position.y);
38
+ const vx = arch.field(Velocity.vx);
39
+ const vy = arch.field(Velocity.vy);
40
+ for (let i = 0; i < arch.count; i++) {
41
+ px[i] += vx[i];
42
+ py[i] += vy[i];
43
+ }
44
+ });
68
45
  }
69
46
  return (performance.now() - t0) / FRAMES;
70
47
  }
@@ -107,15 +84,16 @@ function benchBitECSLoop(entityCount) {
107
84
  // =========================================================================
108
85
 
109
86
  function benchArchetypeCreate(entityCount) {
87
+ const Position = component('CPos', 'f32', ['x', 'y']);
88
+ const Velocity = component('CVel', 'f32', ['vx', 'vy']);
110
89
  const em = createEntityManager();
111
- const Position = Symbol('Position');
112
- const Velocity = Symbol('Velocity');
113
90
 
114
91
  const t0 = performance.now();
115
92
  for (let i = 0; i < entityCount; i++) {
116
- const id = em.createEntity();
117
- em.addComponent(id, Position, { x: i, y: i });
118
- em.addComponent(id, Velocity, { vx: 1, vy: 1 });
93
+ em.createEntityWith(
94
+ Position, { x: i, y: i },
95
+ Velocity, { vx: 1, vy: 1 },
96
+ );
119
97
  }
120
98
  return performance.now() - t0;
121
99
  }
@@ -149,9 +127,9 @@ function benchBitECSCreate(entityCount) {
149
127
  // =========================================================================
150
128
 
151
129
  function benchArchetypeChurn(entityCount) {
130
+ const Position = component('ChPos', 'f32', ['x', 'y']);
131
+ const Health = component('ChHp', 'f32', ['hp']);
152
132
  const em = createEntityManager();
153
- const Position = Symbol('Position');
154
- const Health = Symbol('Health');
155
133
 
156
134
  const ids = [];
157
135
  for (let i = 0; i < entityCount; i++) {
@@ -208,25 +186,22 @@ function benchBitECSChurn(entityCount) {
208
186
 
209
187
  // Warmup
210
188
  benchArchetypeLoop(100);
211
- benchArchetypeTypedLoop(100);
212
189
  benchBitECSLoop(100);
213
190
 
214
191
  console.log(`\n=== System loop: Position += Velocity (${FRAMES} frames, per-frame time) ===\n`);
215
- console.log('Entities | arch (now) | arch+typed | bitECS | arch+typed vs bitECS');
216
- console.log('------------|---------------|--------------|--------------|---------------------');
192
+ console.log('Entities | archetype-ecs | bitECS | Δ');
193
+ console.log('------------|---------------|--------------|---------------------');
217
194
 
218
195
  for (const count of ENTITY_COUNTS) {
219
196
  const arch = benchArchetypeLoop(count);
220
- const archTyped = benchArchetypeTypedLoop(count);
221
197
  const bit = benchBitECSLoop(count);
222
- const vsNow = arch / archTyped;
223
- const vsBit = bit / archTyped;
198
+ const ratio = bit / arch;
199
+ const label = ratio > 1.1 ? `arch ${ratio.toFixed(1)}x sneller` : ratio < 0.9 ? `bitECS ${(1/ratio).toFixed(1)}x sneller` : '~gelijk';
224
200
  console.log(
225
201
  `${pad(count.toLocaleString(), 11)} | ` +
226
202
  `${pad(arch.toFixed(3), 10)} ms | ` +
227
- `${pad(archTyped.toFixed(3), 9)} ms | ` +
228
203
  `${pad(bit.toFixed(3), 9)} ms | ` +
229
- `${vsBit > 1.1 ? `arch+typed ${vsBit.toFixed(1)}x sneller` : vsBit < 0.9 ? `bitECS ${(1/vsBit).toFixed(1)}x sneller` : '~gelijk'}`
204
+ `${label}`
230
205
  );
231
206
  }
232
207
 
@@ -0,0 +1,18 @@
1
+ export declare const TYPED: unique symbol;
2
+ type TypedArrayConstructor = typeof Float32Array | typeof Float64Array | typeof Int8Array | typeof Int16Array | typeof Int32Array | typeof Uint8Array | typeof Uint16Array | typeof Uint32Array | typeof Array;
3
+ export declare const TYPE_MAP: Record<string, TypedArrayConstructor>;
4
+ export type TypeSpec = TypedArrayConstructor | [TypedArrayConstructor, number];
5
+ export declare function parseTypeSpec(typeStr: string): TypeSpec;
6
+ export declare const componentSchemas: Map<symbol, Record<string, TypeSpec>>;
7
+ export interface FieldRef {
8
+ readonly _sym: symbol;
9
+ readonly _field: string;
10
+ }
11
+ export type ComponentDef<F extends string = never> = {
12
+ readonly _sym: symbol;
13
+ readonly _name: string;
14
+ } & {
15
+ readonly [K in F]: FieldRef;
16
+ };
17
+ export declare function toSym(type: ComponentDef | symbol): symbol;
18
+ export {};