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 +97 -11
- 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/src/ComponentRegistry.js +0 -21
- package/src/EntityManager.js +0 -578
- package/src/index.d.ts +0 -118
- package/src/index.js +0 -37
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
An Entity Component System for games and simulations in
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
###
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|
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();
|
package/bench/vs-bitecs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Benchmark: archetype-ecs
|
|
1
|
+
// Benchmark: archetype-ecs vs bitECS
|
|
2
2
|
// Tests: system loop, entity creation, component add/remove churn
|
|
3
3
|
|
|
4
|
-
import { createEntityManager } from '../src/
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 |
|
|
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
|
|
223
|
-
const
|
|
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
|
-
`${
|
|
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
|
+
}
|