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 +119 -40
- package/bench/allocations-1m.js +1 -1
- package/bench/multi-ecs-bench.js +1 -1
- package/bench/typed-vs-bitecs-1m.js +1 -1
- package/bench/typed-vs-untyped.js +81 -81
- package/bench/vs-bitecs.js +31 -56
- package/dist/src/ComponentRegistry.d.ts +13 -0
- package/dist/src/ComponentRegistry.js +29 -0
- package/dist/src/EntityManager.d.ts +52 -0
- package/dist/src/EntityManager.js +787 -0
- package/dist/src/Profiler.d.ts +12 -0
- package/dist/src/Profiler.js +38 -0
- package/dist/src/System.d.ts +37 -0
- package/dist/src/System.js +139 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +29 -0
- package/dist/tests/EntityManager.test.d.ts +1 -0
- package/dist/tests/EntityManager.test.js +499 -0
- package/dist/tests/System.test.d.ts +1 -0
- package/dist/tests/System.test.js +630 -0
- package/dist/tests/types.d.ts +1 -0
- package/dist/tests/types.js +129 -0
- package/package.json +8 -7
- package/src/ComponentRegistry.ts +45 -0
- package/src/EntityManager.ts +909 -0
- package/src/{Profiler.js → Profiler.ts} +18 -5
- package/src/System.ts +201 -0
- package/src/index.ts +38 -0
- package/tests/{EntityManager.test.js → EntityManager.test.ts} +182 -57
- package/tests/System.test.ts +546 -0
- package/tests/types.ts +69 -68
- package/tsconfig.json +8 -5
- package/.claude/settings.local.json +0 -32
- package/src/ComponentRegistry.js +0 -21
- package/src/EntityManager.js +0 -462
- package/src/index.d.ts +0 -111
- package/src/index.js +0 -37
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<br><br>
|
|
5
5
|
<strong>archetype-ecs</strong>
|
|
6
6
|
<br>
|
|
7
|
-
<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
|
|
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>
|
|
61
|
-
<tr><td><strong>
|
|
62
|
-
<tr><td><strong>
|
|
63
|
-
<tr><td><strong>
|
|
64
|
-
<tr><td><strong>
|
|
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 —
|
|
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 —
|
|
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
|
-
```
|
|
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
|
|
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
|
-
```
|
|
120
|
-
//
|
|
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
|
-
###
|
|
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` —
|
|
128
|
+
#### `forEach` — bulk processing
|
|
135
129
|
|
|
136
|
-
|
|
130
|
+
Iterates over matching archetypes. You get the backing TypedArrays directly.
|
|
137
131
|
|
|
138
|
-
```
|
|
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
|
-
|
|
149
|
+
Returns entity IDs for when you need to target specific entities.
|
|
156
150
|
|
|
157
|
-
```
|
|
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** |
|
|
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
|
-
```
|
|
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
|
-
|
|
255
|
+
Supports stripping components, skipping entities, and custom serializers.
|
|
204
256
|
|
|
205
257
|
---
|
|
206
258
|
|
|
207
259
|
## TypeScript
|
|
208
260
|
|
|
209
|
-
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|
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
|
|
279
|
-
| `set(id, Comp.field, value)` | Write a single field
|
|
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
|
|
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
|
-
```
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
package/bench/allocations-1m.js
CHANGED
|
@@ -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;
|
package/bench/multi-ecs-bench.js
CHANGED
|
@@ -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;
|
|
@@ -1,38 +1,34 @@
|
|
|
1
|
-
// Benchmark:
|
|
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
|
-
// ---
|
|
9
|
-
function
|
|
10
|
-
const Pos = component('
|
|
11
|
-
const Vel = component('
|
|
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
|
-
|
|
28
|
-
for (let
|
|
29
|
-
em.
|
|
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
|
-
// ---
|
|
53
|
-
function
|
|
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
|
-
|
|
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
|
|
61
|
-
pos.y
|
|
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) /
|
|
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===
|
|
133
|
-
|
|
134
|
-
// Warmup
|
|
135
|
-
benchTypedCreate();
|
|
136
|
-
benchUntypedCreate();
|
|
144
|
+
console.log(`\n=== Access patterns: ${(COUNT / 1e6).toFixed(0)}M entities ===\n`);
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
console.log(`
|
|
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
|
-
|
|
148
|
-
|
|
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(`\
|
|
152
|
-
console.log(`
|
|
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
|
|
159
|
-
|
|
160
|
-
console.log(`
|
|
161
|
-
console.log(`
|
|
162
|
-
console.log(`
|
|
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();
|