@codehz/ecs 0.0.0 → 0.0.2
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 +66 -12
- package/archetype.d.ts +8 -1
- package/index.d.ts +1 -0
- package/index.js +246 -14
- package/package.json +1 -1
- package/types.d.ts +13 -0
- package/world.d.ts +66 -2
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
|
-
- 🚀
|
|
7
|
+
- 🚀 高性能:基于 Archetype 的组件存储和高效的查询系统
|
|
8
8
|
- 🔧 类型安全:完整的 TypeScript 支持
|
|
9
9
|
- 🏗️ 模块化:清晰的架构,支持自定义系统和组件
|
|
10
10
|
- 📦 轻量级:零依赖,易于集成
|
|
11
|
+
- ⚡ 内存高效:连续内存布局,优化的迭代性能
|
|
11
12
|
|
|
12
13
|
## 安装
|
|
13
14
|
|
|
@@ -44,14 +45,48 @@ world.flushCommands();
|
|
|
44
45
|
|
|
45
46
|
// 创建查询并更新
|
|
46
47
|
const query = world.createQuery([PositionId, VelocityId]);
|
|
48
|
+
const deltaTime = 1.0 / 60.0; // 假设60FPS
|
|
47
49
|
query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
|
|
48
50
|
position.x += velocity.x * deltaTime;
|
|
49
51
|
position.y += velocity.y * deltaTime;
|
|
50
52
|
});
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
### 组件生命周期钩子
|
|
56
|
+
|
|
57
|
+
ECS 支持在组件添加或移除时执行回调函数:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// 注册组件生命周期钩子
|
|
61
|
+
world.registerComponentLifecycleHook(PositionId, {
|
|
62
|
+
onAdded: (entityId, componentType, component) => {
|
|
63
|
+
console.log(`组件 ${componentType} 被添加到实体 ${entityId}`);
|
|
64
|
+
},
|
|
65
|
+
onRemoved: (entityId, componentType) => {
|
|
66
|
+
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 你也可以只注册其中一个钩子
|
|
71
|
+
world.registerComponentLifecycleHook(VelocityId, {
|
|
72
|
+
onRemoved: (entityId, componentType) => {
|
|
73
|
+
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 添加组件时会触发钩子
|
|
78
|
+
world.addComponent(entity, PositionId, { x: 0, y: 0 });
|
|
79
|
+
world.flushCommands(); // 钩子在这里被调用
|
|
80
|
+
```
|
|
81
|
+
|
|
53
82
|
### 运行示例
|
|
54
83
|
|
|
84
|
+
```bash
|
|
85
|
+
bun run demo
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
或者直接运行:
|
|
89
|
+
|
|
55
90
|
```bash
|
|
56
91
|
bun run examples/simple/demo.ts
|
|
57
92
|
```
|
|
@@ -65,6 +100,8 @@ bun run examples/simple/demo.ts
|
|
|
65
100
|
- `removeComponent(entity, componentId)`: 从实体移除组件
|
|
66
101
|
- `createQuery(componentIds)`: 创建查询
|
|
67
102
|
- `registerSystem(system)`: 注册系统
|
|
103
|
+
- `registerComponentLifecycleHook(componentId, hook)`: 注册组件生命周期钩子
|
|
104
|
+
- `unregisterComponentLifecycleHook(componentId, hook)`: 注销组件生命周期钩子
|
|
68
105
|
- `update(deltaTime)`: 更新世界
|
|
69
106
|
- `flushCommands()`: 应用命令缓冲区
|
|
70
107
|
|
|
@@ -75,6 +112,8 @@ bun run examples/simple/demo.ts
|
|
|
75
112
|
### Query
|
|
76
113
|
|
|
77
114
|
- `forEach(componentIds, callback)`: 遍历匹配的实体
|
|
115
|
+
- `getEntities()`: 获取所有匹配实体的ID列表
|
|
116
|
+
- `getEntitiesWithComponents(componentIds)`: 获取实体及其组件数据
|
|
78
117
|
|
|
79
118
|
### System
|
|
80
119
|
|
|
@@ -88,6 +127,13 @@ class MySystem implements System {
|
|
|
88
127
|
}
|
|
89
128
|
```
|
|
90
129
|
|
|
130
|
+
## 性能特点
|
|
131
|
+
|
|
132
|
+
- **Archetype 系统**:实体按组件组合分组,实现连续内存访问
|
|
133
|
+
- **缓存查询**:查询结果自动缓存,减少重复计算
|
|
134
|
+
- **命令缓冲区**:延迟执行组件添加/移除,提高批处理效率
|
|
135
|
+
- **类型安全**:编译时类型检查,无运行时开销
|
|
136
|
+
|
|
91
137
|
## 开发
|
|
92
138
|
|
|
93
139
|
### 运行测试
|
|
@@ -106,20 +152,28 @@ bunx tsc --noEmit
|
|
|
106
152
|
|
|
107
153
|
```
|
|
108
154
|
src/
|
|
109
|
-
├── index.ts
|
|
110
|
-
├── entity.ts
|
|
111
|
-
├── world.ts
|
|
112
|
-
├── archetype.ts
|
|
113
|
-
├── query.ts
|
|
114
|
-
├──
|
|
115
|
-
├──
|
|
116
|
-
├──
|
|
117
|
-
|
|
155
|
+
├── index.ts # 入口文件
|
|
156
|
+
├── entity.ts # 实体和组件管理
|
|
157
|
+
├── world.ts # 世界管理
|
|
158
|
+
├── archetype.ts # Archetype 系统(高效组件存储)
|
|
159
|
+
├── query.ts # 查询系统
|
|
160
|
+
├── query-filter.ts # 查询过滤器
|
|
161
|
+
├── system.ts # 系统接口
|
|
162
|
+
├── command-buffer.ts # 命令缓冲区
|
|
163
|
+
├── types.ts # 类型定义
|
|
164
|
+
├── utils.ts # 工具函数
|
|
165
|
+
├── *.test.ts # 单元测试
|
|
166
|
+
├── query.example.ts # 查询示例
|
|
167
|
+
└── *.perf.test.ts # 性能测试
|
|
118
168
|
|
|
119
169
|
examples/
|
|
120
170
|
└── simple/
|
|
121
|
-
├── demo.ts
|
|
122
|
-
└── README.md
|
|
171
|
+
├── demo.ts # 基本示例
|
|
172
|
+
└── README.md # 示例说明
|
|
173
|
+
|
|
174
|
+
scripts/
|
|
175
|
+
├── build.ts # 构建脚本
|
|
176
|
+
└── release.ts # 发布脚本
|
|
123
177
|
```
|
|
124
178
|
|
|
125
179
|
## 许可证
|
package/archetype.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityId } from "./entity";
|
|
1
|
+
import type { EntityId, WildcardRelationId } from "./entity";
|
|
2
2
|
import type { ComponentTuple } from "./types";
|
|
3
3
|
/**
|
|
4
4
|
* Archetype class for ECS architecture
|
|
@@ -60,6 +60,13 @@ export declare class Archetype {
|
|
|
60
60
|
* @param entityId The entity to check
|
|
61
61
|
*/
|
|
62
62
|
hasEntity(entityId: EntityId): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Get component data for a specific entity and wildcard relation type
|
|
65
|
+
* Returns an array of all matching relation instances
|
|
66
|
+
* @param entityId The entity
|
|
67
|
+
* @param componentType The wildcard relation type
|
|
68
|
+
*/
|
|
69
|
+
getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][] | undefined;
|
|
63
70
|
/**
|
|
64
71
|
* Get component data for a specific entity and component type
|
|
65
72
|
* @param entityId The entity
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -275,9 +275,29 @@ class Archetype {
|
|
|
275
275
|
getComponent(entityId, componentType) {
|
|
276
276
|
const index = this.entityToIndex.get(entityId);
|
|
277
277
|
if (index === undefined) {
|
|
278
|
-
|
|
278
|
+
if (getIdType(componentType) === "wildcard-relation") {
|
|
279
|
+
return [];
|
|
280
|
+
} else {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (getIdType(componentType) === "wildcard-relation") {
|
|
285
|
+
const decoded = decodeRelationId(componentType);
|
|
286
|
+
const componentId = decoded.componentId;
|
|
287
|
+
const relations = [];
|
|
288
|
+
for (const relType of this.componentTypes) {
|
|
289
|
+
const relDecoded = decodeRelationId(relType);
|
|
290
|
+
if (relDecoded.componentId === componentId && (getIdType(relType) === "entity-relation" || getIdType(relType) === "component-relation")) {
|
|
291
|
+
const dataArray = this.componentData.get(relType);
|
|
292
|
+
if (dataArray && dataArray[index] !== undefined) {
|
|
293
|
+
relations.push([relDecoded.targetId, dataArray[index]]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return relations;
|
|
298
|
+
} else {
|
|
299
|
+
return this.componentData.get(componentType)?.[index];
|
|
279
300
|
}
|
|
280
|
-
return this.componentData.get(componentType)?.[index];
|
|
281
301
|
}
|
|
282
302
|
setComponent(entityId, componentType, data) {
|
|
283
303
|
const index = this.entityToIndex.get(entityId);
|
|
@@ -374,17 +394,26 @@ class CommandBuffer {
|
|
|
374
394
|
this.commands.push({ type: "destroyEntity", entityId });
|
|
375
395
|
}
|
|
376
396
|
execute() {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
397
|
+
const MAX_ITERATIONS = 100;
|
|
398
|
+
let iterations = 0;
|
|
399
|
+
while (this.commands.length > 0) {
|
|
400
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
401
|
+
throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
|
|
402
|
+
}
|
|
403
|
+
iterations++;
|
|
404
|
+
const currentCommands = [...this.commands];
|
|
405
|
+
this.commands = [];
|
|
406
|
+
const entityCommands = new Map;
|
|
407
|
+
for (const cmd of currentCommands) {
|
|
408
|
+
if (!entityCommands.has(cmd.entityId)) {
|
|
409
|
+
entityCommands.set(cmd.entityId, []);
|
|
410
|
+
}
|
|
411
|
+
entityCommands.get(cmd.entityId).push(cmd);
|
|
412
|
+
}
|
|
413
|
+
for (const [entityId, commands] of entityCommands) {
|
|
414
|
+
this.executeEntityCommands(entityId, commands);
|
|
381
415
|
}
|
|
382
|
-
entityCommands.get(cmd.entityId).push(cmd);
|
|
383
|
-
}
|
|
384
|
-
for (const [entityId, commands] of entityCommands) {
|
|
385
|
-
this.executeEntityCommands(entityId, commands);
|
|
386
416
|
}
|
|
387
|
-
this.commands = [];
|
|
388
417
|
}
|
|
389
418
|
getCommands() {
|
|
390
419
|
return [...this.commands];
|
|
@@ -512,6 +541,9 @@ class World {
|
|
|
512
541
|
queries = [];
|
|
513
542
|
commandBuffer;
|
|
514
543
|
componentToArchetypes = new Map;
|
|
544
|
+
componentLifecycleHooks = new Map;
|
|
545
|
+
wildcardRelationLifecycleHooks = new Map;
|
|
546
|
+
entityReverseIndex = new Map;
|
|
515
547
|
constructor() {
|
|
516
548
|
this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
517
549
|
}
|
|
@@ -530,7 +562,36 @@ class World {
|
|
|
530
562
|
if (!archetype) {
|
|
531
563
|
return;
|
|
532
564
|
}
|
|
565
|
+
const componentReferences = this.getComponentReferences(entityId);
|
|
566
|
+
for (const { sourceEntityId, componentType } of componentReferences) {
|
|
567
|
+
const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
|
|
568
|
+
if (sourceArchetype) {
|
|
569
|
+
const currentComponents = new Map;
|
|
570
|
+
for (const compType of sourceArchetype.componentTypes) {
|
|
571
|
+
if (compType !== componentType) {
|
|
572
|
+
const data = sourceArchetype.getComponent(sourceEntityId, compType);
|
|
573
|
+
if (data !== undefined) {
|
|
574
|
+
currentComponents.set(compType, data);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const newComponentTypes = Array.from(currentComponents.keys()).sort((a, b) => a - b);
|
|
579
|
+
const newArchetype = this.getOrCreateArchetype(newComponentTypes);
|
|
580
|
+
sourceArchetype.removeEntity(sourceEntityId);
|
|
581
|
+
if (sourceArchetype.getEntities().length === 0) {
|
|
582
|
+
this.removeEmptyArchetype(sourceArchetype);
|
|
583
|
+
}
|
|
584
|
+
newArchetype.addEntity(sourceEntityId, currentComponents);
|
|
585
|
+
this.entityToArchetype.set(sourceEntityId, newArchetype);
|
|
586
|
+
this.removeComponentReference(sourceEntityId, componentType, entityId);
|
|
587
|
+
this.executeComponentLifecycleHooks(sourceEntityId, new Map, new Set([componentType]));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
this.entityReverseIndex.delete(entityId);
|
|
533
591
|
archetype.removeEntity(entityId);
|
|
592
|
+
if (archetype.getEntities().length === 0) {
|
|
593
|
+
this.removeEmptyArchetype(archetype);
|
|
594
|
+
}
|
|
534
595
|
this.entityToArchetype.delete(entityId);
|
|
535
596
|
this.entityIdManager.deallocate(entityId);
|
|
536
597
|
}
|
|
@@ -541,12 +602,23 @@ class World {
|
|
|
541
602
|
if (!this.hasEntity(entityId)) {
|
|
542
603
|
throw new Error(`Entity ${entityId} does not exist`);
|
|
543
604
|
}
|
|
605
|
+
const detailedType = getDetailedIdType(componentType);
|
|
606
|
+
if (detailedType.type === "invalid") {
|
|
607
|
+
throw new Error(`Invalid component type: ${componentType}`);
|
|
608
|
+
}
|
|
609
|
+
if (detailedType.type === "wildcard-relation") {
|
|
610
|
+
throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
611
|
+
}
|
|
544
612
|
this.commandBuffer.addComponent(entityId, componentType, component);
|
|
545
613
|
}
|
|
546
614
|
removeComponent(entityId, componentType) {
|
|
547
615
|
if (!this.hasEntity(entityId)) {
|
|
548
616
|
throw new Error(`Entity ${entityId} does not exist`);
|
|
549
617
|
}
|
|
618
|
+
const detailedType = getDetailedIdType(componentType);
|
|
619
|
+
if (detailedType.type === "invalid") {
|
|
620
|
+
throw new Error(`Invalid component type: ${componentType}`);
|
|
621
|
+
}
|
|
550
622
|
this.commandBuffer.removeComponent(entityId, componentType);
|
|
551
623
|
}
|
|
552
624
|
destroyEntity(entityId) {
|
|
@@ -558,7 +630,14 @@ class World {
|
|
|
558
630
|
}
|
|
559
631
|
getComponent(entityId, componentType) {
|
|
560
632
|
const archetype = this.entityToArchetype.get(entityId);
|
|
561
|
-
|
|
633
|
+
if (!archetype) {
|
|
634
|
+
if (getIdType(componentType) === "wildcard-relation") {
|
|
635
|
+
return [];
|
|
636
|
+
} else {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return archetype.getComponent(entityId, componentType);
|
|
562
641
|
}
|
|
563
642
|
registerSystem(system) {
|
|
564
643
|
this.systems.push(system);
|
|
@@ -569,6 +648,36 @@ class World {
|
|
|
569
648
|
this.systems.splice(index, 1);
|
|
570
649
|
}
|
|
571
650
|
}
|
|
651
|
+
registerComponentLifecycleHook(componentType, hook) {
|
|
652
|
+
if (!this.componentLifecycleHooks.has(componentType)) {
|
|
653
|
+
this.componentLifecycleHooks.set(componentType, new Set);
|
|
654
|
+
}
|
|
655
|
+
this.componentLifecycleHooks.get(componentType).add(hook);
|
|
656
|
+
}
|
|
657
|
+
unregisterComponentLifecycleHook(componentType, hook) {
|
|
658
|
+
const hooks = this.componentLifecycleHooks.get(componentType);
|
|
659
|
+
if (hooks) {
|
|
660
|
+
hooks.delete(hook);
|
|
661
|
+
if (hooks.size === 0) {
|
|
662
|
+
this.componentLifecycleHooks.delete(componentType);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
registerWildcardRelationLifecycleHook(baseComponentType, hook) {
|
|
667
|
+
if (!this.wildcardRelationLifecycleHooks.has(baseComponentType)) {
|
|
668
|
+
this.wildcardRelationLifecycleHooks.set(baseComponentType, new Set);
|
|
669
|
+
}
|
|
670
|
+
this.wildcardRelationLifecycleHooks.get(baseComponentType).add(hook);
|
|
671
|
+
}
|
|
672
|
+
unregisterWildcardRelationLifecycleHook(baseComponentType, hook) {
|
|
673
|
+
const hooks = this.wildcardRelationLifecycleHooks.get(baseComponentType);
|
|
674
|
+
if (hooks) {
|
|
675
|
+
hooks.delete(hook);
|
|
676
|
+
if (hooks.size === 0) {
|
|
677
|
+
this.wildcardRelationLifecycleHooks.delete(baseComponentType);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
572
681
|
update(...params) {
|
|
573
682
|
for (const system of this.systems) {
|
|
574
683
|
system.update(this, ...params);
|
|
@@ -687,8 +796,22 @@ class World {
|
|
|
687
796
|
break;
|
|
688
797
|
case "removeComponent":
|
|
689
798
|
if (cmd.componentType) {
|
|
690
|
-
|
|
691
|
-
|
|
799
|
+
const detailedType = getDetailedIdType(cmd.componentType);
|
|
800
|
+
if (detailedType.type === "wildcard-relation") {
|
|
801
|
+
const baseComponentId = detailedType.componentId;
|
|
802
|
+
for (const componentType of currentArchetype.componentTypes) {
|
|
803
|
+
const componentDetailedType = getDetailedIdType(componentType);
|
|
804
|
+
if (componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") {
|
|
805
|
+
if (componentDetailedType.componentId === baseComponentId) {
|
|
806
|
+
removes.add(componentType);
|
|
807
|
+
adds.delete(componentType);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
} else {
|
|
812
|
+
removes.add(cmd.componentType);
|
|
813
|
+
adds.delete(cmd.componentType);
|
|
814
|
+
}
|
|
692
815
|
}
|
|
693
816
|
break;
|
|
694
817
|
}
|
|
@@ -713,6 +836,25 @@ class World {
|
|
|
713
836
|
currentArchetype.setComponent(entityId, componentType, component);
|
|
714
837
|
}
|
|
715
838
|
}
|
|
839
|
+
for (const componentType of removes) {
|
|
840
|
+
const detailedType = getDetailedIdType(componentType);
|
|
841
|
+
if (detailedType.type === "entity-relation") {
|
|
842
|
+
const targetEntityId = detailedType.targetId;
|
|
843
|
+
this.removeComponentReference(entityId, componentType, targetEntityId);
|
|
844
|
+
} else if (detailedType.type === "entity") {
|
|
845
|
+
this.removeComponentReference(entityId, componentType, componentType);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
for (const [componentType, component] of adds) {
|
|
849
|
+
const detailedType = getDetailedIdType(componentType);
|
|
850
|
+
if (detailedType.type === "entity-relation") {
|
|
851
|
+
const targetEntityId = detailedType.targetId;
|
|
852
|
+
this.addComponentReference(entityId, componentType, targetEntityId);
|
|
853
|
+
} else if (detailedType.type === "entity") {
|
|
854
|
+
this.addComponentReference(entityId, componentType, componentType);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
this.executeComponentLifecycleHooks(entityId, adds, removes);
|
|
716
858
|
}
|
|
717
859
|
getOrCreateArchetype(componentTypes) {
|
|
718
860
|
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
@@ -731,6 +873,96 @@ class World {
|
|
|
731
873
|
return newArchetype;
|
|
732
874
|
});
|
|
733
875
|
}
|
|
876
|
+
addComponentReference(sourceEntityId, componentType, targetEntityId) {
|
|
877
|
+
if (!this.entityReverseIndex.has(targetEntityId)) {
|
|
878
|
+
this.entityReverseIndex.set(targetEntityId, new Set);
|
|
879
|
+
}
|
|
880
|
+
this.entityReverseIndex.get(targetEntityId).add({ sourceEntityId, componentType });
|
|
881
|
+
}
|
|
882
|
+
removeComponentReference(sourceEntityId, componentType, targetEntityId) {
|
|
883
|
+
const references = this.entityReverseIndex.get(targetEntityId);
|
|
884
|
+
if (references) {
|
|
885
|
+
references.forEach((ref) => {
|
|
886
|
+
if (ref.sourceEntityId === sourceEntityId && ref.componentType === componentType) {
|
|
887
|
+
references.delete(ref);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
if (references.size === 0) {
|
|
891
|
+
this.entityReverseIndex.delete(targetEntityId);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
getComponentReferences(targetEntityId) {
|
|
896
|
+
const references = this.entityReverseIndex.get(targetEntityId);
|
|
897
|
+
return references ? Array.from(references) : [];
|
|
898
|
+
}
|
|
899
|
+
removeEmptyArchetype(archetype) {
|
|
900
|
+
if (archetype.getEntities().length > 0) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const index = this.archetypes.indexOf(archetype);
|
|
904
|
+
if (index !== -1) {
|
|
905
|
+
this.archetypes.splice(index, 1);
|
|
906
|
+
}
|
|
907
|
+
const hashKey = this.getComponentTypesHash(archetype.componentTypes);
|
|
908
|
+
this.archetypeMap.delete(hashKey);
|
|
909
|
+
for (const componentType of archetype.componentTypes) {
|
|
910
|
+
const archetypes = this.componentToArchetypes.get(componentType);
|
|
911
|
+
if (archetypes) {
|
|
912
|
+
const compIndex = archetypes.indexOf(archetype);
|
|
913
|
+
if (compIndex !== -1) {
|
|
914
|
+
archetypes.splice(compIndex, 1);
|
|
915
|
+
if (archetypes.length === 0) {
|
|
916
|
+
this.componentToArchetypes.delete(componentType);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
executeComponentLifecycleHooks(entityId, addedComponents, removedComponents) {
|
|
923
|
+
for (const [componentType, component] of addedComponents) {
|
|
924
|
+
const hooks = this.componentLifecycleHooks.get(componentType);
|
|
925
|
+
if (hooks) {
|
|
926
|
+
for (const hook of hooks) {
|
|
927
|
+
if (hook.onAdded) {
|
|
928
|
+
hook.onAdded(entityId, componentType, component);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const detailedType = getDetailedIdType(componentType);
|
|
933
|
+
if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
|
|
934
|
+
const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId);
|
|
935
|
+
if (wildcardHooks) {
|
|
936
|
+
for (const hook of wildcardHooks) {
|
|
937
|
+
if (hook.onAdded) {
|
|
938
|
+
hook.onAdded(entityId, componentType, component);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
for (const componentType of removedComponents) {
|
|
945
|
+
const hooks = this.componentLifecycleHooks.get(componentType);
|
|
946
|
+
if (hooks) {
|
|
947
|
+
for (const hook of hooks) {
|
|
948
|
+
if (hook.onRemoved) {
|
|
949
|
+
hook.onRemoved(entityId, componentType);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const detailedType = getDetailedIdType(componentType);
|
|
954
|
+
if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
|
|
955
|
+
const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId);
|
|
956
|
+
if (wildcardHooks) {
|
|
957
|
+
for (const hook of wildcardHooks) {
|
|
958
|
+
if (hook.onRemoved) {
|
|
959
|
+
hook.onRemoved(entityId, componentType);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
734
966
|
}
|
|
735
967
|
export {
|
|
736
968
|
isRelationId,
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { EntityId, WildcardRelationId } from "./entity";
|
|
2
|
+
/**
|
|
3
|
+
* Hook types for component lifecycle events
|
|
4
|
+
*/
|
|
5
|
+
export interface LifecycleHook<T = unknown> {
|
|
6
|
+
/**
|
|
7
|
+
* Called when a component is added to an entity
|
|
8
|
+
*/
|
|
9
|
+
onAdded?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Called when a component is removed from an entity
|
|
12
|
+
*/
|
|
13
|
+
onRemoved?: (entityId: EntityId, componentType: EntityId<T>) => void;
|
|
14
|
+
}
|
|
2
15
|
/**
|
|
3
16
|
* Type helper for component tuples extracted from EntityId array
|
|
4
17
|
*/
|
package/world.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Archetype } from "./archetype";
|
|
2
2
|
import { type Command } from "./command-buffer";
|
|
3
|
-
import type { EntityId } from "./entity";
|
|
3
|
+
import type { EntityId, WildcardRelationId } from "./entity";
|
|
4
4
|
import { Query } from "./query";
|
|
5
5
|
import type { QueryFilter } from "./query-filter";
|
|
6
|
-
import type { ComponentTuple } from "./types";
|
|
6
|
+
import type { ComponentTuple, LifecycleHook } from "./types";
|
|
7
7
|
import type { System } from "./system";
|
|
8
8
|
/**
|
|
9
9
|
* World class for ECS architecture
|
|
@@ -18,6 +18,21 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
18
18
|
private queries;
|
|
19
19
|
private commandBuffer;
|
|
20
20
|
private componentToArchetypes;
|
|
21
|
+
/**
|
|
22
|
+
* Hook storage for component lifecycle events
|
|
23
|
+
*/
|
|
24
|
+
private componentLifecycleHooks;
|
|
25
|
+
/**
|
|
26
|
+
* Hook storage for wildcard relation lifecycle events
|
|
27
|
+
* Maps base component type to set of wildcard relation hooks
|
|
28
|
+
*/
|
|
29
|
+
private wildcardRelationLifecycleHooks;
|
|
30
|
+
/**
|
|
31
|
+
* Reverse index tracking which entities use each entity as a component type
|
|
32
|
+
* Maps entity ID to set of {sourceEntityId, componentType} pairs where componentType uses this entity
|
|
33
|
+
* This includes both relation components and direct usage of entities as component types
|
|
34
|
+
*/
|
|
35
|
+
private entityReverseIndex;
|
|
21
36
|
constructor();
|
|
22
37
|
/**
|
|
23
38
|
* Generate a hash key for component types array
|
|
@@ -51,6 +66,10 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
51
66
|
* Check if an entity has a specific component
|
|
52
67
|
*/
|
|
53
68
|
hasComponent<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Get wildcard relations from an entity
|
|
71
|
+
*/
|
|
72
|
+
getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][] | undefined;
|
|
54
73
|
/**
|
|
55
74
|
* Get a component from an entity
|
|
56
75
|
*/
|
|
@@ -63,6 +82,23 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
63
82
|
* Unregister a system
|
|
64
83
|
*/
|
|
65
84
|
unregisterSystem(system: System<ExtraParams>): void;
|
|
85
|
+
/**
|
|
86
|
+
* Register a lifecycle hook for component events
|
|
87
|
+
*/
|
|
88
|
+
registerComponentLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
|
|
89
|
+
/**
|
|
90
|
+
* Unregister a lifecycle hook for component events
|
|
91
|
+
*/
|
|
92
|
+
unregisterComponentLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
|
|
93
|
+
/**
|
|
94
|
+
* Register a lifecycle hook for wildcard relation events
|
|
95
|
+
* The hook will be triggered for any component that matches the wildcard relation pattern
|
|
96
|
+
*/
|
|
97
|
+
registerWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: LifecycleHook<T>): void;
|
|
98
|
+
/**
|
|
99
|
+
* Unregister a lifecycle hook for wildcard relation events
|
|
100
|
+
*/
|
|
101
|
+
unregisterWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: LifecycleHook<T>): void;
|
|
66
102
|
/**
|
|
67
103
|
* Update the world (run all systems)
|
|
68
104
|
*/
|
|
@@ -103,4 +139,32 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
103
139
|
* Get or create an archetype for the given component types
|
|
104
140
|
*/
|
|
105
141
|
private getOrCreateArchetype;
|
|
142
|
+
/**
|
|
143
|
+
* Add a component reference to the reverse index when an entity is used as a component type
|
|
144
|
+
* @param sourceEntityId The entity that has the component
|
|
145
|
+
* @param componentType The component type (which may be an entity ID used as component type)
|
|
146
|
+
* @param targetEntityId The entity being used as component type
|
|
147
|
+
*/
|
|
148
|
+
private addComponentReference;
|
|
149
|
+
/**
|
|
150
|
+
* Remove a component reference from the reverse index
|
|
151
|
+
* @param sourceEntityId The entity that has the component
|
|
152
|
+
* @param componentType The component type
|
|
153
|
+
* @param targetEntityId The entity being used as component type
|
|
154
|
+
*/
|
|
155
|
+
private removeComponentReference;
|
|
156
|
+
/**
|
|
157
|
+
* Get all component references where a target entity is used as a component type
|
|
158
|
+
* @param targetEntityId The target entity
|
|
159
|
+
* @returns Array of {sourceEntityId, componentType} pairs
|
|
160
|
+
*/
|
|
161
|
+
private getComponentReferences;
|
|
162
|
+
/**
|
|
163
|
+
* Remove an empty archetype from all internal data structures
|
|
164
|
+
*/
|
|
165
|
+
private removeEmptyArchetype;
|
|
166
|
+
/**
|
|
167
|
+
* Execute component lifecycle hooks for added and removed components
|
|
168
|
+
*/
|
|
169
|
+
private executeComponentLifecycleHooks;
|
|
106
170
|
}
|