archetype-ecs 1.2.0 → 1.3.2
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 +126 -63
- 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/ComponentRegistry.d.ts +18 -0
- package/dist/ComponentRegistry.js +29 -0
- package/dist/EntityManager.d.ts +52 -0
- package/dist/EntityManager.js +891 -0
- package/dist/Profiler.d.ts +12 -0
- package/dist/Profiler.js +38 -0
- package/dist/System.d.ts +41 -0
- package/dist/System.js +159 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +29 -0
- package/dist/src/ComponentRegistry.d.ts +18 -0
- package/dist/src/ComponentRegistry.js +29 -0
- package/dist/src/EntityManager.d.ts +49 -0
- package/dist/src/EntityManager.js +853 -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 +14 -0
- package/dist/src/index.js +29 -0
- package/dist/tests/EntityManager.test.d.ts +1 -0
- package/dist/tests/EntityManager.test.js +651 -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 +9 -7
- package/src/ComponentRegistry.ts +49 -0
- package/src/EntityManager.ts +1018 -0
- package/src/{Profiler.js → Profiler.ts} +18 -5
- package/src/System.ts +226 -0
- package/src/index.ts +44 -0
- package/tests/{EntityManager.test.js → EntityManager.test.ts} +338 -70
- package/tests/System.test.ts +730 -0
- package/tests/types.ts +67 -66
- package/tsconfig.json +8 -5
- package/tsconfig.test.json +13 -0
- 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,16 +121,16 @@ 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
|
-
```
|
|
139
|
-
function movementSystem(dt) {
|
|
132
|
+
```ts
|
|
133
|
+
function movementSystem(dt: number) {
|
|
140
134
|
em.forEach([Position, Velocity], (arch) => {
|
|
141
135
|
const px = arch.field(Position.x) // Float32Array
|
|
142
136
|
const py = arch.field(Position.y)
|
|
@@ -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, type EntityId } 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: EntityId) {
|
|
206
|
+
console.log(`Entity ${id} spawned with ${this.em.get(id, Health.hp)} HP`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@OnRemoved(Health)
|
|
210
|
+
onDeath(id: EntityId) {
|
|
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,41 +252,25 @@ 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
|
-
// Schema is inferred — Position becomes ComponentDef<
|
|
264
|
+
// Schema is inferred — Position becomes ComponentDef<'x' | 'y'>
|
|
213
265
|
const Position = component('Position', 'f32', ['x', 'y'])
|
|
214
266
|
|
|
215
|
-
Position.x // autocompletes to .x and .y
|
|
216
|
-
Position.z // Property 'z' does not exist
|
|
267
|
+
Position.x // FieldRef — autocompletes to .x and .y
|
|
268
|
+
Position.z // compile error: Property 'z' does not exist
|
|
217
269
|
|
|
218
|
-
em.get(id, Position.x) //
|
|
219
|
-
em.set(id, Position.
|
|
270
|
+
em.get(id, Position.x) // zero-alloc field access
|
|
271
|
+
em.set(id, Position.x, 5) // zero-alloc field write
|
|
220
272
|
|
|
221
|
-
|
|
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
|
|
273
|
+
arch.field(Position.x) // Float32Array — direct TypedArray access
|
|
238
274
|
```
|
|
239
275
|
|
|
240
276
|
---
|
|
@@ -249,7 +285,7 @@ Tag component — no data, used as a marker for queries.
|
|
|
249
285
|
|
|
250
286
|
Schema component with uniform field type.
|
|
251
287
|
|
|
252
|
-
```
|
|
288
|
+
```ts
|
|
253
289
|
const Position = component('Position', 'f32', ['x', 'y'])
|
|
254
290
|
const Name = component('Name', 'string', ['name', 'title'])
|
|
255
291
|
```
|
|
@@ -258,7 +294,7 @@ const Name = component('Name', 'string', ['name', 'title'])
|
|
|
258
294
|
|
|
259
295
|
Schema component with mixed field types.
|
|
260
296
|
|
|
261
|
-
```
|
|
297
|
+
```ts
|
|
262
298
|
const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })
|
|
263
299
|
```
|
|
264
300
|
|
|
@@ -269,27 +305,51 @@ Returns an entity manager with the following methods:
|
|
|
269
305
|
| Method | Description |
|
|
270
306
|
|---|---|
|
|
271
307
|
| `createEntity()` | Create an empty entity |
|
|
272
|
-
| `createEntityWith(Comp, data, ...)` | Create entity with components
|
|
308
|
+
| `createEntityWith(Comp, data, ...)` | Create entity with components in one call |
|
|
273
309
|
| `destroyEntity(id)` | Remove entity and all its components |
|
|
274
310
|
| `addComponent(id, Comp, data)` | Add a component to an existing entity |
|
|
275
311
|
| `removeComponent(id, Comp)` | Remove a component |
|
|
276
312
|
| `hasComponent(id, Comp)` | Check if entity has a component |
|
|
277
313
|
| `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
|
|
314
|
+
| `get(id, Comp.field)` | Read a single field |
|
|
315
|
+
| `set(id, Comp.field, value)` | Write a single field |
|
|
280
316
|
| `query(include, exclude?)` | Get matching entity IDs |
|
|
281
317
|
| `count(include, exclude?)` | Count matching entities |
|
|
282
|
-
| `forEach(include, callback, exclude?)` | Iterate archetypes with
|
|
318
|
+
| `forEach(include, callback, exclude?)` | Iterate archetypes with TypedArray access |
|
|
319
|
+
| `onAdd(Comp, callback)` | Register callback for component additions *(deferred)* |
|
|
320
|
+
| `onRemove(Comp, callback)` | Register callback for component removals *(deferred)* |
|
|
321
|
+
| `flushHooks()` | Collect pending add/remove events for registered hooks |
|
|
283
322
|
| `serialize(symbolToName, strip?, skip?, opts?)` | Serialize world to JSON-friendly object |
|
|
284
323
|
| `deserialize(data, nameToSymbol, opts?)` | Restore world from serialized data |
|
|
285
324
|
|
|
325
|
+
### `System`
|
|
326
|
+
|
|
327
|
+
Base class for decorator-based systems.
|
|
328
|
+
|
|
329
|
+
| | Description |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `@OnAdded(...Comps)` | Decorator — method fires when entity gains **all** specified components |
|
|
332
|
+
| `@OnRemoved(...Comps)` | Decorator — method fires when **any** specified component is removed |
|
|
333
|
+
| `tick()` | Override — called every `run()` after hook callbacks |
|
|
334
|
+
| `forEach(types, callback, exclude?)` | Shorthand for `this.em.forEach(...)` |
|
|
335
|
+
| `run()` | Fire buffered hook callbacks, then `tick()` |
|
|
336
|
+
| `dispose()` | Unsubscribe all hooks |
|
|
337
|
+
|
|
338
|
+
### `createSystem(em, constructor)`
|
|
339
|
+
|
|
340
|
+
Functional alternative to class-based systems. The constructor receives a context with `onAdded`, `onRemoved`, and `forEach`, and optionally returns a tick function.
|
|
341
|
+
|
|
342
|
+
### `createSystems(em, entries)`
|
|
343
|
+
|
|
344
|
+
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.
|
|
345
|
+
|
|
286
346
|
---
|
|
287
347
|
|
|
288
348
|
## Benchmarks
|
|
289
349
|
|
|
290
350
|
1M entities, Position += Velocity, 5 runs (median), Node.js:
|
|
291
351
|
|
|
292
|
-
| | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex |
|
|
352
|
+
| | 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
353
|
|---|---:|---:|---:|---:|---:|
|
|
294
354
|
| **Iteration** (ms/frame) | **1.7** | 2.2 | 2.2 | 1.8 | 32.5 |
|
|
295
355
|
| **Entity creation** (ms) | 401 | 366 | **106** | 248 | 265 |
|
|
@@ -297,7 +357,7 @@ Returns an entity manager with the following methods:
|
|
|
297
357
|
|
|
298
358
|
Each library runs the same test — iterate 1M entities over 500 frames:
|
|
299
359
|
|
|
300
|
-
```
|
|
360
|
+
```ts
|
|
301
361
|
// archetype-ecs
|
|
302
362
|
em.forEach([Position, Velocity], (arch) => {
|
|
303
363
|
const px = arch.field(Position.x) // Float32Array, dense
|
|
@@ -311,7 +371,7 @@ em.forEach([Position, Velocity], (arch) => {
|
|
|
311
371
|
})
|
|
312
372
|
```
|
|
313
373
|
|
|
314
|
-
archetype-ecs
|
|
374
|
+
archetype-ecs is fastest at iteration. Harmony-ecs and wolf-ecs are close; miniplex is ~20x slower due to object-based storage.
|
|
315
375
|
|
|
316
376
|
Run them yourself:
|
|
317
377
|
|
|
@@ -323,7 +383,7 @@ npm run bench
|
|
|
323
383
|
|
|
324
384
|
## Feature comparison
|
|
325
385
|
|
|
326
|
-
|
|
386
|
+
Compared against other JS ECS libraries:
|
|
327
387
|
|
|
328
388
|
### Unique to archetype-ecs
|
|
329
389
|
|
|
@@ -333,6 +393,7 @@ How archetype-ecs stacks up against other JS ECS libraries:
|
|
|
333
393
|
| Mixed string + numeric components | ✓ | — | — | — | — |
|
|
334
394
|
| `forEach` with dense TypedArray field access | ✓ | — | — | — | — |
|
|
335
395
|
| Field descriptors for both per-entity and bulk access | ✓ | — | — | — | — |
|
|
396
|
+
| TC39 decorator system (`@OnAdded` / `@OnRemoved`) | ✓ | — | — | — | — |
|
|
336
397
|
| Built-in profiler | ✓ | — | — | — | — |
|
|
337
398
|
|
|
338
399
|
### Full comparison
|
|
@@ -345,12 +406,14 @@ How archetype-ecs stacks up against other JS ECS libraries:
|
|
|
345
406
|
| TypeScript type inference | ✓ | — | ✓ | ✓ | ✓✓ |
|
|
346
407
|
| Batch entity creation | ✓ | — | — | ✓ | ✓ |
|
|
347
408
|
| Zero-alloc per-entity access | ✓ | ✓ | ✓ | ✓ | — |
|
|
409
|
+
| System framework (class + functional) | ✓ | — | — | — | — |
|
|
410
|
+
| Component lifecycle hooks | ✓ | — | — | — | ✓ |
|
|
348
411
|
| Relations / hierarchies | — | ✓ | — | — | — |
|
|
349
412
|
| React integration | — | — | — | — | ✓ |
|
|
350
413
|
|
|
351
414
|
✓✓ = notably stronger implementation in that library.
|
|
352
415
|
|
|
353
|
-
archetype-ecs is the only
|
|
416
|
+
archetype-ecs is the only one that combines fast iteration, string storage, serialization, decorator-based systems, and type safety.
|
|
354
417
|
|
|
355
418
|
---
|
|
356
419
|
|
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 '../
|
|
7
|
+
import { createEntityManager, component } from '../dist/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 '../
|
|
5
|
+
import { createEntityManager, component } from '../dist/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 '../
|
|
3
|
+
import { createEntityManager, component } from '../dist/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();
|