archetype-ecs 1.4.0 → 1.4.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 +12 -28
- package/dist/src/System.d.ts +2 -2
- package/dist/src/System.js +7 -7
- package/dist/src/index.d.ts +8 -1
- package/package.json +1 -1
- package/src/System.ts +7 -7
- package/src/index.ts +4 -0
- package/tests/EntityManager.test.ts +26 -26
- package/tests/types.ts +3 -3
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ Two ways to work with entities in bulk. Pick the right one for the job:
|
|
|
130
130
|
Iterates over matching archetypes. You get the backing TypedArrays directly.
|
|
131
131
|
|
|
132
132
|
```ts
|
|
133
|
-
function movementSystem(dt) {
|
|
133
|
+
function movementSystem(dt: number) {
|
|
134
134
|
em.forEach([Position, Velocity], (arch) => {
|
|
135
135
|
const px = arch.field(Position.x) // Float32Array
|
|
136
136
|
const py = arch.field(Position.y)
|
|
@@ -183,7 +183,7 @@ const total = em.count([Position])
|
|
|
183
183
|
Class-based systems with decorators for component lifecycle hooks:
|
|
184
184
|
|
|
185
185
|
```ts
|
|
186
|
-
import { System, OnAdded, OnRemoved, createSystems } from 'archetype-ecs'
|
|
186
|
+
import { System, OnAdded, OnRemoved, createSystems, type EntityId } from 'archetype-ecs'
|
|
187
187
|
|
|
188
188
|
class MovementSystem extends System {
|
|
189
189
|
tick() {
|
|
@@ -202,12 +202,12 @@ class MovementSystem extends System {
|
|
|
202
202
|
|
|
203
203
|
class DeathSystem extends System {
|
|
204
204
|
@OnAdded(Health)
|
|
205
|
-
onSpawn(id) {
|
|
205
|
+
onSpawn(id: EntityId) {
|
|
206
206
|
console.log(`Entity ${id} spawned with ${this.em.get(id, Health.hp)} HP`)
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
@OnRemoved(Health)
|
|
210
|
-
onDeath(id) {
|
|
210
|
+
onDeath(id: EntityId) {
|
|
211
211
|
this.em.addComponent(id, Dead)
|
|
212
212
|
}
|
|
213
213
|
}
|
|
@@ -228,8 +228,8 @@ A functional API is also available:
|
|
|
228
228
|
import { createSystem } from 'archetype-ecs'
|
|
229
229
|
|
|
230
230
|
const deathSystem = createSystem(em, (sys) => {
|
|
231
|
-
sys.onAdded(Health, (id) => console.log(`${id} spawned`))
|
|
232
|
-
sys.onRemoved(Health, (id) => console.log(`${id} died`))
|
|
231
|
+
sys.onAdded(Health, (id: EntityId) => console.log(`${id} spawned`))
|
|
232
|
+
sys.onRemoved(Health, (id: EntityId) => console.log(`${id} died`))
|
|
233
233
|
})
|
|
234
234
|
|
|
235
235
|
em.flushHooks()
|
|
@@ -261,32 +261,16 @@ Supports stripping components, skipping entities, and custom serializers.
|
|
|
261
261
|
Component types are inferred from their definition. Field names autocomplete, wrong fields are compile errors.
|
|
262
262
|
|
|
263
263
|
```ts
|
|
264
|
-
//
|
|
264
|
+
// Fields are typed as FieldRef — Position becomes ComponentDef & { x: FieldRef; y: FieldRef }
|
|
265
265
|
const Position = component('Position', 'f32', ['x', 'y'])
|
|
266
266
|
|
|
267
|
-
Position.x // autocompletes to .x and .y
|
|
268
|
-
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
|
|
269
269
|
|
|
270
|
-
em.get(id, Position.x) //
|
|
271
|
-
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
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
em.addComponent(id, Position, { x: 1 }) // Property 'y' is missing
|
|
275
|
-
|
|
276
|
-
em.getComponent(id, Position) // { x: number; y: number } | undefined
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
String fields are fully typed too:
|
|
280
|
-
|
|
281
|
-
```ts
|
|
282
|
-
const Name = component('Name', 'string', ['name', 'title'])
|
|
283
|
-
|
|
284
|
-
em.get(id, Name.name) // string | undefined
|
|
285
|
-
em.set(id, Name.name, 'Hero') // ok
|
|
286
|
-
em.set(id, Name.name, 42) // number not assignable to string
|
|
287
|
-
|
|
288
|
-
em.addComponent(id, Name, { name: 'Hero', title: 'Sir' }) // ok
|
|
289
|
-
em.addComponent(id, Name, { foo: 'bar' }) // type error
|
|
273
|
+
arch.field(Position.x) // Float32Array — direct TypedArray access
|
|
290
274
|
```
|
|
291
275
|
|
|
292
276
|
---
|
package/dist/src/System.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ComponentDef } from './ComponentRegistry.js';
|
|
2
2
|
import type { EntityId, EntityManager, ArchetypeView } from './EntityManager.js';
|
|
3
|
-
export declare function OnAdded(...types: ComponentDef[]): (
|
|
4
|
-
export declare function OnRemoved(...types: ComponentDef[]): (
|
|
3
|
+
export declare function OnAdded(...types: ComponentDef[]): (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) => void;
|
|
4
|
+
export declare function OnRemoved(...types: ComponentDef[]): (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) => void;
|
|
5
5
|
interface Hook {
|
|
6
6
|
buffer: Set<EntityId>;
|
|
7
7
|
handler: (id: EntityId) => void;
|
package/dist/src/System.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// ── Decorators (TC39 Stage 3) ────────────────────────────
|
|
2
2
|
export function OnAdded(...types) {
|
|
3
|
-
return function (
|
|
4
|
-
|
|
3
|
+
return function (method, _context) {
|
|
4
|
+
_context.addInitializer(function () {
|
|
5
5
|
const self = this;
|
|
6
|
-
self._registerHook('add', types,
|
|
6
|
+
self._registerHook('add', types, method.bind(self));
|
|
7
7
|
});
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
export function OnRemoved(...types) {
|
|
11
|
-
return function (
|
|
12
|
-
|
|
11
|
+
return function (method, _context) {
|
|
12
|
+
_context.addInitializer(function () {
|
|
13
13
|
const self = this;
|
|
14
|
-
self._registerHook('remove', types,
|
|
14
|
+
self._registerHook('remove', types, method.bind(self));
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
}
|
|
@@ -121,7 +121,7 @@ export function createSystem(em, constructor) {
|
|
|
121
121
|
}
|
|
122
122
|
export function createSystems(em, entries) {
|
|
123
123
|
const systems = entries.map(Entry => {
|
|
124
|
-
if (Entry.prototype instanceof System) {
|
|
124
|
+
if ('prototype' in Entry && Entry.prototype instanceof System) {
|
|
125
125
|
return new Entry(em);
|
|
126
126
|
}
|
|
127
127
|
const sys = createSystem(em, Entry);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,4 +7,11 @@ export type { Profiler, ProfilerEntry } from './Profiler.js';
|
|
|
7
7
|
export { TYPED, componentSchemas, parseTypeSpec } from './ComponentRegistry.js';
|
|
8
8
|
export type { ComponentDef, TypeSpec } from './ComponentRegistry.js';
|
|
9
9
|
import { type ComponentDef } from './ComponentRegistry.js';
|
|
10
|
-
|
|
10
|
+
import type { FieldRef } from './EntityManager.js';
|
|
11
|
+
export declare function component(name: string): ComponentDef;
|
|
12
|
+
export declare function component<F extends readonly string[]>(name: string, type: string, fields: F): ComponentDef & {
|
|
13
|
+
readonly [K in F[number]]: FieldRef;
|
|
14
|
+
};
|
|
15
|
+
export declare function component<S extends Record<string, string>>(name: string, schema: S): ComponentDef & {
|
|
16
|
+
readonly [K in keyof S]: FieldRef;
|
|
17
|
+
};
|
package/package.json
CHANGED
package/src/System.ts
CHANGED
|
@@ -4,19 +4,19 @@ import type { EntityId, EntityManager, ArchetypeView } from './EntityManager.js'
|
|
|
4
4
|
// ── Decorators (TC39 Stage 3) ────────────────────────────
|
|
5
5
|
|
|
6
6
|
export function OnAdded(...types: ComponentDef[]) {
|
|
7
|
-
return function (
|
|
8
|
-
|
|
7
|
+
return function (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) {
|
|
8
|
+
_context.addInitializer(function () {
|
|
9
9
|
const self = this as unknown as System;
|
|
10
|
-
self._registerHook('add', types,
|
|
10
|
+
self._registerHook('add', types, method.bind(self));
|
|
11
11
|
});
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function OnRemoved(...types: ComponentDef[]) {
|
|
16
|
-
return function (
|
|
17
|
-
|
|
16
|
+
return function (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) {
|
|
17
|
+
_context.addInitializer(function () {
|
|
18
18
|
const self = this as unknown as System;
|
|
19
|
-
self._registerHook('remove', types,
|
|
19
|
+
self._registerHook('remove', types, method.bind(self));
|
|
20
20
|
});
|
|
21
21
|
};
|
|
22
22
|
}
|
|
@@ -182,7 +182,7 @@ export interface Pipeline {
|
|
|
182
182
|
|
|
183
183
|
export function createSystems(em: EntityManager, entries: (FunctionalSystemConstructor | (new (em: EntityManager) => System))[]): Pipeline {
|
|
184
184
|
const systems: Runnable[] = entries.map(Entry => {
|
|
185
|
-
if (
|
|
185
|
+
if ('prototype' in Entry && Entry.prototype instanceof System) {
|
|
186
186
|
return new (Entry as new (em: EntityManager) => System)(em);
|
|
187
187
|
}
|
|
188
188
|
const sys = createSystem(em, Entry as FunctionalSystemConstructor);
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,11 @@ export { TYPED, componentSchemas, parseTypeSpec } from './ComponentRegistry.js';
|
|
|
8
8
|
export type { ComponentDef, TypeSpec } from './ComponentRegistry.js';
|
|
9
9
|
|
|
10
10
|
import { parseTypeSpec, componentSchemas, type ComponentDef, type TypeSpec } from './ComponentRegistry.js';
|
|
11
|
+
import type { FieldRef } from './EntityManager.js';
|
|
11
12
|
|
|
13
|
+
export function component(name: string): ComponentDef;
|
|
14
|
+
export function component<F extends readonly string[]>(name: string, type: string, fields: F): ComponentDef & { readonly [K in F[number]]: FieldRef };
|
|
15
|
+
export function component<S extends Record<string, string>>(name: string, schema: S): ComponentDef & { readonly [K in keyof S]: FieldRef };
|
|
12
16
|
export function component(name: string, typeOrSchema?: string | Record<string, string>, fields?: string[]): ComponentDef {
|
|
13
17
|
const sym = Symbol(name);
|
|
14
18
|
const comp: any = { _sym: sym, _name: name };
|
|
@@ -300,10 +300,10 @@ describe('Typed Components (SoA)', () => {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
em.forEach([Pos, Vel], (arch) => {
|
|
303
|
-
const px = arch.field(Pos.x
|
|
304
|
-
const py = arch.field(Pos.y
|
|
305
|
-
const vx = arch.field(Vel.vx
|
|
306
|
-
const vy = arch.field(Vel.vy
|
|
303
|
+
const px = arch.field(Pos.x);
|
|
304
|
+
const py = arch.field(Pos.y);
|
|
305
|
+
const vx = arch.field(Vel.vx);
|
|
306
|
+
const vy = arch.field(Vel.vy);
|
|
307
307
|
for (let i = 0; i < arch.count; i++) {
|
|
308
308
|
px[i] += vx[i];
|
|
309
309
|
py[i] += vy[i];
|
|
@@ -367,16 +367,16 @@ describe('Typed Components (SoA)', () => {
|
|
|
367
367
|
const Pos = component('PosGS', 'f32', ['x', 'y']);
|
|
368
368
|
const id = em.createEntity();
|
|
369
369
|
em.addComponent(id, Pos, { x: 3.5, y: 7.5 });
|
|
370
|
-
assert.ok(Math.abs(em.get(id, Pos.x
|
|
371
|
-
em.set(id, Pos.x
|
|
372
|
-
assert.ok(Math.abs(em.get(id, Pos.x
|
|
370
|
+
assert.ok(Math.abs(em.get(id, Pos.x) - 3.5) < 0.001);
|
|
371
|
+
em.set(id, Pos.x, 42);
|
|
372
|
+
assert.ok(Math.abs(em.get(id, Pos.x) - 42) < 0.001);
|
|
373
373
|
});
|
|
374
374
|
|
|
375
375
|
it('get returns undefined for missing entity/component', () => {
|
|
376
376
|
const Pos = component('PosGFM', 'f32', ['x', 'y']);
|
|
377
|
-
assert.equal(em.get(999, Pos.x
|
|
377
|
+
assert.equal(em.get(999, Pos.x), undefined);
|
|
378
378
|
const id = em.createEntity();
|
|
379
|
-
assert.equal(em.get(id, Pos.x
|
|
379
|
+
assert.equal(em.get(id, Pos.x), undefined);
|
|
380
380
|
});
|
|
381
381
|
|
|
382
382
|
it('forEach field returns undefined for tag component', () => {
|
|
@@ -387,7 +387,7 @@ describe('Typed Components (SoA)', () => {
|
|
|
387
387
|
em.addComponent(id, Tag, {});
|
|
388
388
|
|
|
389
389
|
em.forEach([Pos, Tag], (arch) => {
|
|
390
|
-
assert.ok(arch.field(Pos.x
|
|
390
|
+
assert.ok(arch.field(Pos.x) instanceof Float32Array);
|
|
391
391
|
});
|
|
392
392
|
});
|
|
393
393
|
|
|
@@ -396,11 +396,11 @@ describe('Typed Components (SoA)', () => {
|
|
|
396
396
|
const id = em.createEntity();
|
|
397
397
|
em.addComponent(id, Name, { name: 'Hero', title: 'Sir' });
|
|
398
398
|
|
|
399
|
-
assert.equal(em.get(id, Name.name
|
|
400
|
-
assert.equal(em.get(id, Name.title
|
|
399
|
+
assert.equal(em.get(id, Name.name), 'Hero');
|
|
400
|
+
assert.equal(em.get(id, Name.title), 'Sir');
|
|
401
401
|
|
|
402
|
-
em.set(id, Name.name
|
|
403
|
-
assert.equal(em.get(id, Name.name
|
|
402
|
+
em.set(id, Name.name, 'Villain');
|
|
403
|
+
assert.equal(em.get(id, Name.name), 'Villain');
|
|
404
404
|
|
|
405
405
|
const obj = em.getComponent(id, Name);
|
|
406
406
|
assert.deepEqual(obj, { name: 'Villain', title: 'Sir' });
|
|
@@ -411,8 +411,8 @@ describe('Typed Components (SoA)', () => {
|
|
|
411
411
|
const id = em.createEntity();
|
|
412
412
|
em.addComponent(id, Label, { text: 'hello', color: 'red' });
|
|
413
413
|
|
|
414
|
-
assert.equal(em.get(id, Label.text
|
|
415
|
-
assert.equal(em.get(id, Label.color
|
|
414
|
+
assert.equal(em.get(id, Label.text), 'hello');
|
|
415
|
+
assert.equal(em.get(id, Label.color), 'red');
|
|
416
416
|
});
|
|
417
417
|
|
|
418
418
|
it('string component growth past capacity', () => {
|
|
@@ -423,8 +423,8 @@ describe('Typed Components (SoA)', () => {
|
|
|
423
423
|
}
|
|
424
424
|
const ids = em.query([Name]);
|
|
425
425
|
assert.equal(ids.length, 100);
|
|
426
|
-
assert.equal(em.get(ids[0], Name.value
|
|
427
|
-
assert.equal(em.get(ids[99], Name.value
|
|
426
|
+
assert.equal(em.get(ids[0], Name.value), 'entity_0');
|
|
427
|
+
assert.equal(em.get(ids[99], Name.value), 'entity_99');
|
|
428
428
|
});
|
|
429
429
|
|
|
430
430
|
it('string component swap-remove preserves data', () => {
|
|
@@ -437,8 +437,8 @@ describe('Typed Components (SoA)', () => {
|
|
|
437
437
|
em.addComponent(c, Name, { value: 'ccc' });
|
|
438
438
|
|
|
439
439
|
em.destroyEntity(a);
|
|
440
|
-
assert.equal(em.get(b, Name.value
|
|
441
|
-
assert.equal(em.get(c, Name.value
|
|
440
|
+
assert.equal(em.get(b, Name.value), 'bbb');
|
|
441
|
+
assert.equal(em.get(c, Name.value), 'ccc');
|
|
442
442
|
});
|
|
443
443
|
|
|
444
444
|
it('mixed string + numeric fields in one component', () => {
|
|
@@ -446,8 +446,8 @@ describe('Typed Components (SoA)', () => {
|
|
|
446
446
|
const id = em.createEntity();
|
|
447
447
|
em.addComponent(id, Item, { name: 'Sword', weight: 3.5 });
|
|
448
448
|
|
|
449
|
-
assert.equal(em.get(id, Item.name
|
|
450
|
-
assert.ok(Math.abs(em.get(id, Item.weight
|
|
449
|
+
assert.equal(em.get(id, Item.name), 'Sword');
|
|
450
|
+
assert.ok(Math.abs(em.get(id, Item.weight) - 3.5) < 0.01);
|
|
451
451
|
});
|
|
452
452
|
|
|
453
453
|
it('string component forEach field access', () => {
|
|
@@ -458,7 +458,7 @@ describe('Typed Components (SoA)', () => {
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
em.forEach([Name], (arch) => {
|
|
461
|
-
const values = arch.field(Name.value
|
|
461
|
+
const values = arch.field(Name.value);
|
|
462
462
|
assert.ok(Array.isArray(values));
|
|
463
463
|
assert.equal(values[0], 'e0');
|
|
464
464
|
assert.equal(values[4], 'e4');
|
|
@@ -539,7 +539,7 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
539
539
|
em.addComponent(a, Pos, { x: 1, y: 2 });
|
|
540
540
|
|
|
541
541
|
em.forEach([Pos], (arch) => {
|
|
542
|
-
const px = arch.field(Pos.x
|
|
542
|
+
const px = arch.field(Pos.x);
|
|
543
543
|
// Overwrite via addComponent — same archetype, should be immediate
|
|
544
544
|
em.addComponent(a, Pos, { x: 99, y: 88 });
|
|
545
545
|
// The array should reflect the change immediately
|
|
@@ -615,9 +615,9 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
615
615
|
em.addComponent(a, Pos, { x: 1, y: 2 });
|
|
616
616
|
|
|
617
617
|
em.forEach([Pos], (arch) => {
|
|
618
|
-
em.set(a, Pos.x
|
|
618
|
+
em.set(a, Pos.x, 42);
|
|
619
619
|
// Should be immediately visible
|
|
620
|
-
const px = arch.field(Pos.x
|
|
620
|
+
const px = arch.field(Pos.x);
|
|
621
621
|
assert.ok(Math.abs(px[0] - 42) < 0.001);
|
|
622
622
|
});
|
|
623
623
|
});
|
package/tests/types.ts
CHANGED
|
@@ -33,14 +33,14 @@ em.addComponent(id, Name, { name: 'Hero', title: 'Sir' });
|
|
|
33
33
|
const pos = em.getComponent(id, Position);
|
|
34
34
|
|
|
35
35
|
// get/set: field descriptor access
|
|
36
|
-
const px: any = em.get(id, Position.x
|
|
37
|
-
em.set(id, Position.x
|
|
36
|
+
const px: any = em.get(id, Position.x);
|
|
37
|
+
em.set(id, Position.x, 10);
|
|
38
38
|
|
|
39
39
|
// forEach + field: accepts FieldRef
|
|
40
40
|
em.forEach([Position, Velocity], (arch) => {
|
|
41
41
|
const count: number = arch.count;
|
|
42
42
|
const ids: EntityId[] = arch.entityIds;
|
|
43
|
-
const arrX = arch.field(Position.x
|
|
43
|
+
const arrX = arch.field(Position.x);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// createEntityWith: alternating type, data
|