@codehz/ecs 0.1.2 → 0.1.4
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 +3 -0
- package/index.js +120 -124
- package/package.json +1 -1
- package/world.d.ts +40 -38
package/README.md
CHANGED
|
@@ -171,6 +171,8 @@ bun run examples/simple/demo.ts
|
|
|
171
171
|
|
|
172
172
|
- `new()`: 创建新实体
|
|
173
173
|
- `set(entity, componentId, data)`: 向实体添加组件
|
|
174
|
+
- `get(entity, componentId)`: 获取实体的组件数据(注意:只能获取已设置的组件,使用前请先用 `has()` 检查组件是否存在)
|
|
175
|
+
- `has(entity, componentId)`: 检查实体是否拥有指定组件
|
|
174
176
|
- `delete(entity, componentId)`: 从实体移除组件
|
|
175
177
|
- `setExclusive(componentId)`: 将组件标记为独占关系
|
|
176
178
|
- `createQuery(componentIds)`: 创建查询
|
|
@@ -251,6 +253,7 @@ const restored = World.deserialize(readySnapshot);
|
|
|
251
253
|
|
|
252
254
|
注意事项
|
|
253
255
|
|
|
256
|
+
- **重要警告**:`get()` 方法只能获取实体已设置的组件。如果尝试获取不存在的组件,会抛出错误。由于 `undefined` 是组件的有效值,不能使用 `get()` 的返回值是否为 `undefined` 来判断组件是否存在。请在使用 `get()` 之前先用 `has()` 方法检查组件是否存在。
|
|
254
257
|
- 快照只包含实体、组件、以及 `EntityIdManager` 的分配器状态(用于保留下一次分配的 ID);并不会自动恢复已注册的系统、查询缓存或生命周期钩子。恢复后应由应用负责重新注册系统与钩子。
|
|
255
258
|
- 若需要跨版本兼容,建议在持久化格式中包含 `version` 字段,并在恢复时进行格式兼容性检查与迁移。
|
|
256
259
|
|
package/index.js
CHANGED
|
@@ -660,64 +660,94 @@ class SystemScheduler {
|
|
|
660
660
|
class World {
|
|
661
661
|
entityIdManager = new EntityIdManager;
|
|
662
662
|
archetypes = [];
|
|
663
|
-
|
|
663
|
+
archetypeBySignature = new Map;
|
|
664
664
|
entityToArchetype = new Map;
|
|
665
|
-
|
|
665
|
+
archetypesByComponent = new Map;
|
|
666
|
+
entityReferences = new Map;
|
|
666
667
|
queries = [];
|
|
667
668
|
queryCache = new Map;
|
|
668
|
-
|
|
669
|
-
|
|
669
|
+
systemScheduler = new SystemScheduler;
|
|
670
|
+
commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
670
671
|
lifecycleHooks = new Map;
|
|
671
|
-
entityReverseIndex = new Map;
|
|
672
672
|
exclusiveComponents = new Set;
|
|
673
|
-
constructor(
|
|
674
|
-
|
|
675
|
-
|
|
673
|
+
constructor(snapshot) {
|
|
674
|
+
if (snapshot && typeof snapshot === "object") {
|
|
675
|
+
if (snapshot.entityManager) {
|
|
676
|
+
this.entityIdManager.deserializeState(snapshot.entityManager);
|
|
677
|
+
}
|
|
678
|
+
if (Array.isArray(snapshot.exclusiveComponents)) {
|
|
679
|
+
for (const id of snapshot.exclusiveComponents) {
|
|
680
|
+
this.exclusiveComponents.add(id);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (Array.isArray(snapshot.entities)) {
|
|
684
|
+
for (const entry of snapshot.entities) {
|
|
685
|
+
const entityId = entry.id;
|
|
686
|
+
const componentsArray = entry.components || [];
|
|
687
|
+
const componentMap = new Map;
|
|
688
|
+
const componentTypes = [];
|
|
689
|
+
for (const componentEntry of componentsArray) {
|
|
690
|
+
componentMap.set(componentEntry.type, componentEntry.value);
|
|
691
|
+
componentTypes.push(componentEntry.type);
|
|
692
|
+
}
|
|
693
|
+
const archetype = this.ensureArchetype(componentTypes);
|
|
694
|
+
archetype.addEntity(entityId, componentMap);
|
|
695
|
+
this.entityToArchetype.set(entityId, archetype);
|
|
696
|
+
for (const compType of componentTypes) {
|
|
697
|
+
const detailedType = getDetailedIdType(compType);
|
|
698
|
+
if (detailedType.type === "entity-relation") {
|
|
699
|
+
const targetEntityId = detailedType.targetId;
|
|
700
|
+
this.trackEntityReference(entityId, compType, targetEntityId);
|
|
701
|
+
} else if (detailedType.type === "entity") {
|
|
702
|
+
this.trackEntityReference(entityId, compType, compType);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
676
708
|
}
|
|
677
|
-
|
|
709
|
+
createArchetypeSignature(componentTypes) {
|
|
678
710
|
return componentTypes.join(",");
|
|
679
711
|
}
|
|
680
712
|
new() {
|
|
681
713
|
const entityId = this.entityIdManager.allocate();
|
|
682
|
-
let emptyArchetype = this.
|
|
714
|
+
let emptyArchetype = this.ensureArchetype([]);
|
|
683
715
|
emptyArchetype.addEntity(entityId, new Map);
|
|
684
716
|
this.entityToArchetype.set(entityId, emptyArchetype);
|
|
685
717
|
return entityId;
|
|
686
718
|
}
|
|
687
|
-
|
|
719
|
+
destroyEntityImmediate(entityId) {
|
|
688
720
|
const archetype = this.entityToArchetype.get(entityId);
|
|
689
721
|
if (!archetype) {
|
|
690
722
|
return;
|
|
691
723
|
}
|
|
692
|
-
const componentReferences = this.
|
|
724
|
+
const componentReferences = this.getEntityReferences(entityId);
|
|
693
725
|
for (const { sourceEntityId, componentType } of componentReferences) {
|
|
694
726
|
const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
|
|
695
727
|
if (sourceArchetype) {
|
|
696
728
|
const currentComponents = new Map;
|
|
697
|
-
for (const
|
|
698
|
-
if (
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
currentComponents.set(compType, data);
|
|
702
|
-
}
|
|
729
|
+
for (const archetypeComponentType of sourceArchetype.componentTypes) {
|
|
730
|
+
if (archetypeComponentType !== componentType) {
|
|
731
|
+
const componentData = sourceArchetype.get(sourceEntityId, archetypeComponentType);
|
|
732
|
+
currentComponents.set(archetypeComponentType, componentData);
|
|
703
733
|
}
|
|
704
734
|
}
|
|
705
735
|
const newComponentTypes = Array.from(currentComponents.keys()).sort((a, b) => a - b);
|
|
706
|
-
const newArchetype = this.
|
|
736
|
+
const newArchetype = this.ensureArchetype(newComponentTypes);
|
|
707
737
|
sourceArchetype.removeEntity(sourceEntityId);
|
|
708
738
|
if (sourceArchetype.getEntities().length === 0) {
|
|
709
|
-
this.
|
|
739
|
+
this.cleanupEmptyArchetype(sourceArchetype);
|
|
710
740
|
}
|
|
711
741
|
newArchetype.addEntity(sourceEntityId, currentComponents);
|
|
712
742
|
this.entityToArchetype.set(sourceEntityId, newArchetype);
|
|
713
|
-
this.
|
|
714
|
-
this.
|
|
743
|
+
this.untrackEntityReference(sourceEntityId, componentType, entityId);
|
|
744
|
+
this.triggerLifecycleHooks(sourceEntityId, new Map, new Set([componentType]));
|
|
715
745
|
}
|
|
716
746
|
}
|
|
717
|
-
this.
|
|
747
|
+
this.entityReferences.delete(entityId);
|
|
718
748
|
archetype.removeEntity(entityId);
|
|
719
749
|
if (archetype.getEntities().length === 0) {
|
|
720
|
-
this.
|
|
750
|
+
this.cleanupEmptyArchetype(archetype);
|
|
721
751
|
}
|
|
722
752
|
this.entityToArchetype.delete(entityId);
|
|
723
753
|
this.entityIdManager.deallocate(entityId);
|
|
@@ -760,6 +790,12 @@ class World {
|
|
|
760
790
|
if (!archetype) {
|
|
761
791
|
throw new Error(`Entity ${entityId} does not exist`);
|
|
762
792
|
}
|
|
793
|
+
const detailedType = getDetailedIdType(componentType);
|
|
794
|
+
if (detailedType.type !== "wildcard-relation") {
|
|
795
|
+
if (!archetype.componentTypes.includes(componentType)) {
|
|
796
|
+
throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
763
799
|
return archetype.get(entityId, componentType);
|
|
764
800
|
}
|
|
765
801
|
registerSystem(system) {
|
|
@@ -796,7 +832,7 @@ class World {
|
|
|
796
832
|
createQuery(componentTypes, filter = {}) {
|
|
797
833
|
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
798
834
|
const filterKey = serializeQueryFilter(filter);
|
|
799
|
-
const key = `${this.
|
|
835
|
+
const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
|
|
800
836
|
const cached = this.queryCache.get(key);
|
|
801
837
|
if (cached) {
|
|
802
838
|
cached.refCount++;
|
|
@@ -844,15 +880,15 @@ class World {
|
|
|
844
880
|
}
|
|
845
881
|
const regularComponents = [];
|
|
846
882
|
const wildcardRelations = [];
|
|
847
|
-
for (const
|
|
848
|
-
const detailedType = getDetailedIdType(
|
|
883
|
+
for (const componentType of componentTypes) {
|
|
884
|
+
const detailedType = getDetailedIdType(componentType);
|
|
849
885
|
if (detailedType.type === "wildcard-relation") {
|
|
850
886
|
wildcardRelations.push({
|
|
851
887
|
componentId: detailedType.componentId,
|
|
852
|
-
relationId:
|
|
888
|
+
relationId: componentType
|
|
853
889
|
});
|
|
854
890
|
} else {
|
|
855
|
-
regularComponents.push(
|
|
891
|
+
regularComponents.push(componentType);
|
|
856
892
|
}
|
|
857
893
|
}
|
|
858
894
|
let matchingArchetypes = [];
|
|
@@ -860,15 +896,15 @@ class World {
|
|
|
860
896
|
const sortedRegularTypes = [...regularComponents].sort((a, b) => a - b);
|
|
861
897
|
if (sortedRegularTypes.length === 1) {
|
|
862
898
|
const componentType = sortedRegularTypes[0];
|
|
863
|
-
matchingArchetypes = this.
|
|
899
|
+
matchingArchetypes = this.archetypesByComponent.get(componentType) || [];
|
|
864
900
|
} else {
|
|
865
|
-
const archetypeLists = sortedRegularTypes.map((type) => this.
|
|
901
|
+
const archetypeLists = sortedRegularTypes.map((type) => this.archetypesByComponent.get(type) || []);
|
|
866
902
|
const firstList = archetypeLists[0] || [];
|
|
867
903
|
const intersection = new Set;
|
|
868
904
|
for (const archetype of firstList) {
|
|
869
905
|
let hasAllComponents = true;
|
|
870
|
-
for (let
|
|
871
|
-
const otherList = archetypeLists[
|
|
906
|
+
for (let listIndex = 1;listIndex < archetypeLists.length; listIndex++) {
|
|
907
|
+
const otherList = archetypeLists[listIndex];
|
|
872
908
|
if (!otherList.includes(archetype)) {
|
|
873
909
|
hasAllComponents = false;
|
|
874
910
|
break;
|
|
@@ -884,12 +920,12 @@ class World {
|
|
|
884
920
|
matchingArchetypes = [...this.archetypes];
|
|
885
921
|
}
|
|
886
922
|
for (const wildcard of wildcardRelations) {
|
|
887
|
-
const componentArchetypes = this.
|
|
923
|
+
const componentArchetypes = this.archetypesByComponent.get(wildcard.componentId) || [];
|
|
888
924
|
matchingArchetypes = matchingArchetypes.filter((archetype) => componentArchetypes.includes(archetype));
|
|
889
925
|
}
|
|
890
926
|
return matchingArchetypes;
|
|
891
927
|
}
|
|
892
|
-
|
|
928
|
+
query(componentTypes, includeComponents) {
|
|
893
929
|
const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
|
|
894
930
|
if (includeComponents) {
|
|
895
931
|
const result = [];
|
|
@@ -910,7 +946,7 @@ class World {
|
|
|
910
946
|
const changeset = new ComponentChangeset;
|
|
911
947
|
const hasDestroy = commands.some((cmd) => cmd.type === "destroy");
|
|
912
948
|
if (hasDestroy) {
|
|
913
|
-
this.
|
|
949
|
+
this.destroyEntityImmediate(entityId);
|
|
914
950
|
return changeset;
|
|
915
951
|
}
|
|
916
952
|
const currentArchetype = this.entityToArchetype.get(entityId);
|
|
@@ -919,14 +955,14 @@ class World {
|
|
|
919
955
|
}
|
|
920
956
|
const currentComponents = new Map;
|
|
921
957
|
for (const componentType of currentArchetype.componentTypes) {
|
|
922
|
-
const
|
|
923
|
-
currentComponents.set(componentType,
|
|
958
|
+
const componentData = currentArchetype.get(entityId, componentType);
|
|
959
|
+
currentComponents.set(componentType, componentData);
|
|
924
960
|
}
|
|
925
|
-
for (const
|
|
926
|
-
switch (
|
|
961
|
+
for (const command of commands) {
|
|
962
|
+
switch (command.type) {
|
|
927
963
|
case "set":
|
|
928
|
-
if (
|
|
929
|
-
const detailedType = getDetailedIdType(
|
|
964
|
+
if (command.componentType) {
|
|
965
|
+
const detailedType = getDetailedIdType(command.componentType);
|
|
930
966
|
if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && this.exclusiveComponents.has(detailedType.componentId)) {
|
|
931
967
|
for (const componentType of currentArchetype.componentTypes) {
|
|
932
968
|
const componentDetailedType = getDetailedIdType(componentType);
|
|
@@ -935,12 +971,12 @@ class World {
|
|
|
935
971
|
}
|
|
936
972
|
}
|
|
937
973
|
}
|
|
938
|
-
changeset.set(
|
|
974
|
+
changeset.set(command.componentType, command.component);
|
|
939
975
|
}
|
|
940
976
|
break;
|
|
941
977
|
case "delete":
|
|
942
|
-
if (
|
|
943
|
-
const detailedType = getDetailedIdType(
|
|
978
|
+
if (command.componentType) {
|
|
979
|
+
const detailedType = getDetailedIdType(command.componentType);
|
|
944
980
|
if (detailedType.type === "wildcard-relation") {
|
|
945
981
|
const baseComponentId = detailedType.componentId;
|
|
946
982
|
for (const componentType of currentArchetype.componentTypes) {
|
|
@@ -952,7 +988,7 @@ class World {
|
|
|
952
988
|
}
|
|
953
989
|
}
|
|
954
990
|
} else {
|
|
955
|
-
changeset.delete(
|
|
991
|
+
changeset.delete(command.componentType);
|
|
956
992
|
}
|
|
957
993
|
}
|
|
958
994
|
break;
|
|
@@ -963,7 +999,7 @@ class World {
|
|
|
963
999
|
const currentComponentTypes = currentArchetype.componentTypes.sort((a, b) => a - b);
|
|
964
1000
|
const needsArchetypeChange = finalComponentTypes.length !== currentComponentTypes.length || !finalComponentTypes.every((type, index) => type === currentComponentTypes[index]);
|
|
965
1001
|
if (needsArchetypeChange) {
|
|
966
|
-
const newArchetype = this.
|
|
1002
|
+
const newArchetype = this.ensureArchetype(finalComponentTypes);
|
|
967
1003
|
currentArchetype.removeEntity(entityId);
|
|
968
1004
|
newArchetype.addEntity(entityId, finalComponents);
|
|
969
1005
|
this.entityToArchetype.set(entityId, newArchetype);
|
|
@@ -976,33 +1012,33 @@ class World {
|
|
|
976
1012
|
const detailedType = getDetailedIdType(componentType);
|
|
977
1013
|
if (detailedType.type === "entity-relation") {
|
|
978
1014
|
const targetEntityId = detailedType.targetId;
|
|
979
|
-
this.
|
|
1015
|
+
this.untrackEntityReference(entityId, componentType, targetEntityId);
|
|
980
1016
|
} else if (detailedType.type === "entity") {
|
|
981
|
-
this.
|
|
1017
|
+
this.untrackEntityReference(entityId, componentType, componentType);
|
|
982
1018
|
}
|
|
983
1019
|
}
|
|
984
1020
|
for (const [componentType, component2] of changeset.adds) {
|
|
985
1021
|
const detailedType = getDetailedIdType(componentType);
|
|
986
1022
|
if (detailedType.type === "entity-relation") {
|
|
987
1023
|
const targetEntityId = detailedType.targetId;
|
|
988
|
-
this.
|
|
1024
|
+
this.trackEntityReference(entityId, componentType, targetEntityId);
|
|
989
1025
|
} else if (detailedType.type === "entity") {
|
|
990
|
-
this.
|
|
1026
|
+
this.trackEntityReference(entityId, componentType, componentType);
|
|
991
1027
|
}
|
|
992
1028
|
}
|
|
993
|
-
this.
|
|
1029
|
+
this.triggerLifecycleHooks(entityId, changeset.adds, changeset.removes);
|
|
994
1030
|
return changeset;
|
|
995
1031
|
}
|
|
996
|
-
|
|
1032
|
+
ensureArchetype(componentTypes) {
|
|
997
1033
|
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
998
|
-
const hashKey = this.
|
|
999
|
-
return getOrCreateWithSideEffect(this.
|
|
1034
|
+
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
1035
|
+
return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => {
|
|
1000
1036
|
const newArchetype = new Archetype(sortedTypes);
|
|
1001
1037
|
this.archetypes.push(newArchetype);
|
|
1002
1038
|
for (const componentType of sortedTypes) {
|
|
1003
|
-
const archetypes = this.
|
|
1039
|
+
const archetypes = this.archetypesByComponent.get(componentType) || [];
|
|
1004
1040
|
archetypes.push(newArchetype);
|
|
1005
|
-
this.
|
|
1041
|
+
this.archetypesByComponent.set(componentType, archetypes);
|
|
1006
1042
|
}
|
|
1007
1043
|
for (const query of this.queries) {
|
|
1008
1044
|
query.checkNewArchetype(newArchetype);
|
|
@@ -1010,30 +1046,30 @@ class World {
|
|
|
1010
1046
|
return newArchetype;
|
|
1011
1047
|
});
|
|
1012
1048
|
}
|
|
1013
|
-
|
|
1014
|
-
if (!this.
|
|
1015
|
-
this.
|
|
1049
|
+
trackEntityReference(sourceEntityId, componentType, targetEntityId) {
|
|
1050
|
+
if (!this.entityReferences.has(targetEntityId)) {
|
|
1051
|
+
this.entityReferences.set(targetEntityId, new Set);
|
|
1016
1052
|
}
|
|
1017
|
-
this.
|
|
1053
|
+
this.entityReferences.get(targetEntityId).add({ sourceEntityId, componentType });
|
|
1018
1054
|
}
|
|
1019
|
-
|
|
1020
|
-
const references = this.
|
|
1055
|
+
untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
|
|
1056
|
+
const references = this.entityReferences.get(targetEntityId);
|
|
1021
1057
|
if (references) {
|
|
1022
|
-
references.forEach((
|
|
1023
|
-
if (
|
|
1024
|
-
references.delete(
|
|
1058
|
+
references.forEach((reference) => {
|
|
1059
|
+
if (reference.sourceEntityId === sourceEntityId && reference.componentType === componentType) {
|
|
1060
|
+
references.delete(reference);
|
|
1025
1061
|
}
|
|
1026
1062
|
});
|
|
1027
1063
|
if (references.size === 0) {
|
|
1028
|
-
this.
|
|
1064
|
+
this.entityReferences.delete(targetEntityId);
|
|
1029
1065
|
}
|
|
1030
1066
|
}
|
|
1031
1067
|
}
|
|
1032
|
-
|
|
1033
|
-
const references = this.
|
|
1068
|
+
getEntityReferences(targetEntityId) {
|
|
1069
|
+
const references = this.entityReferences.get(targetEntityId);
|
|
1034
1070
|
return references ? Array.from(references) : [];
|
|
1035
1071
|
}
|
|
1036
|
-
|
|
1072
|
+
cleanupEmptyArchetype(archetype) {
|
|
1037
1073
|
if (archetype.getEntities().length > 0) {
|
|
1038
1074
|
return;
|
|
1039
1075
|
}
|
|
@@ -1041,28 +1077,28 @@ class World {
|
|
|
1041
1077
|
if (index !== -1) {
|
|
1042
1078
|
this.archetypes.splice(index, 1);
|
|
1043
1079
|
}
|
|
1044
|
-
const hashKey = this.
|
|
1045
|
-
this.
|
|
1080
|
+
const hashKey = this.createArchetypeSignature(archetype.componentTypes);
|
|
1081
|
+
this.archetypeBySignature.delete(hashKey);
|
|
1046
1082
|
for (const componentType of archetype.componentTypes) {
|
|
1047
|
-
const archetypes = this.
|
|
1083
|
+
const archetypes = this.archetypesByComponent.get(componentType);
|
|
1048
1084
|
if (archetypes) {
|
|
1049
1085
|
const compIndex = archetypes.indexOf(archetype);
|
|
1050
1086
|
if (compIndex !== -1) {
|
|
1051
1087
|
archetypes.splice(compIndex, 1);
|
|
1052
1088
|
if (archetypes.length === 0) {
|
|
1053
|
-
this.
|
|
1089
|
+
this.archetypesByComponent.delete(componentType);
|
|
1054
1090
|
}
|
|
1055
1091
|
}
|
|
1056
1092
|
}
|
|
1057
1093
|
}
|
|
1058
1094
|
}
|
|
1059
|
-
|
|
1095
|
+
triggerLifecycleHooks(entityId, addedComponents, removedComponents) {
|
|
1060
1096
|
for (const [componentType, component2] of addedComponents) {
|
|
1061
1097
|
const directHooks = this.lifecycleHooks.get(componentType);
|
|
1062
1098
|
if (directHooks) {
|
|
1063
|
-
for (const
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1099
|
+
for (const lifecycleHook of directHooks) {
|
|
1100
|
+
if (lifecycleHook.onAdded) {
|
|
1101
|
+
lifecycleHook.onAdded(entityId, componentType, component2);
|
|
1066
1102
|
}
|
|
1067
1103
|
}
|
|
1068
1104
|
}
|
|
@@ -1071,9 +1107,9 @@ class World {
|
|
|
1071
1107
|
const wildcardRelationId = relation(detailedType.componentId, "*");
|
|
1072
1108
|
const wildcardHooks = this.lifecycleHooks.get(wildcardRelationId);
|
|
1073
1109
|
if (wildcardHooks) {
|
|
1074
|
-
for (const
|
|
1075
|
-
if (
|
|
1076
|
-
|
|
1110
|
+
for (const lifecycleHook of wildcardHooks) {
|
|
1111
|
+
if (lifecycleHook.onAdded) {
|
|
1112
|
+
lifecycleHook.onAdded(entityId, componentType, component2);
|
|
1077
1113
|
}
|
|
1078
1114
|
}
|
|
1079
1115
|
}
|
|
@@ -1082,9 +1118,9 @@ class World {
|
|
|
1082
1118
|
for (const componentType of removedComponents) {
|
|
1083
1119
|
const directHooks = this.lifecycleHooks.get(componentType);
|
|
1084
1120
|
if (directHooks) {
|
|
1085
|
-
for (const
|
|
1086
|
-
if (
|
|
1087
|
-
|
|
1121
|
+
for (const lifecycleHook of directHooks) {
|
|
1122
|
+
if (lifecycleHook.onRemoved) {
|
|
1123
|
+
lifecycleHook.onRemoved(entityId, componentType);
|
|
1088
1124
|
}
|
|
1089
1125
|
}
|
|
1090
1126
|
}
|
|
@@ -1119,46 +1155,6 @@ class World {
|
|
|
1119
1155
|
entities
|
|
1120
1156
|
};
|
|
1121
1157
|
}
|
|
1122
|
-
static deserialize(obj) {
|
|
1123
|
-
if (!obj || typeof obj !== "object") {
|
|
1124
|
-
throw new Error("World.deserialize expects a snapshot object (not a JSON string)");
|
|
1125
|
-
}
|
|
1126
|
-
const entityManager = new EntityIdManager;
|
|
1127
|
-
if (obj && obj.entityManager) {
|
|
1128
|
-
entityManager.deserializeState(obj.entityManager);
|
|
1129
|
-
}
|
|
1130
|
-
const world = new World(entityManager);
|
|
1131
|
-
if (obj && Array.isArray(obj.exclusiveComponents)) {
|
|
1132
|
-
for (const id of obj.exclusiveComponents) {
|
|
1133
|
-
world.exclusiveComponents.add(id);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
if (obj && Array.isArray(obj.entities)) {
|
|
1137
|
-
for (const entry of obj.entities) {
|
|
1138
|
-
const entityId = entry.id;
|
|
1139
|
-
const componentsArray = entry.components || [];
|
|
1140
|
-
const componentMap = new Map;
|
|
1141
|
-
const componentTypes = [];
|
|
1142
|
-
for (const c of componentsArray) {
|
|
1143
|
-
componentMap.set(c.type, c.value);
|
|
1144
|
-
componentTypes.push(c.type);
|
|
1145
|
-
}
|
|
1146
|
-
const archetype = world.getOrCreateArchetype(componentTypes);
|
|
1147
|
-
archetype.addEntity(entityId, componentMap);
|
|
1148
|
-
world.entityToArchetype.set(entityId, archetype);
|
|
1149
|
-
for (const compType of componentTypes) {
|
|
1150
|
-
const detailedType = getDetailedIdType(compType);
|
|
1151
|
-
if (detailedType.type === "entity-relation") {
|
|
1152
|
-
const targetEntityId = detailedType.targetId;
|
|
1153
|
-
world.addComponentReference(entityId, compType, targetEntityId);
|
|
1154
|
-
} else if (detailedType.type === "entity") {
|
|
1155
|
-
world.addComponentReference(entityId, compType, compType);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
return world;
|
|
1161
|
-
}
|
|
1162
1158
|
}
|
|
1163
1159
|
export {
|
|
1164
1160
|
relation,
|
package/package.json
CHANGED
package/world.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { Archetype } from "./archetype";
|
|
|
2
2
|
import { ComponentChangeset } from "./changeset";
|
|
3
3
|
import { type Command } from "./command-buffer";
|
|
4
4
|
import type { EntityId, WildcardRelationId } from "./entity";
|
|
5
|
-
import { EntityIdManager } from "./entity";
|
|
6
5
|
import { Query } from "./query";
|
|
7
6
|
import { type QueryFilter } from "./query-filter";
|
|
8
7
|
import type { System } from "./system";
|
|
@@ -12,43 +11,50 @@ import type { ComponentTuple, LifecycleHook } from "./types";
|
|
|
12
11
|
* Manages entities, components, and systems
|
|
13
12
|
*/
|
|
14
13
|
export declare class World<UpdateParams extends any[] = []> {
|
|
14
|
+
/** Manages allocation and deallocation of entity IDs */
|
|
15
15
|
private entityIdManager;
|
|
16
|
+
/** Array of all archetypes in the world */
|
|
16
17
|
private archetypes;
|
|
17
|
-
|
|
18
|
+
/** Maps archetype signatures (component type signatures) to archetype instances */
|
|
19
|
+
private archetypeBySignature;
|
|
20
|
+
/** Maps entity IDs to their current archetype */
|
|
18
21
|
private entityToArchetype;
|
|
19
|
-
|
|
22
|
+
/** Maps component types to arrays of archetypes that contain them */
|
|
23
|
+
private archetypesByComponent;
|
|
24
|
+
/** Tracks which entities reference each entity as a component type */
|
|
25
|
+
private entityReferences;
|
|
26
|
+
/** Array of all active queries for archetype change notifications */
|
|
20
27
|
private queries;
|
|
28
|
+
/** Cache for queries keyed by component types and filter signatures */
|
|
21
29
|
private queryCache;
|
|
30
|
+
/** Schedules and executes systems in dependency order */
|
|
31
|
+
private systemScheduler;
|
|
32
|
+
/** Buffers structural changes for deferred execution */
|
|
22
33
|
private commandBuffer;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Hook storage for component and wildcard relation lifecycle events
|
|
26
|
-
*/
|
|
34
|
+
/** Stores lifecycle hooks for component and relation events */
|
|
27
35
|
private lifecycleHooks;
|
|
36
|
+
/** Set of component IDs marked as exclusive relations */
|
|
37
|
+
private exclusiveComponents;
|
|
28
38
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
39
|
+
* Create a new World.
|
|
40
|
+
* If an optional snapshot object is provided (previously produced by `world.serialize()`),
|
|
41
|
+
* the world will be restored from that snapshot. The snapshot may contain non-JSON values.
|
|
32
42
|
*/
|
|
33
|
-
|
|
43
|
+
constructor(snapshot?: any);
|
|
34
44
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
45
|
+
* Generate a signature string for component types array
|
|
46
|
+
* @returns A string signature for the component types
|
|
37
47
|
*/
|
|
38
|
-
private
|
|
39
|
-
constructor(entityIdManager?: EntityIdManager);
|
|
40
|
-
/**
|
|
41
|
-
* Generate a hash key for component types array
|
|
42
|
-
*/
|
|
43
|
-
private getComponentTypesHash;
|
|
48
|
+
private createArchetypeSignature;
|
|
44
49
|
/**
|
|
45
50
|
* Create a new entity
|
|
51
|
+
* @returns The ID of the newly created entity
|
|
46
52
|
*/
|
|
47
53
|
new(): EntityId;
|
|
48
54
|
/**
|
|
49
55
|
* Destroy an entity and remove all its components (immediate execution)
|
|
50
56
|
*/
|
|
51
|
-
private
|
|
57
|
+
private destroyEntityImmediate;
|
|
52
58
|
/**
|
|
53
59
|
* Check if an entity exists
|
|
54
60
|
*/
|
|
@@ -75,12 +81,14 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
75
81
|
* Returns an array of all matching relation instances
|
|
76
82
|
* @param entityId The entity
|
|
77
83
|
* @param componentType The wildcard relation type
|
|
84
|
+
* @returns Array of [targetEntityId, componentData] pairs for all matching relations
|
|
78
85
|
*/
|
|
79
86
|
get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][];
|
|
80
87
|
/**
|
|
81
88
|
* Get component data for a specific entity and component type
|
|
82
89
|
* @param entityId The entity
|
|
83
90
|
* @param componentType The component type
|
|
91
|
+
* @returns The component data
|
|
84
92
|
*/
|
|
85
93
|
get<T>(entityId: EntityId, componentType: EntityId<T>): T;
|
|
86
94
|
/**
|
|
@@ -110,6 +118,7 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
110
118
|
sync(): void;
|
|
111
119
|
/**
|
|
112
120
|
* Create a cached query for efficient entity lookups
|
|
121
|
+
* @returns A Query object for the specified component types and filter
|
|
113
122
|
*/
|
|
114
123
|
createQuery(componentTypes: EntityId<any>[], filter?: QueryFilter): Query;
|
|
115
124
|
/**
|
|
@@ -131,62 +140,55 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
131
140
|
getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[];
|
|
132
141
|
/**
|
|
133
142
|
* Query entities with specific components
|
|
143
|
+
* @returns Array of entity IDs that have all the specified components
|
|
134
144
|
*/
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
query(componentTypes: EntityId<any>[]): EntityId[];
|
|
146
|
+
query<const T extends readonly EntityId<any>[]>(componentTypes: T, includeComponents: true): Array<{
|
|
137
147
|
entity: EntityId;
|
|
138
148
|
components: ComponentTuple<T>;
|
|
139
149
|
}>;
|
|
140
150
|
/**
|
|
141
151
|
* @internal Execute commands for a single entity (for internal use by CommandBuffer)
|
|
152
|
+
* @returns ComponentChangeset describing the changes made
|
|
142
153
|
*/
|
|
143
154
|
executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
|
|
144
155
|
/**
|
|
145
156
|
* Get or create an archetype for the given component types
|
|
157
|
+
* @returns The archetype for the given component types
|
|
146
158
|
*/
|
|
147
|
-
private
|
|
159
|
+
private ensureArchetype;
|
|
148
160
|
/**
|
|
149
161
|
* Add a component reference to the reverse index when an entity is used as a component type
|
|
150
162
|
* @param sourceEntityId The entity that has the component
|
|
151
163
|
* @param componentType The component type (which may be an entity ID used as component type)
|
|
152
164
|
* @param targetEntityId The entity being used as component type
|
|
153
165
|
*/
|
|
154
|
-
private
|
|
166
|
+
private trackEntityReference;
|
|
155
167
|
/**
|
|
156
168
|
* Remove a component reference from the reverse index
|
|
157
169
|
* @param sourceEntityId The entity that has the component
|
|
158
170
|
* @param componentType The component type
|
|
159
171
|
* @param targetEntityId The entity being used as component type
|
|
160
172
|
*/
|
|
161
|
-
private
|
|
173
|
+
private untrackEntityReference;
|
|
162
174
|
/**
|
|
163
175
|
* Get all component references where a target entity is used as a component type
|
|
164
176
|
* @param targetEntityId The target entity
|
|
165
177
|
* @returns Array of {sourceEntityId, componentType} pairs
|
|
166
178
|
*/
|
|
167
|
-
private
|
|
179
|
+
private getEntityReferences;
|
|
168
180
|
/**
|
|
169
181
|
* Remove an empty archetype from all internal data structures
|
|
170
182
|
*/
|
|
171
|
-
private
|
|
183
|
+
private cleanupEmptyArchetype;
|
|
172
184
|
/**
|
|
173
185
|
* Execute component lifecycle hooks for added and removed components
|
|
174
186
|
*/
|
|
175
|
-
private
|
|
176
|
-
/**
|
|
177
|
-
* Convert the world into a plain JSON-serializable object.
|
|
178
|
-
* Note: component values must be JSON-serializable by the caller.
|
|
179
|
-
*/
|
|
187
|
+
private triggerLifecycleHooks;
|
|
180
188
|
/**
|
|
181
189
|
* Convert the world into a plain snapshot object.
|
|
182
190
|
* This returns an in-memory structure and does not perform JSON stringification.
|
|
183
191
|
* Component values are stored as-is (they may be non-JSON-serializable).
|
|
184
192
|
*/
|
|
185
193
|
serialize(): any;
|
|
186
|
-
/**
|
|
187
|
-
* Deserialize a world from a previously-created snapshot object.
|
|
188
|
-
* The snapshot must have been produced by `world.serialize()` and may contain
|
|
189
|
-
* non-JSON values (they will be copied by reference).
|
|
190
|
-
*/
|
|
191
|
-
static deserialize<T extends any[] = []>(obj: any): World<T>;
|
|
192
194
|
}
|