@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.
Files changed (4) hide show
  1. package/README.md +29 -0
  2. package/index.js +189 -9
  3. package/package.json +1 -1
  4. 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 entityCommands = new Map;
378
- for (const cmd of this.commands) {
379
- if (!entityCommands.has(cmd.entityId)) {
380
- entityCommands.set(cmd.entityId, []);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.0.0",
3
+ "version": "0.0.1",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
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
  }