archetype-ecs 1.0.0 → 1.2.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.
@@ -0,0 +1,32 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node /mnt/d/Projects/ECS/bench/vs-bitecs.js:*)",
5
+ "Bash(node:*)",
6
+ "Bash(npm test:*)",
7
+ "Bash(git add:*)",
8
+ "Bash(git commit -m \"$\\(cat <<''EOF''\nAdd getField/setField for zero-allocation field access with TS generics\n\ngetField and setField read/write individual component fields directly\nfrom TypedArrays without reconstructing an object. TypeScript generics\non component\\(\\) flow through to autocomplete field names and catch\ninvalid fields at compile time.\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")",
9
+ "Bash(git push:*)",
10
+ "Bash(git commit -m \"$\\(cat <<''EOF''\nAdd compile-time type tests for TS generics validation\n\ntests/types.ts validates that component\\(\\) schema inference flows\nthrough getField, setField, addComponent, and forEach field\\(\\) with\n@ts-expect-error assertions for invalid field names. npm test now\nruns both runtime tests and tsc --noEmit.\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")",
11
+ "Bash(git -C /mnt/d/Projects/ECS status)",
12
+ "Bash(git -C /mnt/d/Projects/ECS diff)",
13
+ "Bash(git -C /mnt/d/Projects/ECS log --oneline -5)",
14
+ "Bash(git -C /mnt/d/Projects/ECS add bench/typed-vs-bitecs-1m.js src/ComponentRegistry.js src/EntityManager.js src/index.d.ts src/index.js tests/EntityManager.test.js tests/types.ts)",
15
+ "Bash(git -C /mnt/d/Projects/ECS commit -m \"$\\(cat <<''EOF''\nAdd field descriptor API: Position.x instead of string literals\n\nComponent objects now have field descriptors as properties, enabling\nem.get\\(id, Position.x\\) / em.set\\(id, Position.x, 5\\) for zero-alloc\nfield access and arch.field\\(Position.x\\) in forEach hot loops.\n\n- component\\(\\) supports short form: component\\(''Pos'', ''f32'', [''x'',''y'']\\)\n- toSym\\(\\) helper normalizes component objects to underlying symbols\n- Updated TS types: ComponentDef<T> with FieldRef descriptors\n- All tests and benchmarks updated to new API\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")",
16
+ "Bash(wc:*)",
17
+ "Bash(git commit:*)",
18
+ "WebSearch",
19
+ "WebFetch(domain:github.com)",
20
+ "WebFetch(domain:www.webgamedev.com)",
21
+ "WebFetch(domain:3mcd.github.io)",
22
+ "WebFetch(domain:www.npmjs.com)",
23
+ "WebFetch(domain:raw.githubusercontent.com)",
24
+ "Bash(npm view:*)",
25
+ "Bash(npm pack:*)",
26
+ "Bash(curl:*)",
27
+ "Bash(npm install:*)",
28
+ "Bash(npm run bench:*)",
29
+ "Bash(FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch:*)"
30
+ ]
31
+ }
32
+ }
package/README.md ADDED
@@ -0,0 +1,359 @@
1
+ <p align="center">
2
+ <br>
3
+ <img src="https://em-content.zobj.net/source/apple/391/dna_1f9ec.png" width="80" />
4
+ <br><br>
5
+ <strong>archetype-ecs</strong>
6
+ <br>
7
+ <sub>Tiny, fast ECS with TypedArray storage. Zero dependencies.</sub>
8
+ <br><br>
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
+ <img src="https://img.shields.io/badge/gzip-~5kb-000?style=flat-square" alt="size" />
11
+ <a href="https://github.com/RvRooijen/archetype-ecs/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/archetype-ecs.svg?style=flat-square&color=000" alt="license" /></a>
12
+ </p>
13
+
14
+ ---
15
+
16
+ Entities grouped by component composition. Numeric fields in contiguous TypedArrays, strings in SoA arrays. Bitmask query matching. Zero-allocation hot paths.
17
+
18
+ ```
19
+ npm i archetype-ecs
20
+ ```
21
+
22
+ ---
23
+
24
+ ### The full picture in 20 lines
25
+
26
+ ```ts
27
+ import { createEntityManager, component } from 'archetype-ecs'
28
+
29
+ const Position = component('Position', 'f32', ['x', 'y'])
30
+ const Velocity = component('Velocity', 'f32', ['vx', 'vy'])
31
+
32
+ const em = createEntityManager()
33
+
34
+ for (let i = 0; i < 10_000; i++) {
35
+ em.createEntityWith(
36
+ Position, { x: Math.random() * 800, y: Math.random() * 600 },
37
+ Velocity, { vx: Math.random() - 0.5, vy: Math.random() - 0.5 },
38
+ )
39
+ }
40
+
41
+ em.forEach([Position, Velocity], (arch) => {
42
+ const px = arch.field(Position.x) // Float32Array
43
+ const py = arch.field(Position.y)
44
+ const vx = arch.field(Velocity.vx)
45
+ const vy = arch.field(Velocity.vy)
46
+ for (let i = 0; i < arch.count; i++) {
47
+ px[i] += vx[i]
48
+ py[i] += vy[i]
49
+ }
50
+ })
51
+ ```
52
+
53
+ Define components, spawn entities, iterate with raw TypedArrays — no allocations, no cache misses, full type safety.
54
+
55
+ ---
56
+
57
+ ### Why archetype-ecs?
58
+
59
+ <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>
65
+ </table>
66
+
67
+ ---
68
+
69
+ ### Components
70
+
71
+ ```ts
72
+ import { createEntityManager, component } from 'archetype-ecs'
73
+
74
+ // Numeric — backed by TypedArrays for cache-friendly iteration
75
+ const Position = component('Position', 'f32', ['x', 'y'])
76
+ const Velocity = component('Velocity', 'f32', ['vx', 'vy'])
77
+ const Health = component('Health', { hp: 'i32', maxHp: 'i32' })
78
+
79
+ // Strings — backed by SoA arrays, same field access API
80
+ const Name = component('Name', 'string', ['name', 'title'])
81
+
82
+ // Mixed — numeric and string fields in one component
83
+ const Item = component('Item', { name: 'string', weight: 'f32' })
84
+
85
+ // Tag — no data, just a marker
86
+ const Enemy = component('Enemy')
87
+ ```
88
+
89
+ > Field types: `f32` `f64` `i8` `i16` `i32` `u8` `u16` `u32` `string`
90
+
91
+ ### Entities
92
+
93
+ ```js
94
+ const em = createEntityManager()
95
+
96
+ // One at a time
97
+ const player = em.createEntity()
98
+ em.addComponent(player, Position, { x: 0, y: 0 })
99
+ em.addComponent(player, Velocity, { vx: 0, vy: 0 })
100
+ em.addComponent(player, Health, { hp: 100, maxHp: 100 })
101
+ em.addComponent(player, Name, { name: 'Hero', title: 'Sir' })
102
+
103
+ // Or all at once — no archetype migration overhead
104
+ for (let i = 0; i < 10_000; i++) {
105
+ em.createEntityWith(
106
+ Position, { x: Math.random() * 800, y: Math.random() * 600 },
107
+ Velocity, { vx: Math.random() - 0.5, vy: Math.random() - 0.5 },
108
+ Enemy, {},
109
+ )
110
+ }
111
+
112
+ em.hasComponent(player, Health) // true
113
+ em.removeComponent(player, Health)
114
+ em.destroyEntity(player)
115
+ ```
116
+
117
+ ### Read & write
118
+
119
+ ```js
120
+ // Zero allocation — access any field directly
121
+ em.get(player, Position.x) // 0
122
+ em.get(player, Name.name) // 'Hero'
123
+ em.set(player, Velocity.vx, 5)
124
+
125
+ // Or grab the whole component as an object (allocates)
126
+ em.getComponent(player, Position) // { x: 0, y: 0 }
127
+ em.getComponent(player, Name) // { name: 'Hero', title: 'Sir' }
128
+ ```
129
+
130
+ ### Systems — `forEach` vs `query`
131
+
132
+ Two ways to work with entities in bulk. Pick the right one for the job:
133
+
134
+ #### `forEach` — zero-alloc bulk processing
135
+
136
+ Best for **systems that run every frame**. Gives you raw TypedArrays — no entity lookups, no object allocations, no cache misses.
137
+
138
+ ```js
139
+ function movementSystem(dt) {
140
+ em.forEach([Position, Velocity], (arch) => {
141
+ const px = arch.field(Position.x) // Float32Array
142
+ const py = arch.field(Position.y)
143
+ const vx = arch.field(Velocity.vx)
144
+ const vy = arch.field(Velocity.vy)
145
+ for (let i = 0; i < arch.count; i++) {
146
+ px[i] += vx[i] * dt
147
+ py[i] += vy[i] * dt
148
+ }
149
+ })
150
+ }
151
+ ```
152
+
153
+ #### `query` — when you need entity IDs
154
+
155
+ Best for **event-driven logic** where you need to store, pass around, or target specific entity IDs.
156
+
157
+ ```js
158
+ // Find the closest enemy to the player
159
+ const enemies = em.query([Position, Enemy])
160
+ let closest = -1, minDist = Infinity
161
+ for (const id of enemies) {
162
+ const dx = em.get(id, Position.x) - playerX
163
+ const dy = em.get(id, Position.y) - playerY
164
+ const dist = dx * dx + dy * dy
165
+ if (dist < minDist) { minDist = dist; closest = id }
166
+ }
167
+
168
+ // Store the result as a component
169
+ em.addComponent(player, Target, { entityId: closest })
170
+
171
+ // Exclude enemies from friendly queries
172
+ const friendly = em.query([Health], [Enemy])
173
+
174
+ // Just need a count? No allocation needed
175
+ const total = em.count([Position])
176
+ ```
177
+
178
+ #### When to use which
179
+
180
+ | | `forEach` | `query` |
181
+ |---|---|---|
182
+ | **Use for** | Movement, physics, rendering | Damage events, UI, spawning |
183
+ | **Runs** | Every frame | On demand |
184
+ | **Allocates** | Nothing | `number[]` of entity IDs |
185
+ | **Access** | Raw TypedArrays by field | `get` / `set` by entity ID |
186
+
187
+ ### Serialize
188
+
189
+ ```js
190
+ const symbolToName = new Map([
191
+ [Position._sym, 'Position'],
192
+ [Velocity._sym, 'Velocity'],
193
+ [Health._sym, 'Health'],
194
+ ])
195
+
196
+ const snapshot = em.serialize(symbolToName)
197
+ const json = JSON.stringify(snapshot)
198
+
199
+ // Later...
200
+ em.deserialize(JSON.parse(json), { Position, Velocity, Health })
201
+ ```
202
+
203
+ Strip components, skip entities, or plug in custom serializers — see the API section below.
204
+
205
+ ---
206
+
207
+ ## TypeScript
208
+
209
+ Every component carries its type. Field names autocomplete, wrong fields and shapes are compile errors.
210
+
211
+ ```ts
212
+ // Schema is inferred — Position becomes ComponentDef<{ x: number; y: number }>
213
+ const Position = component('Position', 'f32', ['x', 'y'])
214
+
215
+ Position.x // autocompletes to .x and .y
216
+ Position.z // Property 'z' does not exist
217
+
218
+ em.get(id, Position.x) // number | undefined
219
+ em.set(id, Position.z, 5) // Property 'z' does not exist
220
+
221
+ em.addComponent(id, Position, { x: 1, y: 2 }) // ok
222
+ em.addComponent(id, Position, { x: 1 }) // Property 'y' is missing
223
+
224
+ em.getComponent(id, Position) // { x: number; y: number } | undefined
225
+ ```
226
+
227
+ String fields are fully typed too:
228
+
229
+ ```ts
230
+ const Name = component('Name', 'string', ['name', 'title'])
231
+
232
+ em.get(id, Name.name) // string | undefined
233
+ em.set(id, Name.name, 'Hero') // ok
234
+ em.set(id, Name.name, 42) // number not assignable to string
235
+
236
+ em.addComponent(id, Name, { name: 'Hero', title: 'Sir' }) // ok
237
+ em.addComponent(id, Name, { foo: 'bar' }) // type error
238
+ ```
239
+
240
+ ---
241
+
242
+ ## API reference
243
+
244
+ ### `component(name)`
245
+
246
+ Tag component — no data, used as a marker for queries.
247
+
248
+ ### `component(name, type, fields)`
249
+
250
+ Schema component with uniform field type.
251
+
252
+ ```js
253
+ const Position = component('Position', 'f32', ['x', 'y'])
254
+ const Name = component('Name', 'string', ['name', 'title'])
255
+ ```
256
+
257
+ ### `component(name, schema)`
258
+
259
+ Schema component with mixed field types.
260
+
261
+ ```js
262
+ const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })
263
+ ```
264
+
265
+ ### `createEntityManager()`
266
+
267
+ Returns an entity manager with the following methods:
268
+
269
+ | Method | Description |
270
+ |---|---|
271
+ | `createEntity()` | Create an empty entity |
272
+ | `createEntityWith(Comp, data, ...)` | Create entity with components — no migration cost |
273
+ | `destroyEntity(id)` | Remove entity and all its components |
274
+ | `addComponent(id, Comp, data)` | Add a component to an existing entity |
275
+ | `removeComponent(id, Comp)` | Remove a component |
276
+ | `hasComponent(id, Comp)` | Check if entity has a component |
277
+ | `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)* |
280
+ | `query(include, exclude?)` | Get matching entity IDs |
281
+ | `count(include, exclude?)` | Count matching entities |
282
+ | `forEach(include, callback, exclude?)` | Iterate archetypes with raw TypedArray access |
283
+ | `serialize(symbolToName, strip?, skip?, opts?)` | Serialize world to JSON-friendly object |
284
+ | `deserialize(data, nameToSymbol, opts?)` | Restore world from serialized data |
285
+
286
+ ---
287
+
288
+ ## Benchmarks
289
+
290
+ 1M entities, Position += Velocity, 5 runs (median), Node.js:
291
+
292
+ | | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex |
293
+ |---|---:|---:|---:|---:|---:|
294
+ | **Iteration** (ms/frame) | **1.7** | 2.2 | 2.2 | 1.8 | 32.5 |
295
+ | **Entity creation** (ms) | 401 | 366 | **106** | 248 | 265 |
296
+ | **Memory** (MB) | 86 | 204 | 60 | **31** | 166 |
297
+
298
+ Each library runs the same test — iterate 1M entities over 500 frames:
299
+
300
+ ```js
301
+ // archetype-ecs
302
+ em.forEach([Position, Velocity], (arch) => {
303
+ const px = arch.field(Position.x) // Float32Array, dense
304
+ const py = arch.field(Position.y)
305
+ const vx = arch.field(Velocity.vx)
306
+ const vy = arch.field(Velocity.vy)
307
+ for (let i = 0; i < arch.count; i++) {
308
+ px[i] += vx[i]
309
+ py[i] += vy[i]
310
+ }
311
+ })
312
+ ```
313
+
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.
315
+
316
+ Run them yourself:
317
+
318
+ ```bash
319
+ npm run bench
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Feature comparison
325
+
326
+ How archetype-ecs stacks up against other JS ECS libraries:
327
+
328
+ ### Unique to archetype-ecs
329
+
330
+ | Feature | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex |
331
+ |---|:---:|:---:|:---:|:---:|:---:|
332
+ | String SoA storage | ✓ | — | — | — | — |
333
+ | Mixed string + numeric components | ✓ | — | — | — | — |
334
+ | `forEach` with dense TypedArray field access | ✓ | — | — | — | — |
335
+ | Field descriptors for both per-entity and bulk access | ✓ | — | — | — | — |
336
+ | Built-in profiler | ✓ | — | — | — | — |
337
+
338
+ ### Full comparison
339
+
340
+ | Feature | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex |
341
+ |---|:---:|:---:|:---:|:---:|:---:|
342
+ | TypedArray iteration | ✓ | ✓ | ✓ | ✓ | — |
343
+ | String support | ✓ | ✓ | — | — | ✓ |
344
+ | Serialize / deserialize | ✓ | ✓✓ | — | — | — |
345
+ | TypeScript type inference | ✓ | — | ✓ | ✓ | ✓✓ |
346
+ | Batch entity creation | ✓ | — | — | ✓ | ✓ |
347
+ | Zero-alloc per-entity access | ✓ | ✓ | ✓ | ✓ | — |
348
+ | Relations / hierarchies | — | ✓ | — | — | — |
349
+ | React integration | — | — | — | — | ✓ |
350
+
351
+ ✓✓ = notably stronger implementation in that library.
352
+
353
+ archetype-ecs is the only library that combines fastest iteration, string SoA storage, serialization, type safety, and a built-in profiler.
354
+
355
+ ---
356
+
357
+ ## License
358
+
359
+ MIT
@@ -0,0 +1,161 @@
1
+ // Allocation benchmark: creating 1M entities with Position + Velocity
2
+ // Compares: archetype-ecs (typed) vs bitECS vs archetype-ecs (untyped/legacy)
3
+
4
+ import {
5
+ createWorld, addEntity, addComponent, removeEntity, query
6
+ } from 'bitecs';
7
+ import { createEntityManager, component } from '../src/index.js';
8
+
9
+ const COUNT = 1_000_000;
10
+ const RUNS = 5;
11
+
12
+ function median(arr) {
13
+ const s = [...arr].sort((a, b) => a - b);
14
+ return s[Math.floor(s.length / 2)];
15
+ }
16
+
17
+ function memMB() {
18
+ if (globalThis.gc) globalThis.gc();
19
+ return process.memoryUsage().heapUsed / 1024 / 1024;
20
+ }
21
+
22
+ // --- archetype-ecs: typed (SoA) ---
23
+ function benchOursTyped() {
24
+ const Position = component('AllocPos', { x: 'f32', y: 'f32' });
25
+ const Velocity = component('AllocVel', { vx: 'f32', vy: 'f32' });
26
+ const em = createEntityManager();
27
+
28
+ const memBefore = memMB();
29
+ const t0 = performance.now();
30
+
31
+ for (let i = 0; i < COUNT; i++) {
32
+ const id = em.createEntity();
33
+ em.addComponent(id, Position, { x: i, y: i });
34
+ em.addComponent(id, Velocity, { vx: 1, vy: 1 });
35
+ }
36
+
37
+ const createTime = performance.now() - t0;
38
+ const memAfter = memMB();
39
+
40
+ // Destroy all
41
+ const t1 = performance.now();
42
+ const ids = em.query([Position]);
43
+ for (const id of ids) em.destroyEntity(id);
44
+ const destroyTime = performance.now() - t1;
45
+
46
+ return { createTime, destroyTime, memDelta: memAfter - memBefore };
47
+ }
48
+
49
+ // --- archetype-ecs: typed (SoA) with createEntityWith (no migration) ---
50
+ function benchOursTypedBatch() {
51
+ const Position = component('AllocPosBatch', { x: 'f32', y: 'f32' });
52
+ const Velocity = component('AllocVelBatch', { vx: 'f32', vy: 'f32' });
53
+ const em = createEntityManager();
54
+
55
+ const memBefore = memMB();
56
+ const t0 = performance.now();
57
+
58
+ for (let i = 0; i < COUNT; i++) {
59
+ em.createEntityWith(Position, { x: i, y: i }, Velocity, { vx: 1, vy: 1 });
60
+ }
61
+
62
+ const createTime = performance.now() - t0;
63
+ const memAfter = memMB();
64
+
65
+ const t1 = performance.now();
66
+ const ids = em.query([Position]);
67
+ for (const id of ids) em.destroyEntity(id);
68
+ const destroyTime = performance.now() - t1;
69
+
70
+ return { createTime, destroyTime, memDelta: memAfter - memBefore };
71
+ }
72
+
73
+ // --- archetype-ecs: untyped (legacy object arrays) ---
74
+ function benchOursUntyped() {
75
+ const Position = Symbol('AllocPosUn');
76
+ const Velocity = Symbol('AllocVelUn');
77
+ const em = createEntityManager();
78
+
79
+ const memBefore = memMB();
80
+ const t0 = performance.now();
81
+
82
+ for (let i = 0; i < COUNT; i++) {
83
+ const id = em.createEntity();
84
+ em.addComponent(id, Position, { x: i, y: i });
85
+ em.addComponent(id, Velocity, { vx: 1, vy: 1 });
86
+ }
87
+
88
+ const createTime = performance.now() - t0;
89
+ const memAfter = memMB();
90
+
91
+ const t1 = performance.now();
92
+ const ids = em.query([Position]);
93
+ for (const id of ids) em.destroyEntity(id);
94
+ const destroyTime = performance.now() - t1;
95
+
96
+ return { createTime, destroyTime, memDelta: memAfter - memBefore };
97
+ }
98
+
99
+ // --- bitECS ---
100
+ function benchBitECS() {
101
+ const world = createWorld();
102
+ const Position = { x: new Float32Array(COUNT + 10), y: new Float32Array(COUNT + 10) };
103
+ const Velocity = { vx: new Float32Array(COUNT + 10), vy: new Float32Array(COUNT + 10) };
104
+
105
+ const memBefore = memMB();
106
+ const t0 = performance.now();
107
+
108
+ for (let i = 0; i < COUNT; i++) {
109
+ const eid = addEntity(world);
110
+ addComponent(world, eid, Position);
111
+ addComponent(world, eid, Velocity);
112
+ Position.x[eid] = i;
113
+ Position.y[eid] = i;
114
+ Velocity.vx[eid] = 1;
115
+ Velocity.vy[eid] = 1;
116
+ }
117
+
118
+ const createTime = performance.now() - t0;
119
+ const memAfter = memMB();
120
+
121
+ const t1 = performance.now();
122
+ const entities = query(world, [Position, Velocity]);
123
+ for (const eid of entities) removeEntity(world, eid);
124
+ const destroyTime = performance.now() - t1;
125
+
126
+ return { createTime, destroyTime, memDelta: memAfter - memBefore };
127
+ }
128
+
129
+ // --- Run ---
130
+ console.log(`\n=== Allocation benchmark: ${(COUNT / 1e6).toFixed(0)}M entities (Position + Velocity) ===`);
131
+ console.log(` ${RUNS} runs, median taken\n`);
132
+
133
+ const results = { typed: [], typedBatch: [], untyped: [], bitecs: [] };
134
+
135
+ for (let r = 0; r < RUNS; r++) {
136
+ results.typed.push(benchOursTyped());
137
+ results.typedBatch.push(benchOursTypedBatch());
138
+ results.untyped.push(benchOursUntyped());
139
+ results.bitecs.push(benchBitECS());
140
+ }
141
+
142
+ function report(label, runs) {
143
+ const create = median(runs.map(r => r.createTime));
144
+ const destroy = median(runs.map(r => r.destroyTime));
145
+ const mem = median(runs.map(r => r.memDelta));
146
+ console.log(` ${label}`);
147
+ console.log(` create: ${create.toFixed(1)} ms`);
148
+ console.log(` destroy: ${destroy.toFixed(1)} ms`);
149
+ console.log(` heap: +${mem.toFixed(1)} MB`);
150
+ return { create, destroy, mem };
151
+ }
152
+
153
+ const t = report('typed + addComponent (2 migraties)', results.typed);
154
+ const tb = report('typed + createEntityWith (0 migraties)', results.typedBatch);
155
+ const u = report('untyped + addComponent', results.untyped);
156
+ const b = report('bitECS', results.bitecs);
157
+
158
+ console.log(`\n createEntityWith vs addComponent: ${(t.create / tb.create).toFixed(1)}x sneller`);
159
+ console.log(` createEntityWith vs bitECS: ${(tb.create / b.create).toFixed(1)}x (${tb.create < b.create ? 'sneller' : 'trager'})`);
160
+ console.log(` memory: typed ${t.mem.toFixed(0)} MB, batch ${tb.mem.toFixed(0)} MB, bitECS ${b.mem.toFixed(0)} MB`);
161
+ console.log();