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
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
import { TYPED, componentSchemas, toSym, type ComponentDef, type TypeSpec } from './ComponentRegistry.js';
|
|
2
|
+
|
|
3
|
+
export type EntityId = number;
|
|
4
|
+
|
|
5
|
+
export interface FieldRef {
|
|
6
|
+
readonly _sym: symbol;
|
|
7
|
+
readonly _field: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ArchetypeView {
|
|
11
|
+
readonly id: number;
|
|
12
|
+
readonly entityIds: EntityId[];
|
|
13
|
+
readonly count: number;
|
|
14
|
+
readonly snapshotEntityIds: EntityId[] | null;
|
|
15
|
+
readonly snapshotCount: number;
|
|
16
|
+
field(ref: FieldRef): any;
|
|
17
|
+
fieldStride(ref: FieldRef): number;
|
|
18
|
+
snapshot(ref: FieldRef): any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SerializedData {
|
|
22
|
+
nextId: number;
|
|
23
|
+
entities: EntityId[];
|
|
24
|
+
components: Record<string, Record<string, unknown>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EntityManager {
|
|
28
|
+
createEntity(): EntityId;
|
|
29
|
+
destroyEntity(id: EntityId): void;
|
|
30
|
+
addComponent(entityId: EntityId, type: ComponentDef, data?: any): void;
|
|
31
|
+
removeComponent(entityId: EntityId, type: ComponentDef): void;
|
|
32
|
+
getComponent(entityId: EntityId, type: ComponentDef): any;
|
|
33
|
+
get(entityId: EntityId, fieldRef: FieldRef): any;
|
|
34
|
+
set(entityId: EntityId, fieldRef: FieldRef, value: any): void;
|
|
35
|
+
hasComponent(entityId: EntityId, type: ComponentDef): boolean;
|
|
36
|
+
query(include: ComponentDef[], exclude?: ComponentDef[]): EntityId[];
|
|
37
|
+
getAllEntities(): EntityId[];
|
|
38
|
+
createEntityWith(...args: unknown[]): EntityId;
|
|
39
|
+
count(include: ComponentDef[], exclude?: ComponentDef[]): number;
|
|
40
|
+
forEach(include: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void;
|
|
41
|
+
onAdd(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
|
|
42
|
+
onRemove(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
|
|
43
|
+
flushHooks(): void;
|
|
44
|
+
enableTracking(filterComponent: ComponentDef): void;
|
|
45
|
+
flushChanges(): { created: Set<EntityId>; destroyed: Set<EntityId> };
|
|
46
|
+
flushSnapshots(): void;
|
|
47
|
+
serialize(
|
|
48
|
+
symbolToName: Map<symbol, string>,
|
|
49
|
+
stripComponents?: ComponentDef[],
|
|
50
|
+
skipEntitiesWith?: ComponentDef[],
|
|
51
|
+
options?: { serializers?: Map<string, (data: unknown) => unknown> }
|
|
52
|
+
): SerializedData;
|
|
53
|
+
deserialize(
|
|
54
|
+
data: SerializedData,
|
|
55
|
+
nameToSymbol: Record<string, ComponentDef>,
|
|
56
|
+
options?: { deserializers?: Map<string, (data: unknown) => unknown> }
|
|
57
|
+
): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Internal types ───────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
interface SoAStore {
|
|
63
|
+
[TYPED]: true;
|
|
64
|
+
_schema: Record<string, TypeSpec>;
|
|
65
|
+
_capacity: number;
|
|
66
|
+
_arraySizes: Record<string, number>;
|
|
67
|
+
[field: string]: any;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type SnapshotStore = Record<string, any>;
|
|
71
|
+
|
|
72
|
+
interface Archetype {
|
|
73
|
+
key: Uint32Array;
|
|
74
|
+
id: number;
|
|
75
|
+
types: Set<symbol>;
|
|
76
|
+
entityIds: EntityId[];
|
|
77
|
+
components: Map<symbol, SoAStore | null>;
|
|
78
|
+
snapshots: Map<symbol, SnapshotStore> | null;
|
|
79
|
+
snapshotEntityIds: EntityId[] | null;
|
|
80
|
+
snapshotCount: number;
|
|
81
|
+
entityToIndex: Map<EntityId, number>;
|
|
82
|
+
count: number;
|
|
83
|
+
capacity: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type HookCallback = (entityId: EntityId) => void;
|
|
87
|
+
|
|
88
|
+
interface Hooks {
|
|
89
|
+
addCbs: Map<symbol, HookCallback[]>;
|
|
90
|
+
removeCbs: Map<symbol, HookCallback[]>;
|
|
91
|
+
pendingAdd: Map<symbol, EntityId[]>;
|
|
92
|
+
pendingRemove: Map<symbol, EntityId[]>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const INITIAL_CAPACITY = 64;
|
|
96
|
+
|
|
97
|
+
// ── Array-based bitmask helpers ──────────────────────────
|
|
98
|
+
|
|
99
|
+
function slotsNeeded(bitCount: number): number {
|
|
100
|
+
return ((bitCount - 1) >>> 5) + 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createMask(slots: number): Uint32Array {
|
|
104
|
+
return new Uint32Array(slots);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function maskSetBit(mask: Uint32Array, bit: number): Uint32Array {
|
|
108
|
+
const slot = bit >>> 5;
|
|
109
|
+
if (slot >= mask.length) {
|
|
110
|
+
const grown = new Uint32Array(slot + 1);
|
|
111
|
+
grown.set(mask);
|
|
112
|
+
grown[slot] |= (1 << (bit & 31));
|
|
113
|
+
return grown;
|
|
114
|
+
}
|
|
115
|
+
mask[slot] |= (1 << (bit & 31));
|
|
116
|
+
return mask;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function maskContains(a: Uint32Array, b: Uint32Array): boolean {
|
|
120
|
+
for (let i = 0; i < b.length; i++) {
|
|
121
|
+
const av = i < a.length ? a[i] : 0;
|
|
122
|
+
if ((av & b[i]) !== b[i]) return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function maskDisjoint(a: Uint32Array, b: Uint32Array): boolean {
|
|
128
|
+
const len = Math.min(a.length, b.length);
|
|
129
|
+
for (let i = 0; i < len; i++) {
|
|
130
|
+
if ((a[i] & b[i]) !== 0) return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function maskOverlaps(a: Uint32Array, b: Uint32Array): boolean {
|
|
136
|
+
const len = Math.min(a.length, b.length);
|
|
137
|
+
for (let i = 0; i < len; i++) {
|
|
138
|
+
if ((a[i] & b[i]) !== 0) return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function maskKey(mask: Uint32Array): string {
|
|
144
|
+
let key = '';
|
|
145
|
+
for (let i = 0; i < mask.length; i++) {
|
|
146
|
+
if (i > 0) key += ',';
|
|
147
|
+
key += mask[i];
|
|
148
|
+
}
|
|
149
|
+
return key;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── SoA helpers ──────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
function unpackSpec(spec: TypeSpec): [any, number] {
|
|
155
|
+
if (Array.isArray(spec)) return spec;
|
|
156
|
+
return [spec, 0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createSoAStore(schema: Record<string, TypeSpec>, capacity: number): SoAStore {
|
|
160
|
+
const store: any = { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: {} };
|
|
161
|
+
for (const [field, spec] of Object.entries(schema)) {
|
|
162
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
163
|
+
if (size > 0) {
|
|
164
|
+
store[field] = new Ctor(capacity * size);
|
|
165
|
+
store._arraySizes[field] = size;
|
|
166
|
+
} else {
|
|
167
|
+
store[field] = new Ctor(capacity);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return store;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function growSoAStore(store: SoAStore, newCapacity: number): void {
|
|
174
|
+
store._capacity = newCapacity;
|
|
175
|
+
for (const [field, spec] of Object.entries(store._schema)) {
|
|
176
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
177
|
+
const old = store[field];
|
|
178
|
+
const allocSize = size > 0 ? newCapacity * size : newCapacity;
|
|
179
|
+
store[field] = new Ctor(allocSize);
|
|
180
|
+
if (Ctor === Array) {
|
|
181
|
+
for (let i = 0; i < old.length; i++) store[field][i] = old[i];
|
|
182
|
+
} else {
|
|
183
|
+
store[field].set(old);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function soaWrite(store: SoAStore, idx: number, data: any): void {
|
|
189
|
+
if (!data) {
|
|
190
|
+
for (const field in store._schema) {
|
|
191
|
+
const size = store._arraySizes[field] || 0;
|
|
192
|
+
if (size > 0) {
|
|
193
|
+
const base = idx * size;
|
|
194
|
+
for (let j = 0; j < size; j++) store[field][base + j] = 0;
|
|
195
|
+
} else {
|
|
196
|
+
store[field][idx] = 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
for (const field in store._schema) {
|
|
202
|
+
const size = store._arraySizes[field] || 0;
|
|
203
|
+
if (size > 0) {
|
|
204
|
+
const base = idx * size;
|
|
205
|
+
const src = data[field];
|
|
206
|
+
if (src) {
|
|
207
|
+
for (let j = 0; j < size; j++) {
|
|
208
|
+
store[field][base + j] = src[j] ?? 0;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
store[field][idx] = data[field];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function soaRead(store: SoAStore, idx: number): Record<string, any> {
|
|
218
|
+
const obj: Record<string, any> = {};
|
|
219
|
+
for (const field in store._schema) {
|
|
220
|
+
const size = store._arraySizes[field] || 0;
|
|
221
|
+
if (size > 0) {
|
|
222
|
+
const base = idx * size;
|
|
223
|
+
obj[field] = Array.from(store[field].subarray(base, base + size));
|
|
224
|
+
} else {
|
|
225
|
+
obj[field] = store[field][idx];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return obj;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function soaSwap(store: SoAStore, idxA: number, idxB: number): void {
|
|
232
|
+
for (const field in store._schema) {
|
|
233
|
+
const arr = store[field];
|
|
234
|
+
const size = store._arraySizes[field] || 0;
|
|
235
|
+
if (size > 0) {
|
|
236
|
+
const baseA = idxA * size;
|
|
237
|
+
const baseB = idxB * size;
|
|
238
|
+
for (let j = 0; j < size; j++) {
|
|
239
|
+
const tmp = arr[baseA + j];
|
|
240
|
+
arr[baseA + j] = arr[baseB + j];
|
|
241
|
+
arr[baseB + j] = tmp;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
const tmp = arr[idxA];
|
|
245
|
+
arr[idxA] = arr[idxB];
|
|
246
|
+
arr[idxB] = tmp;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createSnapshotStore(schema: Record<string, TypeSpec>, capacity: number): SnapshotStore {
|
|
252
|
+
const snap: SnapshotStore = {};
|
|
253
|
+
for (const [field, spec] of Object.entries(schema)) {
|
|
254
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
255
|
+
snap[field] = new Ctor(size > 0 ? capacity * size : capacity);
|
|
256
|
+
}
|
|
257
|
+
return snap;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function growSnapshotStore(snap: SnapshotStore, schema: Record<string, TypeSpec>, newCapacity: number): void {
|
|
261
|
+
for (const [field, spec] of Object.entries(schema)) {
|
|
262
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
263
|
+
const old = snap[field];
|
|
264
|
+
snap[field] = new Ctor(size > 0 ? newCapacity * size : newCapacity);
|
|
265
|
+
if (Ctor === Array) {
|
|
266
|
+
for (let i = 0; i < old.length; i++) snap[field][i] = old[i];
|
|
267
|
+
} else {
|
|
268
|
+
snap[field].set(old);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Entity Manager ───────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
export function createEntityManager(): EntityManager {
|
|
276
|
+
let nextId: EntityId = 1;
|
|
277
|
+
let nextArchId = 1;
|
|
278
|
+
const allEntityIds = new Set<EntityId>();
|
|
279
|
+
|
|
280
|
+
let trackFilter: Uint32Array | null = null;
|
|
281
|
+
let createdSet: Set<EntityId> | null = null;
|
|
282
|
+
let destroyedSet: Set<EntityId> | null = null;
|
|
283
|
+
const trackedArchetypes: Archetype[] = [];
|
|
284
|
+
|
|
285
|
+
let hooks: Hooks | null = null;
|
|
286
|
+
|
|
287
|
+
const componentBitIndex = new Map<symbol, number>();
|
|
288
|
+
let nextBitIndex = 0;
|
|
289
|
+
|
|
290
|
+
function getBit(type: symbol | ComponentDef): number {
|
|
291
|
+
const sym = toSym(type);
|
|
292
|
+
let bit = componentBitIndex.get(sym);
|
|
293
|
+
if (bit === undefined) {
|
|
294
|
+
bit = nextBitIndex++;
|
|
295
|
+
componentBitIndex.set(sym, bit);
|
|
296
|
+
}
|
|
297
|
+
return bit;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function computeMask(types: (symbol | ComponentDef)[]): Uint32Array {
|
|
301
|
+
const slots = nextBitIndex > 0 ? slotsNeeded(nextBitIndex) : 1;
|
|
302
|
+
let mask = createMask(slots);
|
|
303
|
+
for (const t of types) {
|
|
304
|
+
mask = maskSetBit(mask, getBit(t));
|
|
305
|
+
}
|
|
306
|
+
return mask;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const archetypes = new Map<string, Archetype>();
|
|
310
|
+
const entityArchetype = new Map<EntityId, Archetype>();
|
|
311
|
+
|
|
312
|
+
let queryCacheVersion = 0;
|
|
313
|
+
const queryCache = new Map<string, { version: number; archetypes: Archetype[] }>();
|
|
314
|
+
|
|
315
|
+
function getOrCreateArchetype(types: symbol[]): Archetype {
|
|
316
|
+
const mask = computeMask(types);
|
|
317
|
+
const key = maskKey(mask);
|
|
318
|
+
let arch = archetypes.get(key);
|
|
319
|
+
if (!arch) {
|
|
320
|
+
const tracked = trackFilter !== null && maskOverlaps(mask, trackFilter);
|
|
321
|
+
arch = {
|
|
322
|
+
key: mask,
|
|
323
|
+
id: nextArchId++,
|
|
324
|
+
types: new Set(types),
|
|
325
|
+
entityIds: [],
|
|
326
|
+
components: new Map(),
|
|
327
|
+
snapshots: tracked ? new Map() : null,
|
|
328
|
+
snapshotEntityIds: tracked ? [] : null,
|
|
329
|
+
snapshotCount: 0,
|
|
330
|
+
entityToIndex: new Map(),
|
|
331
|
+
count: 0,
|
|
332
|
+
capacity: INITIAL_CAPACITY
|
|
333
|
+
};
|
|
334
|
+
for (const t of types) {
|
|
335
|
+
const schema = componentSchemas.get(t);
|
|
336
|
+
const store = schema ? createSoAStore(schema, INITIAL_CAPACITY) : null;
|
|
337
|
+
arch.components.set(t, store);
|
|
338
|
+
if (tracked && store) {
|
|
339
|
+
arch.snapshots!.set(t, createSnapshotStore(schema!, INITIAL_CAPACITY));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
archetypes.set(key, arch);
|
|
343
|
+
if (tracked) trackedArchetypes.push(arch);
|
|
344
|
+
queryCacheVersion++;
|
|
345
|
+
}
|
|
346
|
+
return arch;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function ensureCapacity(arch: Archetype): void {
|
|
350
|
+
if (arch.count < arch.capacity) return;
|
|
351
|
+
const newCap = arch.capacity * 2;
|
|
352
|
+
arch.capacity = newCap;
|
|
353
|
+
for (const [type, store] of arch.components) {
|
|
354
|
+
if (store) {
|
|
355
|
+
growSoAStore(store, newCap);
|
|
356
|
+
if (arch.snapshots) {
|
|
357
|
+
const snap = arch.snapshots.get(type);
|
|
358
|
+
if (snap) growSnapshotStore(snap, store._schema, newCap);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function addToArchetype(arch: Archetype, entityId: EntityId, componentMap: Record<symbol, any>): void {
|
|
365
|
+
ensureCapacity(arch);
|
|
366
|
+
const idx = arch.count;
|
|
367
|
+
arch.entityIds[idx] = entityId;
|
|
368
|
+
for (const t of arch.types) {
|
|
369
|
+
const store = arch.components.get(t);
|
|
370
|
+
if (store) soaWrite(store, idx, (componentMap as any)[t]);
|
|
371
|
+
}
|
|
372
|
+
arch.entityToIndex.set(entityId, idx);
|
|
373
|
+
arch.count++;
|
|
374
|
+
entityArchetype.set(entityId, arch);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function removeFromArchetype(arch: Archetype, entityId: EntityId): void {
|
|
378
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
379
|
+
const lastIdx = arch.count - 1;
|
|
380
|
+
|
|
381
|
+
if (idx !== lastIdx) {
|
|
382
|
+
const lastEntity = arch.entityIds[lastIdx];
|
|
383
|
+
arch.entityIds[idx] = lastEntity;
|
|
384
|
+
for (const [, store] of arch.components) {
|
|
385
|
+
if (store) soaSwap(store, idx, lastIdx);
|
|
386
|
+
}
|
|
387
|
+
arch.entityToIndex.set(lastEntity, idx);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
arch.entityIds.length = lastIdx;
|
|
391
|
+
arch.entityToIndex.delete(entityId);
|
|
392
|
+
arch.count--;
|
|
393
|
+
entityArchetype.delete(entityId);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function readComponentData(arch: Archetype, type: symbol, idx: number): any {
|
|
397
|
+
const store = arch.components.get(type);
|
|
398
|
+
if (!store) return undefined;
|
|
399
|
+
return soaRead(store, idx);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getMatchingArchetypes(types: (symbol | ComponentDef)[], excludeTypes?: (symbol | ComponentDef)[]): Archetype[] {
|
|
403
|
+
const includeMask = computeMask(types);
|
|
404
|
+
const excludeMask = excludeTypes && excludeTypes.length > 0 ? computeMask(excludeTypes) : null;
|
|
405
|
+
const queryStr = maskKey(includeMask) + ':' + (excludeMask ? maskKey(excludeMask) : '');
|
|
406
|
+
|
|
407
|
+
const cached = queryCache.get(queryStr);
|
|
408
|
+
if (cached && cached.version === queryCacheVersion) {
|
|
409
|
+
return cached.archetypes;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const matching: Archetype[] = [];
|
|
413
|
+
for (const arch of archetypes.values()) {
|
|
414
|
+
if (maskContains(arch.key, includeMask) &&
|
|
415
|
+
(!excludeMask || maskDisjoint(arch.key, excludeMask))) {
|
|
416
|
+
matching.push(arch);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
queryCache.set(queryStr, { version: queryCacheVersion, archetypes: matching });
|
|
421
|
+
return matching;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
createEntity(): EntityId {
|
|
426
|
+
const id = nextId++;
|
|
427
|
+
allEntityIds.add(id);
|
|
428
|
+
return id;
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
destroyEntity(id: EntityId): void {
|
|
432
|
+
const arch = entityArchetype.get(id);
|
|
433
|
+
if (arch) {
|
|
434
|
+
if (hooks) {
|
|
435
|
+
for (const type of arch.types) {
|
|
436
|
+
const pending = hooks.pendingRemove.get(type);
|
|
437
|
+
if (pending) pending.push(id);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(id);
|
|
441
|
+
removeFromArchetype(arch, id);
|
|
442
|
+
}
|
|
443
|
+
allEntityIds.delete(id);
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
addComponent(entityId: EntityId, comp: ComponentDef, data?: any): void {
|
|
447
|
+
const type = toSym(comp);
|
|
448
|
+
const arch = entityArchetype.get(entityId);
|
|
449
|
+
|
|
450
|
+
if (!arch) {
|
|
451
|
+
const newArch = getOrCreateArchetype([type]);
|
|
452
|
+
addToArchetype(newArch, entityId, { [type as unknown as string]: data });
|
|
453
|
+
if (hooks) {
|
|
454
|
+
const pending = hooks.pendingAdd.get(type);
|
|
455
|
+
if (pending) pending.push(entityId);
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (arch.types.has(type)) {
|
|
461
|
+
const store = arch.components.get(type);
|
|
462
|
+
if (store) {
|
|
463
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
464
|
+
soaWrite(store, idx, data);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const newTypes = [...arch.types, type];
|
|
470
|
+
const newArch = getOrCreateArchetype(newTypes);
|
|
471
|
+
|
|
472
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
473
|
+
const map: any = { [type as unknown as string]: data };
|
|
474
|
+
for (const t of arch.types) {
|
|
475
|
+
map[t as unknown as string] = readComponentData(arch, t, idx);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
removeFromArchetype(arch, entityId);
|
|
479
|
+
addToArchetype(newArch, entityId, map);
|
|
480
|
+
if (hooks) {
|
|
481
|
+
const pending = hooks.pendingAdd.get(type);
|
|
482
|
+
if (pending) pending.push(entityId);
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
removeComponent(entityId: EntityId, comp: ComponentDef): void {
|
|
487
|
+
const type = toSym(comp);
|
|
488
|
+
const arch = entityArchetype.get(entityId);
|
|
489
|
+
if (!arch || !arch.types.has(type)) return;
|
|
490
|
+
|
|
491
|
+
if (hooks) {
|
|
492
|
+
const pending = hooks.pendingRemove.get(type);
|
|
493
|
+
if (pending) pending.push(entityId);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(entityId);
|
|
497
|
+
|
|
498
|
+
if (arch.types.size === 1) {
|
|
499
|
+
removeFromArchetype(arch, entityId);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const newTypes: symbol[] = [];
|
|
504
|
+
for (const t of arch.types) {
|
|
505
|
+
if (t !== type) newTypes.push(t);
|
|
506
|
+
}
|
|
507
|
+
const newArch = getOrCreateArchetype(newTypes);
|
|
508
|
+
|
|
509
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
510
|
+
const map: any = {};
|
|
511
|
+
for (const t of newTypes) {
|
|
512
|
+
map[t as unknown as string] = readComponentData(arch, t, idx);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
removeFromArchetype(arch, entityId);
|
|
516
|
+
addToArchetype(newArch, entityId, map);
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
getComponent(entityId: EntityId, comp: ComponentDef): any {
|
|
520
|
+
const type = toSym(comp);
|
|
521
|
+
const arch = entityArchetype.get(entityId);
|
|
522
|
+
if (!arch) return undefined;
|
|
523
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
524
|
+
if (idx === undefined) return undefined;
|
|
525
|
+
return readComponentData(arch, type, idx);
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
get(entityId: EntityId, fieldRef: FieldRef): any {
|
|
529
|
+
const arch = entityArchetype.get(entityId);
|
|
530
|
+
if (!arch) return undefined;
|
|
531
|
+
const store = arch.components.get(fieldRef._sym);
|
|
532
|
+
if (!store) return undefined;
|
|
533
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
534
|
+
const size = store._arraySizes[fieldRef._field] || 0;
|
|
535
|
+
if (size > 0) {
|
|
536
|
+
const base = idx * size;
|
|
537
|
+
return store[fieldRef._field].subarray(base, base + size);
|
|
538
|
+
}
|
|
539
|
+
return store[fieldRef._field][idx];
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
set(entityId: EntityId, fieldRef: FieldRef, value: any): void {
|
|
543
|
+
const arch = entityArchetype.get(entityId);
|
|
544
|
+
if (!arch) return;
|
|
545
|
+
const store = arch.components.get(fieldRef._sym);
|
|
546
|
+
if (!store) return;
|
|
547
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
548
|
+
const size = store._arraySizes[fieldRef._field] || 0;
|
|
549
|
+
if (size > 0) {
|
|
550
|
+
store[fieldRef._field].set(value, idx * size);
|
|
551
|
+
} else {
|
|
552
|
+
store[fieldRef._field][idx] = value;
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
hasComponent(entityId: EntityId, comp: ComponentDef): boolean {
|
|
557
|
+
const type = toSym(comp);
|
|
558
|
+
const arch = entityArchetype.get(entityId);
|
|
559
|
+
return arch ? arch.types.has(type) : false;
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
query(includeTypes: ComponentDef[], excludeTypes?: ComponentDef[]): EntityId[] {
|
|
563
|
+
const matching = getMatchingArchetypes(includeTypes, excludeTypes);
|
|
564
|
+
const result: EntityId[] = [];
|
|
565
|
+
for (let a = 0; a < matching.length; a++) {
|
|
566
|
+
const arch = matching[a];
|
|
567
|
+
const ids = arch.entityIds;
|
|
568
|
+
for (let i = 0; i < arch.count; i++) {
|
|
569
|
+
result.push(ids[i]);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
getAllEntities(): EntityId[] {
|
|
576
|
+
return [...allEntityIds];
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
createEntityWith(...args: unknown[]): EntityId {
|
|
580
|
+
const id = nextId++;
|
|
581
|
+
allEntityIds.add(id);
|
|
582
|
+
|
|
583
|
+
const types: symbol[] = [];
|
|
584
|
+
const map: any = {};
|
|
585
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
586
|
+
const sym = toSym(args[i] as ComponentDef);
|
|
587
|
+
types.push(sym);
|
|
588
|
+
map[sym as unknown as string] = args[i + 1];
|
|
589
|
+
}
|
|
590
|
+
const arch = getOrCreateArchetype(types);
|
|
591
|
+
addToArchetype(arch, id, map);
|
|
592
|
+
|
|
593
|
+
if (hooks) {
|
|
594
|
+
for (let i = 0; i < types.length; i++) {
|
|
595
|
+
const pending = hooks.pendingAdd.get(types[i]);
|
|
596
|
+
if (pending) pending.push(id);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (createdSet && trackFilter && maskOverlaps(arch.key, trackFilter)) createdSet.add(id);
|
|
601
|
+
return id;
|
|
602
|
+
},
|
|
603
|
+
|
|
604
|
+
count(includeTypes: ComponentDef[], excludeTypes?: ComponentDef[]): number {
|
|
605
|
+
const matching = getMatchingArchetypes(includeTypes, excludeTypes);
|
|
606
|
+
let total = 0;
|
|
607
|
+
for (let a = 0; a < matching.length; a++) {
|
|
608
|
+
total += matching[a].count;
|
|
609
|
+
}
|
|
610
|
+
return total;
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
forEach(includeTypes: ComponentDef[], callback: (view: ArchetypeView) => void, excludeTypes?: ComponentDef[]): void {
|
|
614
|
+
const matching = getMatchingArchetypes(includeTypes, excludeTypes);
|
|
615
|
+
for (let a = 0; a < matching.length; a++) {
|
|
616
|
+
const arch = matching[a];
|
|
617
|
+
if (arch.count === 0) continue;
|
|
618
|
+
const snaps = arch.snapshots;
|
|
619
|
+
const view: ArchetypeView = {
|
|
620
|
+
id: arch.id,
|
|
621
|
+
entityIds: arch.entityIds,
|
|
622
|
+
count: arch.count,
|
|
623
|
+
snapshotEntityIds: arch.snapshotEntityIds,
|
|
624
|
+
snapshotCount: arch.snapshotCount,
|
|
625
|
+
field(ref: FieldRef) {
|
|
626
|
+
const sym = ref._sym || ref;
|
|
627
|
+
const store = arch.components.get(sym as symbol);
|
|
628
|
+
if (!store) return undefined;
|
|
629
|
+
return store[ref._field];
|
|
630
|
+
},
|
|
631
|
+
fieldStride(ref: FieldRef) {
|
|
632
|
+
const sym = ref._sym || ref;
|
|
633
|
+
const store = arch.components.get(sym as symbol);
|
|
634
|
+
if (!store) return 1;
|
|
635
|
+
return store._arraySizes[ref._field] || 1;
|
|
636
|
+
},
|
|
637
|
+
snapshot(ref: FieldRef) {
|
|
638
|
+
if (!snaps) return undefined;
|
|
639
|
+
const sym = ref._sym || ref;
|
|
640
|
+
const snap = snaps.get(sym as symbol);
|
|
641
|
+
if (!snap) return undefined;
|
|
642
|
+
return snap[ref._field];
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
callback(view);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
enableTracking(filterComponent: ComponentDef): void {
|
|
650
|
+
const bit = getBit(filterComponent);
|
|
651
|
+
const slots = slotsNeeded(bit + 1);
|
|
652
|
+
trackFilter = createMask(slots);
|
|
653
|
+
trackFilter = maskSetBit(trackFilter, bit);
|
|
654
|
+
createdSet = new Set();
|
|
655
|
+
destroyedSet = new Set();
|
|
656
|
+
for (const arch of archetypes.values()) {
|
|
657
|
+
if (maskOverlaps(arch.key, trackFilter) && !arch.snapshots) {
|
|
658
|
+
arch.snapshots = new Map();
|
|
659
|
+
arch.snapshotEntityIds = [];
|
|
660
|
+
arch.snapshotCount = 0;
|
|
661
|
+
for (const [t, store] of arch.components) {
|
|
662
|
+
if (store) {
|
|
663
|
+
arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
trackedArchetypes.push(arch);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
flushChanges() {
|
|
672
|
+
const result = { created: createdSet!, destroyed: destroyedSet! };
|
|
673
|
+
createdSet = new Set();
|
|
674
|
+
destroyedSet = new Set();
|
|
675
|
+
return result;
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
flushSnapshots(): void {
|
|
679
|
+
for (let a = 0; a < trackedArchetypes.length; a++) {
|
|
680
|
+
const arch = trackedArchetypes[a];
|
|
681
|
+
const count = arch.count;
|
|
682
|
+
const eids = arch.entityIds;
|
|
683
|
+
const snapEids = arch.snapshotEntityIds!;
|
|
684
|
+
for (let i = 0; i < count; i++) snapEids[i] = eids[i];
|
|
685
|
+
arch.snapshotCount = count;
|
|
686
|
+
for (const [type, store] of arch.components) {
|
|
687
|
+
if (!store) continue;
|
|
688
|
+
const snap = arch.snapshots!.get(type);
|
|
689
|
+
if (!snap) continue;
|
|
690
|
+
for (const field in store._schema) {
|
|
691
|
+
const src = store[field];
|
|
692
|
+
const dst = snap[field];
|
|
693
|
+
const size = store._arraySizes[field] || 0;
|
|
694
|
+
const len = size > 0 ? count * size : count;
|
|
695
|
+
if (src.set) {
|
|
696
|
+
dst.set(src.subarray(0, len));
|
|
697
|
+
} else {
|
|
698
|
+
for (let i = 0; i < len; i++) dst[i] = src[i];
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
onAdd(comp: ComponentDef, callback: HookCallback): () => void {
|
|
706
|
+
const type = toSym(comp);
|
|
707
|
+
if (!hooks) {
|
|
708
|
+
hooks = {
|
|
709
|
+
addCbs: new Map(),
|
|
710
|
+
removeCbs: new Map(),
|
|
711
|
+
pendingAdd: new Map(),
|
|
712
|
+
pendingRemove: new Map(),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
if (!hooks.addCbs.has(type)) {
|
|
716
|
+
hooks.addCbs.set(type, []);
|
|
717
|
+
hooks.pendingAdd.set(type, []);
|
|
718
|
+
}
|
|
719
|
+
hooks.addCbs.get(type)!.push(callback);
|
|
720
|
+
return () => {
|
|
721
|
+
const cbs = hooks && hooks.addCbs.get(type);
|
|
722
|
+
if (!cbs) return;
|
|
723
|
+
const idx = cbs.indexOf(callback);
|
|
724
|
+
if (idx !== -1) cbs.splice(idx, 1);
|
|
725
|
+
if (cbs.length === 0) {
|
|
726
|
+
hooks!.addCbs.delete(type);
|
|
727
|
+
hooks!.pendingAdd.delete(type);
|
|
728
|
+
}
|
|
729
|
+
if (hooks!.addCbs.size === 0 && hooks!.removeCbs.size === 0) hooks = null;
|
|
730
|
+
};
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
onRemove(comp: ComponentDef, callback: HookCallback): () => void {
|
|
734
|
+
const type = toSym(comp);
|
|
735
|
+
if (!hooks) {
|
|
736
|
+
hooks = {
|
|
737
|
+
addCbs: new Map(),
|
|
738
|
+
removeCbs: new Map(),
|
|
739
|
+
pendingAdd: new Map(),
|
|
740
|
+
pendingRemove: new Map(),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
if (!hooks.removeCbs.has(type)) {
|
|
744
|
+
hooks.removeCbs.set(type, []);
|
|
745
|
+
hooks.pendingRemove.set(type, []);
|
|
746
|
+
}
|
|
747
|
+
hooks.removeCbs.get(type)!.push(callback);
|
|
748
|
+
return () => {
|
|
749
|
+
const cbs = hooks && hooks.removeCbs.get(type);
|
|
750
|
+
if (!cbs) return;
|
|
751
|
+
const idx = cbs.indexOf(callback);
|
|
752
|
+
if (idx !== -1) cbs.splice(idx, 1);
|
|
753
|
+
if (cbs.length === 0) {
|
|
754
|
+
hooks!.removeCbs.delete(type);
|
|
755
|
+
hooks!.pendingRemove.delete(type);
|
|
756
|
+
}
|
|
757
|
+
if (hooks!.addCbs.size === 0 && hooks!.removeCbs.size === 0) hooks = null;
|
|
758
|
+
};
|
|
759
|
+
},
|
|
760
|
+
|
|
761
|
+
flushHooks(): void {
|
|
762
|
+
if (!hooks) return;
|
|
763
|
+
for (const [sym, pending] of hooks.pendingAdd) {
|
|
764
|
+
if (pending.length === 0) continue;
|
|
765
|
+
const cbs = hooks.addCbs.get(sym)!;
|
|
766
|
+
for (let c = 0; c < cbs.length; c++) {
|
|
767
|
+
for (let i = 0; i < pending.length; i++) cbs[c](pending[i]);
|
|
768
|
+
}
|
|
769
|
+
pending.length = 0;
|
|
770
|
+
}
|
|
771
|
+
for (const [sym, pending] of hooks.pendingRemove) {
|
|
772
|
+
if (pending.length === 0) continue;
|
|
773
|
+
const cbs = hooks.removeCbs.get(sym)!;
|
|
774
|
+
for (let c = 0; c < cbs.length; c++) {
|
|
775
|
+
for (let i = 0; i < pending.length; i++) cbs[c](pending[i]);
|
|
776
|
+
}
|
|
777
|
+
pending.length = 0;
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
serialize(
|
|
782
|
+
symbolToName: Map<symbol, string>,
|
|
783
|
+
stripComponents: ComponentDef[] = [],
|
|
784
|
+
skipEntitiesWith: ComponentDef[] = [],
|
|
785
|
+
{ serializers }: { serializers?: Map<string, (data: unknown) => unknown> } = {}
|
|
786
|
+
): SerializedData {
|
|
787
|
+
const stripSymbols = new Set(stripComponents.map(toSym));
|
|
788
|
+
const skipSymbols = new Set(skipEntitiesWith.map(toSym));
|
|
789
|
+
const skipEntityIds = new Set<EntityId>();
|
|
790
|
+
|
|
791
|
+
if (skipSymbols.size > 0) {
|
|
792
|
+
for (const arch of archetypes.values()) {
|
|
793
|
+
let hasSkip = false;
|
|
794
|
+
for (const sym of skipSymbols) {
|
|
795
|
+
if (arch.types.has(sym)) { hasSkip = true; break; }
|
|
796
|
+
}
|
|
797
|
+
if (!hasSkip) continue;
|
|
798
|
+
for (let i = 0; i < arch.count; i++) {
|
|
799
|
+
skipEntityIds.add(arch.entityIds[i]);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const serializedComponents: Record<string, Record<string, unknown>> = {};
|
|
805
|
+
|
|
806
|
+
for (const arch of archetypes.values()) {
|
|
807
|
+
for (const [sym, store] of arch.components) {
|
|
808
|
+
if (!store) continue;
|
|
809
|
+
if (stripSymbols.has(sym) || skipSymbols.has(sym)) continue;
|
|
810
|
+
const name = symbolToName.get(sym);
|
|
811
|
+
if (!name) continue;
|
|
812
|
+
|
|
813
|
+
if (!serializedComponents[name]) {
|
|
814
|
+
serializedComponents[name] = {};
|
|
815
|
+
}
|
|
816
|
+
const entries = serializedComponents[name];
|
|
817
|
+
|
|
818
|
+
const customSerializer = serializers && serializers.get(name);
|
|
819
|
+
|
|
820
|
+
for (let i = 0; i < arch.count; i++) {
|
|
821
|
+
const entityId = arch.entityIds[i];
|
|
822
|
+
if (skipEntityIds.has(entityId)) continue;
|
|
823
|
+
const value = soaRead(store, i);
|
|
824
|
+
entries[entityId] = customSerializer ? customSerializer(value) : value;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
for (const name of Object.keys(serializedComponents)) {
|
|
830
|
+
if (Object.keys(serializedComponents[name]).length === 0) {
|
|
831
|
+
delete serializedComponents[name];
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const serializedEntities: EntityId[] = [];
|
|
836
|
+
for (const id of allEntityIds) {
|
|
837
|
+
if (!skipEntityIds.has(id)) serializedEntities.push(id);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
nextId,
|
|
842
|
+
entities: serializedEntities,
|
|
843
|
+
components: serializedComponents
|
|
844
|
+
};
|
|
845
|
+
},
|
|
846
|
+
|
|
847
|
+
deserialize(
|
|
848
|
+
data: SerializedData,
|
|
849
|
+
nameToSymbol: Record<string, ComponentDef>,
|
|
850
|
+
{ deserializers }: { deserializers?: Map<string, (data: unknown) => unknown> } = {}
|
|
851
|
+
): void {
|
|
852
|
+
allEntityIds.clear();
|
|
853
|
+
archetypes.clear();
|
|
854
|
+
entityArchetype.clear();
|
|
855
|
+
queryCache.clear();
|
|
856
|
+
queryCacheVersion = 0;
|
|
857
|
+
|
|
858
|
+
nextId = data.nextId;
|
|
859
|
+
|
|
860
|
+
const entityComponents = new Map<EntityId, any>();
|
|
861
|
+
|
|
862
|
+
for (const id of data.entities) {
|
|
863
|
+
allEntityIds.add(id);
|
|
864
|
+
entityComponents.set(id, {});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
for (const [name, store] of Object.entries(data.components)) {
|
|
868
|
+
const entry = nameToSymbol[name];
|
|
869
|
+
if (!entry) continue;
|
|
870
|
+
const sym = toSym(entry);
|
|
871
|
+
|
|
872
|
+
const customDeserializer = deserializers && deserializers.get(name);
|
|
873
|
+
|
|
874
|
+
for (const [entityIdStr, compData] of Object.entries(store as Record<string, unknown>)) {
|
|
875
|
+
const entityId = Number(entityIdStr);
|
|
876
|
+
const obj = entityComponents.get(entityId);
|
|
877
|
+
if (!obj) continue;
|
|
878
|
+
|
|
879
|
+
if (customDeserializer) {
|
|
880
|
+
obj[sym] = customDeserializer(compData);
|
|
881
|
+
} else {
|
|
882
|
+
obj[sym] = compData;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const groupedByKey = new Map<string, { entityId: EntityId; compMap: any }[]>();
|
|
888
|
+
for (const [entityId, compMap] of entityComponents) {
|
|
889
|
+
const types = Object.getOwnPropertySymbols(compMap);
|
|
890
|
+
if (types.length === 0) continue;
|
|
891
|
+
|
|
892
|
+
const key = maskKey(computeMask(types));
|
|
893
|
+
if (!groupedByKey.has(key)) {
|
|
894
|
+
groupedByKey.set(key, []);
|
|
895
|
+
}
|
|
896
|
+
groupedByKey.get(key)!.push({ entityId, compMap });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
for (const [, entries] of groupedByKey) {
|
|
900
|
+
const types = Object.getOwnPropertySymbols(entries[0].compMap);
|
|
901
|
+
const arch = getOrCreateArchetype(types);
|
|
902
|
+
|
|
903
|
+
for (const { entityId, compMap } of entries) {
|
|
904
|
+
addToArchetype(arch, entityId, compMap);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|