@codehz/ecs 0.8.1 → 0.9.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.en.md +26 -3
- package/README.md +28 -3
- package/dist/builder.d.mts +296 -46
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/world.mjs +452 -179
- package/dist/world.mjs.map +1 -1
- package/examples/debug-observability.ts +92 -0
- package/examples/inventory-system-relations.ts +1 -1
- package/examples/parent-child-hierarchy.ts +18 -38
- package/package.json +1 -1
- package/skills/ecs/SKILL.md +9 -4
- package/src/__tests__/component/singleton.test.ts +40 -1
- package/src/__tests__/core/archetype.test.ts +155 -13
- package/src/__tests__/core/bitset.test.ts +12 -0
- package/src/__tests__/entity/entity.test.ts +33 -0
- package/src/__tests__/entity/id-system.test.ts +40 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
- package/src/__tests__/perf/serialization.perf.test.ts +242 -0
- package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
- package/src/__tests__/query/caching.test.ts +62 -0
- package/src/__tests__/query/filter.test.ts +16 -22
- package/src/__tests__/query/perf.test.ts +3 -5
- package/src/__tests__/relations/hierarchy.test.ts +208 -0
- package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
- package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
- package/src/__tests__/serialization/bounds.test.ts +134 -1
- package/src/__tests__/world/commands.test.ts +337 -0
- package/src/__tests__/world/debug-stats.test.ts +206 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
- package/src/__tests__/world/serialize.test.ts +17 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
- package/src/archetype/archetype.ts +96 -46
- package/src/archetype/helpers.ts +7 -29
- package/src/archetype/store.ts +35 -20
- package/src/commands/buffer.ts +5 -2
- package/src/commands/changeset.ts +0 -31
- package/src/component/registry.ts +64 -63
- package/src/entity/index.ts +6 -3
- package/src/index.ts +13 -0
- package/src/query/filter.ts +4 -10
- package/src/query/query.ts +12 -12
- package/src/storage/serialization.ts +29 -2
- package/src/types/index.ts +71 -0
- package/src/world/commands.ts +44 -56
- package/src/world/hooks.ts +8 -0
- package/src/world/serialization.ts +32 -18
- package/src/world/world.ts +387 -20
package/src/types/index.ts
CHANGED
|
@@ -97,3 +97,74 @@ export interface LifecycleHookEntry {
|
|
|
97
97
|
/** Archetypes that match this hook, used for precise cleanup on unsubscription */
|
|
98
98
|
matchedArchetypes?: Set<any>;
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Statistics payload delivered to callbacks registered via `World.createDebugStatsCollector`.
|
|
103
|
+
*
|
|
104
|
+
* All structural counts are snapshots taken after the sync that triggered delivery.
|
|
105
|
+
* `activity` always reflects work performed during that specific sync.
|
|
106
|
+
*
|
|
107
|
+
* Timestamps are raw `performance.now()` values suitable for `performance.measure`.
|
|
108
|
+
*/
|
|
109
|
+
export interface SyncDebugStats {
|
|
110
|
+
readonly timestamps: {
|
|
111
|
+
readonly syncStart: number;
|
|
112
|
+
readonly syncEnd: number;
|
|
113
|
+
readonly commandBufferStart: number;
|
|
114
|
+
readonly commandBufferEnd: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Number of iterations the internal command buffer loop performed during this sync. */
|
|
118
|
+
readonly commandIterations: number;
|
|
119
|
+
|
|
120
|
+
readonly entities: {
|
|
121
|
+
readonly total: number;
|
|
122
|
+
readonly freelistSize: number;
|
|
123
|
+
readonly nextId: number;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
readonly archetypes: {
|
|
127
|
+
readonly total: number;
|
|
128
|
+
readonly empty: number;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
readonly queries: {
|
|
132
|
+
readonly cached: number;
|
|
133
|
+
readonly registered: number;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
readonly hooks: {
|
|
137
|
+
readonly total: number;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** Sizes of stable internal reverse indices (conservative set). */
|
|
141
|
+
readonly indices: {
|
|
142
|
+
readonly entityReferences: number;
|
|
143
|
+
readonly entityToReferencingArchetypes: number;
|
|
144
|
+
readonly archetypesByComponent: number;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Activity that occurred as a direct result of this sync.
|
|
149
|
+
* All fields are always present (never optional).
|
|
150
|
+
*/
|
|
151
|
+
readonly activity: {
|
|
152
|
+
/** Number of entities that performed an archetype migration (hasArchetypeStructuralChange was true). */
|
|
153
|
+
readonly migrations: number;
|
|
154
|
+
/** Total number of individual hook callback invocations (invokeHook calls). */
|
|
155
|
+
readonly hooksExecuted: number;
|
|
156
|
+
/** Number of new archetypes created during this sync. */
|
|
157
|
+
readonly archetypesCreated: number;
|
|
158
|
+
/** Number of archetypes removed during this sync. */
|
|
159
|
+
readonly archetypesRemoved: number;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle returned by `World.createDebugStatsCollector`.
|
|
165
|
+
* The object itself carries no data — its only responsibility is lifetime management.
|
|
166
|
+
* Use with `using` or call `[Symbol.dispose]()` when you no longer need collection.
|
|
167
|
+
*/
|
|
168
|
+
export interface DebugStatsCollector {
|
|
169
|
+
[Symbol.dispose](): void;
|
|
170
|
+
}
|
package/src/world/commands.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Archetype } from "../archetype/archetype";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SparseStore } from "../archetype/store";
|
|
3
3
|
import type { Command } from "../commands/buffer";
|
|
4
4
|
import type { ComponentChangeset } from "../commands/changeset";
|
|
5
5
|
import { normalizeComponentTypes } from "../component/type-utils";
|
|
6
6
|
import {
|
|
7
7
|
getComponentIdFromRelationId,
|
|
8
8
|
getComponentMerge,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
isSparseComponent,
|
|
10
|
+
isSparseRelation,
|
|
11
|
+
isSparseWildcard,
|
|
12
12
|
isWildcardRelationId,
|
|
13
13
|
relation,
|
|
14
14
|
type ComponentId,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "../entity";
|
|
17
17
|
|
|
18
18
|
export interface CommandProcessorContext {
|
|
19
|
-
|
|
19
|
+
sparseStore: SparseStore;
|
|
20
20
|
ensureArchetype: (componentTypes: Iterable<EntityId<any>>) => Archetype;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -59,8 +59,8 @@ function processSetCommand(
|
|
|
59
59
|
// Handle exclusive relations by removing existing relations with the same base component
|
|
60
60
|
handleExclusiveRelation(entityId, currentArchetype, componentId);
|
|
61
61
|
|
|
62
|
-
// For
|
|
63
|
-
if (
|
|
62
|
+
// For sparse relations, ensure wildcard marker is in archetype signature
|
|
63
|
+
if (isSparseComponent(componentId)) {
|
|
64
64
|
const wildcardMarker = relation(componentId, "*");
|
|
65
65
|
// Add wildcard marker to changeset if not already in archetype
|
|
66
66
|
if (!currentArchetype.componentTypeSet.has(wildcardMarker)) {
|
|
@@ -111,10 +111,10 @@ export function removeMatchingRelations(
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// Check
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
for (const componentType of
|
|
114
|
+
// Check sparse relations stored on entity
|
|
115
|
+
const sparseData = archetype.getEntitySparseRelations(entityId);
|
|
116
|
+
if (sparseData) {
|
|
117
|
+
for (const componentType of sparseData.keys()) {
|
|
118
118
|
if (getComponentIdFromRelationId(componentType) === baseComponentId) {
|
|
119
119
|
changeset.delete(componentType);
|
|
120
120
|
}
|
|
@@ -130,8 +130,8 @@ function removeWildcardRelations(
|
|
|
130
130
|
): void {
|
|
131
131
|
removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
|
|
132
132
|
|
|
133
|
-
// If removing
|
|
134
|
-
if (
|
|
133
|
+
// If removing sparse relations, also remove the wildcard marker
|
|
134
|
+
if (isSparseComponent(baseComponentId)) {
|
|
135
135
|
changeset.delete(relation(baseComponentId, "*"));
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -143,7 +143,7 @@ export function maybeRemoveWildcardMarker(
|
|
|
143
143
|
componentId: ComponentId<any> | undefined,
|
|
144
144
|
changeset: ComponentChangeset,
|
|
145
145
|
): void {
|
|
146
|
-
if (componentId === undefined || !
|
|
146
|
+
if (componentId === undefined || !isSparseComponent(componentId)) {
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -160,9 +160,9 @@ export function maybeRemoveWildcardMarker(
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
for (const otherComponentType of
|
|
163
|
+
const sparseData = archetype.getEntitySparseRelations(entityId);
|
|
164
|
+
if (sparseData) {
|
|
165
|
+
for (const otherComponentType of sparseData.keys()) {
|
|
166
166
|
if (otherComponentType === removedComponentType) continue;
|
|
167
167
|
if (changeset.removes.has(otherComponentType)) continue;
|
|
168
168
|
|
|
@@ -173,7 +173,7 @@ export function maybeRemoveWildcardMarker(
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
// Also check if this changeset itself is adding another relation of the same kind
|
|
176
|
-
// (common in exclusive
|
|
176
|
+
// (common in exclusive sparse flips: remove old target + add new target in one batch)
|
|
177
177
|
for (const addedType of changeset.adds.keys()) {
|
|
178
178
|
if (addedType === removedComponentType) continue;
|
|
179
179
|
if (getComponentIdFromRelationId(addedType) === componentId) {
|
|
@@ -189,7 +189,7 @@ function hasEntityComponent(archetype: Archetype, entityId: EntityId, componentT
|
|
|
189
189
|
return true;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
return archetype.
|
|
192
|
+
return archetype.getEntitySparseRelations(entityId)?.has(componentType) ?? false;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
function pruneMissingRemovals(changeset: ComponentChangeset, archetype: Archetype, entityId: EntityId): void {
|
|
@@ -210,13 +210,13 @@ function pruneMissingRemovals(changeset: ComponentChangeset, archetype: Archetyp
|
|
|
210
210
|
|
|
211
211
|
function hasArchetypeStructuralChange(changeset: ComponentChangeset, currentArchetype: Archetype): boolean {
|
|
212
212
|
for (const componentType of changeset.removes) {
|
|
213
|
-
if (!
|
|
213
|
+
if (!isSparseRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) {
|
|
214
214
|
return true;
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
for (const componentType of changeset.adds.keys()) {
|
|
219
|
-
if (!
|
|
219
|
+
if (!isSparseRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) {
|
|
220
220
|
return true;
|
|
221
221
|
}
|
|
222
222
|
}
|
|
@@ -228,13 +228,13 @@ function buildFinalRegularComponentTypes(currentArchetype: Archetype, changeset:
|
|
|
228
228
|
const finalRegularTypes = new Set(currentArchetype.componentTypes);
|
|
229
229
|
|
|
230
230
|
for (const componentType of changeset.removes) {
|
|
231
|
-
if (!
|
|
231
|
+
if (!isSparseRelation(componentType)) {
|
|
232
232
|
finalRegularTypes.delete(componentType);
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
for (const [componentType] of changeset.adds) {
|
|
237
|
-
if (!
|
|
237
|
+
if (!isSparseRelation(componentType)) {
|
|
238
238
|
finalRegularTypes.add(componentType);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
@@ -269,16 +269,16 @@ export function applyChangeset(
|
|
|
269
269
|
return newArchetype;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
// No archetype move needed: only component payload updates and/or
|
|
272
|
+
// No archetype move needed: only component payload updates and/or sparse relation updates.
|
|
273
273
|
if (removedComponents !== null) {
|
|
274
|
-
|
|
274
|
+
applySparseChanges(ctx.sparseStore, entityId, changeset, removedComponents);
|
|
275
275
|
} else {
|
|
276
|
-
|
|
276
|
+
applySparseChangesNoHooks(ctx.sparseStore, entityId, changeset);
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
// Direct update for regular components in archetype
|
|
280
280
|
for (const [componentType, component] of changeset.adds) {
|
|
281
|
-
if (
|
|
281
|
+
if (isSparseRelation(componentType)) {
|
|
282
282
|
continue;
|
|
283
283
|
}
|
|
284
284
|
currentArchetype.set(entityId, componentType, component);
|
|
@@ -287,52 +287,40 @@ export function applyChangeset(
|
|
|
287
287
|
return currentArchetype;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
*
|
|
293
|
-
* Rewritten for the new DontFragmentStore interface (ComponentId-primary storage).
|
|
294
|
-
*/
|
|
295
|
-
function applyDontFragmentChanges(
|
|
296
|
-
dontFragmentRelations: DontFragmentStore,
|
|
290
|
+
function applySparseChanges(
|
|
291
|
+
sparseStore: SparseStore,
|
|
297
292
|
entityId: EntityId,
|
|
298
293
|
changeset: ComponentChangeset,
|
|
299
294
|
removedComponents: Map<EntityId<any>, any>,
|
|
300
295
|
): void {
|
|
301
296
|
for (const componentType of changeset.removes) {
|
|
302
|
-
if (
|
|
303
|
-
const removedValue =
|
|
297
|
+
if (isSparseRelation(componentType)) {
|
|
298
|
+
const removedValue = sparseStore.getValue(entityId, componentType);
|
|
304
299
|
// Record for hooks if we are actually removing something
|
|
305
|
-
if (
|
|
306
|
-
removedValue !== undefined ||
|
|
307
|
-
dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)
|
|
308
|
-
) {
|
|
300
|
+
if (removedValue !== undefined || sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)) {
|
|
309
301
|
removedComponents.set(componentType, removedValue);
|
|
310
302
|
}
|
|
311
|
-
|
|
303
|
+
sparseStore.deleteValue(entityId, componentType);
|
|
312
304
|
}
|
|
313
305
|
}
|
|
314
306
|
|
|
315
307
|
for (const [componentType, component] of changeset.adds) {
|
|
316
|
-
if (
|
|
317
|
-
|
|
308
|
+
if (isSparseRelation(componentType)) {
|
|
309
|
+
sparseStore.setValue(entityId, componentType, component);
|
|
318
310
|
}
|
|
319
311
|
}
|
|
320
312
|
}
|
|
321
313
|
|
|
322
|
-
function
|
|
323
|
-
dontFragmentRelations: DontFragmentStore,
|
|
324
|
-
entityId: EntityId,
|
|
325
|
-
changeset: ComponentChangeset,
|
|
326
|
-
): void {
|
|
314
|
+
function applySparseChangesNoHooks(sparseStore: SparseStore, entityId: EntityId, changeset: ComponentChangeset): void {
|
|
327
315
|
for (const componentType of changeset.removes) {
|
|
328
|
-
if (
|
|
329
|
-
|
|
316
|
+
if (isSparseRelation(componentType)) {
|
|
317
|
+
sparseStore.deleteValue(entityId, componentType);
|
|
330
318
|
}
|
|
331
319
|
}
|
|
332
320
|
|
|
333
321
|
for (const [componentType, component] of changeset.adds) {
|
|
334
|
-
if (
|
|
335
|
-
|
|
322
|
+
if (isSparseRelation(componentType)) {
|
|
323
|
+
sparseStore.setValue(entityId, componentType, component);
|
|
336
324
|
}
|
|
337
325
|
}
|
|
338
326
|
}
|
|
@@ -341,14 +329,14 @@ export function filterRegularComponentTypes(componentTypes: Iterable<EntityId<an
|
|
|
341
329
|
const regularTypes: EntityId<any>[] = [];
|
|
342
330
|
|
|
343
331
|
for (const componentType of componentTypes) {
|
|
344
|
-
// Keep wildcard markers for
|
|
345
|
-
if (
|
|
332
|
+
// Keep wildcard markers for sparse components (they mark the archetype)
|
|
333
|
+
if (isSparseWildcard(componentType)) {
|
|
346
334
|
regularTypes.push(componentType);
|
|
347
335
|
continue;
|
|
348
336
|
}
|
|
349
337
|
|
|
350
|
-
// Skip specific
|
|
351
|
-
if (
|
|
338
|
+
// Skip specific sparse relations from archetype signature
|
|
339
|
+
if (isSparseRelation(componentType)) {
|
|
352
340
|
continue;
|
|
353
341
|
}
|
|
354
342
|
|
package/src/world/hooks.ts
CHANGED
|
@@ -7,6 +7,12 @@ import {
|
|
|
7
7
|
} from "../entity";
|
|
8
8
|
import { isOptionalEntityId, type ComponentType, type LifecycleHookEntry } from "../types";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Debug-only counter incremented on every invokeHook call when armed.
|
|
12
|
+
* World reads and resets this during armed syncs.
|
|
13
|
+
*/
|
|
14
|
+
export const debugHookExecutionCounter = { value: 0 };
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Unified hook invocation: prefers entry.callback (callback style) over hook.on_* (object style).
|
|
12
18
|
*/
|
|
@@ -16,6 +22,8 @@ function invokeHook(
|
|
|
16
22
|
entityId: EntityId,
|
|
17
23
|
components: any[],
|
|
18
24
|
): void {
|
|
25
|
+
debugHookExecutionCounter.value++;
|
|
26
|
+
|
|
19
27
|
if (entry.callback) {
|
|
20
28
|
entry.callback(event as any, entityId, ...components);
|
|
21
29
|
return;
|
|
@@ -3,7 +3,7 @@ import type { ComponentEntityStore } from "../component/entity-store";
|
|
|
3
3
|
import { getDetailedIdType, type EntityId, type EntityIdManager } from "../entity";
|
|
4
4
|
import {
|
|
5
5
|
decodeSerializedId,
|
|
6
|
-
|
|
6
|
+
encodeEntityIdCached,
|
|
7
7
|
type SerializedComponent,
|
|
8
8
|
type SerializedEntity,
|
|
9
9
|
type SerializedWorld,
|
|
@@ -18,29 +18,25 @@ export function serializeWorld(
|
|
|
18
18
|
componentEntities: ComponentEntityStore,
|
|
19
19
|
entityIdManager: EntityIdManager,
|
|
20
20
|
): SerializedWorld {
|
|
21
|
+
// ID cache turns repeated encode work (especially component type IDs) into O(#unique IDs)
|
|
22
|
+
const idCache = new Map<any, any>();
|
|
23
|
+
|
|
21
24
|
const entities: SerializedEntity[] = [];
|
|
22
25
|
|
|
23
26
|
for (const archetype of archetypes) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
value: value === MISSING_COMPONENT ? undefined : value,
|
|
31
|
-
})),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
27
|
+
// Pre-encode this archetype's component type IDs exactly once (big win when many entities share the archetype)
|
|
28
|
+
const encodedComponentTypes = archetype.componentTypes.map((t) => encodeEntityIdCached(t, idCache));
|
|
29
|
+
|
|
30
|
+
// The append method will use the bulk helper internally when a pre-fetched map is supplied.
|
|
31
|
+
// For now we rely on the per-entity fallback inside the archetype (already much cheaper than old dump path).
|
|
32
|
+
archetype.appendSerializedEntities(entities, (id) => encodeEntityIdCached(id, idCache), encodedComponentTypes);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
const componentEntitiesArr: SerializedEntity[] = [];
|
|
37
36
|
for (const [entityId, components] of componentEntities.entries()) {
|
|
38
37
|
componentEntitiesArr.push({
|
|
39
|
-
id:
|
|
40
|
-
components:
|
|
41
|
-
type: encodeEntityId(rawType),
|
|
42
|
-
value: value === MISSING_COMPONENT ? undefined : value,
|
|
43
|
-
})),
|
|
38
|
+
id: encodeEntityIdCached(entityId, idCache),
|
|
39
|
+
components: serializeComponentsFromMap(components, idCache),
|
|
44
40
|
});
|
|
45
41
|
}
|
|
46
42
|
|
|
@@ -52,6 +48,21 @@ export function serializeWorld(
|
|
|
52
48
|
};
|
|
53
49
|
}
|
|
54
50
|
|
|
51
|
+
/** Small helper to avoid duplicating the "Map → SerializedComponent[] with cache" pattern. */
|
|
52
|
+
function serializeComponentsFromMap(
|
|
53
|
+
components: Map<EntityId<any>, any>,
|
|
54
|
+
idCache: Map<any, any>,
|
|
55
|
+
): SerializedComponent[] {
|
|
56
|
+
const result: SerializedComponent[] = [];
|
|
57
|
+
for (const [rawType, value] of components) {
|
|
58
|
+
result.push({
|
|
59
|
+
type: encodeEntityIdCached(rawType, idCache),
|
|
60
|
+
value: value === MISSING_COMPONENT ? undefined : value,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
/**
|
|
56
67
|
* Context needed by `deserializeWorld` to populate world-internal state.
|
|
57
68
|
* Defined as an interface to avoid a circular import between world.ts and this module.
|
|
@@ -96,14 +107,17 @@ export function deserializeWorld(ctx: WorldDeserializationContext, snapshot: Ser
|
|
|
96
107
|
const componentsArray: SerializedComponent[] = entry.components || [];
|
|
97
108
|
|
|
98
109
|
const componentMap = new Map<EntityId<any>, any>();
|
|
99
|
-
const componentTypes: EntityId<any>[] = [];
|
|
100
110
|
|
|
101
111
|
for (const componentEntry of componentsArray) {
|
|
102
112
|
const componentType = decodeSerializedId(componentEntry.type);
|
|
103
113
|
componentMap.set(componentType, componentEntry.value);
|
|
104
|
-
componentTypes.push(componentType);
|
|
105
114
|
}
|
|
106
115
|
|
|
116
|
+
// Build the list of component types from the map we just populated (no redundant push loop)
|
|
117
|
+
const componentTypes = Array.from(componentMap.keys());
|
|
118
|
+
|
|
119
|
+
// ensureArchetype is internally memoized (getOrCompute on signature), so repeated calls
|
|
120
|
+
// for the same component set are cheap after the first archetype is created.
|
|
107
121
|
const archetype = ctx.ensureArchetype(componentTypes);
|
|
108
122
|
archetype.addEntity(entityId, componentMap);
|
|
109
123
|
ctx.setEntityToArchetype(entityId, archetype);
|