archetype-ecs 1.2.0 → 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
@@ -4,7 +4,7 @@
4
4
  <br><br>
5
5
  <strong>archetype-ecs</strong>
6
6
  <br>
7
- <sub>Tiny, fast ECS with TypedArray storage. Zero dependencies.</sub>
7
+ <sub>ECS with TypedArray storage. No dependencies.</sub>
8
8
  <br><br>
9
9
  <a href="https://www.npmjs.com/package/archetype-ecs"><img src="https://img.shields.io/npm/v/archetype-ecs.svg?style=flat-square&color=000" alt="npm" /></a>
10
10
  <img src="https://img.shields.io/badge/gzip-~5kb-000?style=flat-square" alt="size" />
@@ -13,16 +13,12 @@
13
13
 
14
14
  ---
15
15
 
16
- Entities grouped by component composition. Numeric fields in contiguous TypedArrays, strings in SoA arrays. Bitmask query matching. Zero-allocation hot paths.
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
20
20
  ```
21
21
 
22
- ---
23
-
24
- ### The full picture in 20 lines
25
-
26
22
  ```ts
27
23
  import { createEntityManager, component } from 'archetype-ecs'
28
24
 
@@ -50,18 +46,16 @@ em.forEach([Position, Velocity], (arch) => {
50
46
  })
51
47
  ```
52
48
 
53
- Define components, spawn entities, iterate with raw TypedArrays — no allocations, no cache misses, full type safety.
54
-
55
49
  ---
56
50
 
57
51
  ### Why archetype-ecs?
58
52
 
59
53
  <table>
60
- <tr><td><strong>Fastest iteration</strong></td><td>SoA TypedArrays give the fastest iteration of any JS ECS we've tested. 1.7 ms/frame over 1M entities — see <a href="#benchmarks">benchmarks</a>.</td></tr>
61
- <tr><td><strong>Compact memory</strong></td><td>Packed archetypes store 1M entities in 86 MB. Up to 2.4x less than sparse-array alternatives.</td></tr>
62
- <tr><td><strong>Zero-alloc hot path</strong></td><td><code>em.get</code>, <code>em.set</code>, and <code>forEach</code> never allocate. Your GC stays quiet.</td></tr>
63
- <tr><td><strong>Type-safe</strong></td><td>Full TypeScript generics. Field names autocomplete. Wrong fields don't compile.</td></tr>
64
- <tr><td><strong>Zero dependencies</strong></td><td>~5kb gzipped. No build step. Ships as ES modules.</td></tr>
54
+ <tr><td><strong>Fast iteration</strong></td><td>1.7 ms/frame over 1M entities. Faster than bitecs, wolf-ecs, harmony-ecs — see <a href="#benchmarks">benchmarks</a>.</td></tr>
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
+ <tr><td><strong>No allocations</strong></td><td><code>get</code>, <code>set</code>, and <code>forEach</code> don't allocate.</td></tr>
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>
65
59
  </table>
66
60
 
67
61
  ---
@@ -71,12 +65,12 @@ Define components, spawn entities, iterate with raw TypedArrays — no allocatio
71
65
  ```ts
72
66
  import { createEntityManager, component } from 'archetype-ecs'
73
67
 
74
- // Numeric — backed by TypedArrays for cache-friendly iteration
68
+ // Numeric — stored as TypedArrays
75
69
  const Position = component('Position', 'f32', ['x', 'y'])
76
70
  const Velocity = component('Velocity', 'f32', ['vx', 'vy'])
77
71
  const Health = component('Health', { hp: 'i32', maxHp: 'i32' })
78
72
 
79
- // Strings — backed by SoA arrays, same field access API
73
+ // Strings — stored as arrays, same API
80
74
  const Name = component('Name', 'string', ['name', 'title'])
81
75
 
82
76
  // Mixed — numeric and string fields in one component
@@ -90,7 +84,7 @@ const Enemy = component('Enemy')
90
84
 
91
85
  ### Entities
92
86
 
93
- ```js
87
+ ```ts
94
88
  const em = createEntityManager()
95
89
 
96
90
  // One at a time
@@ -100,7 +94,7 @@ em.addComponent(player, Velocity, { vx: 0, vy: 0 })
100
94
  em.addComponent(player, Health, { hp: 100, maxHp: 100 })
101
95
  em.addComponent(player, Name, { name: 'Hero', title: 'Sir' })
102
96
 
