archetype-ecs 1.3.1 → 2.0.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 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,7 +129,7 @@ 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
+ ```ts
132
133
  function movementSystem(dt) {
133
134
  em.forEach([Position, Velocity], (arch) => {
134
135
  const px = arch.field(Position.x) // Float32Array
@@ -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 } 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) {
206
+ console.log(`Entity ${id} spawned with ${this.em.get(id, Health.hp)} HP`)
207
+ }
208
+
209
+ @OnRemoved(Health)
210
+ onDeath(id) {
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'],
@@ -242,7 +301,7 @@ Tag component — no data, used as a marker for queries.
242
301
 
243
302
  Schema component with uniform field type.
244
303
 
245
- ```js
304
+ ```ts
246
305
  const Position = component('Position', 'f32', ['x', 'y'])
247
306
  const Name = component('Name', 'string', ['name', 'title'])
248
307
  ```
@@ -251,7 +310,7 @@ const Name = component('Name', 'string', ['name', 'title'])
251
310
 
252
311
  Schema component with mixed field types.
253
312
 
254
- ```js
313
+ ```ts
255
314
  const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })
256
315
  ```
257
316
 
@@ -273,9 +332,33 @@ Returns an entity manager with the following methods:
273
332
  | `query(include, exclude?)` | Get matching entity IDs |
274
333
  | `count(include, exclude?)` | Count matching entities |
275
334
  | `forEach(include, callback, exclude?)` | Iterate archetypes with TypedArray access |
335
+ | `onAdd(Comp, callback)` | Register callback for component additions *(deferred)* |
336
+ | `onRemove(Comp, callback)` | Register callback for component removals *(deferred)* |
337
+ | `flushHooks()` | Collect pending add/remove events for registered hooks |
276
338
  | `serialize(symbolToName, strip?, skip?, opts?)` | Serialize world to JSON-friendly object |
277
339
  | `deserialize(data, nameToSymbol, opts?)` | Restore world from serialized data |
278
340
 
341
+ ### `System`
342
+
343
+ Base class for decorator-based systems.
344
+
345
+ | | Description |
346
+ |---|---|
347
+ | `@OnAdded(...Comps)` | Decorator — method fires when entity gains **all** specified components |
348
+ | `@OnRemoved(...Comps)` | Decorator — method fires when **any** specified component is removed |
349
+ | `tick()` | Override — called every `run()` after hook callbacks |
350
+ | `forEach(types, callback, exclude?)` | Shorthand for `this.em.forEach(...)` |
351
+ | `run()` | Fire buffered hook callbacks, then `tick()` |
352
+ | `dispose()` | Unsubscribe all hooks |
353
+
354
+ ### `createSystem(em, constructor)`
355
+
356
+ Functional alternative to class-based systems. The constructor receives a context with `onAdded`, `onRemoved`, and `forEach`, and optionally returns a tick function.
357
+
358
+ ### `createSystems(em, entries)`
359
+
360
+ 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.
361
+
279
362
  ---
280
363
 
281
364
  ## Benchmarks
@@ -290,7 +373,7 @@ Returns an entity manager with the following methods:
290
373
 
291
374
  Each library runs the same test — iterate 1M entities over 500 frames:
292
375
 
293
- ```js
376
+ ```ts
294
377
  // archetype-ecs
295
378
  em.forEach([Position, Velocity], (arch) => {
296
379
  const px = arch.field(Position.x) // Float32Array, dense
@@ -326,6 +409,7 @@ Compared against other JS ECS libraries:
326
409
  | Mixed string + numeric components | ✓ | — | — | — | — |
327
410
  | `forEach` with dense TypedArray field access | ✓ | — | — | — | — |
328
411
  | Field descriptors for both per-entity and bulk access | ✓ | — | — | — | — |
412
+ | TC39 decorator system (`@OnAdded` / `@OnRemoved`) | ✓ | — | — | — | — |
329
413
  | Built-in profiler | ✓ | — | — | — | — |
330
414
 
331
415
  ### Full comparison
@@ -338,12 +422,14 @@ Compared against other JS ECS libraries:
338
422
  | TypeScript type inference | ✓ | — | ✓ | ✓ | ✓✓ |
339
423
  | Batch entity creation | ✓ | — | — | ✓ | ✓ |
340
424
  | Zero-alloc per-entity access | ✓ | ✓ | ✓ | ✓ | — |
425
+ | System framework (class + functional) | ✓ | — | — | — | — |
426
+ | Component lifecycle hooks | ✓ | — | — | — | ✓ |
341
427
  | Relations / hierarchies | — | ✓ | — | — | — |
342
428
  | React integration | — | — | — | — | ✓ |
343
429
 
344
430
  ✓✓ = notably stronger implementation in that library.
345
431
 
346
- archetype-ecs is the only one that combines fast iteration, string storage, serialization, and type safety.
432
+ archetype-ecs is the only one that combines fast iteration, string storage, serialization, decorator-based systems, and type safety.
347
433
 
348
434
  ---
349
435
 
@@ -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/src/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/src/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/src/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/src/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/src/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,13 @@
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 ComponentDef {
8
+ readonly _sym: symbol;
9
+ readonly _name: string;
10
+ [key: string]: unknown;
11
+ }
12
+ export declare function toSym(type: ComponentDef | symbol): symbol;
13
+ export {};
@@ -0,0 +1,29 @@
1
+ export const TYPED = Symbol('typed');
2
+ export const TYPE_MAP = {
3
+ 'f32': Float32Array,
4
+ 'f64': Float64Array,
5
+ 'i8': Int8Array,
6
+ 'i16': Int16Array,
7
+ 'i32': Int32Array,
8
+ 'u8': Uint8Array,
9
+ 'u16': Uint16Array,
10
+ 'u32': Uint32Array,
11
+ 'string': Array,
12
+ };
13
+ export function parseTypeSpec(typeStr) {
14
+ const match = typeStr.match(/^(\w+)\[(\d+)\]$/);
15
+ if (match) {
16
+ const Ctor = TYPE_MAP[match[1]];
17
+ if (!Ctor)
18
+ throw new Error(`Unknown base type "${match[1]}"`);
19
+ return [Ctor, parseInt(match[2])];
20
+ }
21
+ const Ctor = TYPE_MAP[typeStr];
22
+ if (!Ctor)
23
+ throw new Error(`Unknown type "${typeStr}"`);
24
+ return Ctor;
25
+ }
26
+ export const componentSchemas = new Map();
27
+ export function toSym(type) {
28
+ return type._sym || type;
29
+ }