@codehz/ecs 0.1.1 → 0.1.3

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 CHANGED
@@ -180,6 +180,80 @@ bun run examples/simple/demo.ts
180
180
  - `update(...params)`: 更新世界(参数取决于泛型配置)
181
181
  - `sync()`: 应用命令缓冲区
182
182
 
183
+ ### 序列化(快照)
184
+
185
+ 库提供了对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件的数据。注意关键点:
186
+
187
+ - `World.serialize()` 返回一个内存中的快照对象(snapshot),快照会按引用保存组件的实际值;它不会对数据做 JSON.stringify 操作,也不会尝试把组件值转换为可序列化格式。
188
+ - `World.deserialize(snapshot)` 接受由 `World.serialize()` 生成的快照对象并重建世界状态。它期望一个内存对象(非 JSON 字符串)。
189
+
190
+ 为什么采用这种设计?很多情况下组件值可能包含函数、类实例、循环引用或其他无法用 JSON 表示的值。库不对组件值强行进行序列化/字符串化,以避免数据丢失或不可信的自动转换。
191
+
192
+ 示例:内存回环(component 值可为任意对象)
193
+
194
+ ```ts
195
+ // 获取快照(内存对象)
196
+ const snapshot = world.serialize();
197
+
198
+ // 在同一进程内直接恢复
199
+ const restored = World.deserialize(snapshot);
200
+ ```
201
+
202
+ 持久化到磁盘或跨进程传输
203
+
204
+ 如果你需要把世界保存到文件或通过网络传输,需要自己实现组件值的编码/解码策略:
205
+
206
+ 1. 使用 `World.serialize()` 得到 snapshot。
207
+ 2. 对 snapshot 中的组件值逐项进行可自定义的编码(例如将类实例转成纯数据、把函数替换为标识符,或使用自定义二进制编码)。
208
+ 3. 将编码后的对象字符串化并持久化。恢复时执行相反的解码步骤,得到与 `World.serialize()` 兼容的快照对象,然后调用 `World.deserialize(decodedSnapshot)`。
209
+
210
+ 简单示例:当组件值都是 JSON-友好时
211
+
212
+ ```ts
213
+ const snapshot = world.serialize();
214
+ // 如果组件值都可 JSON 化,可以直接 stringify
215
+ const text = JSON.stringify(snapshot);
216
+ // 写入文件或发送到网络
217
+
218
+ // 恢复:parse -> deserialize
219
+ const parsed = JSON.parse(text);
220
+ const restored = World.deserialize(parsed);
221
+ ```
222
+
223
+ 示例:带自定义编码的持久化(伪代码)
224
+
225
+ ```ts
226
+ const snapshot = world.serialize();
227
+
228
+ // 将组件值编码为可持久化格式
229
+ const encoded = {
230
+ ...snapshot,
231
+ entities: snapshot.entities.map((e) => ({
232
+ id: e.id,
233
+ components: e.components.map((c) => ({ type: c.type, value: myEncode(c.value) })),
234
+ })),
235
+ };
236
+
237
+ // 持久化 encoded(JSON.stringify / 二进制写入等)
238
+
239
+ // 恢复时解码回原始组件值
240
+ const decoded = /* parse file and decode */ encoded;
241
+ const readySnapshot = {
242
+ ...decoded,
243
+ entities: decoded.entities.map((e) => ({
244
+ id: e.id,
245
+ components: e.components.map((c) => ({ type: c.type, value: myDecode(c.value) })),
246
+ })),
247
+ };
248
+
249
+ const restored = World.deserialize(readySnapshot);
250
+ ```
251
+
252
+ 注意事项
253
+
254
+ - 快照只包含实体、组件、以及 `EntityIdManager` 的分配器状态(用于保留下一次分配的 ID);并不会自动恢复已注册的系统、查询缓存或生命周期钩子。恢复后应由应用负责重新注册系统与钩子。
255
+ - 若需要跨版本兼容,建议在持久化格式中包含 `version` 字段,并在恢复时进行格式兼容性检查与迁移。
256
+
183
257
  ### Entity
