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.
- package/README.md +119 -40
- 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/.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/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
|
+
}
|
|
@@ -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;
|