archetype-ecs 1.3.1 → 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 +104 -34
- 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/src/ComponentRegistry.js +0 -21
- package/src/EntityManager.js +0 -578
- package/src/index.d.ts +0 -118
- package/src/index.js +0 -37
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, beforeEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { createEntityManager } from '../src/EntityManager.js';
|
|
4
|
-
import { component } from '../src/index.js';
|
|
3
|
+
import { createEntityManager, type EntityManager } from '../src/EntityManager.js';
|
|
4
|
+
import { component, type ComponentDef } from '../src/index.js';
|
|
5
5
|
|
|
6
6
|
describe('EntityManager', () => {
|
|
7
|
-
let em;
|
|
7
|
+
let em: EntityManager;
|
|
8
8
|
const Position = component('Position', 'f32', ['x', 'y']);
|
|
9
9
|
const Velocity = component('Velocity', 'f32', ['vx', 'vy']);
|
|
10
10
|
const Health = component('Health', 'f32', ['hp']);
|
|
@@ -140,7 +140,7 @@ describe('EntityManager', () => {
|
|
|
140
140
|
[Velocity._sym, 'Velocity'],
|
|
141
141
|
[Health._sym, 'Health']
|
|
142
142
|
]);
|
|
143
|
-
const nameToSymbol = { Position, Velocity, Health };
|
|
143
|
+
const nameToSymbol: Record<string, ComponentDef> = { Position, Velocity, Health };
|
|
144
144
|
|
|
145
145
|
it('round-trips entities and components', () => {
|
|
146
146
|
const a = em.createEntity();
|
|
@@ -189,33 +189,33 @@ describe('EntityManager', () => {
|
|
|
189
189
|
const a = em.createEntity();
|
|
190
190
|
em.addComponent(a, Meta, { x: 1, y: 2, secret: 42 });
|
|
191
191
|
|
|
192
|
-
const serializers = new Map([
|
|
193
|
-
['Meta', (data) => ({ x: data.x, y: data.y })]
|
|
192
|
+
const serializers = new Map<string, (data: any) => any>([
|
|
193
|
+
['Meta', (data: any) => ({ x: data.x, y: data.y })]
|
|
194
194
|
]);
|
|
195
195
|
|
|
196
196
|
const result = em.serialize(metaSymbolToName, [], [], { serializers });
|
|
197
197
|
assert.deepEqual(result.components['Meta'][a], { x: 1, y: 2 });
|
|
198
|
-
assert.equal(result.components['Meta'][a].secret, undefined);
|
|
198
|
+
assert.equal((result.components['Meta'][a] as Record<string, unknown>).secret, undefined);
|
|
199
199
|
});
|
|
200
200
|
|
|
201
201
|
it('custom deserializers are used when provided', () => {
|
|
202
202
|
const Meta = component('Meta2', { x: 'f32', y: 'f32' });
|
|
203
203
|
const metaSymbolToName = new Map([...symbolToName, [Meta._sym, 'Meta2']]);
|
|
204
|
-
const metaNameToSymbol = { ...nameToSymbol, Meta2: Meta };
|
|
204
|
+
const metaNameToSymbol: Record<string, ComponentDef> = { ...nameToSymbol, Meta2: Meta };
|
|
205
205
|
|
|
206
206
|
const a = em.createEntity();
|
|
207
207
|
em.addComponent(a, Meta, { x: 1, y: 2 });
|
|
208
208
|
|
|
209
209
|
const data = em.serialize(metaSymbolToName);
|
|
210
210
|
|
|
211
|
-
const deserializers = new Map([
|
|
212
|
-
['Meta2', (compData) => ({ ...compData, restored: true })]
|
|
211
|
+
const deserializers = new Map<string, (data: unknown) => unknown>([
|
|
212
|
+
['Meta2', (compData) => ({ ...(compData as Record<string, unknown>), restored: true })]
|
|
213
213
|
]);
|
|
214
214
|
|
|
215
215
|
em.deserialize(data, metaNameToSymbol, { deserializers });
|
|
216
|
-
const result = em.getComponent(a, Meta)
|
|
217
|
-
assert.ok(Math.abs(result.x - 1) < 0.01);
|
|
218
|
-
assert.ok(Math.abs(result.y - 2) < 0.01);
|
|
216
|
+
const result = em.getComponent(a, Meta)!;
|
|
217
|
+
assert.ok(Math.abs(result.x as number - 1) < 0.01);
|
|
218
|
+
assert.ok(Math.abs(result.y as number - 2) < 0.01);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
it('deserialize clears previous state', () => {
|
|
@@ -229,7 +229,7 @@ describe('EntityManager', () => {
|
|
|
229
229
|
});
|
|
230
230
|
|
|
231
231
|
describe('Typed Components (SoA)', () => {
|
|
232
|
-
let em;
|
|
232
|
+
let em: EntityManager;
|
|
233
233
|
|
|
234
234
|
beforeEach(() => {
|
|
235
235
|
em = createEntityManager();
|
|
@@ -239,23 +239,23 @@ describe('Typed Components (SoA)', () => {
|
|
|
239
239
|
const Pos = component('Pos', 'f32', ['x', 'y']);
|
|
240
240
|
const id = em.createEntity();
|
|
241
241
|
em.addComponent(id, Pos, { x: 1.5, y: 2.5 });
|
|
242
|
-
const result = em.getComponent(id, Pos)
|
|
243
|
-
assert.ok(Math.abs(result.x - 1.5) < 0.001);
|
|
244
|
-
assert.ok(Math.abs(result.y - 2.5) < 0.001);
|
|
242
|
+
const result = em.getComponent(id, Pos)!;
|
|
243
|
+
assert.ok(Math.abs(result.x as number - 1.5) < 0.001);
|
|
244
|
+
assert.ok(Math.abs(result.y as number - 2.5) < 0.001);
|
|
245
245
|
});
|
|
246
246
|
|
|
247
247
|
it('growth past initial capacity (>64 entities)', () => {
|
|
248
248
|
const Pos = component('PosGrow', 'f32', ['x', 'y']);
|
|
249
|
-
const ids = [];
|
|
249
|
+
const ids: number[] = [];
|
|
250
250
|
for (let i = 0; i < 100; i++) {
|
|
251
251
|
const id = em.createEntity();
|
|
252
252
|
em.addComponent(id, Pos, { x: i, y: i * 2 });
|
|
253
253
|
ids.push(id);
|
|
254
254
|
}
|
|
255
255
|
for (let i = 0; i < 100; i++) {
|
|
256
|
-
const result = em.getComponent(ids[i], Pos)
|
|
257
|
-
assert.ok(Math.abs(result.x - i) < 0.001);
|
|
258
|
-
assert.ok(Math.abs(result.y - i * 2) < 0.001);
|
|
256
|
+
const result = em.getComponent(ids[i], Pos)!;
|
|
257
|
+
assert.ok(Math.abs(result.x as number - i) < 0.001);
|
|
258
|
+
assert.ok(Math.abs(result.y as number - i * 2) < 0.001);
|
|
259
259
|
}
|
|
260
260
|
});
|
|
261
261
|
|
|
@@ -270,13 +270,10 @@ describe('Typed Components (SoA)', () => {
|
|
|
270
270
|
|
|
271
271
|
em.destroyEntity(a);
|
|
272
272
|
|
|
273
|
-
const resultB = em.getComponent(b, Pos)
|
|
274
|
-
assert.ok(Math.abs(resultB.x - 3) < 0.001);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const resultC = em.getComponent(c, Pos);
|
|
278
|
-
assert.ok(Math.abs(resultC.x - 5) < 0.001);
|
|
279
|
-
assert.ok(Math.abs(resultC.y - 6) < 0.001);
|
|
273
|
+
const resultB = em.getComponent(b, Pos)!;
|
|
274
|
+
assert.ok(Math.abs(resultB.x as number - 3) < 0.001);
|
|
275
|
+
const resultC = em.getComponent(c, Pos)!;
|
|
276
|
+
assert.ok(Math.abs(resultC.x as number - 5) < 0.001);
|
|
280
277
|
});
|
|
281
278
|
|
|
282
279
|
it('typed + tag on same entity', () => {
|
|
@@ -286,11 +283,8 @@ describe('Typed Components (SoA)', () => {
|
|
|
286
283
|
em.addComponent(id, Pos, { x: 10, y: 20 });
|
|
287
284
|
em.addComponent(id, Tag, {});
|
|
288
285
|
|
|
289
|
-
const pos = em.getComponent(id, Pos)
|
|
290
|
-
assert.ok(Math.abs(pos.x - 10) < 0.001);
|
|
291
|
-
assert.ok(Math.abs(pos.y - 20) < 0.001);
|
|
292
|
-
|
|
293
|
-
// Tag has no schema, getComponent returns undefined
|
|
286
|
+
const pos = em.getComponent(id, Pos)!;
|
|
287
|
+
assert.ok(Math.abs(pos.x as number - 10) < 0.001);
|
|
294
288
|
assert.equal(em.getComponent(id, Tag), undefined);
|
|
295
289
|
assert.equal(em.hasComponent(id, Tag), true);
|
|
296
290
|
});
|
|
@@ -306,10 +300,10 @@ describe('Typed Components (SoA)', () => {
|
|
|
306
300
|
}
|
|
307
301
|
|
|
308
302
|
em.forEach([Pos, Vel], (arch) => {
|
|
309
|
-
const px = arch.field(Pos.x);
|
|
310
|
-
const py = arch.field(Pos.y);
|
|
311
|
-
const vx = arch.field(Vel.vx);
|
|
312
|
-
const vy = arch.field(Vel.vy);
|
|
303
|
+
const px = arch.field(Pos.x) as Float32Array;
|
|
304
|
+
const py = arch.field(Pos.y) as Float32Array;
|
|
305
|
+
const vx = arch.field(Vel.vx) as Float32Array;
|
|
306
|
+
const vy = arch.field(Vel.vy) as Float32Array;
|
|
313
307
|
for (let i = 0; i < arch.count; i++) {
|
|
314
308
|
px[i] += vx[i];
|
|
315
309
|
py[i] += vy[i];
|
|
@@ -318,16 +312,16 @@ describe('Typed Components (SoA)', () => {
|
|
|
318
312
|
|
|
319
313
|
const ids = em.query([Pos, Vel]);
|
|
320
314
|
for (const id of ids) {
|
|
321
|
-
const pos = em.getComponent(id, Pos)
|
|
322
|
-
assert.ok(pos.x >= 1);
|
|
323
|
-
assert.ok(Math.abs(pos.y - 2) < 0.001);
|
|
315
|
+
const pos = em.getComponent(id, Pos)!;
|
|
316
|
+
assert.ok((pos.x as number) >= 1);
|
|
317
|
+
assert.ok(Math.abs(pos.y as number - 2) < 0.001);
|
|
324
318
|
}
|
|
325
319
|
});
|
|
326
320
|
|
|
327
321
|
it('serialize/deserialize round-trip with typed components', () => {
|
|
328
322
|
const Pos = component('PosSer', 'f32', ['x', 'y']);
|
|
329
323
|
const symbolToName = new Map([[Pos._sym, 'PosSer']]);
|
|
330
|
-
const nameToSymbol = { PosSer: Pos };
|
|
324
|
+
const nameToSymbol: Record<string, ComponentDef> = { PosSer: Pos };
|
|
331
325
|
|
|
332
326
|
const a = em.createEntity();
|
|
333
327
|
em.addComponent(a, Pos, { x: 1.5, y: 2.5 });
|
|
@@ -337,13 +331,10 @@ describe('Typed Components (SoA)', () => {
|
|
|
337
331
|
const data = em.serialize(symbolToName);
|
|
338
332
|
em.deserialize(data, nameToSymbol);
|
|
339
333
|
|
|
340
|
-
const posA = em.getComponent(a, Pos)
|
|
341
|
-
assert.ok(Math.abs(posA.x - 1.5) < 0.01);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const posB = em.getComponent(b, Pos);
|
|
345
|
-
assert.ok(Math.abs(posB.x - 3.5) < 0.01);
|
|
346
|
-
assert.ok(Math.abs(posB.y - 4.5) < 0.01);
|
|
334
|
+
const posA = em.getComponent(a, Pos)!;
|
|
335
|
+
assert.ok(Math.abs(posA.x as number - 1.5) < 0.01);
|
|
336
|
+
const posB = em.getComponent(b, Pos)!;
|
|
337
|
+
assert.ok(Math.abs(posB.x as number - 3.5) < 0.01);
|
|
347
338
|
});
|
|
348
339
|
|
|
349
340
|
it('archetype migration with typed components', () => {
|
|
@@ -354,18 +345,12 @@ describe('Typed Components (SoA)', () => {
|
|
|
354
345
|
em.addComponent(id, Pos, { x: 5, y: 10 });
|
|
355
346
|
em.addComponent(id, Vel, { vx: 1, vy: 2 });
|
|
356
347
|
|
|
357
|
-
const pos = em.getComponent(id, Pos)
|
|
358
|
-
assert.ok(Math.abs(pos.x - 5) < 0.001);
|
|
359
|
-
assert.ok(Math.abs(pos.y - 10) < 0.001);
|
|
360
|
-
|
|
361
|
-
const vel = em.getComponent(id, Vel);
|
|
362
|
-
assert.ok(Math.abs(vel.vx - 1) < 0.001);
|
|
363
|
-
assert.ok(Math.abs(vel.vy - 2) < 0.001);
|
|
348
|
+
const pos = em.getComponent(id, Pos)!;
|
|
349
|
+
assert.ok(Math.abs(pos.x as number - 5) < 0.001);
|
|
364
350
|
|
|
365
351
|
em.removeComponent(id, Vel);
|
|
366
|
-
const pos2 = em.getComponent(id, Pos)
|
|
367
|
-
assert.ok(Math.abs(pos2.x - 5) < 0.001);
|
|
368
|
-
assert.ok(Math.abs(pos2.y - 10) < 0.001);
|
|
352
|
+
const pos2 = em.getComponent(id, Pos)!;
|
|
353
|
+
assert.ok(Math.abs(pos2.x as number - 5) < 0.001);
|
|
369
354
|
assert.equal(em.hasComponent(id, Vel), false);
|
|
370
355
|
});
|
|
371
356
|
|
|
@@ -374,20 +359,17 @@ describe('Typed Components (SoA)', () => {
|
|
|
374
359
|
const id = em.createEntity();
|
|
375
360
|
em.addComponent(id, Pos, { x: 1, y: 2 });
|
|
376
361
|
em.addComponent(id, Pos, { x: 99, y: 88 });
|
|
377
|
-
const result = em.getComponent(id, Pos)
|
|
378
|
-
assert.ok(Math.abs(result.x - 99) < 0.001);
|
|
379
|
-
assert.ok(Math.abs(result.y - 88) < 0.001);
|
|
362
|
+
const result = em.getComponent(id, Pos)!;
|
|
363
|
+
assert.ok(Math.abs(result.x as number - 99) < 0.001);
|
|
380
364
|
});
|
|
381
365
|
|
|
382
366
|
it('get/set for zero-allocation field access', () => {
|
|
383
367
|
const Pos = component('PosGS', 'f32', ['x', 'y']);
|
|
384
368
|
const id = em.createEntity();
|
|
385
369
|
em.addComponent(id, Pos, { x: 3.5, y: 7.5 });
|
|
386
|
-
assert.ok(Math.abs(em.get(id, Pos.x) - 3.5) < 0.001);
|
|
387
|
-
assert.ok(Math.abs(em.get(id, Pos.y) - 7.5) < 0.001);
|
|
370
|
+
assert.ok(Math.abs(em.get(id, Pos.x)! as number - 3.5) < 0.001);
|
|
388
371
|
em.set(id, Pos.x, 42);
|
|
389
|
-
assert.ok(Math.abs(em.get(id, Pos.x) - 42) < 0.001);
|
|
390
|
-
assert.ok(Math.abs(em.get(id, Pos.y) - 7.5) < 0.001);
|
|
372
|
+
assert.ok(Math.abs(em.get(id, Pos.x)! as number - 42) < 0.001);
|
|
391
373
|
});
|
|
392
374
|
|
|
393
375
|
it('get returns undefined for missing entity/component', () => {
|
|
@@ -465,11 +447,7 @@ describe('Typed Components (SoA)', () => {
|
|
|
465
447
|
em.addComponent(id, Item, { name: 'Sword', weight: 3.5 });
|
|
466
448
|
|
|
467
449
|
assert.equal(em.get(id, Item.name), 'Sword');
|
|
468
|
-
assert.ok(Math.abs(em.get(id, Item.weight) - 3.5) < 0.01);
|
|
469
|
-
|
|
470
|
-
const obj = em.getComponent(id, Item);
|
|
471
|
-
assert.equal(obj.name, 'Sword');
|
|
472
|
-
assert.ok(Math.abs(obj.weight - 3.5) < 0.01);
|
|
450
|
+
assert.ok(Math.abs(em.get(id, Item.weight)! as number - 3.5) < 0.01);
|
|
473
451
|
});
|
|
474
452
|
|
|
475
453
|
it('string component forEach field access', () => {
|
|
@@ -487,3 +465,293 @@ describe('Typed Components (SoA)', () => {
|
|
|
487
465
|
});
|
|
488
466
|
});
|
|
489
467
|
});
|
|
468
|
+
|
|
469
|
+
describe('Deferred Structural Changes during forEach', () => {
|
|
470
|
+
let em: EntityManager;
|
|
471
|
+
const Pos = component('DPos', 'f32', ['x', 'y']);
|
|
472
|
+
const Vel = component('DVel', 'f32', ['vx', 'vy']);
|
|
473
|
+
|
|
474
|
+
beforeEach(() => {
|
|
475
|
+
em = createEntityManager();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('removeComponent during forEach is deferred and applied after', () => {
|
|
479
|
+
const a = em.createEntity();
|
|
480
|
+
const b = em.createEntity();
|
|
481
|
+
const c = em.createEntity();
|
|
482
|
+
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
483
|
+
em.addComponent(b, Pos, { x: 2, y: 0 });
|
|
484
|
+
em.addComponent(c, Pos, { x: 3, y: 0 });
|
|
485
|
+
|
|
486
|
+
const visited: number[] = [];
|
|
487
|
+
em.forEach([Pos], (arch) => {
|
|
488
|
+
const ids = arch.entityIds;
|
|
489
|
+
for (let i = 0; i < arch.count; i++) {
|
|
490
|
+
visited.push(ids[i]);
|
|
491
|
+
if (ids[i] === a) em.removeComponent(a, Pos);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
assert.equal(visited.length, 3);
|
|
496
|
+
assert.equal(em.hasComponent(a, Pos), false);
|
|
497
|
+
assert.equal(em.hasComponent(b, Pos), true);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('addComponent (migration) during forEach is deferred', () => {
|
|
501
|
+
const a = em.createEntity();
|
|
502
|
+
const b = em.createEntity();
|
|
503
|
+
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
504
|
+
em.addComponent(b, Pos, { x: 2, y: 0 });
|
|
505
|
+
|
|
506
|
+
em.forEach([Pos], (arch) => {
|
|
507
|
+
const ids = arch.entityIds;
|
|
508
|
+
for (let i = 0; i < arch.count; i++) {
|
|
509
|
+
if (ids[i] === a) em.addComponent(a, Vel, { vx: 10, vy: 20 });
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
assert.equal(em.hasComponent(a, Vel), true);
|
|
514
|
+
const vel = em.getComponent(a, Vel)!;
|
|
515
|
+
assert.ok(Math.abs((vel.vx as number) - 10) < 0.001);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('addComponent overwrite during forEach is immediate (no migration)', () => {
|
|
519
|
+
const a = em.createEntity();
|
|
520
|
+
em.addComponent(a, Pos, { x: 1, y: 2 });
|
|
521
|
+
|
|
522
|
+
em.forEach([Pos], (arch) => {
|
|
523
|
+
const px = arch.field(Pos.x) as Float32Array;
|
|
524
|
+
em.addComponent(a, Pos, { x: 99, y: 88 });
|
|
525
|
+
assert.ok(Math.abs(px[0] - 99) < 0.001);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('destroyEntity during forEach is deferred', () => {
|
|
530
|
+
const a = em.createEntity();
|
|
531
|
+
const b = em.createEntity();
|
|
532
|
+
const c = em.createEntity();
|
|
533
|
+
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
534
|
+
em.addComponent(b, Pos, { x: 2, y: 0 });
|
|
535
|
+
em.addComponent(c, Pos, { x: 3, y: 0 });
|
|
536
|
+
|
|
537
|
+
const visited: number[] = [];
|
|
538
|
+
em.forEach([Pos], (arch) => {
|
|
539
|
+
const ids = arch.entityIds;
|
|
540
|
+
for (let i = 0; i < arch.count; i++) {
|
|
541
|
+
visited.push(ids[i]);
|
|
542
|
+
if (ids[i] === b) em.destroyEntity(b);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
assert.equal(visited.length, 3);
|
|
547
|
+
assert.equal(em.hasComponent(b, Pos), false);
|
|
548
|
+
assert.equal(em.getAllEntities().includes(b), false);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('multiple deferred operations are applied in order', () => {
|
|
552
|
+
const a = em.createEntity();
|
|
553
|
+
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
554
|
+
|
|
555
|
+
em.forEach([Pos], () => {
|
|
556
|
+
em.removeComponent(a, Pos);
|
|
557
|
+
em.addComponent(a, Vel, { vx: 5, vy: 6 });
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
assert.equal(em.hasComponent(a, Pos), false);
|
|
561
|
+
assert.equal(em.hasComponent(a, Vel), true);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('nested forEach properly defers until outermost forEach completes', () => {
|
|
565
|
+
const a = em.createEntity();
|
|
566
|
+
const b = em.createEntity();
|
|
567
|
+
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
568
|
+
em.addComponent(b, Vel, { vx: 2, vy: 0 });
|
|
569
|
+
|
|
570
|
+
em.forEach([Pos], () => {
|
|
571
|
+
em.forEach([Vel], () => {
|
|
572
|
+
em.removeComponent(b, Vel);
|
|
573
|
+
});
|
|
574
|
+
assert.equal(em.hasComponent(b, Vel), true); // still deferred
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
assert.equal(em.hasComponent(b, Vel), false);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('em.set() remains immediate during forEach', () => {
|
|
581
|
+
const a = em.createEntity();
|
|
582
|
+
em.addComponent(a, Pos, { x: 1, y: 2 });
|
|
583
|
+
|
|
584
|
+
em.forEach([Pos], (arch) => {
|
|
585
|
+
em.set(a, Pos.x, 42);
|
|
586
|
+
const px = arch.field(Pos.x) as Float32Array;
|
|
587
|
+
assert.ok(Math.abs(px[0] - 42) < 0.001);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('hooks still fire correctly with deferred structural changes', () => {
|
|
592
|
+
const added: number[] = [];
|
|
593
|
+
const removed: number[] = [];
|
|
594
|
+
em.onAdd(Vel, (id) => added.push(id));
|
|
595
|
+
em.onRemove(Pos, (id) => removed.push(id));
|
|
596
|
+
|
|
597
|
+
const a = em.createEntity();
|
|
598
|
+
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
599
|
+
em.flushHooks();
|
|
600
|
+
|
|
601
|
+
em.forEach([Pos], () => {
|
|
602
|
+
em.removeComponent(a, Pos);
|
|
603
|
+
em.addComponent(a, Vel, { vx: 1, vy: 2 });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
em.flushHooks();
|
|
607
|
+
assert.deepEqual(removed, [a]);
|
|
608
|
+
assert.deepEqual(added, [a]);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe('Deferred Hooks (onAdd / onRemove)', () => {
|
|
613
|
+
let em: EntityManager;
|
|
614
|
+
const Position = component('HPos', 'f32', ['x', 'y']);
|
|
615
|
+
const Velocity = component('HVel', 'f32', ['vx', 'vy']);
|
|
616
|
+
const Health = component('HHealth', 'f32', ['hp']);
|
|
617
|
+
|
|
618
|
+
beforeEach(() => {
|
|
619
|
+
em = createEntityManager();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('onAdd fires after flushHooks with correct entity IDs', () => {
|
|
623
|
+
const added: number[] = [];
|
|
624
|
+
em.onAdd(Position, (id) => added.push(id));
|
|
625
|
+
|
|
626
|
+
const a = em.createEntity();
|
|
627
|
+
em.addComponent(a, Position, { x: 1, y: 2 });
|
|
628
|
+
const b = em.createEntity();
|
|
629
|
+
em.addComponent(b, Position, { x: 3, y: 4 });
|
|
630
|
+
|
|
631
|
+
assert.deepEqual(added, []);
|
|
632
|
+
em.flushHooks();
|
|
633
|
+
assert.deepEqual(added, [a, b]);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('onRemove fires after flushHooks on removeComponent', () => {
|
|
637
|
+
const removed: number[] = [];
|
|
638
|
+
em.onRemove(Position, (id) => removed.push(id));
|
|
639
|
+
|
|
640
|
+
const id = em.createEntity();
|
|
641
|
+
em.addComponent(id, Position, { x: 1, y: 2 });
|
|
642
|
+
em.removeComponent(id, Position);
|
|
643
|
+
|
|
644
|
+
assert.deepEqual(removed, []);
|
|
645
|
+
em.flushHooks();
|
|
646
|
+
assert.deepEqual(removed, [id]);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('onRemove fires for each component on destroyEntity', () => {
|
|
650
|
+
const removedPos: number[] = [];
|
|
651
|
+
const removedVel: number[] = [];
|
|
652
|
+
em.onRemove(Position, (id) => removedPos.push(id));
|
|
653
|
+
em.onRemove(Velocity, (id) => removedVel.push(id));
|
|
654
|
+
|
|
655
|
+
const id = em.createEntityWith(Position, { x: 1, y: 2 }, Velocity, { vx: 3, vy: 4 });
|
|
656
|
+
em.destroyEntity(id);
|
|
657
|
+
em.flushHooks();
|
|
658
|
+
|
|
659
|
+
assert.deepEqual(removedPos, [id]);
|
|
660
|
+
assert.deepEqual(removedVel, [id]);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('createEntityWith triggers onAdd for all component types', () => {
|
|
664
|
+
const addedPos: number[] = [];
|
|
665
|
+
const addedVel: number[] = [];
|
|
666
|
+
em.onAdd(Position, (id) => addedPos.push(id));
|
|
667
|
+
em.onAdd(Velocity, (id) => addedVel.push(id));
|
|
668
|
+
|
|
669
|
+
const id = em.createEntityWith(Position, { x: 1, y: 2 }, Velocity, { vx: 3, vy: 4 });
|
|
670
|
+
em.flushHooks();
|
|
671
|
+
|
|
672
|
+
assert.deepEqual(addedPos, [id]);
|
|
673
|
+
assert.deepEqual(addedVel, [id]);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('callbacks do not fire before flushHooks (deferred)', () => {
|
|
677
|
+
const added: number[] = [];
|
|
678
|
+
em.onAdd(Position, (id) => added.push(id));
|
|
679
|
+
|
|
680
|
+
const id = em.createEntity();
|
|
681
|
+
em.addComponent(id, Position, { x: 1, y: 2 });
|
|
682
|
+
|
|
683
|
+
assert.deepEqual(added, []);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('unsubscribe prevents callback from firing', () => {
|
|
687
|
+
const added: number[] = [];
|
|
688
|
+
const unsub = em.onAdd(Position, (id) => added.push(id));
|
|
689
|
+
|
|
690
|
+
const a = em.createEntity();
|
|
691
|
+
em.addComponent(a, Position, { x: 1, y: 2 });
|
|
692
|
+
unsub();
|
|
693
|
+
em.flushHooks();
|
|
694
|
+
|
|
695
|
+
assert.deepEqual(added, []);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('overwrite (addComponent with existing component) does not trigger onAdd', () => {
|
|
699
|
+
const added: number[] = [];
|
|
700
|
+
em.onAdd(Position, (id) => added.push(id));
|
|
701
|
+
|
|
702
|
+
const id = em.createEntity();
|
|
703
|
+
em.addComponent(id, Position, { x: 1, y: 2 });
|
|
704
|
+
em.flushHooks();
|
|
705
|
+
added.length = 0;
|
|
706
|
+
|
|
707
|
+
em.addComponent(id, Position, { x: 99, y: 88 });
|
|
708
|
+
em.flushHooks();
|
|
709
|
+
|
|
710
|
+
assert.deepEqual(added, []);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('multiple callbacks on the same component', () => {
|
|
714
|
+
const added1: number[] = [];
|
|
715
|
+
const added2: number[] = [];
|
|
716
|
+
em.onAdd(Position, (id) => added1.push(id));
|
|
717
|
+
em.onAdd(Position, (id) => added2.push(id));
|
|
718
|
+
|
|
719
|
+
const id = em.createEntity();
|
|
720
|
+
em.addComponent(id, Position, { x: 1, y: 2 });
|
|
721
|
+
em.flushHooks();
|
|
722
|
+
|
|
723
|
+
assert.deepEqual(added1, [id]);
|
|
724
|
+
assert.deepEqual(added2, [id]);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('flushHooks is a no-op when no hooks registered', () => {
|
|
728
|
+
const id = em.createEntity();
|
|
729
|
+
em.addComponent(id, Position, { x: 1, y: 2 });
|
|
730
|
+
em.flushHooks();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('addComponent migration triggers onAdd', () => {
|
|
734
|
+
const added: number[] = [];
|
|
735
|
+
em.onAdd(Velocity, (id) => added.push(id));
|
|
736
|
+
|
|
737
|
+
const id = em.createEntity();
|
|
738
|
+
em.addComponent(id, Position, { x: 1, y: 2 });
|
|
739
|
+
em.addComponent(id, Velocity, { vx: 1, vy: 1 });
|
|
740
|
+
em.flushHooks();
|
|
741
|
+
|
|
742
|
+
assert.deepEqual(added, [id]);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('pending arrays are cleared after flushHooks', () => {
|
|
746
|
+
const added: number[] = [];
|
|
747
|
+
em.onAdd(Position, (id) => added.push(id));
|
|
748
|
+
|
|
749
|
+
const a = em.createEntity();
|
|
750
|
+
em.addComponent(a, Position, { x: 1, y: 2 });
|
|
751
|
+
em.flushHooks();
|
|
752
|
+
assert.deepEqual(added, [a]);
|
|
753
|
+
|
|
754
|
+
em.flushHooks();
|
|
755
|
+
assert.deepEqual(added, [a]);
|
|
756
|
+
});
|
|
757
|
+
});
|