184
258
 
185
259
  - `component<T>(id)`: 分配类型安全的组件ID(上限:1022个)
package/entity.d.ts CHANGED
@@ -129,6 +129,22 @@ export declare class EntityIdManager {
129
129
  * Get the next ID that would be allocated (for debugging)
130
130
  */
131
131
  getNextId(): number;
132
+ /**
133
+ * Serialize internal state for persistence.
134
+ * Returns a plain object representing allocator state. Values may be non-JSON-serializable.
135
+ */
136
+ serializeState(): {
137
+ nextId: number;
138
+ freelist: number[];
139
+ };
140
+ /**
141
+ * Restore internal state from a previously-serialized object.
142
+ * Overwrites the current nextId and freelist.
143
+ */
144
+ deserializeState(state: {
145
+ nextId: number;
146
+ freelist?: number[];
147
+ }): void;
132
148
  }
133
149
  /**
134
150
  * Component ID Manager for automatic allocation
package/index.js CHANGED
@@ -184,6 +184,16 @@ class EntityIdManager {
184
184
  getNextId() {
185
185
  return this.nextId;
186
186
  }
187
+ serializeState() {
188
+ return { nextId: this.nextId, freelist: Array.from(this.freelist) };
189
+ }
190
+ deserializeState(state) {
191
+ if (typeof state.nextId !== "number") {
192
+ throw new Error("Invalid state for EntityIdManager.deserializeState");
193
+ }
194
+ this.nextId = state.nextId;
195
+ this.freelist = new Set(state.freelist || []);
196
+ }
187
197
  }
188
198
 
189
199
  class ComponentIdAllocator {
@@ -650,63 +660,94 @@ class SystemScheduler {
650
660
  class World {
651
661
  entityIdManager = new EntityIdManager;
652
662
  archetypes = [];
653
- archetypeMap = new Map;
663
+ archetypeBySignature = new Map;
654
664
  entityToArchetype = new Map;
655
- systemScheduler = new SystemScheduler;
665
+ archetypesByComponent = new Map;
666
+ entityReferences = new Map;
656
667
  queries = [];
657
668
  queryCache = new Map;
658
- commandBuffer;
659
- componentToArchetypes = new Map;
669
+ systemScheduler = new SystemScheduler;
670
+ commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
660
671
  lifecycleHooks = new Map;
661
- entityReverseIndex = new Map;
662
672
  exclusiveComponents = new Set;
663
- constructor() {
664
- this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
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
+ }
665
708
  }
666
- getComponentTypesHash(componentTypes) {
709
+ createArchetypeSignature(componentTypes) {
667
710
  return componentTypes.join(",");
668
711
  }
669
712
  new() {
670
713
  const entityId = this.entityIdManager.allocate();
671
- let emptyArchetype = this.getOrCreateArchetype([]);
714
+ let emptyArchetype = this.ensureArchetype([]);
672
715
  emptyArchetype.addEntity(entityId, new Map);
673
716
  this.entityToArchetype.set(entityId, emptyArchetype);
674
717
  return entityId;
675
718
  }
676
- _destroyEntity(entityId) {
719
+ destroyEntityImmediate(entityId) {
677
720
  const archetype = this.entityToArchetype.get(entityId);
678
721
  if (!archetype) {
679
722
  return;
680
723
  }
681
- const componentReferences = this.getComponentReferences(entityId);
724
+ const componentReferences = this.getEntityReferences(entityId);
682
725
  for (const { sourceEntityId, componentType } of componentReferences) {
683
726
  const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
684
727
  if (sourceArchetype) {
685
728
  const currentComponents = new Map;
686
- for (const compType of sourceArchetype.componentTypes) {
687
- if (compType !== componentType) {
688
- const data = sourceArchetype.get(sourceEntityId, compType);
689
- if (data !== undefined) {
690
- currentComponents.set(compType, data);
691
- }
729
+ for (const archetypeComponentType of sourceArchetype.componentTypes) {
730
+ if (archetypeComponentType !== componentType) {
731
+ const componentData = sourceArchetype.get(sourceEntityId, archetypeComponentType);
732
+ currentComponents.set(archetypeComponentType, componentData);
692
733
  }
693
734
  }
694
735
  const newComponentTypes = Array.from(currentComponents.keys()).sort((a, b) => a - b);
695
- const newArchetype = this.getOrCreateArchetype(newComponentTypes);
736
+ const newArchetype = this.ensureArchetype(newComponentTypes);
696
737
  sourceArchetype.removeEntity(sourceEntityId);
697
738
  if (sourceArchetype.getEntities().length === 0) {
698
- this.removeEmptyArchetype(sourceArchetype);
739
+ this.cleanupEmptyArchetype(sourceArchetype);
699
740
  }
700
741
  newArchetype.addEntity(sourceEntityId, currentComponents);
701
742
  this.entityToArchetype.set(sourceEntityId, newArchetype);
702
- this.removeComponentReference(sourceEntityId, componentType, entityId);
703
- this.executeComponentLifecycleHooks(sourceEntityId, new Map, new Set([componentType]));
743
+ this.untrackEntityReference(sourceEntityId, componentType, entityId);
744
+ this.triggerLifecycleHooks(sourceEntityId, new Map, new Set([componentType]));
704
745
  }
705
746
  }
706
- this.entityReverseIndex.delete(entityId);
747
+ this.entityReferences.delete(entityId);
707
748
  archetype.removeEntity(entityId);
708
749
  if (archetype.getEntities().length === 0) {
709
- this.removeEmptyArchetype(archetype);
750
+ this.cleanupEmptyArchetype(archetype);
710
751
  }
711
752
  this.entityToArchetype.delete(entityId);
712
753
  this.entityIdManager.deallocate(entityId);
@@ -785,7 +826,7 @@ class World {
785
826
  createQuery(componentTypes, filter = {}) {
786
827
  const sortedTypes = [...componentTypes].sort((a, b) => a - b);
787
828
  const filterKey = serializeQueryFilter(filter);
788
- const key = `${this.getComponentTypesHash(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
829
+ const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
789
830
  const cached = this.queryCache.get(key);
790
831
  if (cached) {
791
832
  cached.refCount++;
@@ -833,15 +874,15 @@ class World {
833
874
  }
834
875
  const regularComponents = [];
835
876
  const wildcardRelations = [];
836
- for (const type of componentTypes) {
837
- const detailedType = getDetailedIdType(type);
877
+ for (const componentType of componentTypes) {
878
+ const detailedType = getDetailedIdType(componentType);
838
879
  if (detailedType.type === "wildcard-relation") {
839
880
  wildcardRelations.push({
840
881
  componentId: detailedType.componentId,
841
- relationId: type
882
+ relationId: componentType
842
883
  });
843
884
  } else {
844
- regularComponents.push(type);
885
+ regularComponents.push(componentType);
845
886
  }
846
887
  }
847
888
  let matchingArchetypes = [];
@@ -849,15 +890,15 @@ class World {
849
890
  const sortedRegularTypes = [...regularComponents].sort((a, b) => a - b);
850
891
  if (sortedRegularTypes.length === 1) {
851
892
  const componentType = sortedRegularTypes[0];
852
- matchingArchetypes = this.componentToArchetypes.get(componentType) || [];
893
+ matchingArchetypes = this.archetypesByComponent.get(componentType) || [];
853
894
  } else {
854
- const archetypeLists = sortedRegularTypes.map((type) => this.componentToArchetypes.get(type) || []);
895
+ const archetypeLists = sortedRegularTypes.map((type) => this.archetypesByComponent.get(type) || []);
855
896
  const firstList = archetypeLists[0] || [];
856
897
  const intersection = new Set;
857
898
  for (const archetype of firstList) {
858
899
  let hasAllComponents = true;
859
- for (let i = 1;i < archetypeLists.length; i++) {
860
- const otherList = archetypeLists[i];
900
+ for (let listIndex = 1;listIndex < archetypeLists.length; listIndex++) {
901
+ const otherList = archetypeLists[listIndex];
861
902
  if (!otherList.includes(archetype)) {
862
903
  hasAllComponents = false;
863
904
  break;
@@ -873,12 +914,12 @@ class World {
873
914
  matchingArchetypes = [...this.archetypes];
874
915
  }
875
916
  for (const wildcard of wildcardRelations) {
876
- const componentArchetypes = this.componentToArchetypes.get(wildcard.componentId) || [];
917
+ const componentArchetypes = this.archetypesByComponent.get(wildcard.componentId) || [];
877
918
  matchingArchetypes = matchingArchetypes.filter((archetype) => componentArchetypes.includes(archetype));
878
919
  }
879
920
  return matchingArchetypes;
880
921
  }
881
- queryEntities(componentTypes, includeComponents) {
922
+ query(componentTypes, includeComponents) {
882
923
  const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
883
924
  if (includeComponents) {
884
925
  const result = [];
@@ -899,7 +940,7 @@ class World {
899
940
  const changeset = new ComponentChangeset;
900
941
  const hasDestroy = commands.some((cmd) => cmd.type === "destroy");
901
942
  if (hasDestroy) {
902
- this._destroyEntity(entityId);
943
+ this.destroyEntityImmediate(entityId);
903
944
  return changeset;
904
945
  }
905
946
  const currentArchetype = this.entityToArchetype.get(entityId);
@@ -908,14 +949,14 @@ class World {
908
949
  }
909
950
  const currentComponents = new Map;
910
951
  for (const componentType of currentArchetype.componentTypes) {
911
- const data = currentArchetype.get(entityId, componentType);
912
- currentComponents.set(componentType, data);
952
+ const componentData = currentArchetype.get(entityId, componentType);
953
+ currentComponents.set(componentType, componentData);
913
954
  }
914
- for (const cmd of commands) {
915
- switch (cmd.type) {
955
+ for (const command of commands) {
956
+ switch (command.type) {
916
957
  case "set":
917
- if (cmd.componentType) {
918
- const detailedType = getDetailedIdType(cmd.componentType);
958
+ if (command.componentType) {
959
+ const detailedType = getDetailedIdType(command.componentType);
919
960
  if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && this.exclusiveComponents.has(detailedType.componentId)) {
920
961
  for (const componentType of currentArchetype.componentTypes) {
921
962
  const componentDetailedType = getDetailedIdType(componentType);
@@ -924,12 +965,12 @@ class World {
924
965
  }
925
966
  }
926
967
  }
927
- changeset.set(cmd.componentType, cmd.component);
968
+ changeset.set(command.componentType, command.component);
928
969
  }
929
970
  break;
930
971
  case "delete":
931
- if (cmd.componentType) {
932
- const detailedType = getDetailedIdType(cmd.componentType);
972
+ if (command.componentType) {
973
+ const detailedType = getDetailedIdType(command.componentType);
933
974
  if (detailedType.type === "wildcard-relation") {
934
975
  const baseComponentId = detailedType.componentId;
935
976
  for (const componentType of currentArchetype.componentTypes) {
@@ -941,7 +982,7 @@ class World {
941
982
  }
942
983
  }
943
984
  } else {
944
- changeset.delete(cmd.componentType);
985
+ changeset.delete(command.componentType);
945
986
  }
946
987
  }
947
988
  break;
@@ -952,7 +993,7 @@ class World {
952
993
  const currentComponentTypes = currentArchetype.componentTypes.sort((a, b) => a - b);
953
994
  const needsArchetypeChange = finalComponentTypes.length !== currentComponentTypes.length || !finalComponentTypes.every((type, index) => type === currentComponentTypes[index]);
954
995
  if (needsArchetypeChange) {
955
- const newArchetype = this.getOrCreateArchetype(finalComponentTypes);
996
+ const newArchetype = this.ensureArchetype(finalComponentTypes);
956
997
  currentArchetype.removeEntity(entityId);
957
998
  newArchetype.addEntity(entityId, finalComponents);
958
999
  this.entityToArchetype.set(entityId, newArchetype);
@@ -965,33 +1006,33 @@ class World {
965
1006
  const detailedType = getDetailedIdType(componentType);
966
1007
  if (detailedType.type === "entity-relation") {
967
1008
  const targetEntityId = detailedType.targetId;
968
- this.removeComponentReference(entityId, componentType, targetEntityId);
1009
+ this.untrackEntityReference(entityId, componentType, targetEntityId);
969
1010
  } else if (detailedType.type === "entity") {
970
- this.removeComponentReference(entityId, componentType, componentType);
1011
+ this.untrackEntityReference(entityId, componentType, componentType);
971
1012
  }
972
1013
  }
973
1014
  for (const [componentType, component2] of changeset.adds) {
974
1015
  const detailedType = getDetailedIdType(componentType);
975
1016
  if (detailedType.type === "entity-relation") {
976
1017
  const targetEntityId = detailedType.targetId;
977
- this.addComponentReference(entityId, componentType, targetEntityId);
1018
+ this.trackEntityReference(entityId, componentType, targetEntityId);
978
1019
  } else if (detailedType.type === "entity") {
979
- this.addComponentReference(entityId, componentType, componentType);
1020
+ this.trackEntityReference(entityId, componentType, componentType);
980
1021
  }
981
1022
  }
982
- this.executeComponentLifecycleHooks(entityId, changeset.adds, changeset.removes);
1023
+ this.triggerLifecycleHooks(entityId, changeset.adds, changeset.removes);
983
1024
  return changeset;
984
1025
  }
985
- getOrCreateArchetype(componentTypes) {
1026
+ ensureArchetype(componentTypes) {
986
1027
  const sortedTypes = [...componentTypes].sort((a, b) => a - b);
987
- const hashKey = this.getComponentTypesHash(sortedTypes);
988
- return getOrCreateWithSideEffect(this.archetypeMap, hashKey, () => {
1028
+ const hashKey = this.createArchetypeSignature(sortedTypes);
1029
+ return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => {
989
1030
  const newArchetype = new Archetype(sortedTypes);
990
1031
  this.archetypes.push(newArchetype);
991
1032
  for (const componentType of sortedTypes) {
992
- const archetypes = this.componentToArchetypes.get(componentType) || [];
1033
+ const archetypes = this.archetypesByComponent.get(componentType) || [];
993
1034
  archetypes.push(newArchetype);
994
- this.componentToArchetypes.set(componentType, archetypes);
1035
+ this.archetypesByComponent.set(componentType, archetypes);
995
1036
  }
996
1037
  for (const query of this.queries) {
997
1038
  query.checkNewArchetype(newArchetype);
@@ -999,30 +1040,30 @@ class World {
999
1040
  return newArchetype;
1000
1041
  });
1001
1042
  }
1002
- addComponentReference(sourceEntityId, componentType, targetEntityId) {
1003
- if (!this.entityReverseIndex.has(targetEntityId)) {
1004
- this.entityReverseIndex.set(targetEntityId, new Set);
1043
+ trackEntityReference(sourceEntityId, componentType, targetEntityId) {
1044
+ if (!this.entityReferences.has(targetEntityId)) {
1045
+ this.entityReferences.set(targetEntityId, new Set);
1005
1046
  }
1006
- this.entityReverseIndex.get(targetEntityId).add({ sourceEntityId, componentType });
1047
+ this.entityReferences.get(targetEntityId).add({ sourceEntityId, componentType });
1007
1048
  }
1008
- removeComponentReference(sourceEntityId, componentType, targetEntityId) {
1009
- const references = this.entityReverseIndex.get(targetEntityId);
1049
+ untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
1050
+ const references = this.entityReferences.get(targetEntityId);
1010
1051
  if (references) {
1011
- references.forEach((ref) => {
1012
- if (ref.sourceEntityId === sourceEntityId && ref.componentType === componentType) {
1013
- references.delete(ref);
1052
+ references.forEach((reference) => {
1053
+ if (reference.sourceEntityId === sourceEntityId && reference.componentType === componentType) {
1054
+ references.delete(reference);
1014
1055
  }
1015
1056
  });
1016
1057
  if (references.size === 0) {
1017
- this.entityReverseIndex.delete(targetEntityId);
1058
+ this.entityReferences.delete(targetEntityId);
1018
1059
  }
1019
1060
  }
1020
1061
  }
1021
- getComponentReferences(targetEntityId) {
1022
- const references = this.entityReverseIndex.get(targetEntityId);
1062
+ getEntityReferences(targetEntityId) {
1063
+ const references = this.entityReferences.get(targetEntityId);
1023
1064
  return references ? Array.from(references) : [];
1024
1065
  }
1025
- removeEmptyArchetype(archetype) {
1066
+ cleanupEmptyArchetype(archetype) {
1026
1067
  if (archetype.getEntities().length > 0) {
1027
1068
  return;
1028
1069
  }
@@ -1030,28 +1071,28 @@ class World {
1030
1071
  if (index !== -1) {
1031
1072
  this.archetypes.splice(index, 1);
1032
1073
  }
1033
- const hashKey = this.getComponentTypesHash(archetype.componentTypes);
1034
- this.archetypeMap.delete(hashKey);
1074
+ const hashKey = this.createArchetypeSignature(archetype.componentTypes);
1075
+ this.archetypeBySignature.delete(hashKey);
1035
1076
  for (const componentType of archetype.componentTypes) {
1036
- const archetypes = this.componentToArchetypes.get(componentType);
1077
+ const archetypes = this.archetypesByComponent.get(componentType);
1037
1078
  if (archetypes) {
1038
1079
  const compIndex = archetypes.indexOf(archetype);
1039
1080
  if (compIndex !== -1) {
1040
1081
  archetypes.splice(compIndex, 1);
1041
1082
  if (archetypes.length === 0) {
1042
- this.componentToArchetypes.delete(componentType);
1083
+ this.archetypesByComponent.delete(componentType);
1043
1084
  }
1044
1085
  }
1045
1086
  }
1046
1087
  }
1047
1088
  }
1048
- executeComponentLifecycleHooks(entityId, addedComponents, removedComponents) {
1089
+ triggerLifecycleHooks(entityId, addedComponents, removedComponents) {
1049
1090
  for (const [componentType, component2] of addedComponents) {
1050
1091
  const directHooks = this.lifecycleHooks.get(componentType);
1051
1092
  if (directHooks) {
1052
- for (const hook of directHooks) {
1053
- if (hook.onAdded) {
1054
- hook.onAdded(entityId, componentType, component2);
1093
+ for (const lifecycleHook of directHooks) {
1094
+ if (lifecycleHook.onAdded) {
1095
+ lifecycleHook.onAdded(entityId, componentType, component2);
1055
1096
  }
1056
1097
  }
1057
1098
  }
@@ -1060,9 +1101,9 @@ class World {
1060
1101
  const wildcardRelationId = relation(detailedType.componentId, "*");
1061
1102
  const wildcardHooks = this.lifecycleHooks.get(wildcardRelationId);
1062
1103
  if (wildcardHooks) {
1063
- for (const hook of wildcardHooks) {
1064
- if (hook.onAdded) {
1065
- hook.onAdded(entityId, componentType, component2);
1104
+ for (const lifecycleHook of wildcardHooks) {
1105
+ if (lifecycleHook.onAdded) {
1106
+ lifecycleHook.onAdded(entityId, componentType, component2);
1066
1107
  }
1067
1108
  }
1068
1109
  }
@@ -1071,9 +1112,9 @@ class World {
1071
1112
  for (const componentType of removedComponents) {
1072
1113
  const directHooks = this.lifecycleHooks.get(componentType);
1073
1114
  if (directHooks) {
1074
- for (const hook of directHooks) {
1075
- if (hook.onRemoved) {
1076
- hook.onRemoved(entityId, componentType);
1115
+ for (const lifecycleHook of directHooks) {
1116
+ if (lifecycleHook.onRemoved) {
1117
+ lifecycleHook.onRemoved(entityId, componentType);
1077
1118
  }
1078
1119
  }
1079
1120
  }
@@ -1091,6 +1132,23 @@ class World {
1091
1132
  }
1092
1133
  }
1093
1134
  }
1135
+ serialize() {
1136
+ const entities = [];
1137
+ for (const [entityId, archetype] of this.entityToArchetype.entries()) {
1138
+ const compEntries = [];
1139
+ for (const compType of archetype.componentTypes) {
1140
+ const value = archetype.get(entityId, compType);
1141
+ compEntries.push({ type: compType, value });
1142
+ }
1143
+ entities.push({ id: entityId, components: compEntries });
1144
+ }
1145
+ return {
1146
+ version: 1,
1147
+ entityManager: this.entityIdManager.serializeState(),
1148
+ exclusiveComponents: Array.from(this.exclusiveComponents),
1149
+ entities
1150
+ };
1151
+ }
1094
1152
  }
1095
1153
  export {
1096
1154
  relation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
package/world.d.ts CHANGED
@@ -11,43 +11,50 @@ import type { ComponentTuple, LifecycleHook } from "./types";
11
11
  * Manages entities, components, and systems
12
12
  */
