archetype-ecs 1.4.2 → 1.5.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 +79 -14
- package/bench/allocations-1m.js +1 -1
- package/bench/component-churn-bench.js +193 -0
- package/bench/iterate.wat +95 -0
- package/bench/multi-ecs-bench.js +1 -1
- package/bench/run-js-vs-go-ts.sh +147 -0
- package/bench/typed-vs-bitecs-1m.js +1 -1
- package/bench/typed-vs-untyped.js +1 -1
- package/bench/vs-bitecs.js +1 -1
- package/bench/wasm-iteration-bench.js +289 -0
- package/dist/{src/ComponentRegistry.d.ts → ComponentRegistry.d.ts} +8 -3
- package/dist/{src/EntityManager.d.ts → EntityManager.d.ts} +15 -10
- package/dist/{src/EntityManager.js → EntityManager.js} +196 -95
- package/dist/{src/System.d.ts → System.d.ts} +4 -0
- package/dist/{src/System.js → System.js} +25 -5
- package/dist/WasmArena.d.ts +13 -0
- package/dist/WasmArena.js +48 -0
- package/dist/{src/index.d.ts → index.d.ts} +7 -9
- package/dist/{src/index.js → index.js} +2 -0
- package/dist/wasm-kernels.d.ts +10 -0
- package/dist/wasm-kernels.js +59 -0
- package/package.json +12 -7
- package/src/ComponentRegistry.ts +7 -3
- package/src/EntityManager.ts +209 -119
- package/src/System.ts +34 -9
- package/src/WasmArena.ts +83 -0
- package/src/index.ts +16 -11
- package/src/iterate.wat +135 -0
- package/src/wasm-kernels.ts +68 -0
- package/tests/EntityManager.test.ts +51 -86
- package/tests/System.test.ts +184 -0
- package/tests/types.ts +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.test.json +13 -0
- package/dist/tests/EntityManager.test.d.ts +0 -1
- package/dist/tests/EntityManager.test.js +0 -651
- package/dist/tests/System.test.d.ts +0 -1
- package/dist/tests/System.test.js +0 -630
- package/dist/tests/types.d.ts +0 -1
- package/dist/tests/types.js +0 -129
- /package/dist/{src/ComponentRegistry.js → ComponentRegistry.js} +0 -0
- /package/dist/{src/Profiler.d.ts → Profiler.d.ts} +0 -0
- /package/dist/{src/Profiler.js → Profiler.js} +0 -0
package/src/EntityManager.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { TYPED, componentSchemas, toSym, type ComponentDef, type TypeSpec } from './ComponentRegistry.js';
|
|
1
|
+
import { TYPED, componentSchemas, toSym, type ComponentDef, type TypeSpec, type FieldRef } from './ComponentRegistry.js';
|
|
2
|
+
import { WasmArena, type NumericTypedArrayConstructor } from './WasmArena.js';
|
|
3
|
+
import { instantiateKernelsSync, isWasmSimdAvailable, type IterateKernels } from './wasm-kernels.js';
|
|
4
|
+
|
|
5
|
+
export type { FieldRef } from './ComponentRegistry.js';
|
|
2
6
|
|
|
3
7
|
export type EntityId = number;
|
|
4
8
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
readonly _field: string;
|
|
8
|
-
}
|
|
9
|
+
export type SoAArrayValue = Float32Array | Float64Array | Int8Array | Int16Array | Int32Array
|
|
10
|
+
| Uint8Array | Uint16Array | Uint32Array | unknown[];
|
|
9
11
|
|
|
10
12
|
export interface ArchetypeView {
|
|
11
13
|
readonly id: number;
|
|
@@ -15,6 +17,8 @@ export interface ArchetypeView {
|
|
|
15
17
|
readonly snapshotCount: number;
|
|
16
18
|
field(ref: FieldRef): any;
|
|
17
19
|
fieldStride(ref: FieldRef): number;
|
|
20
|
+
fieldOffset(ref: FieldRef): number;
|
|
21
|
+
fieldAdd(target: FieldRef, source: FieldRef): void;
|
|
18
22
|
snapshot(ref: FieldRef): any;
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -24,14 +28,16 @@ export interface SerializedData {
|
|
|
24
28
|
components: Record<string, Record<string, unknown>>;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
export type ComponentData = Record<string, number | string | ArrayLike<number>> | null | undefined;
|
|
32
|
+
|
|
27
33
|
export interface EntityManager {
|
|
28
34
|
createEntity(): EntityId;
|
|
29
35
|
destroyEntity(id: EntityId): void;
|
|
30
|
-
addComponent(entityId: EntityId, type: ComponentDef, data?:
|
|
36
|
+
addComponent(entityId: EntityId, type: ComponentDef, data?: ComponentData): void;
|
|
31
37
|
removeComponent(entityId: EntityId, type: ComponentDef): void;
|
|
32
|
-
getComponent(entityId: EntityId, type: ComponentDef):
|
|
33
|
-
get(entityId: EntityId, fieldRef: FieldRef):
|
|
34
|
-
set(entityId: EntityId, fieldRef: FieldRef, value:
|
|
38
|
+
getComponent(entityId: EntityId, type: ComponentDef): Record<string, number | string | number[]> | undefined;
|
|
39
|
+
get(entityId: EntityId, fieldRef: FieldRef): number | string | undefined;
|
|
40
|
+
set(entityId: EntityId, fieldRef: FieldRef, value: number | string | ArrayLike<number>): void;
|
|
35
41
|
hasComponent(entityId: EntityId, type: ComponentDef): boolean;
|
|
36
42
|
query(include: ComponentDef[], exclude?: ComponentDef[]): EntityId[];
|
|
37
43
|
getAllEntities(): EntityId[];
|
|
@@ -41,6 +47,7 @@ export interface EntityManager {
|
|
|
41
47
|
onAdd(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
|
|
42
48
|
onRemove(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
|
|
43
49
|
flushHooks(): void;
|
|
50
|
+
commitRemovals(): void;
|
|
44
51
|
enableTracking(filterComponent: ComponentDef): void;
|
|
45
52
|
flushChanges(): { created: Set<EntityId>; destroyed: Set<EntityId> };
|
|
46
53
|
flushSnapshots(): void;
|
|
@@ -55,6 +62,7 @@ export interface EntityManager {
|
|
|
55
62
|
nameToSymbol: Record<string, ComponentDef>,
|
|
56
63
|
options?: { deserializers?: Map<string, (data: unknown) => unknown> }
|
|
57
64
|
): void;
|
|
65
|
+
readonly wasmMemory: WebAssembly.Memory | null;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
// ── Internal types ───────────────────────────────────────
|
|
@@ -64,10 +72,10 @@ interface SoAStore {
|
|
|
64
72
|
_schema: Record<string, TypeSpec>;
|
|
65
73
|
_capacity: number;
|
|
66
74
|
_arraySizes: Record<string, number>;
|
|
67
|
-
|
|
75
|
+
_fields: Record<string, SoAArrayValue>;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
type SnapshotStore = Record<string,
|
|
78
|
+
type SnapshotStore = Record<string, SoAArrayValue>;
|
|
71
79
|
|
|
72
80
|
interface Archetype {
|
|
73
81
|
key: Uint32Array;
|
|
@@ -151,78 +159,93 @@ function maskKey(mask: Uint32Array): string {
|
|
|
151
159
|
|
|
152
160
|
// ── SoA helpers ──────────────────────────────────────────
|
|
153
161
|
|
|
154
|
-
function unpackSpec(spec: TypeSpec): [
|
|
162
|
+
function unpackSpec(spec: TypeSpec): [{ new(len: number): SoAArrayValue }, number] {
|
|
155
163
|
if (Array.isArray(spec)) return spec;
|
|
156
164
|
return [spec, 0];
|
|
157
165
|
}
|
|
158
166
|
|
|
159
|
-
function createSoAStore(schema: Record<string, TypeSpec>, capacity: number): SoAStore {
|
|
160
|
-
const
|
|
167
|
+
function createSoAStore(schema: Record<string, TypeSpec>, capacity: number, arena?: WasmArena): SoAStore {
|
|
168
|
+
const fields: Record<string, SoAArrayValue> = {};
|
|
169
|
+
const arraySizes: Record<string, number> = {};
|
|
161
170
|
for (const [field, spec] of Object.entries(schema)) {
|
|
162
171
|
const [Ctor, size] = unpackSpec(spec);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
const count = size > 0 ? capacity * size : capacity;
|
|
173
|
+
if (size > 0) arraySizes[field] = size;
|
|
174
|
+
if (arena && Ctor !== Array) {
|
|
175
|
+
const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
|
|
176
|
+
const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
|
|
177
|
+
arena.createView(NumCtor, offset, count, fields, field);
|
|
166
178
|
} else {
|
|
167
|
-
|
|
179
|
+
fields[field] = new Ctor(count);
|
|
168
180
|
}
|
|
169
181
|
}
|
|
170
|
-
return
|
|
182
|
+
return { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: arraySizes, _fields: fields };
|
|
171
183
|
}
|
|
172
184
|
|
|
173
|
-
function growSoAStore(store: SoAStore, newCapacity: number): void {
|
|
185
|
+
function growSoAStore(store: SoAStore, newCapacity: number, arena?: WasmArena): void {
|
|
174
186
|
store._capacity = newCapacity;
|
|
175
187
|
for (const [field, spec] of Object.entries(store._schema)) {
|
|
176
188
|
const [Ctor, size] = unpackSpec(spec);
|
|
177
|
-
const old = store[field];
|
|
189
|
+
const old = store._fields[field];
|
|
178
190
|
const allocSize = size > 0 ? newCapacity * size : newCapacity;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
if (arena && Ctor !== Array) {
|
|
192
|
+
const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
|
|
193
|
+
const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
|
|
194
|
+
arena.updateView(store._fields, field, offset, NumCtor, allocSize);
|
|
195
|
+
(store._fields[field] as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
|
|
182
196
|
} else {
|
|
183
|
-
|
|
197
|
+
const grown = new Ctor(allocSize);
|
|
198
|
+
if (Ctor === Array) {
|
|
199
|
+
for (let i = 0; i < old.length; i++) (grown as unknown[])[i] = (old as unknown[])[i];
|
|
200
|
+
} else {
|
|
201
|
+
(grown as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
|
|
202
|
+
}
|
|
203
|
+
store._fields[field] = grown;
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
}
|
|
187
207
|
|
|
188
|
-
function soaWrite(store: SoAStore, idx: number, data:
|
|
208
|
+
function soaWrite(store: SoAStore, idx: number, data: ComponentData): void {
|
|
189
209
|
if (!data) {
|
|
190
210
|
for (const field in store._schema) {
|
|
211
|
+
const arr = store._fields[field];
|
|
191
212
|
const size = store._arraySizes[field] || 0;
|
|
192
213
|
if (size > 0) {
|
|
193
214
|
const base = idx * size;
|
|
194
|
-
for (let j = 0; j < size; j++)
|
|
215
|
+
for (let j = 0; j < size; j++) (arr as never[])[base + j] = 0 as never;
|
|
195
216
|
} else {
|
|
196
|
-
|
|
217
|
+
(arr as never[])[idx] = 0 as never;
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
220
|
return;
|
|
200
221
|
}
|
|
201
222
|
for (const field in store._schema) {
|
|
223
|
+
const arr = store._fields[field];
|
|
202
224
|
const size = store._arraySizes[field] || 0;
|
|
203
225
|
if (size > 0) {
|
|
204
226
|
const base = idx * size;
|
|
205
227
|
const src = data[field];
|
|
206
228
|
if (src) {
|
|
207
229
|
for (let j = 0; j < size; j++) {
|
|
208
|
-
|
|
230
|
+
(arr as never[])[base + j] = ((src as ArrayLike<number>)[j] ?? 0) as never;
|
|
209
231
|
}
|
|
210
232
|
}
|
|
211
233
|
} else {
|
|
212
|
-
|
|
234
|
+
(arr as never[])[idx] = data[field] as never;
|
|
213
235
|
}
|
|
214
236
|
}
|
|
215
237
|
}
|
|
216
238
|
|
|
217
|
-
function soaRead(store: SoAStore, idx: number): Record<string,
|
|
218
|
-
const obj: Record<string,
|
|
239
|
+
function soaRead(store: SoAStore, idx: number): Record<string, number | string | number[]> {
|
|
240
|
+
const obj: Record<string, number | string | number[]> = {};
|
|
219
241
|
for (const field in store._schema) {
|
|
242
|
+
const arr = store._fields[field];
|
|
220
243
|
const size = store._arraySizes[field] || 0;
|
|
221
244
|
if (size > 0) {
|
|
222
245
|
const base = idx * size;
|
|
223
|
-
obj[field] = Array.from(
|
|
246
|
+
obj[field] = Array.from((arr as Float32Array).subarray(base, base + size));
|
|
224
247
|
} else {
|
|
225
|
-
obj[field] =
|
|
248
|
+
obj[field] = (arr as never[])[idx];
|
|
226
249
|
}
|
|
227
250
|
}
|
|
228
251
|
return obj;
|
|
@@ -230,7 +253,7 @@ function soaRead(store: SoAStore, idx: number): Record<string, any> {
|
|
|
230
253
|
|
|
231
254
|
function soaSwap(store: SoAStore, idxA: number, idxB: number): void {
|
|
232
255
|
for (const field in store._schema) {
|
|
233
|
-
const arr = store[field];
|
|
256
|
+
const arr = store._fields[field] as never[];
|
|
234
257
|
const size = store._arraySizes[field] || 0;
|
|
235
258
|
if (size > 0) {
|
|
236
259
|
const baseA = idxA * size;
|
|
@@ -248,36 +271,51 @@ function soaSwap(store: SoAStore, idxA: number, idxB: number): void {
|
|
|
248
271
|
}
|
|
249
272
|
}
|
|
250
273
|
|
|
251
|
-
function createSnapshotStore(schema: Record<string, TypeSpec>, capacity: number): SnapshotStore {
|
|
274
|
+
function createSnapshotStore(schema: Record<string, TypeSpec>, capacity: number, arena?: WasmArena): SnapshotStore {
|
|
252
275
|
const snap: SnapshotStore = {};
|
|
253
276
|
for (const [field, spec] of Object.entries(schema)) {
|
|
254
277
|
const [Ctor, size] = unpackSpec(spec);
|
|
255
|
-
|
|
278
|
+
const count = size > 0 ? capacity * size : capacity;
|
|
279
|
+
if (arena && Ctor !== Array) {
|
|
280
|
+
const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
|
|
281
|
+
const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
|
|
282
|
+
arena.createView(NumCtor, offset, count, snap, field);
|
|
283
|
+
} else {
|
|
284
|
+
snap[field] = new Ctor(count);
|
|
285
|
+
}
|
|
256
286
|
}
|
|
257
287
|
return snap;
|
|
258
288
|
}
|
|
259
289
|
|
|
260
|
-
function growSnapshotStore(snap: SnapshotStore, schema: Record<string, TypeSpec>, newCapacity: number): void {
|
|
290
|
+
function growSnapshotStore(snap: SnapshotStore, schema: Record<string, TypeSpec>, newCapacity: number, arena?: WasmArena): void {
|
|
261
291
|
for (const [field, spec] of Object.entries(schema)) {
|
|
262
292
|
const [Ctor, size] = unpackSpec(spec);
|
|
263
293
|
const old = snap[field];
|
|
264
|
-
|
|
265
|
-
if (Ctor
|
|
266
|
-
|
|
294
|
+
const allocSize = size > 0 ? newCapacity * size : newCapacity;
|
|
295
|
+
if (arena && Ctor !== Array) {
|
|
296
|
+
const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
|
|
297
|
+
const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
|
|
298
|
+
arena.updateView(snap, field, offset, NumCtor, allocSize);
|
|
299
|
+
(snap[field] as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
|
|
267
300
|
} else {
|
|
268
|
-
|
|
301
|
+
const grown = new Ctor(allocSize);
|
|
302
|
+
if (Ctor === Array) {
|
|
303
|
+
for (let i = 0; i < old.length; i++) (grown as unknown[])[i] = (old as unknown[])[i];
|
|
304
|
+
} else {
|
|
305
|
+
(grown as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
|
|
306
|
+
}
|
|
307
|
+
snap[field] = grown;
|
|
269
308
|
}
|
|
270
309
|
}
|
|
271
310
|
}
|
|
272
311
|
|
|
273
312
|
// ── Entity Manager ───────────────────────────────────────
|
|
274
313
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
|
314
|
+
export function createEntityManager(options?: { wasm?: boolean }): EntityManager {
|
|
315
|
+
const useWasm = options?.wasm ?? isWasmSimdAvailable();
|
|
316
|
+
const arena = useWasm ? new WasmArena() : undefined;
|
|
317
|
+
const kernels: IterateKernels | null = arena ? instantiateKernelsSync(arena.memory) : null;
|
|
279
318
|
|
|
280
|
-
export function createEntityManager(): EntityManager {
|
|
281
319
|
let nextId: EntityId = 1;
|
|
282
320
|
let nextArchId = 1;
|
|
283
321
|
const allEntityIds = new Set<EntityId>();
|
|
@@ -289,7 +327,13 @@ export function createEntityManager(): EntityManager {
|
|
|
289
327
|
|
|
290
328
|
let hooks: Hooks | null = null;
|
|
291
329
|
|
|
330
|
+
const removedData = new Map<EntityId, Map<symbol, Record<string, number | string | number[]>>>();
|
|
331
|
+
|
|
292
332
|
// Deferred structural changes during forEach iteration
|
|
333
|
+
type DeferredOp =
|
|
334
|
+
| { kind: 'add'; entityId: EntityId; comp: ComponentDef; data?: ComponentData }
|
|
335
|
+
| { kind: 'remove'; entityId: EntityId; comp: ComponentDef }
|
|
336
|
+
| { kind: 'destroy'; entityId: EntityId };
|
|
293
337
|
let iterating = 0;
|
|
294
338
|
const deferred: DeferredOp[] = [];
|
|
295
339
|
|
|
@@ -342,10 +386,10 @@ export function createEntityManager(): EntityManager {
|
|
|
342
386
|
};
|
|
343
387
|
for (const t of types) {
|
|
344
388
|
const schema = componentSchemas.get(t);
|
|
345
|
-
const store = schema ? createSoAStore(schema, INITIAL_CAPACITY) : null;
|
|
389
|
+
const store = schema ? createSoAStore(schema, INITIAL_CAPACITY, arena) : null;
|
|
346
390
|
arch.components.set(t, store);
|
|
347
391
|
if (tracked && store) {
|
|
348
|
-
arch.snapshots!.set(t, createSnapshotStore(schema!, INITIAL_CAPACITY));
|
|
392
|
+
arch.snapshots!.set(t, createSnapshotStore(schema!, INITIAL_CAPACITY, arena));
|
|
349
393
|
}
|
|
350
394
|
}
|
|
351
395
|
archetypes.set(key, arch);
|
|
@@ -361,22 +405,22 @@ export function createEntityManager(): EntityManager {
|
|
|
361
405
|
arch.capacity = newCap;
|
|
362
406
|
for (const [type, store] of arch.components) {
|
|
363
407
|
if (store) {
|
|
364
|
-
growSoAStore(store, newCap);
|
|
408
|
+
growSoAStore(store, newCap, arena);
|
|
365
409
|
if (arch.snapshots) {
|
|
366
410
|
const snap = arch.snapshots.get(type);
|
|
367
|
-
if (snap) growSnapshotStore(snap, store._schema, newCap);
|
|
411
|
+
if (snap) growSnapshotStore(snap, store._schema, newCap, arena);
|
|
368
412
|
}
|
|
369
413
|
}
|
|
370
414
|
}
|
|
371
415
|
}
|
|
372
416
|
|
|
373
|
-
function addToArchetype(arch: Archetype, entityId: EntityId, componentMap:
|
|
417
|
+
function addToArchetype(arch: Archetype, entityId: EntityId, componentMap: Map<symbol, ComponentData>): void {
|
|
374
418
|
ensureCapacity(arch);
|
|
375
419
|
const idx = arch.count;
|
|
376
420
|
arch.entityIds[idx] = entityId;
|
|
377
421
|
for (const t of arch.types) {
|
|
378
422
|
const store = arch.components.get(t);
|
|
379
|
-
if (store) soaWrite(store, idx, (
|
|
423
|
+
if (store) soaWrite(store, idx, componentMap.get(t));
|
|
380
424
|
}
|
|
381
425
|
arch.entityToIndex.set(entityId, idx);
|
|
382
426
|
arch.count++;
|
|
@@ -402,7 +446,7 @@ export function createEntityManager(): EntityManager {
|
|
|
402
446
|
entityArchetype.delete(entityId);
|
|
403
447
|
}
|
|
404
448
|
|
|
405
|
-
function readComponentData(arch: Archetype, type: symbol, idx: number):
|
|
449
|
+
function readComponentData(arch: Archetype, type: symbol, idx: number): Record<string, number | string | number[]> | undefined {
|
|
406
450
|
const store = arch.components.get(type);
|
|
407
451
|
if (!store) return undefined;
|
|
408
452
|
return soaRead(store, idx);
|
|
@@ -430,8 +474,6 @@ export function createEntityManager(): EntityManager {
|
|
|
430
474
|
return matching;
|
|
431
475
|
}
|
|
432
476
|
|
|
433
|
-
// ── Internal structural change implementations ──────────
|
|
434
|
-
|
|
435
477
|
function doDestroyEntity(id: EntityId): void {
|
|
436
478
|
const arch = entityArchetype.get(id);
|
|
437
479
|
if (arch) {
|
|
@@ -440,6 +482,19 @@ export function createEntityManager(): EntityManager {
|
|
|
440
482
|
const pending = hooks.pendingRemove.get(type);
|
|
441
483
|
if (pending) pending.push(id);
|
|
442
484
|
}
|
|
485
|
+
if (hooks.removeCbs.size > 0) {
|
|
486
|
+
const idx = arch.entityToIndex.get(id)!;
|
|
487
|
+
let entitySnap: Map<symbol, Record<string, number | string | number[]>> | undefined;
|
|
488
|
+
for (const type of arch.types) {
|
|
489
|
+
if (hooks.removeCbs.has(type)) {
|
|
490
|
+
const store = arch.components.get(type);
|
|
491
|
+
if (store) {
|
|
492
|
+
if (!entitySnap) { entitySnap = new Map(); removedData.set(id, entitySnap); }
|
|
493
|
+
entitySnap.set(type, soaRead(store, idx));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
443
498
|
}
|
|
444
499
|
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(id);
|
|
445
500
|
removeFromArchetype(arch, id);
|
|
@@ -447,13 +502,13 @@ export function createEntityManager(): EntityManager {
|
|
|
447
502
|
allEntityIds.delete(id);
|
|
448
503
|
}
|
|
449
504
|
|
|
450
|
-
function doAddComponent(entityId: EntityId, comp: ComponentDef, data?:
|
|
505
|
+
function doAddComponent(entityId: EntityId, comp: ComponentDef, data?: ComponentData): void {
|
|
451
506
|
const type = toSym(comp);
|
|
452
507
|
const arch = entityArchetype.get(entityId);
|
|
453
508
|
|
|
454
509
|
if (!arch) {
|
|
455
510
|
const newArch = getOrCreateArchetype([type]);
|
|
456
|
-
addToArchetype(newArch, entityId,
|
|
511
|
+
addToArchetype(newArch, entityId, new Map([[type, data]]));
|
|
457
512
|
if (hooks) {
|
|
458
513
|
const pending = hooks.pendingAdd.get(type);
|
|
459
514
|
if (pending) pending.push(entityId);
|
|
@@ -474,9 +529,9 @@ export function createEntityManager(): EntityManager {
|
|
|
474
529
|
const newArch = getOrCreateArchetype(newTypes);
|
|
475
530
|
|
|
476
531
|
const idx = arch.entityToIndex.get(entityId)!;
|
|
477
|
-
const map
|
|
532
|
+
const map = new Map<symbol, ComponentData>([[type, data]]);
|
|
478
533
|
for (const t of arch.types) {
|
|
479
|
-
map
|
|
534
|
+
map.set(t, readComponentData(arch, t, idx));
|
|
480
535
|
}
|
|
481
536
|
|
|
482
537
|
removeFromArchetype(arch, entityId);
|
|
@@ -495,6 +550,14 @@ export function createEntityManager(): EntityManager {
|
|
|
495
550
|
if (hooks) {
|
|
496
551
|
const pending = hooks.pendingRemove.get(type);
|
|
497
552
|
if (pending) pending.push(entityId);
|
|
553
|
+
if (hooks.removeCbs.has(type)) {
|
|
554
|
+
const store = arch.components.get(type);
|
|
555
|
+
if (store) {
|
|
556
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
557
|
+
if (!removedData.has(entityId)) removedData.set(entityId, new Map());
|
|
558
|
+
removedData.get(entityId)!.set(type, soaRead(store, idx));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
498
561
|
}
|
|
499
562
|
|
|
500
563
|
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(entityId);
|
|
@@ -511,9 +574,9 @@ export function createEntityManager(): EntityManager {
|
|
|
511
574
|
const newArch = getOrCreateArchetype(newTypes);
|
|
512
575
|
|
|
513
576
|
const idx = arch.entityToIndex.get(entityId)!;
|
|
514
|
-
const map
|
|
577
|
+
const map = new Map<symbol, ComponentData>();
|
|
515
578
|
for (const t of newTypes) {
|
|
516
|
-
map
|
|
579
|
+
map.set(t, readComponentData(arch, t, idx));
|
|
517
580
|
}
|
|
518
581
|
|
|
519
582
|
removeFromArchetype(arch, entityId);
|
|
@@ -521,28 +584,19 @@ export function createEntityManager(): EntityManager {
|
|
|
521
584
|
}
|
|
522
585
|
|
|
523
586
|
function flushDeferred(): void {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
switch (op.kind) {
|
|
531
|
-
case 'add':
|
|
532
|
-
doAddComponent(op.entityId, op.comp, op.data);
|
|
533
|
-
break;
|
|
534
|
-
case 'remove':
|
|
535
|
-
doRemoveComponent(op.entityId, op.comp);
|
|
536
|
-
break;
|
|
537
|
-
case 'destroy':
|
|
538
|
-
doDestroyEntity(op.entityId);
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
587
|
+
const ops = deferred.splice(0);
|
|
588
|
+
for (const op of ops) {
|
|
589
|
+
switch (op.kind) {
|
|
590
|
+
case 'add': doAddComponent(op.entityId, op.comp, op.data); break;
|
|
591
|
+
case 'remove': doRemoveComponent(op.entityId, op.comp); break;
|
|
592
|
+
case 'destroy': doDestroyEntity(op.entityId); break;
|
|
541
593
|
}
|
|
542
594
|
}
|
|
543
595
|
}
|
|
544
596
|
|
|
545
597
|
return {
|
|
598
|
+
wasmMemory: arena ? arena.memory : null,
|
|
599
|
+
|
|
546
600
|
createEntity(): EntityId {
|
|
547
601
|
const id = nextId++;
|
|
548
602
|
allEntityIds.add(id);
|
|
@@ -557,7 +611,7 @@ export function createEntityManager(): EntityManager {
|
|
|
557
611
|
doDestroyEntity(id);
|
|
558
612
|
},
|
|
559
613
|
|
|
560
|
-
addComponent(entityId: EntityId, comp: ComponentDef, data?:
|
|
614
|
+
addComponent(entityId: EntityId, comp: ComponentDef, data?: ComponentData): void {
|
|
561
615
|
if (iterating > 0) {
|
|
562
616
|
// In-place overwrite (entity already has component) is safe — no migration
|
|
563
617
|
const type = toSym(comp);
|
|
@@ -585,30 +639,43 @@ export function createEntityManager(): EntityManager {
|
|
|
585
639
|
doRemoveComponent(entityId, comp);
|
|
586
640
|
},
|
|
587
641
|
|
|
588
|
-
getComponent(entityId: EntityId, comp: ComponentDef):
|
|
642
|
+
getComponent(entityId: EntityId, comp: ComponentDef): Record<string, number | string | number[]> | undefined {
|
|
589
643
|
const type = toSym(comp);
|
|
590
644
|
const arch = entityArchetype.get(entityId);
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
645
|
+
if (arch) {
|
|
646
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
647
|
+
if (idx !== undefined) return readComponentData(arch, type, idx);
|
|
648
|
+
}
|
|
649
|
+
// Fallback: check recently-removed data (accessible during @OnRemoved hooks)
|
|
650
|
+
const removed = removedData.get(entityId);
|
|
651
|
+
if (removed) return removed.get(type);
|
|
652
|
+
return undefined;
|
|
595
653
|
},
|
|
596
654
|
|
|
597
|
-
get(entityId: EntityId, fieldRef: FieldRef):
|
|
655
|
+
get(entityId: EntityId, fieldRef: FieldRef): number | string | undefined {
|
|
598
656
|
const arch = entityArchetype.get(entityId);
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
657
|
+
if (arch) {
|
|
658
|
+
const store = arch.components.get(fieldRef._sym);
|
|
659
|
+
if (store) {
|
|
660
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
661
|
+
const size = store._arraySizes[fieldRef._field] || 0;
|
|
662
|
+
if (size > 0) {
|
|
663
|
+
const base = idx * size;
|
|
664
|
+
return (store._fields[fieldRef._field] as Float32Array).subarray(base, base + size) as unknown as number;
|
|
665
|
+
}
|
|
666
|
+
return (store._fields[fieldRef._field] as never[])[idx];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Fallback: check recently-removed data (accessible during @OnRemoved hooks)
|
|
670
|
+
const removed = removedData.get(entityId);
|
|
671
|
+
if (removed) {
|
|
672
|
+
const compData = removed.get(fieldRef._sym);
|
|
673
|
+
if (compData) return compData[fieldRef._field] as number | string;
|
|
607
674
|
}
|
|
608
|
-
return
|
|
675
|
+
return undefined;
|
|
609
676
|
},
|
|
610
677
|
|
|
611
|
-
set(entityId: EntityId, fieldRef: FieldRef, value:
|
|
678
|
+
set(entityId: EntityId, fieldRef: FieldRef, value: number | string | ArrayLike<number>): void {
|
|
612
679
|
const arch = entityArchetype.get(entityId);
|
|
613
680
|
if (!arch) return;
|
|
614
681
|
const store = arch.components.get(fieldRef._sym);
|
|
@@ -616,9 +683,9 @@ export function createEntityManager(): EntityManager {
|
|
|
616
683
|
const idx = arch.entityToIndex.get(entityId)!;
|
|
617
684
|
const size = store._arraySizes[fieldRef._field] || 0;
|
|
618
685
|
if (size > 0) {
|
|
619
|
-
store[fieldRef._field].set(value
|
|
686
|
+
(store._fields[fieldRef._field] as Float32Array).set(value as ArrayLike<number>, idx * size);
|
|
620
687
|
} else {
|
|
621
|
-
store[fieldRef._field][idx] = value;
|
|
688
|
+
(store._fields[fieldRef._field] as never[])[idx] = value as never;
|
|
622
689
|
}
|
|
623
690
|
},
|
|
624
691
|
|
|
@@ -650,11 +717,11 @@ export function createEntityManager(): EntityManager {
|
|
|
650
717
|
allEntityIds.add(id);
|
|
651
718
|
|
|
652
719
|
const types: symbol[] = [];
|
|
653
|
-
const map
|
|
720
|
+
const map = new Map<symbol, ComponentData>();
|
|
654
721
|
for (let i = 0; i < args.length; i += 2) {
|
|
655
722
|
const sym = toSym(args[i] as ComponentDef);
|
|
656
723
|
types.push(sym);
|
|
657
|
-
map
|
|
724
|
+
map.set(sym, args[i + 1] as ComponentData);
|
|
658
725
|
}
|
|
659
726
|
const arch = getOrCreateArchetype(types);
|
|
660
727
|
addToArchetype(arch, id, map);
|
|
@@ -694,21 +761,40 @@ export function createEntityManager(): EntityManager {
|
|
|
694
761
|
snapshotEntityIds: arch.snapshotEntityIds,
|
|
695
762
|
snapshotCount: arch.snapshotCount,
|
|
696
763
|
field(ref: FieldRef) {
|
|
697
|
-
const
|
|
698
|
-
const store = arch.components.get(sym as symbol);
|
|
764
|
+
const store = arch.components.get(ref._sym);
|
|
699
765
|
if (!store) return undefined;
|
|
700
|
-
return store[ref._field];
|
|
766
|
+
return store._fields[ref._field];
|
|
701
767
|
},
|
|
702
768
|
fieldStride(ref: FieldRef) {
|
|
703
|
-
const
|
|
704
|
-
const store = arch.components.get(sym as symbol);
|
|
769
|
+
const store = arch.components.get(ref._sym);
|
|
705
770
|
if (!store) return 1;
|
|
706
771
|
return store._arraySizes[ref._field] || 1;
|
|
707
772
|
},
|
|
773
|
+
fieldOffset(ref: FieldRef) {
|
|
774
|
+
if (!arena) return -1;
|
|
775
|
+
const store = arch.components.get(ref._sym);
|
|
776
|
+
if (!store) return -1;
|
|
777
|
+
const arr = store._fields[ref._field];
|
|
778
|
+
if (!arr || arr instanceof Array) return -1;
|
|
779
|
+
return (arr as Exclude<SoAArrayValue, unknown[]>).byteOffset;
|
|
780
|
+
},
|
|
781
|
+
fieldAdd(target: FieldRef, source: FieldRef) {
|
|
782
|
+
const tStore = arch.components.get(target._sym);
|
|
783
|
+
const sStore = arch.components.get(source._sym);
|
|
784
|
+
if (!tStore || !sStore) return;
|
|
785
|
+
const dst = tStore._fields[target._field];
|
|
786
|
+
const src = sStore._fields[source._field];
|
|
787
|
+
const stride = tStore._arraySizes[target._field] || 1;
|
|
788
|
+
const n = arch.count * stride;
|
|
789
|
+
if (kernels && dst instanceof Float32Array && src instanceof Float32Array) {
|
|
790
|
+
kernels.add_f32(dst.byteOffset, src.byteOffset, n);
|
|
791
|
+
} else {
|
|
792
|
+
for (let i = 0; i < n; i++) (dst as never[])[i] = ((dst as never[])[i] as number + ((src as never[])[i] as number)) as never;
|
|
793
|
+
}
|
|
794
|
+
},
|
|
708
795
|
snapshot(ref: FieldRef) {
|
|
709
796
|
if (!snaps) return undefined;
|
|
710
|
-
const
|
|
711
|
-
const snap = snaps.get(sym as symbol);
|
|
797
|
+
const snap = snaps.get(ref._sym);
|
|
712
798
|
if (!snap) return undefined;
|
|
713
799
|
return snap[ref._field];
|
|
714
800
|
}
|
|
@@ -737,7 +823,7 @@ export function createEntityManager(): EntityManager {
|
|
|
737
823
|
arch.snapshotCount = 0;
|
|
738
824
|
for (const [t, store] of arch.components) {
|
|
739
825
|
if (store) {
|
|
740
|
-
arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity));
|
|
826
|
+
arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity, arena));
|
|
741
827
|
}
|
|
742
828
|
}
|
|
743
829
|
trackedArchetypes.push(arch);
|
|
@@ -765,14 +851,14 @@ export function createEntityManager(): EntityManager {
|
|
|
765
851
|
const snap = arch.snapshots!.get(type);
|
|
766
852
|
if (!snap) continue;
|
|
767
853
|
for (const field in store._schema) {
|
|
768
|
-
const src = store[field];
|
|
854
|
+
const src = store._fields[field];
|
|
769
855
|
const dst = snap[field];
|
|
770
856
|
const size = store._arraySizes[field] || 0;
|
|
771
857
|
const len = size > 0 ? count * size : count;
|
|
772
|
-
if (src
|
|
773
|
-
dst.set(src.subarray(0, len));
|
|
858
|
+
if ('set' in src) {
|
|
859
|
+
(dst as Exclude<SoAArrayValue, unknown[]>).set((src as Float32Array).subarray(0, len));
|
|
774
860
|
} else {
|
|
775
|
-
for (let i = 0; i < len; i++) dst[i] = src[i];
|
|
861
|
+
for (let i = 0; i < len; i++) (dst as unknown[])[i] = (src as unknown[])[i];
|
|
776
862
|
}
|
|
777
863
|
}
|
|
778
864
|
}
|
|
@@ -855,6 +941,10 @@ export function createEntityManager(): EntityManager {
|
|
|
855
941
|
}
|
|
856
942
|
},
|
|
857
943
|
|
|
944
|
+
commitRemovals(): void {
|
|
945
|
+
removedData.clear();
|
|
946
|
+
},
|
|
947
|
+
|
|
858
948
|
serialize(
|
|
859
949
|
symbolToName: Map<symbol, string>,
|
|
860
950
|
stripComponents: ComponentDef[] = [],
|
|
@@ -934,11 +1024,11 @@ export function createEntityManager(): EntityManager {
|
|
|
934
1024
|
|
|
935
1025
|
nextId = data.nextId;
|
|
936
1026
|
|
|
937
|
-
const entityComponents = new Map<EntityId,
|
|
1027
|
+
const entityComponents = new Map<EntityId, Map<symbol, ComponentData>>();
|
|
938
1028
|
|
|
939
1029
|
for (const id of data.entities) {
|
|
940
1030
|
allEntityIds.add(id);
|
|
941
|
-
entityComponents.set(id,
|
|
1031
|
+
entityComponents.set(id, new Map());
|
|
942
1032
|
}
|
|
943
1033
|
|
|
944
1034
|
for (const [name, store] of Object.entries(data.components)) {
|
|
@@ -954,16 +1044,16 @@ export function createEntityManager(): EntityManager {
|
|
|
954
1044
|
if (!obj) continue;
|
|
955
1045
|
|
|
956
1046
|
if (customDeserializer) {
|
|
957
|
-
obj
|
|
1047
|
+
obj.set(sym, customDeserializer(compData) as ComponentData);
|
|
958
1048
|
} else {
|
|
959
|
-
obj
|
|
1049
|
+
obj.set(sym, compData as ComponentData);
|
|
960
1050
|
}
|
|
961
1051
|
}
|
|
962
1052
|
}
|
|
963
1053
|
|
|
964
|
-
const groupedByKey = new Map<string, { entityId: EntityId; compMap:
|
|
1054
|
+
const groupedByKey = new Map<string, { entityId: EntityId; compMap: Map<symbol, ComponentData> }[]>();
|
|
965
1055
|
for (const [entityId, compMap] of entityComponents) {
|
|
966
|
-
const types =
|
|
1056
|
+
const types = [...compMap.keys()];
|
|
967
1057
|
if (types.length === 0) continue;
|
|
968
1058
|
|
|
969
1059
|
const key = maskKey(computeMask(types));
|
|
@@ -974,7 +1064,7 @@ export function createEntityManager(): EntityManager {
|
|
|
974
1064
|
}
|
|
975
1065
|
|
|
976
1066
|
for (const [, entries] of groupedByKey) {
|
|
977
|
-
const types =
|
|
1067
|
+
const types = [...entries[0].compMap.keys()];
|
|
978
1068
|
const arch = getOrCreateArchetype(types);
|
|
979
1069
|
|
|
980
1070
|
for (const { entityId, compMap } of entries) {
|