@codehz/ecs 0.0.0 → 0.0.1
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 +29 -0
- package/index.js +189 -9
- package/package.json +1 -1
- package/world.d.ts +87 -0
package/README.md
CHANGED
|
@@ -50,6 +50,33 @@ query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
|
|
|
50
50
|
});
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### 组件生命周期钩子
|
|
54
|
+
|
|
55
|
+
ECS 支持在组件添加或移除时执行回调函数:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// 注册组件生命周期钩子
|
|
59
|
+
world.registerComponentLifecycleHook(PositionId, {
|
|
60
|
+
onAdded: (entityId, componentType, component) => {
|
|
61
|
+
console.log(`组件 ${componentType} 被添加到实体 ${entityId}`);
|
|
62
|
+
},
|
|
63
|
+
onRemoved: (entityId, componentType) => {
|
|
64
|
+
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 你也可以只注册其中一个钩子
|
|
69
|
+
world.registerComponentLifecycleHook(VelocityId, {
|
|
70
|
+
onRemoved: (entityId, componentType) => {
|
|
71
|
+
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 添加组件时会触发钩子
|
|
76
|
+
world.addComponent(entity, PositionId, { x: 0, y: 0 });
|
|
77
|
+
world.flushCommands(); // 钩子在这里被调用
|
|
78
|
+
```
|
|
79
|
+
|
|
53
80
|
### 运行示例
|
|
54
81
|
|
|
55
82
|
```bash
|
|
@@ -65,6 +92,8 @@ bun run examples/simple/demo.ts
|
|
|
65
92
|
- `removeComponent(entity, componentId)`: 从实体移除组件
|
|
66
93
|
- `createQuery(componentIds)`: 创建查询
|
|
67
94
|
- `registerSystem(system)`: 注册系统
|
|
95
|
+
- `registerComponentLifecycleHook(componentId, hook)`: 注册组件生命周期钩子
|
|
96
|
+
- `unregisterComponentLifecycleHook(componentId, hook)`: 注销组件生命周期钩子
|
|
68
97
|
- `update(deltaTime)`: 更新世界
|
|
69
98
|
- `flushCommands()`: 应用命令缓冲区
|
|
70
99
|
|
package/index.js
CHANGED
|
@@ -374,17 +374,26 @@ class CommandBuffer {
|
|
|
374
374
|
this.commands.push({ type: "destroyEntity", entityId });
|
|
375
375
|
}
|
|
376
376
|
execute() {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
377
|
+
const MAX_ITERATIONS = 100;
|
|
378
|
+
let iterations = 0;
|
|
379
|
+
while (this.commands.length > 0) {
|
|
380
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
381
|
+
throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
|
|
382
|
+
}
|
|
383
|
+
iterations++;
|
|
384
|
+
const currentCommands = [...this.commands];
|
|
385
|
+
this.commands = [];
|
|
386
|
+
const entityCommands = new Map;
|
|
387
|
+
for (const cmd of currentCommands) {
|
|
388
|
+
if (!entityCommands.has(cmd.entityId)) {
|
|
389
|
+
entityCommands.set(cmd.entityId, []);
|
|
390
|
+
}
|
|
391
|
+
entityCommands.get(cmd.entityId).push(cmd);
|
|
392
|
+
}
|
|
393
|
+
for (const [entityId, commands] of entityCommands) {
|
|
394
|
+
this.executeEntityCommands(entityId, commands);
|
|
381
395
|
}
|
|
382
|
-
entityCommands.get(cmd.entityId).push(cmd);
|
|
383
|
-
}
|
|
384
|
-
for (const [entityId, commands] of entityCommands) {
|
|
385
|
-
this.executeEntityCommands(entityId, commands);
|
|
386
396
|
}
|
|
387
|
-
this.commands = [];
|
|
388
397
|
}
|
|
389
398
|
getCommands() {
|
|
390
399
|
return [...this.commands];
|
|
@@ -512,6 +521,9 @@ class World {
|
|
|
512
521
|
queries = [];
|
|
513
522
|
commandBuffer;
|
|
514
523
|
componentToArchetypes = new Map;
|
|
524
|
+
componentLifecycleHooks = new Map;
|
|
525
|
+
wildcardRelationLifecycleHooks = new Map;
|
|
526
|
+
entityReverseIndex = new Map;
|
|
515
527
|
constructor() {
|
|
516
528
|
this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
517
529
|
}
|
|
@@ -530,7 +542,36 @@ class World {
|
|
|
530
542
|
if (!archetype) {
|
|
531
543
|
return;
|
|
532
544
|
}
|
|
545
|
+
const componentReferences = this.getComponentReferences(entityId);
|
|
546
|
+
for (const { sourceEntityId, componentType } of componentReferences) {
|
|
547
|
+
const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
|
|
548
|
+
if (sourceArchetype) {
|
|
549
|
+
const currentComponents = new Map;
|
|
550
|
+
for (const compType of sourceArchetype.componentTypes) {
|
|
551
|
+
if (compType !== componentType) {
|
|
552
|
+
const data = sourceArchetype.getComponent(sourceEntityId, compType);
|
|
553
|
+
if (data !== undefined) {
|
|
554
|
+
currentComponents.set(compType, data);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const newComponentTypes = Array.from(currentComponents.keys()).sort((a, b) => a - b);
|
|
559
|
+
const newArchetype = this.getOrCreateArchetype(newComponentTypes);
|
|
560
|
+
sourceArchetype.removeEntity(sourceEntityId);
|
|
561
|
+
if (sourceArchetype.getEntities().length === 0) {
|
|
562
|
+
this.removeEmptyArchetype(sourceArchetype);
|
|
563
|
+
}
|
|
564
|
+
newArchetype.addEntity(sourceEntityId, currentComponents);
|
|
565
|
+
this.entityToArchetype.set(sourceEntityId, newArchetype);
|
|
566
|
+
this.removeComponentReference(sourceEntityId, componentType, entityId);
|
|
567
|
+
this.executeComponentLifecycleHooks(sourceEntityId, new Map, new Set([componentType]));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
this.entityReverseIndex.delete(entityId);
|
|
533
571
|
archetype.removeEntity(entityId);
|
|
572
|
+
if (archetype.getEntities().length === 0) {
|
|
573
|
+
this.removeEmptyArchetype(archetype);
|
|
574
|
+
}
|
|
534
575
|
this.entityToArchetype.delete(entityId);
|
|
535
576
|
this.entityIdManager.deallocate(entityId);
|
|
536
577
|
}
|
|
@@ -569,6 +610,36 @@ class World {
|
|
|
569
610
|
this.systems.splice(index, 1);
|
|
570
611
|
}
|
|
571
612
|
}
|
|
613
|
+
registerComponentLifecycleHook(componentType, hook) {
|
|
614
|
+
if (!this.componentLifecycleHooks.has(componentType)) {
|
|
615
|
+
this.componentLifecycleHooks.set(componentType, new Set);
|
|
616
|
+
}
|
|
617
|
+
this.componentLifecycleHooks.get(componentType).add(hook);
|
|
618
|
+
}
|
|
619
|
+
unregisterComponentLifecycleHook(componentType, hook) {
|
|
620
|
+
const hooks = this.componentLifecycleHooks.get(componentType);
|
|
621
|
+
if (hooks) {
|
|
622
|
+
hooks.delete(hook);
|
|
623
|
+
if (hooks.size === 0) {
|
|
624
|
+
this.componentLifecycleHooks.delete(componentType);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
registerWildcardRelationLifecycleHook(baseComponentType, hook) {
|
|
629
|
+
if (!this.wildcardRelationLifecycleHooks.has(baseComponentType)) {
|
|
630
|
+
this.wildcardRelationLifecycleHooks.set(baseComponentType, new Set);
|
|
631
|
+
}
|
|
632
|
+
this.wildcardRelationLifecycleHooks.get(baseComponentType).add(hook);
|
|
633
|
+
}
|
|
634
|
+
unregisterWildcardRelationLifecycleHook(baseComponentType, hook) {
|
|
635
|
+
const hooks = this.wildcardRelationLifecycleHooks.get(baseComponentType);
|
|
636
|
+
if (hooks) {
|
|
637
|
+
hooks.delete(hook);
|
|
638
|
+
if (hooks.size === 0) {
|
|
639
|
+
this.wildcardRelationLifecycleHooks.delete(baseComponentType);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
572
643
|
update(...params) {
|
|
573
644
|
for (const system of this.systems) {
|
|
574
645
|
system.update(this, ...params);
|
|
@@ -713,6 +784,25 @@ class World {
|
|
|
713
784
|
currentArchetype.setComponent(entityId, componentType, component);
|
|
714
785
|
}
|
|
715
786
|
}
|
|
787
|
+
for (const componentType of removes) {
|
|
788
|
+
const detailedType = getDetailedIdType(componentType);
|
|
789
|
+
if (detailedType.type === "entity-relation") {
|
|
790
|
+
const targetEntityId = detailedType.targetId;
|
|
791
|
+
this.removeComponentReference(entityId, componentType, targetEntityId);
|
|
792
|
+
} else if (detailedType.type === "entity") {
|
|
793
|
+
this.removeComponentReference(entityId, componentType, componentType);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const [componentType, component] of adds) {
|
|
797
|
+
const detailedType = getDetailedIdType(componentType);
|
|
798
|
+
if (detailedType.type === "entity-relation") {
|
|
799
|
+
const targetEntityId = detailedType.targetId;
|
|
800
|
+
this.addComponentReference(entityId, componentType, targetEntityId);
|
|
801
|
+
} else if (detailedType.type === "entity") {
|
|
802
|
+
this.addComponentReference(entityId, componentType, componentType);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
this.executeComponentLifecycleHooks(entityId, adds, removes);
|
|
716
806
|
}
|
|
717
807
|
getOrCreateArchetype(componentTypes) {
|
|
718
808
|
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
@@ -731,6 +821,96 @@ class World {
|
|
|
731
821
|
return newArchetype;
|
|
732
822
|
});
|
|
733
823
|
}
|
|
824
|
+
addComponentReference(sourceEntityId, componentType, targetEntityId) {
|
|
825
|
+
if (!this.entityReverseIndex.has(targetEntityId)) {
|
|
826
|
+
this.entityReverseIndex.set(targetEntityId, new Set);
|
|
827
|
+
}
|
|
828
|
+
this.entityReverseIndex.get(targetEntityId).add({ sourceEntityId, componentType });
|
|
829
|
+
}
|
|
830
|
+
removeComponentReference(sourceEntityId, componentType, targetEntityId) {
|
|
831
|
+
const references = this.entityReverseIndex.get(targetEntityId);
|
|
832
|
+
if (references) {
|
|
833
|
+
references.forEach((ref) => {
|
|
834
|
+
if (ref.sourceEntityId === sourceEntityId && ref.componentType === componentType) {
|
|
835
|
+
references.delete(ref);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
if (references.size === 0) {
|
|
839
|
+
this.entityReverseIndex.delete(targetEntityId);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
getComponentReferences(targetEntityId) {
|
|
844
|
+
const references = this.entityReverseIndex.get(targetEntityId);
|
|
845
|
+
return references ? Array.from(references) : [];
|
|
846
|
+
}
|
|
847
|
+
removeEmptyArchetype(archetype) {
|
|
848
|
+
if (archetype.getEntities().length > 0) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const index = this.archetypes.indexOf(archetype);
|
|
852
|
+
if (index !== -1) {
|
|
853
|
+
this.archetypes.splice(index, 1);
|
|
854
|
+
}
|
|
855
|
+
const hashKey = this.getComponentTypesHash(archetype.componentTypes);
|
|
856
|
+
this.archetypeMap.delete(hashKey);
|
|
857
|
+
for (const componentType of archetype.componentTypes) {
|
|
858
|
+
const archetypes = this.componentToArchetypes.get(componentType);
|
|
859
|
+
if (archetypes) {
|
|
860
|
+
const compIndex = archetypes.indexOf(archetype);
|
|
861
|
+
if (compIndex !== -1) {
|
|
862
|
+
archetypes.splice(compIndex, 1);
|
|
863
|
+
if (archetypes.length === 0) {
|
|
864
|
+
this.componentToArchetypes.delete(componentType);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
executeComponentLifecycleHooks(entityId, addedComponents, removedComponents) {
|
|
871
|
+
for (const [componentType, component] of addedComponents) {
|
|
872
|
+
const hooks = this.componentLifecycleHooks.get(componentType);
|
|
873
|
+
if (hooks) {
|
|
874
|
+
for (const hook of hooks) {
|
|
875
|
+
if (hook.onAdded) {
|
|
876
|
+
hook.onAdded(entityId, componentType, component);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const detailedType = getDetailedIdType(componentType);
|
|
881
|
+
if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
|
|
882
|
+
const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId);
|
|
883
|
+
if (wildcardHooks) {
|
|
884
|
+
for (const hook of wildcardHooks) {
|
|
885
|
+
if (hook.onAdded) {
|
|
886
|
+
hook.onAdded(entityId, componentType, component);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
for (const componentType of removedComponents) {
|
|
893
|
+
const hooks = this.componentLifecycleHooks.get(componentType);
|
|
894
|
+
if (hooks) {
|
|
895
|
+
for (const hook of hooks) {
|
|
896
|
+
if (hook.onRemoved) {
|
|
897
|
+
hook.onRemoved(entityId, componentType);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const detailedType = getDetailedIdType(componentType);
|
|
902
|
+
if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
|
|
903
|
+
const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId);
|
|
904
|
+
if (wildcardHooks) {
|
|
905
|
+
for (const hook of wildcardHooks) {
|
|
906
|
+
if (hook.onRemoved) {
|
|
907
|
+
hook.onRemoved(entityId, componentType);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
734
914
|
}
|
|
735
915
|
export {
|
|
736
916
|
isRelationId,
|
package/package.json
CHANGED
package/world.d.ts
CHANGED
|
@@ -5,6 +5,33 @@ import { Query } from "./query";
|
|
|
5
5
|
import type { QueryFilter } from "./query-filter";
|
|
6
6
|
import type { ComponentTuple } from "./types";
|
|
7
7
|
import type { System } from "./system";
|
|
8
|
+
/**
|
|
9
|
+
* Hook types for component lifecycle events
|
|
10
|
+
*/
|
|
11
|
+
export interface ComponentLifecycleHook<T> {
|
|
12
|
+
/**
|
|
13
|
+
* Called when a component is added to an entity
|
|
14
|
+
*/
|
|
15
|
+
onAdded?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Called when a component is removed from an entity
|
|
18
|
+
*/
|
|
19
|
+
onRemoved?: (entityId: EntityId, componentType: EntityId<T>) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Hook types for wildcard relation lifecycle events
|
|
23
|
+
* These hooks are triggered for any component that matches a wildcard relation pattern
|
|
24
|
+
*/
|
|
25
|
+
export interface WildcardRelationLifecycleHook<T = unknown> {
|
|
26
|
+
/**
|
|
27
|
+
* Called when any component matching the wildcard relation pattern is added to an entity
|
|
28
|
+
*/
|
|
29
|
+
onAdded?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Called when any component matching the wildcard relation pattern is removed from an entity
|
|
32
|
+
*/
|
|
33
|
+
onRemoved?: (entityId: EntityId, componentType: EntityId<T>) => void;
|
|
34
|
+
}
|
|
8
35
|
/**
|
|
9
36
|
* World class for ECS architecture
|
|
10
37
|
* Manages entities, components, and systems
|
|
@@ -18,6 +45,21 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
18
45
|
private queries;
|
|
19
46
|
private commandBuffer;
|
|
20
47
|
private componentToArchetypes;
|
|
48
|
+
/**
|
|
49
|
+
* Hook storage for component lifecycle events
|
|
50
|
+
*/
|
|
51
|
+
private componentLifecycleHooks;
|
|
52
|
+
/**
|
|
53
|
+
* Hook storage for wildcard relation lifecycle events
|
|
54
|
+
* Maps base component type to set of wildcard relation hooks
|
|
55
|
+
*/
|
|
56
|
+
private wildcardRelationLifecycleHooks;
|
|
57
|
+
/**
|
|
58
|
+
* Reverse index tracking which entities use each entity as a component type
|
|
59
|
+
* Maps entity ID to set of {sourceEntityId, componentType} pairs where componentType uses this entity
|
|
60
|
+
* This includes both relation components and direct usage of entities as component types
|
|
61
|
+
*/
|
|
62
|
+
private entityReverseIndex;
|
|
21
63
|
constructor();
|
|
22
64
|
/**
|
|
23
65
|
* Generate a hash key for component types array
|
|
@@ -63,6 +105,23 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
63
105
|
* Unregister a system
|
|
64
106
|
*/
|
|
65
107
|
unregisterSystem(system: System<ExtraParams>): void;
|
|
108
|
+
/**
|
|
109
|
+
* Register a lifecycle hook for component events
|
|
110
|
+
*/
|
|
111
|
+
registerComponentLifecycleHook<T>(componentType: EntityId<T>, hook: ComponentLifecycleHook<T>): void;
|
|
112
|
+
/**
|
|
113
|
+
* Unregister a lifecycle hook for component events
|
|
114
|
+
*/
|
|
115
|
+
unregisterComponentLifecycleHook<T>(componentType: EntityId<T>, hook: ComponentLifecycleHook<T>): void;
|
|
116
|
+
/**
|
|
117
|
+
* Register a lifecycle hook for wildcard relation events
|
|
118
|
+
* The hook will be triggered for any component that matches the wildcard relation pattern
|
|
119
|
+
*/
|
|
120
|
+
registerWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: WildcardRelationLifecycleHook<T>): void;
|
|
121
|
+
/**
|
|
122
|
+
* Unregister a lifecycle hook for wildcard relation events
|
|
123
|
+
*/
|
|
124
|
+
unregisterWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: WildcardRelationLifecycleHook<T>): void;
|
|
66
125
|
/**
|
|
67
126
|
* Update the world (run all systems)
|
|
68
127
|
*/
|
|
@@ -103,4 +162,32 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
103
162
|
* Get or create an archetype for the given component types
|
|
104
163
|
*/
|
|
105
164
|
private getOrCreateArchetype;
|
|
165
|
+
/**
|
|
166
|
+
* Add a component reference to the reverse index when an entity is used as a component type
|
|
167
|
+
* @param sourceEntityId The entity that has the component
|
|
168
|
+
* @param componentType The component type (which may be an entity ID used as component type)
|
|
169
|
+
* @param targetEntityId The entity being used as component type
|
|
170
|
+
*/
|
|
171
|
+
private addComponentReference;
|
|
172
|
+
/**
|
|
173
|
+
* Remove a component reference from the reverse index
|
|
174
|
+
* @param sourceEntityId The entity that has the component
|
|
175
|
+
* @param componentType The component type
|
|
176
|
+
* @param targetEntityId The entity being used as component type
|
|
177
|
+
*/
|
|
178
|
+
private removeComponentReference;
|
|
179
|
+
/**
|
|
180
|
+
* Get all component references where a target entity is used as a component type
|
|
181
|
+
* @param targetEntityId The target entity
|
|
182
|
+
* @returns Array of {sourceEntityId, componentType} pairs
|
|
183
|
+
*/
|
|
184
|
+
private getComponentReferences;
|
|
185
|
+
/**
|
|
186
|
+
* Remove an empty archetype from all internal data structures
|
|
187
|
+
*/
|
|
188
|
+
private removeEmptyArchetype;
|
|
189
|
+
/**
|
|
190
|
+
* Execute component lifecycle hooks for added and removed components
|
|
191
|
+
*/
|
|
192
|
+
private executeComponentLifecycleHooks;
|
|
106
193
|
}
|