@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 +74 -0
- package/entity.d.ts +16 -0
- package/index.js +141 -83
- package/package.json +1 -1
- package/world.d.ts +46 -27
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
|
-
|
|
663
|
+
archetypeBySignature = new Map;
|
|
654
664
|
entityToArchetype = new Map;
|
|
655
|
-
|
|
665
|
+
archetypesByComponent = new Map;
|
|
666
|
+
entityReferences = new Map;
|
|
656
667
|
queries = [];
|
|
657
668
|
queryCache = new Map;
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
709
|
+
createArchetypeSignature(componentTypes) {
|
|
667
710
|
return componentTypes.join(",");
|
|
668
711
|
}
|
|
669
712
|
new() {
|
|
670
713
|
const entityId = this.entityIdManager.allocate();
|
|
671
|
-
let emptyArchetype = this.
|
|
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
|
-
|
|
719
|
+
destroyEntityImmediate(entityId) {
|
|
677
720
|
const archetype = this.entityToArchetype.get(entityId);
|
|
678
721
|
if (!archetype) {
|
|
679
722
|
return;
|
|
680
723
|
}
|
|
681
|
-
const componentReferences = this.
|
|
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
|
|
687
|
-
if (
|
|
688
|
-
const
|
|
689
|
-
|
|
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.
|
|
736
|
+
const newArchetype = this.ensureArchetype(newComponentTypes);
|
|
696
737
|
sourceArchetype.removeEntity(sourceEntityId);
|
|
697
738
|
if (sourceArchetype.getEntities().length === 0) {
|
|
698
|
-
this.
|
|
739
|
+
this.cleanupEmptyArchetype(sourceArchetype);
|
|
699
740
|
}
|
|
700
741
|
newArchetype.addEntity(sourceEntityId, currentComponents);
|
|
701
742
|
this.entityToArchetype.set(sourceEntityId, newArchetype);
|
|
702
|
-
this.
|
|
703
|
-
this.
|
|
743
|
+
this.untrackEntityReference(sourceEntityId, componentType, entityId);
|
|
744
|
+
this.triggerLifecycleHooks(sourceEntityId, new Map, new Set([componentType]));
|
|
704
745
|
}
|
|
705
746
|
}
|
|
706
|
-
this.
|
|
747
|
+
this.entityReferences.delete(entityId);
|
|
707
748
|
archetype.removeEntity(entityId);
|
|
708
749
|
if (archetype.getEntities().length === 0) {
|
|
709
|
-
this.
|
|
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.
|
|
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
|
|
837
|
-
const detailedType = getDetailedIdType(
|
|
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:
|
|
882
|
+
relationId: componentType
|
|
842
883
|
});
|
|
843
884
|
} else {
|
|
844
|
-
regularComponents.push(
|
|
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.
|
|
893
|
+
matchingArchetypes = this.archetypesByComponent.get(componentType) || [];
|
|
853
894
|
} else {
|
|
854
|
-
const archetypeLists = sortedRegularTypes.map((type) => this.
|
|
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
|
|
860
|
-
const otherList = archetypeLists[
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
912
|
-
currentComponents.set(componentType,
|
|
952
|
+
const componentData = currentArchetype.get(entityId, componentType);
|
|
953
|
+
currentComponents.set(componentType, componentData);
|
|
913
954
|
}
|
|
914
|
-
for (const
|
|
915
|
-
switch (
|
|
955
|
+
for (const command of commands) {
|
|
956
|
+
switch (command.type) {
|
|
916
957
|
case "set":
|
|
917
|
-
if (
|
|
918
|
-
const detailedType = getDetailedIdType(
|
|
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(
|
|
968
|
+
changeset.set(command.componentType, command.component);
|
|
928
969
|
}
|
|
929
970
|
break;
|
|
930
971
|
case "delete":
|
|
931
|
-
if (
|
|
932
|
-
const detailedType = getDetailedIdType(
|
|
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(
|
|
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.
|
|
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.
|
|
1009
|
+
this.untrackEntityReference(entityId, componentType, targetEntityId);
|
|
969
1010
|
} else if (detailedType.type === "entity") {
|
|
970
|
-
this.
|
|
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.
|
|
1018
|
+
this.trackEntityReference(entityId, componentType, targetEntityId);
|
|
978
1019
|
} else if (detailedType.type === "entity") {
|
|
979
|
-
this.
|
|
1020
|
+
this.trackEntityReference(entityId, componentType, componentType);
|
|
980
1021
|
}
|
|
981
1022
|
}
|
|
982
|
-
this.
|
|
1023
|
+
this.triggerLifecycleHooks(entityId, changeset.adds, changeset.removes);
|
|
983
1024
|
return changeset;
|
|
984
1025
|
}
|
|
985
|
-
|
|
1026
|
+
ensureArchetype(componentTypes) {
|
|
986
1027
|
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
987
|
-
const hashKey = this.
|
|
988
|
-
return getOrCreateWithSideEffect(this.
|
|
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.
|
|
1033
|
+
const archetypes = this.archetypesByComponent.get(componentType) || [];
|
|
993
1034
|
archetypes.push(newArchetype);
|
|
994
|
-
this.
|
|
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
|
-
|
|
1003
|
-
if (!this.
|
|
1004
|
-
this.
|
|
1043
|
+
trackEntityReference(sourceEntityId, componentType, targetEntityId) {
|
|
1044
|
+
if (!this.entityReferences.has(targetEntityId)) {
|
|
1045
|
+
this.entityReferences.set(targetEntityId, new Set);
|
|
1005
1046
|
}
|
|
1006
|
-
this.
|
|
1047
|
+
this.entityReferences.get(targetEntityId).add({ sourceEntityId, componentType });
|
|
1007
1048
|
}
|
|
1008
|
-
|
|
1009
|
-
const references = this.
|
|
1049
|
+
untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
|
|
1050
|
+
const references = this.entityReferences.get(targetEntityId);
|
|
1010
1051
|
if (references) {
|
|
1011
|
-
references.forEach((
|
|
1012
|
-
if (
|
|
1013
|
-
references.delete(
|
|
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.
|
|
1058
|
+
this.entityReferences.delete(targetEntityId);
|
|
1018
1059
|
}
|
|
1019
1060
|
}
|
|
1020
1061
|
}
|
|
1021
|
-
|
|
1022
|
-
const references = this.
|
|
1062
|
+
getEntityReferences(targetEntityId) {
|
|
1063
|
+
const references = this.entityReferences.get(targetEntityId);
|
|
1023
1064
|
return references ? Array.from(references) : [];
|
|
1024
1065
|
}
|
|
1025
|
-
|
|
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.
|
|
1034
|
-
this.
|
|
1074
|
+
const hashKey = this.createArchetypeSignature(archetype.componentTypes);
|
|
1075
|
+
this.archetypeBySignature.delete(hashKey);
|
|
1035
1076
|
for (const componentType of archetype.componentTypes) {
|
|
1036
|
-
const archetypes = this.
|
|
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.
|
|
1083
|
+
this.archetypesByComponent.delete(componentType);
|
|
1043
1084
|
}
|
|
1044
1085
|
}
|
|
1045
1086
|
}
|
|
1046
1087
|
}
|
|
1047
1088
|
}
|
|
1048
|
-
|
|
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
|
|
1053
|
-
if (
|
|
1054
|
-
|
|
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
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
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
|
|
1075
|
-
if (
|
|
1076
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
38
|
-
constructor();
|
|
43
|
+
constructor(snapshot?: any);
|
|
39
44
|
/**
|
|
40
|
-
* Generate a
|
|
45
|
+
* Generate a signature string for component types array
|
|
46
|
+
* @returns A string signature for the component types
|
|
41
47
|
*/
|
|
42
|
-
private
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
179
|
+
private getEntityReferences;
|
|
167
180
|
/**
|
|
168
181
|
* Remove an empty archetype from all internal data structures
|
|
169
182
|
*/
|
|
170
|
-
private
|
|
183
|
+
private cleanupEmptyArchetype;
|
|
171
184
|
/**
|
|
172
185
|
* Execute component lifecycle hooks for added and removed components
|
|
173
186
|
*/
|
|
174
|
-
private
|
|
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
|
}
|