archetype-ecs 1.0.0 → 1.2.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/.claude/settings.local.json +32 -0
- package/README.md +359 -0
- package/bench/allocations-1m.js +161 -0
- package/bench/multi-ecs-bench.js +393 -0
- package/bench/typed-array-vs-objects.js +268 -0
- package/bench/typed-vs-bitecs-1m.js +86 -0
- package/bench/typed-vs-untyped.js +166 -0
- package/bench/vs-bitecs.js +267 -0
- package/package.json +20 -6
- package/src/ComponentRegistry.js +21 -0
- package/src/EntityManager.js +194 -94
- package/src/index.d.ts +111 -0
- package/src/index.js +35 -3
- package/tests/EntityManager.test.js +342 -77
- package/tests/types.ts +101 -0
- package/tsconfig.json +10 -0
- package/src/Container.js +0 -23
- package/src/EventBus.js +0 -29
- package/src/GameLoop.js +0 -110
- package/tests/Container.test.js +0 -38
- package/tests/EventBus.test.js +0 -41
package/src/EntityManager.js
CHANGED
|
@@ -1,26 +1,87 @@
|
|
|
1
|
+
import { TYPED, componentSchemas, toSym } from './ComponentRegistry.js';
|
|
2
|
+
|
|
3
|
+
const INITIAL_CAPACITY = 64;
|
|
4
|
+
|
|
5
|
+
function createSoAStore(schema, capacity) {
|
|
6
|
+
const store = { [TYPED]: true, _schema: schema, _capacity: capacity };
|
|
7
|
+
for (const [field, Ctor] of Object.entries(schema)) {
|
|
8
|
+
store[field] = new Ctor(capacity);
|
|
9
|
+
}
|
|
10
|
+
return store;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function growSoAStore(store, newCapacity) {
|
|
14
|
+
store._capacity = newCapacity;
|
|
15
|
+
for (const [field, Ctor] of Object.entries(store._schema)) {
|
|
16
|
+
const old = store[field];
|
|
17
|
+
store[field] = new Ctor(newCapacity);
|
|
18
|
+
if (Ctor === Array) {
|
|
19
|
+
for (let i = 0; i < old.length; i++) store[field][i] = old[i];
|
|
20
|
+
} else {
|
|
21
|
+
store[field].set(old);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function soaWrite(store, idx, data) {
|
|
27
|
+
for (const field in store._schema) {
|
|
28
|
+
store[field][idx] = data[field];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function soaRead(store, idx) {
|
|
33
|
+
const obj = {};
|
|
34
|
+
for (const field in store._schema) {
|
|
35
|
+
obj[field] = store[field][idx];
|
|
36
|
+
}
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function soaSwap(store, idxA, idxB) {
|
|
41
|
+
for (const field in store._schema) {
|
|
42
|
+
const arr = store[field];
|
|
43
|
+
const tmp = arr[idxA];
|
|
44
|
+
arr[idxA] = arr[idxB];
|
|
45
|
+
arr[idxB] = tmp;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
1
49
|
export function createEntityManager() {
|
|
2
50
|
let nextId = 1;
|
|
3
51
|
const allEntityIds = new Set();
|
|
4
52
|
|
|
53
|
+
// Component bit registry (symbol → bit index 0..31)
|
|
54
|
+
const componentBitIndex = new Map();
|
|
55
|
+
let nextBitIndex = 0;
|
|
56
|
+
|
|
57
|
+
function getBit(type) {
|
|
58
|
+
const sym = toSym(type);
|
|
59
|
+
let bit = componentBitIndex.get(sym);
|
|
60
|
+
if (bit === undefined) {
|
|
61
|
+
bit = nextBitIndex++;
|
|
62
|
+
componentBitIndex.set(sym, bit);
|
|
63
|
+
}
|
|
64
|
+
return bit;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function computeMask(types) {
|
|
68
|
+
let mask = 0;
|
|
69
|
+
for (const t of types) {
|
|
70
|
+
mask |= (1 << getBit(t));
|
|
71
|
+
}
|
|
72
|
+
return mask;
|
|
73
|
+
}
|
|
74
|
+
|
|
5
75
|
// Archetype storage
|
|
6
|
-
const archetypes = new Map(); //
|
|
76
|
+
const archetypes = new Map(); // bitmask → Archetype
|
|
7
77
|
const entityArchetype = new Map(); // entityId → Archetype
|
|
8
78
|
|
|
9
79
|
// Query cache
|
|
10
80
|
let queryCacheVersion = 0;
|
|
11
81
|
const queryCache = new Map(); // queryKey → { version, archetypes[] }
|
|
12
82
|
|
|
13
|
-
function makeArchetypeKey(types) {
|
|
14
|
-
const descs = [];
|
|
15
|
-
for (const sym of types) {
|
|
16
|
-
descs.push(sym.description);
|
|
17
|
-
}
|
|
18
|
-
descs.sort();
|
|
19
|
-
return descs.join('|');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
83
|
function getOrCreateArchetype(types) {
|
|
23
|
-
const key =
|
|
84
|
+
const key = computeMask(types);
|
|
24
85
|
let arch = archetypes.get(key);
|
|
25
86
|
if (!arch) {
|
|
26
87
|
arch = {
|
|
@@ -29,10 +90,12 @@ export function createEntityManager() {
|
|
|
29
90
|
entityIds: [],
|
|
30
91
|
components: new Map(),
|
|
31
92
|
entityToIndex: new Map(),
|
|
32
|
-
count: 0
|
|
93
|
+
count: 0,
|
|
94
|
+
capacity: INITIAL_CAPACITY
|
|
33
95
|
};
|
|
34
96
|
for (const t of types) {
|
|
35
|
-
|
|
97
|
+
const schema = componentSchemas.get(t);
|
|
98
|
+
arch.components.set(t, schema ? createSoAStore(schema, INITIAL_CAPACITY) : null);
|
|
36
99
|
}
|
|
37
100
|
archetypes.set(key, arch);
|
|
38
101
|
queryCacheVersion++;
|
|
@@ -40,11 +103,22 @@ export function createEntityManager() {
|
|
|
40
103
|
return arch;
|
|
41
104
|
}
|
|
42
105
|
|
|
106
|
+
function ensureCapacity(arch) {
|
|
107
|
+
if (arch.count < arch.capacity) return;
|
|
108
|
+
const newCap = arch.capacity * 2;
|
|
109
|
+
arch.capacity = newCap;
|
|
110
|
+
for (const [type, store] of arch.components) {
|
|
111
|
+
if (store) growSoAStore(store, newCap);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
43
115
|
function addToArchetype(arch, entityId, componentMap) {
|
|
116
|
+
ensureCapacity(arch);
|
|
44
117
|
const idx = arch.count;
|
|
45
118
|
arch.entityIds[idx] = entityId;
|
|
46
119
|
for (const t of arch.types) {
|
|
47
|
-
arch.components.get(t)
|
|
120
|
+
const store = arch.components.get(t);
|
|
121
|
+
if (store) soaWrite(store, idx, componentMap[t]);
|
|
48
122
|
}
|
|
49
123
|
arch.entityToIndex.set(entityId, idx);
|
|
50
124
|
arch.count++;
|
|
@@ -56,30 +130,31 @@ export function createEntityManager() {
|
|
|
56
130
|
const lastIdx = arch.count - 1;
|
|
57
131
|
|
|
58
132
|
if (idx !== lastIdx) {
|
|
59
|
-
// Swap with last
|
|
60
133
|
const lastEntity = arch.entityIds[lastIdx];
|
|
61
134
|
arch.entityIds[idx] = lastEntity;
|
|
62
|
-
for (const [type,
|
|
63
|
-
|
|
135
|
+
for (const [type, store] of arch.components) {
|
|
136
|
+
if (store) soaSwap(store, idx, lastIdx);
|
|
64
137
|
}
|
|
65
138
|
arch.entityToIndex.set(lastEntity, idx);
|
|
66
139
|
}
|
|
67
140
|
|
|
68
|
-
// Pop last
|
|
69
141
|
arch.entityIds.length = lastIdx;
|
|
70
|
-
for (const [type, arr] of arch.components) {
|
|
71
|
-
arr.length = lastIdx;
|
|
72
|
-
}
|
|
73
142
|
arch.entityToIndex.delete(entityId);
|
|
74
143
|
arch.count--;
|
|
75
144
|
entityArchetype.delete(entityId);
|
|
76
145
|
}
|
|
77
146
|
|
|
147
|
+
function readComponentData(arch, type, idx) {
|
|
148
|
+
const store = arch.components.get(type);
|
|
149
|
+
if (!store) return undefined;
|
|
150
|
+
return soaRead(store, idx);
|
|
151
|
+
}
|
|
152
|
+
|
|
78
153
|
function getMatchingArchetypes(types, excludeTypes) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
154
|
+
const includeMask = computeMask(types);
|
|
155
|
+
const excludeMask = excludeTypes && excludeTypes.length > 0 ? computeMask(excludeTypes) : 0;
|
|
156
|
+
const queryKey = `${includeMask}:${excludeMask}`;
|
|
157
|
+
|
|
83
158
|
const cached = queryCache.get(queryKey);
|
|
84
159
|
if (cached && cached.version === queryCacheVersion) {
|
|
85
160
|
return cached.archetypes;
|
|
@@ -87,22 +162,10 @@ export function createEntityManager() {
|
|
|
87
162
|
|
|
88
163
|
const matching = [];
|
|
89
164
|
for (const arch of archetypes.values()) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
match = false;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
165
|
+
if ((arch.key & includeMask) === includeMask &&
|
|
166
|
+
(arch.key & excludeMask) === 0) {
|
|
167
|
+
matching.push(arch);
|
|
96
168
|
}
|
|
97
|
-
if (match && excludeTypes) {
|
|
98
|
-
for (const t of excludeTypes) {
|
|
99
|
-
if (arch.types.has(t)) {
|
|
100
|
-
match = false;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (match) matching.push(arch);
|
|
106
169
|
}
|
|
107
170
|
|
|
108
171
|
queryCache.set(queryKey, { version: queryCacheVersion, archetypes: matching });
|
|
@@ -124,78 +187,95 @@ export function createEntityManager() {
|
|
|
124
187
|
allEntityIds.delete(id);
|
|
125
188
|
},
|
|
126
189
|
|
|
127
|
-
addComponent(entityId,
|
|
190
|
+
addComponent(entityId, comp, data) {
|
|
191
|
+
const type = toSym(comp);
|
|
128
192
|
const arch = entityArchetype.get(entityId);
|
|
129
193
|
|
|
130
194
|
if (!arch) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const map = new Map();
|
|
134
|
-
map.set(componentName, data);
|
|
135
|
-
addToArchetype(newArch, entityId, map);
|
|
195
|
+
const newArch = getOrCreateArchetype([type]);
|
|
196
|
+
addToArchetype(newArch, entityId, { [type]: data });
|
|
136
197
|
return;
|
|
137
198
|
}
|
|
138
199
|
|
|
139
|
-
if (arch.types.has(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
200
|
+
if (arch.types.has(type)) {
|
|
201
|
+
const store = arch.components.get(type);
|
|
202
|
+
if (store) {
|
|
203
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
204
|
+
soaWrite(store, idx, data);
|
|
205
|
+
}
|
|
143
206
|
return;
|
|
144
207
|
}
|
|
145
208
|
|
|
146
|
-
|
|
147
|
-
const newTypes = [...arch.types, componentName];
|
|
209
|
+
const newTypes = [...arch.types, type];
|
|
148
210
|
const newArch = getOrCreateArchetype(newTypes);
|
|
149
211
|
|
|
150
|
-
// Collect component data from old archetype
|
|
151
212
|
const idx = arch.entityToIndex.get(entityId);
|
|
152
|
-
const map =
|
|
213
|
+
const map = { [type]: data };
|
|
153
214
|
for (const t of arch.types) {
|
|
154
|
-
map
|
|
215
|
+
map[t] = readComponentData(arch, t, idx);
|
|
155
216
|
}
|
|
156
|
-
map.set(componentName, data);
|
|
157
217
|
|
|
158
218
|
removeFromArchetype(arch, entityId);
|
|
159
219
|
addToArchetype(newArch, entityId, map);
|
|
160
220
|
},
|
|
161
221
|
|
|
162
|
-
removeComponent(entityId,
|
|
222
|
+
removeComponent(entityId, comp) {
|
|
223
|
+
const type = toSym(comp);
|
|
163
224
|
const arch = entityArchetype.get(entityId);
|
|
164
|
-
if (!arch || !arch.types.has(
|
|
225
|
+
if (!arch || !arch.types.has(type)) return;
|
|
165
226
|
|
|
166
227
|
if (arch.types.size === 1) {
|
|
167
|
-
// Removing last component — entity has no archetype
|
|
168
228
|
removeFromArchetype(arch, entityId);
|
|
169
229
|
return;
|
|
170
230
|
}
|
|
171
231
|
|
|
172
232
|
const newTypes = [];
|
|
173
233
|
for (const t of arch.types) {
|
|
174
|
-
if (t !==
|
|
234
|
+
if (t !== type) newTypes.push(t);
|
|
175
235
|
}
|
|
176
236
|
const newArch = getOrCreateArchetype(newTypes);
|
|
177
237
|
|
|
178
238
|
const idx = arch.entityToIndex.get(entityId);
|
|
179
|
-
const map =
|
|
239
|
+
const map = {};
|
|
180
240
|
for (const t of newTypes) {
|
|
181
|
-
map
|
|
241
|
+
map[t] = readComponentData(arch, t, idx);
|
|
182
242
|
}
|
|
183
243
|
|
|
184
244
|
removeFromArchetype(arch, entityId);
|
|
185
245
|
addToArchetype(newArch, entityId, map);
|
|
186
246
|
},
|
|
187
247
|
|
|
188
|
-
getComponent(entityId,
|
|
248
|
+
getComponent(entityId, comp) {
|
|
249
|
+
const type = toSym(comp);
|
|
250
|
+
const arch = entityArchetype.get(entityId);
|
|
251
|
+
if (!arch) return undefined;
|
|
252
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
253
|
+
if (idx === undefined) return undefined;
|
|
254
|
+
return readComponentData(arch, type, idx);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
get(entityId, fieldRef) {
|
|
189
258
|
const arch = entityArchetype.get(entityId);
|
|
190
259
|
if (!arch) return undefined;
|
|
191
|
-
const
|
|
192
|
-
if (!
|
|
193
|
-
|
|
260
|
+
const store = arch.components.get(fieldRef._sym);
|
|
261
|
+
if (!store) return undefined;
|
|
262
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
263
|
+
return store[fieldRef._field][idx];
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
set(entityId, fieldRef, value) {
|
|
267
|
+
const arch = entityArchetype.get(entityId);
|
|
268
|
+
if (!arch) return;
|
|
269
|
+
const store = arch.components.get(fieldRef._sym);
|
|
270
|
+
if (!store) return;
|
|
271
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
272
|
+
store[fieldRef._field][idx] = value;
|
|
194
273
|
},
|
|
195
274
|
|
|
196
|
-
hasComponent(entityId,
|
|
275
|
+
hasComponent(entityId, comp) {
|
|
276
|
+
const type = toSym(comp);
|
|
197
277
|
const arch = entityArchetype.get(entityId);
|
|
198
|
-
return arch ? arch.types.has(
|
|
278
|
+
return arch ? arch.types.has(type) : false;
|
|
199
279
|
},
|
|
200
280
|
|
|
201
281
|
query(includeTypes, excludeTypes) {
|
|
@@ -215,13 +295,19 @@ export function createEntityManager() {
|
|
|
215
295
|
return [...allEntityIds];
|
|
216
296
|
},
|
|
217
297
|
|
|
218
|
-
createEntityWith(
|
|
298
|
+
createEntityWith(...args) {
|
|
219
299
|
const id = nextId++;
|
|
220
300
|
allEntityIds.add(id);
|
|
221
301
|
|
|
222
|
-
const types = [
|
|
302
|
+
const types = [];
|
|
303
|
+
const map = {};
|
|
304
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
305
|
+
const sym = toSym(args[i]);
|
|
306
|
+
types.push(sym);
|
|
307
|
+
map[sym] = args[i + 1];
|
|
308
|
+
}
|
|
223
309
|
const arch = getOrCreateArchetype(types);
|
|
224
|
-
addToArchetype(arch, id,
|
|
310
|
+
addToArchetype(arch, id, map);
|
|
225
311
|
|
|
226
312
|
return id;
|
|
227
313
|
},
|
|
@@ -235,12 +321,30 @@ export function createEntityManager() {
|
|
|
235
321
|
return total;
|
|
236
322
|
},
|
|
237
323
|
|
|
324
|
+
forEach(includeTypes, callback, excludeTypes) {
|
|
325
|
+
const matching = getMatchingArchetypes(includeTypes, excludeTypes);
|
|
326
|
+
for (let a = 0; a < matching.length; a++) {
|
|
327
|
+
const arch = matching[a];
|
|
328
|
+
if (arch.count === 0) continue;
|
|
329
|
+
const view = {
|
|
330
|
+
entityIds: arch.entityIds,
|
|
331
|
+
count: arch.count,
|
|
332
|
+
field(ref) {
|
|
333
|
+
const sym = ref._sym || ref;
|
|
334
|
+
const store = arch.components.get(sym);
|
|
335
|
+
if (!store) return undefined;
|
|
336
|
+
return store[ref._field];
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
callback(view);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
238
343
|
serialize(symbolToName, stripComponents = [], skipEntitiesWith = [], { serializers } = {}) {
|
|
239
|
-
const stripSymbols = new Set(stripComponents);
|
|
240
|
-
const skipSymbols = new Set(skipEntitiesWith);
|
|
344
|
+
const stripSymbols = new Set(stripComponents.map(toSym));
|
|
345
|
+
const skipSymbols = new Set(skipEntitiesWith.map(toSym));
|
|
241
346
|
const skipEntityIds = new Set();
|
|
242
347
|
|
|
243
|
-
// Find entities that have any "skip entity" component — these are fully excluded
|
|
244
348
|
if (skipSymbols.size > 0) {
|
|
245
349
|
for (const arch of archetypes.values()) {
|
|
246
350
|
let hasSkip = false;
|
|
@@ -257,7 +361,8 @@ export function createEntityManager() {
|
|
|
257
361
|
const serializedComponents = {};
|
|
258
362
|
|
|
259
363
|
for (const arch of archetypes.values()) {
|
|
260
|
-
for (const [sym,
|
|
364
|
+
for (const [sym, store] of arch.components) {
|
|
365
|
+
if (!store) continue;
|
|
261
366
|
if (stripSymbols.has(sym) || skipSymbols.has(sym)) continue;
|
|
262
367
|
const name = symbolToName.get(sym);
|
|
263
368
|
if (!name) continue;
|
|
@@ -272,16 +377,12 @@ export function createEntityManager() {
|
|
|
272
377
|
for (let i = 0; i < arch.count; i++) {
|
|
273
378
|
const entityId = arch.entityIds[i];
|
|
274
379
|
if (skipEntityIds.has(entityId)) continue;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
} else {
|
|
278
|
-
entries[entityId] = structuredClone(arr[i]);
|
|
279
|
-
}
|
|
380
|
+
const value = soaRead(store, i);
|
|
381
|
+
entries[entityId] = customSerializer ? customSerializer(value) : value;
|
|
280
382
|
}
|
|
281
383
|
}
|
|
282
384
|
}
|
|
283
385
|
|
|
284
|
-
// Remove empty component groups
|
|
285
386
|
for (const name of Object.keys(serializedComponents)) {
|
|
286
387
|
if (Object.keys(serializedComponents[name]).length === 0) {
|
|
287
388
|
delete serializedComponents[name];
|
|
@@ -301,7 +402,6 @@ export function createEntityManager() {
|
|
|
301
402
|
},
|
|
302
403
|
|
|
303
404
|
deserialize(data, nameToSymbol, { deserializers } = {}) {
|
|
304
|
-
// Clear all state
|
|
305
405
|
allEntityIds.clear();
|
|
306
406
|
archetypes.clear();
|
|
307
407
|
entityArchetype.clear();
|
|
@@ -310,39 +410,39 @@ export function createEntityManager() {
|
|
|
310
410
|
|
|
311
411
|
nextId = data.nextId;
|
|
312
412
|
|
|
313
|
-
// Build per-entity component maps
|
|
314
413
|
const entityComponents = new Map();
|
|
315
414
|
|
|
316
415
|
for (const id of data.entities) {
|
|
317
416
|
allEntityIds.add(id);
|
|
318
|
-
entityComponents.set(id,
|
|
417
|
+
entityComponents.set(id, {});
|
|
319
418
|
}
|
|
320
419
|
|
|
321
420
|
for (const [name, store] of Object.entries(data.components)) {
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
421
|
+
const entry = nameToSymbol[name];
|
|
422
|
+
if (!entry) continue;
|
|
423
|
+
const sym = toSym(entry);
|
|
324
424
|
|
|
325
425
|
const customDeserializer = deserializers && deserializers.get(name);
|
|
326
426
|
|
|
327
427
|
for (const [entityIdStr, compData] of Object.entries(store)) {
|
|
328
428
|
const entityId = Number(entityIdStr);
|
|
329
|
-
const
|
|
330
|
-
if (!
|
|
429
|
+
const obj = entityComponents.get(entityId);
|
|
430
|
+
if (!obj) continue;
|
|
331
431
|
|
|
332
432
|
if (customDeserializer) {
|
|
333
|
-
|
|
433
|
+
obj[sym] = customDeserializer(compData);
|
|
334
434
|
} else {
|
|
335
|
-
|
|
435
|
+
obj[sym] = compData;
|
|
336
436
|
}
|
|
337
437
|
}
|
|
338
438
|
}
|
|
339
439
|
|
|
340
|
-
// Group by archetype key and bulk-insert
|
|
341
440
|
const groupedByKey = new Map();
|
|
342
441
|
for (const [entityId, compMap] of entityComponents) {
|
|
343
|
-
|
|
442
|
+
const types = Object.getOwnPropertySymbols(compMap);
|
|
443
|
+
if (types.length === 0) continue;
|
|
344
444
|
|
|
345
|
-
const key =
|
|
445
|
+
const key = computeMask(types);
|
|
346
446
|
if (!groupedByKey.has(key)) {
|
|
347
447
|
groupedByKey.set(key, []);
|
|
348
448
|
}
|
|
@@ -350,7 +450,7 @@ export function createEntityManager() {
|
|
|
350
450
|
}
|
|
351
451
|
|
|
352
452
|
for (const [key, entries] of groupedByKey) {
|
|
353
|
-
const types =
|
|
453
|
+
const types = Object.getOwnPropertySymbols(entries[0].compMap);
|
|
354
454
|
const arch = getOrCreateArchetype(types);
|
|
355
455
|
|
|
356
456
|
for (const { entityId, compMap } of entries) {
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// === Basis types ===
|
|
2
|
+
export type EntityId = number;
|
|
3
|
+
|
|
4
|
+
// === Field reference descriptor ===
|
|
5
|
+
export interface FieldRef<V = number> {
|
|
6
|
+
readonly _sym: symbol;
|
|
7
|
+
readonly _field: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// === Component definition ===
|
|
11
|
+
declare const __phantom: unique symbol;
|
|
12
|
+
export type ComponentDef<T = unknown> = {
|
|
13
|
+
readonly _sym: symbol;
|
|
14
|
+
readonly _name: string;
|
|
15
|
+
readonly [__phantom]?: T;
|
|
16
|
+
} & (T extends Record<string, number | string>
|
|
17
|
+
? { readonly [K in keyof T & string]: FieldRef<T[K]> }
|
|
18
|
+
: {});
|
|
19
|
+
|
|
20
|
+
/** @deprecated Use ComponentDef<T> instead */
|
|
21
|
+
export type Component<T = unknown> = ComponentDef<T>;
|
|
22
|
+
export type ComponentType = ComponentDef;
|
|
23
|
+
|
|
24
|
+
// === TypedArray schema ===
|
|
25
|
+
export type TypedArrayType = 'f32' | 'f64' | 'i8' | 'i16' | 'i32' | 'u8' | 'u16' | 'u32' | 'string';
|
|
26
|
+
|
|
27
|
+
export type Schema = Record<string, TypedArrayType>;
|
|
28
|
+
|
|
29
|
+
/** Maps a schema type to its runtime value type */
|
|
30
|
+
type FieldToType<T extends TypedArrayType> = T extends 'string' ? string : number;
|
|
31
|
+
|
|
32
|
+
/** Maps a schema definition to its runtime value type */
|
|
33
|
+
type SchemaToType<S extends Schema> = { [K in keyof S]: FieldToType<S[K]> };
|
|
34
|
+
|
|
35
|
+
export declare const TYPED: unique symbol;
|
|
36
|
+
|
|
37
|
+
export declare const componentSchemas: Map<symbol, Record<string, Float32ArrayConstructor | Float64ArrayConstructor | Int8ArrayConstructor | Int16ArrayConstructor | Int32ArrayConstructor | Uint8ArrayConstructor | Uint16ArrayConstructor | Uint32ArrayConstructor | ArrayConstructor>>;
|
|
38
|
+
|
|
39
|
+
// === TypedArray union ===
|
|
40
|
+
type TypedArray = Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array;
|
|
41
|
+
|
|
42
|
+
// === ArchetypeView (forEach callback) ===
|
|
43
|
+
export interface ArchetypeView {
|
|
44
|
+
readonly entityIds: EntityId[];
|
|
45
|
+
readonly count: number;
|
|
46
|
+
field(ref: FieldRef<any>): TypedArray | unknown[] | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// === Serialize/Deserialize ===
|
|
50
|
+
export interface SerializeOptions {
|
|
51
|
+
serializers?: Map<string, (data: unknown) => unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DeserializeOptions {
|
|
55
|
+
deserializers?: Map<string, (data: unknown) => unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SerializedData {
|
|
59
|
+
nextId: number;
|
|
60
|
+
entities: EntityId[];
|
|
61
|
+
components: Record<string, Record<string, unknown>>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// === EntityManager ===
|
|
65
|
+
export interface EntityManager {
|
|
66
|
+
createEntity(): EntityId;
|
|
67
|
+
destroyEntity(id: EntityId): void;
|
|
68
|
+
addComponent<T>(entityId: EntityId, type: ComponentDef<T>, data: T): void;
|
|
69
|
+
removeComponent(entityId: EntityId, type: ComponentDef): void;
|
|
70
|
+
getComponent<T>(entityId: EntityId, type: ComponentDef<T>): T | undefined;
|
|
71
|
+
get<V>(entityId: EntityId, fieldRef: FieldRef<V>): V | undefined;
|
|
72
|
+
set<V>(entityId: EntityId, fieldRef: FieldRef<V>, value: V): void;
|
|
73
|
+
hasComponent(entityId: EntityId, type: ComponentDef): boolean;
|
|
74
|
+
query(include: ComponentDef[], exclude?: ComponentDef[]): EntityId[];
|
|
75
|
+
getAllEntities(): EntityId[];
|
|
76
|
+
createEntityWith(...args: unknown[]): EntityId;
|
|
77
|
+
count(include: ComponentDef[], exclude?: ComponentDef[]): number;
|
|
78
|
+
forEach(include: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void;
|
|
79
|
+
serialize(
|
|
80
|
+
symbolToName: Map<symbol, string>,
|
|
81
|
+
stripComponents?: ComponentDef[],
|
|
82
|
+
skipEntitiesWith?: ComponentDef[],
|
|
83
|
+
options?: SerializeOptions
|
|
84
|
+
): SerializedData;
|
|
85
|
+
deserialize(
|
|
86
|
+
data: SerializedData,
|
|
87
|
+
nameToSymbol: Record<string, ComponentDef>,
|
|
88
|
+
options?: DeserializeOptions
|
|
89
|
+
): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// === Profiler ===
|
|
93
|
+
export interface ProfilerEntry {
|
|
94
|
+
avg: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Profiler {
|
|
98
|
+
readonly enabled: boolean;
|
|
99
|
+
setEnabled(value: boolean): void;
|
|
100
|
+
begin(): number;
|
|
101
|
+
end(name: string, t0: number): void;
|
|
102
|
+
record(name: string, ms: number): void;
|
|
103
|
+
getData(): Map<string, ProfilerEntry>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// === Exports ===
|
|
107
|
+
export function createEntityManager(): EntityManager;
|
|
108
|
+
export function component(name: string): ComponentDef;
|
|
109
|
+
export function component<T extends TypedArrayType, F extends string>(name: string, type: T, fields: F[]): ComponentDef<Record<F, FieldToType<T>>>;
|
|
110
|
+
export function component<S extends Schema>(name: string, schema: S): ComponentDef<SchemaToType<S>>;
|
|
111
|
+
export const profiler: Profiler;
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
export { createEntityManager } from './EntityManager.js';
|
|
2
|
-
export { createEventBus } from './EventBus.js';
|
|
3
|
-
export { createContainer } from './Container.js';
|
|
4
|
-
export { createGameLoop } from './GameLoop.js';
|
|
5
2
|
export { profiler } from './Profiler.js';
|
|
3
|
+
export { TYPED, componentSchemas } from './ComponentRegistry.js';
|
|
4
|
+
import { TYPE_MAP, componentSchemas } from './ComponentRegistry.js';
|
|
5
|
+
|
|
6
|
+
export function component(name, typeOrSchema, fields) {
|
|
7
|
+
const sym = Symbol(name);
|
|
8
|
+
const comp = { _sym: sym, _name: name };
|
|
9
|
+
|
|
10
|
+
let schema;
|
|
11
|
+
|
|
12
|
+
if (typeof typeOrSchema === 'string' && Array.isArray(fields)) {
|
|
13
|
+
// Short form: component('Position', 'f32', ['x', 'y'])
|
|
14
|
+
const Ctor = TYPE_MAP[typeOrSchema];
|
|
15
|
+
if (!Ctor) throw new Error(`Unknown type "${typeOrSchema}"`);
|
|
16
|
+
schema = {};
|
|
17
|
+
for (const f of fields) {
|
|
18
|
+
schema[f] = Ctor;
|
|
19
|
+
comp[f] = { _sym: sym, _field: f };
|
|
20
|
+
}
|
|
21
|
+
} else if (typeOrSchema && typeof typeOrSchema === 'object') {
|
|
22
|
+
// Schema form: component('Position', { x: 'f32', y: 'f32' })
|
|
23
|
+
schema = {};
|
|
24
|
+
for (const [field, type] of Object.entries(typeOrSchema)) {
|
|
25
|
+
const Ctor = TYPE_MAP[type];
|
|
26
|
+
if (!Ctor) throw new Error(`Unknown type "${type}" for field "${field}"`);
|
|
27
|
+
schema[field] = Ctor;
|
|
28
|
+
comp[field] = { _sym: sym, _field: field };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (schema) {
|
|
33
|
+
componentSchemas.set(sym, schema);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return comp;
|
|
37
|
+
}
|