archetype-ecs 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- // Benchmark: archetype-ecs (current) vs archetype+TypedArrays (hypothetical) vs bitECS
1
+ // Benchmark: archetype-ecs vs bitECS
2
2
  // Tests: system loop, entity creation, component add/remove churn
3
3
 
4
- import { createEntityManager } from '../src/EntityManager.js';
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
- const id = em.createEntity();
28
- em.addComponent(id, Position, { x: Math.random() * 100, y: Math.random() * 100 });
29
- em.addComponent(id, Velocity, { vx: Math.random() * 10, vy: Math.random() * 10 });
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
- // Dense iteration — no entity ID lookup, no getComponent, no sparse gaps
64
- for (let i = 0; i < count; i++) {
65
- px[i] += vx[i];
66
- py[i] += vy[i];
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
- const id = em.createEntity();
117
- em.addComponent(id, Position, { x: i, y: i });
118
- em.addComponent(id, Velocity, { vx: 1, vy: 1 });
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 | arch (now) | arch+typed | bitECS | arch+typed vs bitECS');
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 vsNow = arch / archTyped;
223
- const vsBit = bit / archTyped;
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
- `${vsBit > 1.1 ? `arch+typed ${vsBit.toFixed(1)}x sneller` : vsBit < 0.9 ? `bitECS ${(1/vsBit).toFixed(1)}x sneller` : '~gelijk'}`
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
+ }
@@ -0,0 +1,52 @@
1
+ import { type ComponentDef } from './ComponentRegistry.js';
2
+ export type EntityId = number;
3
+ export interface FieldRef {
4
+ readonly _sym: symbol;
5
+ readonly _field: string;
6
+ }
7
+ export interface ArchetypeView {
8
+ readonly id: number;
9
+ readonly entityIds: EntityId[];
10
+ readonly count: number;
11
+ readonly snapshotEntityIds: EntityId[] | null;
12
+ readonly snapshotCount: number;
13
+ field(ref: FieldRef): any;
14
+ fieldStride(ref: FieldRef): number;
15
+ snapshot(ref: FieldRef): any;
16
+ }
17
+ export interface SerializedData {
18
+ nextId: number;
19
+ entities: EntityId[];
20
+ components: Record<string, Record<string, unknown>>;
21
+ }
22
+ export interface EntityManager {
23
+ createEntity(): EntityId;
24
+ destroyEntity(id: EntityId): void;
25
+ addComponent(entityId: EntityId, type: ComponentDef, data?: any): void;
26
+ removeComponent(entityId: EntityId, type: ComponentDef): void;
27
+ getComponent(entityId: EntityId, type: ComponentDef): any;
28
+ get(entityId: EntityId, fieldRef: FieldRef): any;
29
+ set(entityId: EntityId, fieldRef: FieldRef, value: any): void;
30
+ hasComponent(entityId: EntityId, type: ComponentDef): boolean;
31
+ query(include: ComponentDef[], exclude?: ComponentDef[]): EntityId[];
32
+ getAllEntities(): EntityId[];
33
+ createEntityWith(...args: unknown[]): EntityId;
34
+ count(include: ComponentDef[], exclude?: ComponentDef[]): number;
35
+ forEach(include: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void;
36
+ onAdd(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
37
+ onRemove(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
38
+ flushHooks(): void;
39
+ enableTracking(filterComponent: ComponentDef): void;
40
+ flushChanges(): {
41
+ created: Set<EntityId>;
42
+ destroyed: Set<EntityId>;
43
+ };
44
+ flushSnapshots(): void;
45
+ serialize(symbolToName: Map<symbol, string>, stripComponents?: ComponentDef[], skipEntitiesWith?: ComponentDef[], options?: {
46
+ serializers?: Map<string, (data: unknown) => unknown>;
47
+ }): SerializedData;
48
+ deserialize(data: SerializedData, nameToSymbol: Record<string, ComponentDef>, options?: {
49
+ deserializers?: Map<string, (data: unknown) => unknown>;
50
+ }): void;
51
+ }
52
+ export declare function createEntityManager(): EntityManager;