@codehz/ecs 0.3.4 → 0.3.6
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/archetype.d.ts +17 -0
- package/index.js +105 -64
- package/package.json +1 -1
- package/query.d.ts +8 -0
- package/world.d.ts +9 -0
package/archetype.d.ts
CHANGED
|
@@ -117,6 +117,14 @@ export declare class Archetype {
|
|
|
117
117
|
* @returns An array of component data or undefined if not present
|
|
118
118
|
*/
|
|
119
119
|
getOptionalComponentData<T>(componentType: EntityId<T>): T[] | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Helper: compute or return cached data sources for provided componentTypes
|
|
122
|
+
*/
|
|
123
|
+
private getCachedComponentDataSources;
|
|
124
|
+
/**
|
|
125
|
+
* Helper: build component tuples for a specific entity index using precomputed data sources
|
|
126
|
+
*/
|
|
127
|
+
private buildComponentsForIndex;
|
|
120
128
|
/**
|
|
121
129
|
* Get entities with their component data for specified component types
|
|
122
130
|
* Optimized for bulk component access with pre-computed indices
|
|
@@ -127,6 +135,15 @@ export declare class Archetype {
|
|
|
127
135
|
entity: EntityId;
|
|
128
136
|
components: ComponentTuple<T>;
|
|
129
137
|
}>;
|
|
138
|
+
/**
|
|
139
|
+
* Iterate over entities with their component data for specified component types
|
|
140
|
+
* implemented as a generator returning each entity/components pair lazily
|
|
141
|
+
* @param componentTypes Array of component types to retrieve
|
|
142
|
+
*/
|
|
143
|
+
iterateWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<{
|
|
144
|
+
entity: EntityId;
|
|
145
|
+
components: ComponentTuple<T>;
|
|
146
|
+
}>;
|
|
130
147
|
/**
|
|
131
148
|
* Iterate over entities with their component data for specified component types
|
|
132
149
|
* Optimized for bulk component access
|
package/index.js
CHANGED
|
@@ -402,16 +402,9 @@ class Archetype {
|
|
|
402
402
|
getOptionalComponentData(componentType) {
|
|
403
403
|
return this.componentData.get(componentType);
|
|
404
404
|
}
|
|
405
|
-
|
|
406
|
-
const result = [];
|
|
407
|
-
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
408
|
-
result.push({ entity, components });
|
|
409
|
-
});
|
|
410
|
-
return result;
|
|
411
|
-
}
|
|
412
|
-
forEachWithComponents(componentTypes, callback) {
|
|
405
|
+
getCachedComponentDataSources(componentTypes) {
|
|
413
406
|
const cacheKey = componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
|
|
414
|
-
|
|
407
|
+
return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
|
|
415
408
|
return componentTypes.map((compType) => {
|
|
416
409
|
let optional = false;
|
|
417
410
|
if (isOptionalEntityId(compType)) {
|
|
@@ -433,46 +426,67 @@ class Archetype {
|
|
|
433
426
|
}
|
|
434
427
|
});
|
|
435
428
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
const matchingRelations = dataSource;
|
|
454
|
-
const relations = [];
|
|
455
|
-
for (const relType of matchingRelations) {
|
|
456
|
-
const dataArray = this.getComponentData(relType);
|
|
457
|
-
const data = dataArray[entityIndex];
|
|
458
|
-
const decodedRel = decodeRelationId(relType);
|
|
459
|
-
relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
460
|
-
}
|
|
461
|
-
return optional ? { value: relations } : relations;
|
|
462
|
-
} else {
|
|
463
|
-
if (dataSource === undefined) {
|
|
464
|
-
if (optional) {
|
|
465
|
-
return;
|
|
466
|
-
} else {
|
|
467
|
-
throw new Error(`No matching relations found for mandatory wildcard relation component type`);
|
|
468
|
-
}
|
|
429
|
+
}
|
|
430
|
+
buildComponentsForIndex(componentTypes, componentDataSources, entityIndex) {
|
|
431
|
+
return componentDataSources.map((dataSource, i) => {
|
|
432
|
+
let compType = componentTypes[i];
|
|
433
|
+
let optional = false;
|
|
434
|
+
if (isOptionalEntityId(compType)) {
|
|
435
|
+
compType = compType.optional;
|
|
436
|
+
optional = true;
|
|
437
|
+
}
|
|
438
|
+
if (getIdType(compType) === "wildcard-relation") {
|
|
439
|
+
if (dataSource === undefined) {
|
|
440
|
+
if (optional) {
|
|
441
|
+
return;
|
|
442
|
+
} else {
|
|
443
|
+
throw new Error(`No matching relations found for mandatory wildcard relation component type`);
|
|
469
444
|
}
|
|
470
|
-
|
|
445
|
+
}
|
|
446
|
+
const matchingRelations = dataSource;
|
|
447
|
+
const relations = [];
|
|
448
|
+
for (const relType of matchingRelations) {
|
|
449
|
+
const dataArray = this.getComponentData(relType);
|
|
471
450
|
const data = dataArray[entityIndex];
|
|
472
|
-
const
|
|
473
|
-
|
|
451
|
+
const decodedRel = decodeRelationId(relType);
|
|
452
|
+
relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
474
453
|
}
|
|
475
|
-
|
|
454
|
+
return optional ? { value: relations } : relations;
|
|
455
|
+
} else {
|
|
456
|
+
if (dataSource === undefined) {
|
|
457
|
+
if (optional) {
|
|
458
|
+
return;
|
|
459
|
+
} else {
|
|
460
|
+
throw new Error(`No matching relations found for mandatory wildcard relation component type`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const dataArray = dataSource;
|
|
464
|
+
const data = dataArray[entityIndex];
|
|
465
|
+
const result = data === MISSING_COMPONENT ? undefined : data;
|
|
466
|
+
return optional ? { value: result } : result;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
getEntitiesWithComponents(componentTypes) {
|
|
471
|
+
const result = [];
|
|
472
|
+
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
473
|
+
result.push({ entity, components });
|
|
474
|
+
});
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
*iterateWithComponents(componentTypes) {
|
|
478
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
479
|
+
for (let entityIndex = 0;entityIndex < this.entities.length; entityIndex++) {
|
|
480
|
+
const entity = this.entities[entityIndex];
|
|
481
|
+
const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex);
|
|
482
|
+
yield { entity, components };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
forEachWithComponents(componentTypes, callback) {
|
|
486
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
487
|
+
for (let entityIndex = 0;entityIndex < this.entities.length; entityIndex++) {
|
|
488
|
+
const entity = this.entities[entityIndex];
|
|
489
|
+
const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex);
|
|
476
490
|
callback(entity, ...components);
|
|
477
491
|
}
|
|
478
492
|
}
|
|
@@ -756,6 +770,14 @@ class Query {
|
|
|
756
770
|
archetype.forEachWithComponents(componentTypes, callback);
|
|
757
771
|
}
|
|
758
772
|
}
|
|
773
|
+
*iterate(componentTypes) {
|
|
774
|
+
if (this.isDisposed) {
|
|
775
|
+
throw new Error("Query has been disposed");
|
|
776
|
+
}
|
|
777
|
+
for (const archetype of this.cachedArchetypes) {
|
|
778
|
+
yield* archetype.iterateWithComponents(componentTypes);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
759
781
|
getComponentData(componentType) {
|
|
760
782
|
if (this.isDisposed) {
|
|
761
783
|
throw new Error("Query has been disposed");
|
|
@@ -884,6 +906,7 @@ class World {
|
|
|
884
906
|
commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
885
907
|
hooks = new Map;
|
|
886
908
|
exclusiveComponents = new Set;
|
|
909
|
+
cascadeDeleteComponents = new Set;
|
|
887
910
|
constructor(snapshot) {
|
|
888
911
|
if (snapshot && typeof snapshot === "object") {
|
|
889
912
|
if (snapshot.entityManager) {
|
|
@@ -953,14 +976,29 @@ class World {
|
|
|
953
976
|
return entityId;
|
|
954
977
|
}
|
|
955
978
|
destroyEntityImmediate(entityId) {
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
979
|
+
const queue = [entityId];
|
|
980
|
+
const visited = new Set;
|
|
981
|
+
while (queue.length > 0) {
|
|
982
|
+
const cur = queue.shift();
|
|
983
|
+
if (visited.has(cur))
|
|
984
|
+
continue;
|
|
985
|
+
visited.add(cur);
|
|
986
|
+
const archetype = this.entityToArchetype.get(cur);
|
|
987
|
+
if (!archetype) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const componentReferences = Array.from(this.getEntityReferences(cur));
|
|
991
|
+
for (const [sourceEntityId, componentType] of componentReferences) {
|
|
992
|
+
const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
|
|
993
|
+
if (!sourceArchetype)
|
|
994
|
+
continue;
|
|
995
|
+
const detailedType = getDetailedIdType(componentType);
|
|
996
|
+
if (detailedType.type === "entity-relation" && this.cascadeDeleteComponents.has(detailedType.componentId)) {
|
|
997
|
+
if (!visited.has(sourceEntityId)) {
|
|
998
|
+
queue.push(sourceEntityId);
|
|
999
|
+
}
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
964
1002
|
const currentComponents = new Map;
|
|
965
1003
|
let removedComponent = sourceArchetype.get(sourceEntityId, componentType);
|
|
966
1004
|
for (const archetypeComponentType of sourceArchetype.componentTypes) {
|
|
@@ -976,17 +1014,17 @@ class World {
|
|
|
976
1014
|
}
|
|
977
1015
|
newArchetype.addEntity(sourceEntityId, currentComponents);
|
|
978
1016
|
this.entityToArchetype.set(sourceEntityId, newArchetype);
|
|
979
|
-
this.untrackEntityReference(sourceEntityId, componentType,
|
|
1017
|
+
this.untrackEntityReference(sourceEntityId, componentType, cur);
|
|
980
1018
|
this.triggerLifecycleHooks(sourceEntityId, new Map, new Map([[componentType, removedComponent]]));
|
|
981
1019
|
}
|
|
1020
|
+
this.entityReferences.delete(cur);
|
|
1021
|
+
archetype.removeEntity(cur);
|
|
1022
|
+
if (archetype.getEntities().length === 0) {
|
|
1023
|
+
this.cleanupEmptyArchetype(archetype);
|
|
1024
|
+
}
|
|
1025
|
+
this.entityToArchetype.delete(cur);
|
|
1026
|
+
this.entityIdManager.deallocate(cur);
|
|
982
1027
|
}
|
|
983
|
-
this.entityReferences.delete(entityId);
|
|
984
|
-
archetype.removeEntity(entityId);
|
|
985
|
-
if (archetype.getEntities().length === 0) {
|
|
986
|
-
this.cleanupEmptyArchetype(archetype);
|
|
987
|
-
}
|
|
988
|
-
this.entityToArchetype.delete(entityId);
|
|
989
|
-
this.entityIdManager.deallocate(entityId);
|
|
990
1028
|
}
|
|
991
1029
|
exists(entityId) {
|
|
992
1030
|
return this.entityToArchetype.has(entityId);
|
|
@@ -1066,6 +1104,9 @@ class World {
|
|
|
1066
1104
|
setExclusive(componentId) {
|
|
1067
1105
|
this.exclusiveComponents.add(componentId);
|
|
1068
1106
|
}
|
|
1107
|
+
setCascadeDelete(componentId) {
|
|
1108
|
+
this.cascadeDeleteComponents.add(componentId);
|
|
1109
|
+
}
|
|
1069
1110
|
update(...params) {
|
|
1070
1111
|
const result = this.systemScheduler.update(...params);
|
|
1071
1112
|
if (result instanceof Promise) {
|
|
@@ -1293,14 +1334,14 @@ class World {
|
|
|
1293
1334
|
untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
|
|
1294
1335
|
const references = this.entityReferences.get(targetEntityId);
|
|
1295
1336
|
if (references) {
|
|
1296
|
-
references.
|
|
1337
|
+
references.remove(sourceEntityId, componentType);
|
|
1297
1338
|
if (references.keyCount === 0) {
|
|
1298
1339
|
this.entityReferences.delete(targetEntityId);
|
|
1299
1340
|
}
|
|
1300
1341
|
}
|
|
1301
1342
|
}
|
|
1302
1343
|
getEntityReferences(targetEntityId) {
|
|
1303
|
-
return this.entityReferences.get(targetEntityId) ??
|
|
1344
|
+
return this.entityReferences.get(targetEntityId) ?? new MultiMap;
|
|
1304
1345
|
}
|
|
1305
1346
|
cleanupEmptyArchetype(archetype) {
|
|
1306
1347
|
if (archetype.getEntities().length > 0) {
|
package/package.json
CHANGED
package/query.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ export declare class Query {
|
|
|
32
32
|
* @param callback Function called for each entity with its components
|
|
33
33
|
*/
|
|
34
34
|
forEach<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
|
|
35
|
+
/**
|
|
36
|
+
* Iterate over entities with their component data (generator)
|
|
37
|
+
* @param componentTypes Array of component types to retrieve
|
|
38
|
+
*/
|
|
39
|
+
iterate<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<{
|
|
40
|
+
entity: EntityId;
|
|
41
|
+
components: ComponentTuple<T>;
|
|
42
|
+
}>;
|
|
35
43
|
/**
|
|
36
44
|
* Get component data arrays for all matching entities
|
|
37
45
|
* @param componentType The component type to retrieve
|
package/world.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
35
35
|
private hooks;
|
|
36
36
|
/** Set of component IDs marked as exclusive relations */
|
|
37
37
|
private exclusiveComponents;
|
|
38
|
+
/** Set of component IDs that will cascade delete when the relation target is deleted */
|
|
39
|
+
private cascadeDeleteComponents;
|
|
38
40
|
/**
|
|
39
41
|
* Create a new World.
|
|
40
42
|
* If an optional snapshot object is provided (previously produced by `world.serialize()`),
|
|
@@ -108,6 +110,13 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
108
110
|
* For exclusive relations, an entity can have at most one relation per base component
|
|
109
111
|
*/
|
|
110
112
|
setExclusive(componentId: EntityId): void;
|
|
113
|
+
/**
|
|
114
|
+
* Mark a component as cascade-delete relation
|
|
115
|
+
* For cascade relations, when the relation target entity is deleted,
|
|
116
|
+
* the referencing entity will also be deleted (cascade).
|
|
117
|
+
* Only applicable to entity-relation components
|
|
118
|
+
*/
|
|
119
|
+
setCascadeDelete(componentId: EntityId): void;
|
|
111
120
|
/**
|
|
112
121
|
* Update the world (run all systems in dependency order)
|
|
113
122
|
* This function is synchronous when all systems are synchronous,
|