13
13
  export declare class World<UpdateParams extends any[] = []> {
14
+ /** Manages allocation and deallocation of entity IDs */
14
15
  private entityIdManager;
16
+ /** Array of all archetypes in the world */
15
17
  private archetypes;
16
- private archetypeMap;
18
+ /** Maps archetype signatures (component type signatures) to archetype instances */
19
+ private archetypeBySignature;
20
+ /** Maps entity IDs to their current archetype */
17
21
  private entityToArchetype;
18
- private systemScheduler;
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 */
19
27
  private queries;
28
+ /** Cache for queries keyed by component types and filter signatures */
20
29
  private queryCache;
30
+ /** Schedules and executes systems in dependency order */
31
+ private systemScheduler;
32
+ /** Buffers structural changes for deferred execution */
21
33
  private commandBuffer;
22
- private componentToArchetypes;
23
- /**
24
- * Hook storage for component and wildcard relation lifecycle events
25
- */
34
+ /** Stores lifecycle hooks for component and relation events */
26
35
  private lifecycleHooks;
36
+ /** Set of component IDs marked as exclusive relations */
37
+ private exclusiveComponents;
27
38
  /**
28
- * Reverse index tracking which entities use each entity as a component type
29
- * Maps entity ID to set of {sourceEntityId, componentType} pairs where componentType uses this entity
30
- * This includes both relation components and direct usage of entities as component types
31
- */
32
- private entityReverseIndex;
33
- /**
34
- * Set of component IDs that are marked as exclusive relations
35
- * For exclusive relations, an entity can have at most one relation per base component
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.
36
42
  */
37
- private exclusiveComponents;
38
- constructor();
43
+ constructor(snapshot?: any);
39
44
  /**
40
- * Generate a hash key for component types array
45
+ * Generate a signature string for component types array
46
+ * @returns A string signature for the component types
41
47
  */
42
- private getComponentTypesHash;
48
+ private createArchetypeSignature;
43
49
  /**
44
50
  * Create a new entity
51
+ * @returns The ID of the newly created entity
45
52
  */
46
53
  new(): EntityId;
47
54
  /**
48
55
  * Destroy an entity and remove all its components (immediate execution)
49
56
  */
50
- private _destroyEntity;
57
+ private destroyEntityImmediate;
51
58
  /**
52
59
  * Check if an entity exists
53
60
  */
@@ -74,12 +81,14 @@ export declare class World<UpdateParams extends any[] = []> {
74
81
  * Returns an array of all matching relation instances
75
82
  * @param entityId The entity
76
83
  * @param componentType The wildcard relation type
84
+ * @returns Array of [targetEntityId, componentData] pairs for all matching relations
77
85
  */
78
86
  get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][];
79
87
  /**
80
88
  * Get component data for a specific entity and component type
81
89
  * @param entityId The entity
82
90
  * @param componentType The component type
91
+ * @returns The component data
83
92
  */
84
93
  get<T>(entityId: EntityId, componentType: EntityId<T>): T;
85
94
  /**
@@ -109,6 +118,7 @@ export declare class World<UpdateParams extends any[] = []> {
109
118
  sync(): void;
110
119
  /**
111
120
  * Create a cached query for efficient entity lookups
121
+ * @returns A Query object for the specified component types and filter
112
122
  */
113
123
  createQuery(componentTypes: EntityId<any>[], filter?: QueryFilter): Query;
114
124
  /**
@@ -130,46 +140,55 @@ export declare class World<UpdateParams extends any[] = []> {
130
140
  getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[];
131
141
  /**
132
142
  * Query entities with specific components
143
+ * @returns Array of entity IDs that have all the specified components
133
144
  */
134
- queryEntities(componentTypes: EntityId<any>[]): EntityId[];
135
- queryEntities<const T extends readonly EntityId<any>[]>(componentTypes: T, includeComponents: true): Array<{
145
+ query(componentTypes: EntityId<any>[]): EntityId[];
146
+ query<const T extends readonly EntityId<any>[]>(componentTypes: T, includeComponents: true): Array<{
136
147
  entity: EntityId;
137
148
  components: ComponentTuple<T>;
138
149
  }>;
139
150
  /**
140
151
  * @internal Execute commands for a single entity (for internal use by CommandBuffer)
152
+ * @returns ComponentChangeset describing the changes made
141
153
  */
142
154
  executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
143
155
  /**
144
156
  * Get or create an archetype for the given component types
157
+ * @returns The archetype for the given component types
145
158
  */
146
- private getOrCreateArchetype;
159
+ private ensureArchetype;
147
160
  /**
148
161
  * Add a component reference to the reverse index when an entity is used as a component type
149
162
  * @param sourceEntityId The entity that has the component
150
163
  * @param componentType The component type (which may be an entity ID used as component type)
151
164
  * @param targetEntityId The entity being used as component type
152
165
  */
153
- private addComponentReference;
166
+ private trackEntityReference;
154
167
  /**
155
168
  * Remove a component reference from the reverse index
156
169
  * @param sourceEntityId The entity that has the component
157
170
  * @param componentType The component type
158
171
  * @param targetEntityId The entity being used as component type
159
172
  */
160
- private removeComponentReference;
173
+ private untrackEntityReference;
161
174
  /**
162
175
  * Get all component references where a target entity is used as a component type
163
176
  * @param targetEntityId The target entity
164
177
  * @returns Array of {sourceEntityId, componentType} pairs
165
178
  */
166
- private getComponentReferences;
179
+ private getEntityReferences;
167
180
  /**
168
181
  * Remove an empty archetype from all internal data structures
169
182
  */
170
- private removeEmptyArchetype;
183
+ private cleanupEmptyArchetype;
171
184
  /**
172
185
  * Execute component lifecycle hooks for added and removed components
173
186
  */
174
- private executeComponentLifecycleHooks;
187
+ private triggerLifecycleHooks;
188
+ /**
189
+ * Convert the world into a plain snapshot object.
190
+ * This returns an in-memory structure and does not perform JSON stringification.
191
+ * Component values are stored as-is (they may be non-JSON-serializable).
192
+ */
193
+ serialize(): any;
175
194
  }