103
- // Or all at once — no archetype migration overhead
97
+ // Or all at once
104
98
  for (let i = 0; i < 10_000; i++) {
105
99
  em.createEntityWith(
106
100
  Position, { x: Math.random() * 800, y: Math.random() * 600 },
@@ -116,8 +110,8 @@ em.destroyEntity(player)
116
110
 
117
111
  ### Read & write
118
112
 
119
- ```js
120
- // Zero allocation access any field directly
113
+ ```ts
114
+ // Access a single field (doesn't allocate)
121
115
  em.get(player, Position.x) // 0
122
116
  em.get(player, Name.name) // 'Hero'
123
117
  em.set(player, Velocity.vx, 5)
@@ -127,15 +121,15 @@ em.getComponent(player, Position) // { x: 0, y: 0 }
127
121
  em.getComponent(player, Name) // { name: 'Hero', title: 'Sir' }
128
122
  ```
129
123
 
130
- ### Systems — `forEach` vs `query`
124
+ ### Queries — `forEach` vs `query`
131
125
 
132
126
  Two ways to work with entities in bulk. Pick the right one for the job:
133
127
 
134
- #### `forEach` — zero-alloc bulk processing
128
+ #### `forEach` — bulk processing
135
129
 
136
- Best for **systems that run every frame**. Gives you raw TypedArrays — no entity lookups, no object allocations, no cache misses.
130
+ Iterates over matching archetypes. You get the backing TypedArrays directly.
137
131
 
138
- ```js
132
+ ```ts
139
133
  function movementSystem(dt) {
140
134
  em.forEach([Position, Velocity], (arch) => {
141
135
  const px = arch.field(Position.x) // Float32Array
@@ -152,9 +146,9 @@ function movementSystem(dt) {
152
146
 
153
147
  #### `query` — when you need entity IDs
154
148
 
155
- Best for **event-driven logic** where you need to store, pass around, or target specific entity IDs.
149
+ Returns entity IDs for when you need to target specific entities.
156
150
 
157
- ```js
151
+ ```ts
158
152
  // Find the closest enemy to the player
159
153
  const enemies = em.query([Position, Enemy])
160
154
  let closest = -1, minDist = Infinity
@@ -182,11 +176,69 @@ const total = em.count([Position])
182
176
  | **Use for** | Movement, physics, rendering | Damage events, UI, spawning |
183
177
  | **Runs** | Every frame | On demand |
184
178
  | **Allocates** | Nothing | `number[]` of entity IDs |
185
- | **Access** | Raw TypedArrays by field | `get` / `set` by entity ID |
179
+ | **Access** | TypedArrays by field | `get` / `set` by entity ID |
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
+ ```
186
238
 
187
239
  ### Serialize
188
240
 
189
- ```js
241
+ ```ts
190
242
  const symbolToName = new Map([
191
243
  [Position._sym, 'Position'],
192
244
  [Velocity._sym, 'Velocity'],
@@ -200,13 +252,13 @@ const json = JSON.stringify(snapshot)
200
252
  em.deserialize(JSON.parse(json), { Position, Velocity, Health })
201
253
  ```
202
254
 
203
- Strip components, skip entities, or plug in custom serializers — see the API section below.
255
+ Supports stripping components, skipping entities, and custom serializers.
204
256
 
205
257
  ---
206
258
 
207
259
  ## TypeScript
208
260
 
209
- Every component carries its type. Field names autocomplete, wrong fields and shapes are compile errors.
261
+ Component types are inferred from their definition. Field names autocomplete, wrong fields are compile errors.
210
262
 
211
263
  ```ts
212
264
  // Schema is inferred — Position becomes ComponentDef<{ x: number; y: number }>
@@ -249,7 +301,7 @@ Tag component — no data, used as a marker for queries.
249
301
 
250
302
  Schema component with uniform field type.
251
303
 
252
- ```js
304
+ ```ts
253
305
  const Position = component('Position', 'f32', ['x', 'y'])
254
306
  const Name = component('Name', 'string', ['name', 'title'])
255
307
  ```
@@ -258,7 +310,7 @@ const Name = component('Name', 'string', ['name', 'title'])
258
310
 
259
311
  Schema component with mixed field types.
260
312
 
261
- ```js
313
+ ```ts
262
314
  const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })
263
315
  ```
264
316
 
@@ -269,27 +321,51 @@ Returns an entity manager with the following methods:
269
321
  | Method | Description |
270
322
  |---|---|
271
323
  | `createEntity()` | Create an empty entity |
272
- | `createEntityWith(Comp, data, ...)` | Create entity with components no migration cost |
324
+ | `createEntityWith(Comp, data, ...)` | Create entity with components in one call |
273
325
  | `destroyEntity(id)` | Remove entity and all its components |
274
326
  | `addComponent(id, Comp, data)` | Add a component to an existing entity |
275
327
  | `removeComponent(id, Comp)` | Remove a component |
276
328
  | `hasComponent(id, Comp)` | Check if entity has a component |
277
329
  | `getComponent(id, Comp)` | Get component data as object *(allocates)* |
278
- | `get(id, Comp.field)` | Read a single field *(zero-alloc)* |
279
- | `set(id, Comp.field, value)` | Write a single field *(zero-alloc)* |
330
+ | `get(id, Comp.field)` | Read a single field |
331
+ | `set(id, Comp.field, value)` | Write a single field |
280
332
  | `query(include, exclude?)` | Get matching entity IDs |
281
333
  | `count(include, exclude?)` | Count matching entities |
282
- | `forEach(include, callback, exclude?)` | Iterate archetypes with raw TypedArray access |
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 |
283
338
  | `serialize(symbolToName, strip?, skip?, opts?)` | Serialize world to JSON-friendly object |
284
339
  | `deserialize(data, nameToSymbol, opts?)` | Restore world from serialized data |
285
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
+
286
362
  ---
287
363
 
288
364
  ## Benchmarks
289
365
 
290
366
  1M entities, Position += Velocity, 5 runs (median), Node.js:
291
367
 
292
- | | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex |
368
+ | | archetype-ecs | [bitecs](https://github.com/NateTheGreatt/bitECS) | [wolf-ecs](https://github.com/EnderShadow8/wolf-ecs) | [harmony-ecs](https://github.com/3mcd/harmony-ecs) | [miniplex](https://github.com/hmans/miniplex) |
293
369
  |---|---:|---:|---:|---:|---:|
294
370
  | **Iteration** (ms/frame) | **1.7** | 2.2 | 2.2 | 1.8 | 32.5 |
295
371
  | **Entity creation** (ms) | 401 | 366 | **106** | 248 | 265 |
@@ -297,7 +373,7 @@ Returns an entity manager with the following methods:
297
373
 
298
374
  Each library runs the same test — iterate 1M entities over 500 frames:
299
375
 
300
- ```js
376
+ ```ts
301
377
  // archetype-ecs
302
378
  em.forEach([Position, Velocity], (arch) => {
303
379
  const px = arch.field(Position.x) // Float32Array, dense
@@ -311,7 +387,7 @@ em.forEach([Position, Velocity], (arch) => {
311
387
  })
312
388
  ```
313
389
 
314
- archetype-ecs has the fastest iteration — the metric that matters most for game loops. Harmony-ecs and wolf-ecs are close behind; miniplex pays for object-based storage with ~20x slower iteration.
390
+ archetype-ecs is fastest at iteration. Harmony-ecs and wolf-ecs are close; miniplex is ~20x slower due to object-based storage.
315
391
 
316
392
  Run them yourself:
317
393
 
@@ -323,7 +399,7 @@ npm run bench
323
399
 
324
400
  ## Feature comparison
325
401
 
326
- How archetype-ecs stacks up against other JS ECS libraries:
402
+ Compared against other JS ECS libraries:
327
403
 
328
404
  ### Unique to archetype-ecs
329
405
 
@@ -333,6 +409,7 @@ How archetype-ecs stacks up against other JS ECS libraries:
333
409
  | Mixed string + numeric components | ✓ | — | — | — | — |
334
410
  | `forEach` with dense TypedArray field access | ✓ | — | — | — | — |
335
411
  | Field descriptors for both per-entity and bulk access | ✓ | — | — | — | — |
412
+ | TC39 decorator system (`@OnAdded` / `@OnRemoved`) | ✓ | — | — | — | — |
336
413
  | Built-in profiler | ✓ | — | — | — | — |
337
414
 
338
415
  ### Full comparison
@@ -345,12 +422,14 @@ How archetype-ecs stacks up against other JS ECS libraries:
345
422
  | TypeScript type inference | ✓ | — | ✓ | ✓ | ✓✓ |
346
423
  | Batch entity creation | ✓ | — | — | ✓ | ✓ |
347
424
  | Zero-alloc per-entity access | ✓ | ✓ | ✓ | ✓ | — |
425
+ | System framework (class + functional) | ✓ | — | — | — | — |
426
+ | Component lifecycle hooks | ✓ | — | — | — | ✓ |
348
427
  | Relations / hierarchies | — | ✓ | — | — | — |
349
428
  | React integration | — | — | — | — | ✓ |
350
429
 
351
430
  ✓✓ = notably stronger implementation in that library.
352
431
 
353
- archetype-ecs is the only library that combines fastest iteration, string SoA storage, serialization, type safety, and a built-in profiler.
432
+ archetype-ecs is the only one that combines fast iteration, string storage, serialization, decorator-based systems, and type safety.
354
433
 
355
434
  ---
356
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();