@codehz/ecs 0.0.1 → 0.0.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
@@ -4,10 +4,12 @@
4
4
 
5
5
  ## 特性
6
6
 
7
- - 🚀 高性能:基于原型的组件存储和高效的查询系统
7
+ - 🚀 高性能:基于 Archetype 的组件存储和高效的查询系统
8
8
  - 🔧 类型安全:完整的 TypeScript 支持
9
9
  - 🏗️ 模块化:清晰的架构,支持自定义系统和组件
10
10
  - 📦 轻量级:零依赖,易于集成
11
+ - ⚡ 内存高效:连续内存布局,优化的迭代性能
12
+ - 🎣 生命周期钩子:支持组件和通配符关系的事件监听
11
13
 
12
14
  ## 安装
13
15
 
@@ -21,15 +23,15 @@ bun install
21
23
 
22
24
  ```typescript
23
25
  import { World } from "@codehz/ecs";
24
- import { createComponentId } from "@codehz/ecs";
26
+ import { component } from "@codehz/ecs";
25
27
 
26
28
  // 定义组件类型
27
29
  type Position = { x: number; y: number };
28
30
  type Velocity = { x: number; y: number };
29
31
 
30
32
  // 定义组件ID
31
- const PositionId = createComponentId<Position>(1);
32
- const VelocityId = createComponentId<Velocity>(2);
33
+ const PositionId = component<Position>(1);
34
+ const VelocityId = component<Velocity>(2);
33
35
 
34
36
  // 创建世界
35
37
  const world = new World();
@@ -44,6 +46,7 @@ world.flushCommands();
44
46
 
45
47
  // 创建查询并更新
46
48
  const query = world.createQuery([PositionId, VelocityId]);
49
+ const deltaTime = 1.0 / 60.0; // 假设60FPS
47
50
  query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
48
51
  position.x += velocity.x * deltaTime;
49
52
  position.y += velocity.y * deltaTime;
@@ -56,20 +59,20 @@ ECS 支持在组件添加或移除时执行回调函数:
56
59
 
57
60
  ```typescript
58
61
  // 注册组件生命周期钩子
59
- world.registerComponentLifecycleHook(PositionId, {
62
+ world.registerLifecycleHook(PositionId, {
60
63
  onAdded: (entityId, componentType, component) => {
61
64
  console.log(`组件 ${componentType} 被添加到实体 ${entityId}`);
62
65
  },
63
66
  onRemoved: (entityId, componentType) => {
64
67
  console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
65
- }
68
+ },
66
69
  });
67
70
 
68
71
  // 你也可以只注册其中一个钩子
69
- world.registerComponentLifecycleHook(VelocityId, {
72
+ world.registerLifecycleHook(VelocityId, {
70
73
  onRemoved: (entityId, componentType) => {
71
74
  console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
72
- }
75
+ },
73
76
  });
74
77
 
75
78
  // 添加组件时会触发钩子
@@ -77,8 +80,53 @@ world.addComponent(entity, PositionId, { x: 0, y: 0 });
77
80
  world.flushCommands(); // 钩子在这里被调用
78
81
  ```
79
82
 
83
+ ### 通配符关系生命周期钩子
84
+
85
+ ECS 还支持通配符关系生命周期钩子,可以监听特定组件的所有关系变化:
86
+
87
+ ```typescript
88
+ import { World, component, relation } from "@codehz/ecs";
89
+
90
+ // 定义组件类型
91
+ type Position = { x: number; y: number };
92
+
93
+ // 定义组件ID
94
+ const PositionId = component<Position>(1);
95
+
96
+ // 创建世界
97
+ const world = new World();
98
+
99
+ // 创建实体
100
+ const entity = world.createEntity();
101
+
102
+ // 创建通配符关系ID,用于监听所有 Position 相关的关系
103
+ const wildcardPositionRelation = relation(PositionId, "*");
104
+
105
+ // 注册通配符关系钩子
106
+ world.registerLifecycleHook(wildcardPositionRelation, {
107
+ onAdded: (entityId, componentType, component) => {
108
+ console.log(`关系组件 ${componentType} 被添加到实体 ${entityId}`);
109
+ },
110
+ onRemoved: (entityId, componentType) => {
111
+ console.log(`关系组件 ${componentType} 被从实体 ${entityId} 移除`);
112
+ },
113
+ });
114
+
115
+ // 创建实体间的关系
116
+ const entity2 = world.createEntity();
117
+ const positionRelation = relation(PositionId, entity2);
118
+ world.addComponent(entity, positionRelation, { x: 10, y: 20 });
119
+ world.flushCommands(); // 通配符钩子会被触发
120
+ ```
121
+
80
122
  ### 运行示例
81
123
 
124
+ ```bash
125
+ bun run demo
126
+ ```
127
+
128
+ 或者直接运行:
129
+
82
130
  ```bash
83
131
  bun run examples/simple/demo.ts
84
132
  ```
@@ -92,18 +140,20 @@ bun run examples/simple/demo.ts
92
140
  - `removeComponent(entity, componentId)`: 从实体移除组件
93
141
  - `createQuery(componentIds)`: 创建查询
94
142
  - `registerSystem(system)`: 注册系统
95
- - `registerComponentLifecycleHook(componentId, hook)`: 注册组件生命周期钩子
96
- - `unregisterComponentLifecycleHook(componentId, hook)`: 注销组件生命周期钩子
143
+ - `registerLifecycleHook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
144
+ - `unregisterLifecycleHook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
97
145
  - `update(deltaTime)`: 更新世界
98
146
  - `flushCommands()`: 应用命令缓冲区
99
147
 
100
148
  ### Entity
101
149
 
102
- - `createComponentId<T>(id)`: 创建类型安全的组件ID
150
+ - `component<T>(id)`: 分配类型安全的组件ID(上限:1022个)
103
151
 
104
152
  ### Query
105
153
 
106
154
  - `forEach(componentIds, callback)`: 遍历匹配的实体
155
+ - `getEntities()`: 获取所有匹配实体的ID列表
156
+ - `getEntitiesWithComponents(componentIds)`: 获取实体及其组件数据
107
157
 
108
158
  ### System
109
159
 
@@ -117,6 +167,13 @@ class MySystem implements System {
117
167
  }
118
168
  ```
119
169
 
170
+ ## 性能特点
171
+
172
+ - **Archetype 系统**:实体按组件组合分组,实现连续内存访问
173
+ - **缓存查询**:查询结果自动缓存,减少重复计算
174
+ - **命令缓冲区**:延迟执行组件添加/移除,提高批处理效率
175
+ - **类型安全**:编译时类型检查,无运行时开销
176
+
120
177
  ## 开发
121
178
 
122
179
  ### 运行测试
@@ -135,20 +192,28 @@ bunx tsc --noEmit
135
192
 
136
193
  ```
137
194
  src/
138
- ├── index.ts # 入口文件
139
- ├── entity.ts # 实体和组件管理
140
- ├── world.ts # 世界管理
141
- ├── archetype.ts # 原型系统
142
- ├── query.ts # 查询系统
143
- ├── system.ts # 系统接口
144
- ├── command-buffer.ts # 命令缓冲区
145
- ├── types.ts # 类型定义
146
- └── utils.ts # 工具函数
195
+ ├── index.ts # 入口文件
196
+ ├── entity.ts # 实体和组件管理
197
+ ├── world.ts # 世界管理
198
+ ├── archetype.ts # Archetype 系统(高效组件存储)
199
+ ├── query.ts # 查询系统
200
+ ├── query-filter.ts # 查询过滤器
201
+ ├── system.ts # 系统接口
202
+ ├── command-buffer.ts # 命令缓冲区
203
+ ├── types.ts # 类型定义
204
+ ├── utils.ts # 工具函数
205
+ ├── *.test.ts # 单元测试
206
+ ├── query.example.ts # 查询示例
207
+ └── *.perf.test.ts # 性能测试
147
208
 
148
209
  examples/
149
210
  └── simple/
150
- ├── demo.ts # 基本示例
151
- └── README.md # 示例说明
211
+ ├── demo.ts # 基本示例
212
+ └── README.md # 示例说明
213
+
214
+ scripts/
215
+ ├── build.ts # 构建脚本
216
+ └── release.ts # 发布脚本
152
217
  ```
153
218
 
154
219
  ## 许可证
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/entity.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Brand for EntityId to create nominal typing with component type information
2
+ * Unique symbol brand for associating component type information with EntityId
3
3
  */
4
- declare const __entityIdBrand: unique symbol;
4
+ declare const __componentTypeMarker: unique symbol;
5
5
  /**
6
- * Brand for wildcard relation IDs
6
+ * Unique symbol brand for tagging the kind of EntityId (e.g., 'component', 'entity-relation')
7
7
  */
8
- declare const __wildcardRelationBrand: unique symbol;
8
+ declare const __entityIdTypeTag: unique symbol;
9
9
  /**
10
10
  * Entity ID type for ECS architecture
11
11
  * Based on 52-bit integers within safe integer range
@@ -13,12 +13,15 @@ declare const __wildcardRelationBrand: unique symbol;
13
13
  * - Entity IDs: 1024+
14
14
  * - Relation IDs: negative numbers encoding component and entity associations
15
15
  */
16
- export type EntityId<T = void> = number & {
17
- readonly [__entityIdBrand]: T;
18
- };
19
- export type WildcardRelationId<T = void> = EntityId<T> & {
20
- readonly [__wildcardRelationBrand]: true;
16
+ export type EntityId<T = void, U = unknown> = number & {
17
+ readonly [__componentTypeMarker]: T;
18
+ readonly [__entityIdTypeTag]: U;
21
19
  };
20
+ export type ComponentId<T = void> = EntityId<T, "component">;
21
+ export type EntityRelationId<T = void> = EntityId<T, "entity-relation">;
22
+ export type ComponentRelationId<T = void> = EntityId<T, "component-relation">;
23
+ export type WildcardRelationId<T = void> = EntityId<T, "wildcard-relation">;
24
+ export type RelationId<T = void> = EntityRelationId<T> | ComponentRelationId<T> | WildcardRelationId<T>;
22
25
  /**
23
26
  * Constants for ID ranges
24
27
  */
@@ -33,8 +36,9 @@ export declare const WILDCARD_TARGET_ID = 0;
33
36
  /**
34
37
  * Create a component ID
35
38
  * @param id Component identifier (1-1023)
39
+ * @see component
36
40
  */
37
- export declare function createComponentId<T = void>(id: number): EntityId<T>;
41
+ export declare function createComponentId<T = void>(id: number): ComponentId<T>;
38
42
  /**
39
43
  * Create an entity ID
40
44
  * @param id Entity identifier (starting from 1024)
@@ -43,33 +47,37 @@ export declare function createEntityId(id: number): EntityId;
43
47
  /**
44
48
  * Type for relation ID based on component and target types
45
49
  */
46
- type RelationIdType<T, U> = U extends void ? EntityId<T> : T extends void ? EntityId<U> : EntityId<never>;
50
+ type RelationIdType<T, R> = R extends ComponentId<infer U> ? U extends void ? ComponentRelationId<T> : ComponentRelationId<T & U> : R extends EntityId<any> ? EntityRelationId<T> : never;
47
51
  /**
48
52
  * Create a relation ID by associating a component with another ID (entity or component)
49
53
  * @param componentId The component ID (0-1023)
50
54
  * @param targetId The target ID (entity, component, or '*' for wildcard)
51
55
  */
52
- export declare function createRelationId<T>(componentId: EntityId<T>, targetId: "*"): WildcardRelationId<T>;
53
- export declare function createRelationId<T, U>(componentId: EntityId<T>, targetId: EntityId<U>): RelationIdType<T, U>;
56
+ export declare function relation<T>(componentId: ComponentId<T>, targetId: "*"): WildcardRelationId<T>;
57
+ export declare function relation<T, R extends EntityId<any>>(componentId: ComponentId<T>, targetId: R): RelationIdType<T, R>;
54
58
  /**
55
59
  * Check if an ID is a component ID
56
60
  */
57
- export declare function isComponentId(id: EntityId<any>): boolean;
61
+ export declare function isComponentId<T>(id: EntityId<T>): id is ComponentId<T>;
58
62
  /**
59
63
  * Check if an ID is an entity ID
60
64
  */
61
- export declare function isEntityId(id: EntityId<any>): boolean;
65
+ export declare function isEntityId<T>(id: EntityId<T>): id is EntityId<T>;
62
66
  /**
63
67
  * Check if an ID is a relation ID
64
68
  */
65
- export declare function isRelationId(id: EntityId): boolean;
69
+ export declare function isRelationId<T>(id: EntityId<T>): id is RelationId<T>;
70
+ /**
71
+ * Check if an ID is a wildcard relation id
72
+ */
73
+ export declare function isWildcardRelationId<T>(id: EntityId<T>): id is WildcardRelationId<T>;
66
74
  /**
67
75
  * Decode a relation ID into component and target IDs
68
76
  * @param relationId The relation ID (must be negative)
69
77
  * @returns Object with componentId, targetId, and relation type
70
78
  */
71
- export declare function decodeRelationId(relationId: EntityId<any>): {
72
- componentId: EntityId<any>;
79
+ export declare function decodeRelationId(relationId: RelationId<any>): {
80
+ componentId: ComponentId<any>;
73
81
  targetId: EntityId<any>;
74
82
  type: "entity" | "component" | "wildcard";
75
83
  };
@@ -83,9 +91,13 @@ export declare function getIdType(id: EntityId<any>): "component" | "entity" | "
83
91
  * @returns Detailed type information including relation subtypes
84
92
  */
85
93
  export declare function getDetailedIdType(id: EntityId<any>): {
86
- type: "component" | "entity" | "entity-relation" | "component-relation" | "wildcard-relation" | "invalid";
87
- componentId?: EntityId<any>;
88
- targetId?: EntityId<any>;
94
+ type: "component" | "entity" | "invalid";
95
+ componentId?: never;
96
+ targetId?: never;
97
+ } | {
98
+ type: "entity-relation" | "component-relation" | "wildcard-relation";
99
+ componentId: ComponentId<any>;
100
+ targetId: EntityId<any>;
89
101
  };
90
102
  /**
91
103
  * Inspect an EntityId and return a human-readable string representation
@@ -122,13 +134,13 @@ export declare class EntityIdManager {
122
134
  * Component ID Manager for automatic allocation
123
135
  * Components are typically registered once and not recycled
124
136
  */
125
- export declare class ComponentIdManager {
137
+ export declare class ComponentIdAllocator {
126
138
  private nextId;
127
139
  /**
128
140
  * Allocate a new component ID
129
141
  * Increments counter sequentially from 1
130
142
  */
131
- allocate<T = void>(): EntityId<T>;
143
+ allocate<T = void>(): ComponentId<T>;
132
144
  /**
133
145
  * Get the next ID that would be allocated (for debugging)
134
146
  */
@@ -138,4 +150,8 @@ export declare class ComponentIdManager {
138
150
  */
139
151
  hasAvailableIds(): boolean;
140
152
  }
153
+ /**
154
+ * Allocate a new component ID from the global allocator
155
+ */
156
+ export declare function component<T>(): ComponentId<T>;
141
157
  export {};
package/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./world";
3
3
  export * from "./archetype";
4
4
  export * from "./query";
5
5
  export * from "./system";
6
+ export * from "./types";
package/index.js CHANGED
@@ -16,7 +16,7 @@ function createEntityId(id) {
16
16
  }
17
17
  return id;
18
18
  }
19
- function createRelationId(componentId, targetId) {
19
+ function relation(componentId, targetId) {
20
20
  if (!isComponentId(componentId)) {
21
21
  throw new Error("First argument must be a valid component ID");
22
22
  }
@@ -40,6 +40,14 @@ function isEntityId(id) {
40
40
  function isRelationId(id) {
41
41
  return id < 0;
42
42
  }
43
+ function isWildcardRelationId(id) {
44
+ if (!isRelationId(id)) {
45
+ return false;
46
+ }
47
+ const absId = -id;
48
+ const targetId = absId % RELATION_SHIFT;
49
+ return targetId === WILDCARD_TARGET_ID;
50
+ }
43
51
  function decodeRelationId(relationId) {
44
52
  if (!isRelationId(relationId)) {
45
53
  throw new Error("ID is not a relation ID");
@@ -178,7 +186,7 @@ class EntityIdManager {
178
186
  }
179
187
  }
180
188
 
181
- class ComponentIdManager {
189
+ class ComponentIdAllocator {
182
190
  nextId = 1;
183
191
  allocate() {
184
192
  if (this.nextId > COMPONENT_ID_MAX) {
@@ -195,6 +203,10 @@ class ComponentIdManager {
195
203
  return this.nextId <= COMPONENT_ID_MAX;
196
204
  }
197
205
  }
206
+ var globalComponentIdAllocator = new ComponentIdAllocator;
207
+ function component() {
208
+ return globalComponentIdAllocator.allocate();
209
+ }
198
210
  // src/utils.ts
199
211
  function getOrComputeCache(cache, key, compute) {
200
212
  let value = cache.get(key);
@@ -275,9 +287,29 @@ class Archetype {
275
287
  getComponent(entityId, componentType) {
276
288
  const index = this.entityToIndex.get(entityId);
277
289
  if (index === undefined) {
278
- return;
290
+ if (getIdType(componentType) === "wildcard-relation") {
291
+ return [];
292
+ } else {
293
+ return;
294
+ }
295
+ }
296
+ if (isWildcardRelationId(componentType)) {
297
+ const decoded = decodeRelationId(componentType);
298
+ const componentId = decoded.componentId;
299
+ const relations = [];
300
+ for (const relType of this.componentTypes) {
301
+ const relDetailed = getDetailedIdType(relType);
302
+ if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) {
303
+ const dataArray = this.componentData.get(relType);
304
+ if (dataArray && dataArray[index] !== undefined) {
305
+ relations.push([relDetailed.targetId, dataArray[index]]);
306
+ }
307
+ }
308
+ }
309
+ return relations;
310
+ } else {
311
+ return this.componentData.get(componentType)?.[index];
279
312
  }
280
- return this.componentData.get(componentType)?.[index];
281
313
  }
282
314
  setComponent(entityId, componentType, data) {
283
315
  const index = this.entityToIndex.get(entityId);
@@ -307,15 +339,14 @@ class Archetype {
307
339
  const cacheKey = componentTypes.map((id) => id.toString()).join(",");
308
340
  const componentDataSources = getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
309
341
  return componentTypes.map((compType) => {
310
- if (getIdType(compType) === "wildcard-relation") {
311
- const decoded = decodeRelationId(compType);
312
- const componentId = decoded.componentId;
342
+ const detailedType = getDetailedIdType(compType);
343
+ if (detailedType.type === "wildcard-relation") {
344
+ const componentId = detailedType.componentId;
313
345
  const matchingRelations = this.componentTypes.filter((ct) => {
314
- const ctType = getIdType(ct);
315
- if (ctType !== "entity-relation" && ctType !== "component-relation")
346
+ const detailedCt = getDetailedIdType(ct);
347
+ if (detailedCt.type !== "entity-relation" && detailedCt.type !== "component-relation")
316
348
  return false;
317
- const decodedCt = decodeRelationId(ct);
318
- return decodedCt.componentId === componentId;
349
+ return detailedCt.componentId === componentId;
319
350
  });
320
351
  return matchingRelations;
321
352
  } else {
@@ -364,8 +395,8 @@ class CommandBuffer {
364
395
  constructor(executeEntityCommands) {
365
396
  this.executeEntityCommands = executeEntityCommands;
366
397
  }
367
- addComponent(entityId, componentType, component) {
368
- this.commands.push({ type: "addComponent", entityId, componentType, component });
398
+ addComponent(entityId, componentType, component2) {
399
+ this.commands.push({ type: "addComponent", entityId, componentType, component: component2 });
369
400
  }
370
401
  removeComponent(entityId, componentType) {
371
402
  this.commands.push({ type: "removeComponent", entityId, componentType });
@@ -521,8 +552,7 @@ class World {
521
552
  queries = [];
522
553
  commandBuffer;
523
554
  componentToArchetypes = new Map;
524
- componentLifecycleHooks = new Map;
525
- wildcardRelationLifecycleHooks = new Map;
555
+ lifecycleHooks = new Map;
526
556
  entityReverseIndex = new Map;
527
557
  constructor() {
528
558
  this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
@@ -578,16 +608,27 @@ class World {
578
608
  hasEntity(entityId) {
579
609
  return this.entityToArchetype.has(entityId);
580
610
  }
581
- addComponent(entityId, componentType, component) {
611
+ addComponent(entityId, componentType, component2) {
582
612
  if (!this.hasEntity(entityId)) {
583
613
  throw new Error(`Entity ${entityId} does not exist`);
584
614
  }
585
- this.commandBuffer.addComponent(entityId, componentType, component);
615
+ const detailedType = getDetailedIdType(componentType);
616
+ if (detailedType.type === "invalid") {
617
+ throw new Error(`Invalid component type: ${componentType}`);
618
+ }
619
+ if (detailedType.type === "wildcard-relation") {
620
+ throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
621
+ }
622
+ this.commandBuffer.addComponent(entityId, componentType, component2);
586
623
  }
587
624
  removeComponent(entityId, componentType) {
588
625
  if (!this.hasEntity(entityId)) {
589
626
  throw new Error(`Entity ${entityId} does not exist`);
590
627
  }
628
+ const detailedType = getDetailedIdType(componentType);
629
+ if (detailedType.type === "invalid") {
630
+ throw new Error(`Invalid component type: ${componentType}`);
631
+ }
591
632
  this.commandBuffer.removeComponent(entityId, componentType);
592
633
  }
593
634
  destroyEntity(entityId) {
@@ -599,7 +640,14 @@ class World {
599
640
  }
600
641
  getComponent(entityId, componentType) {
601
642
  const archetype = this.entityToArchetype.get(entityId);
602
- return archetype ? archetype.getComponent(entityId, componentType) : undefined;
643
+ if (!archetype) {
644
+ if (getIdType(componentType) === "wildcard-relation") {
645
+ return [];
646
+ } else {
647
+ return;
648
+ }
649
+ }
650
+ return archetype.getComponent(entityId, componentType);
603
651
  }
604
652
  registerSystem(system) {
605
653
  this.systems.push(system);
@@ -610,33 +658,18 @@ class World {
610
658
  this.systems.splice(index, 1);
611
659
  }
612
660
  }
613
- registerComponentLifecycleHook(componentType, hook) {
614
- if (!this.componentLifecycleHooks.has(componentType)) {
615
- this.componentLifecycleHooks.set(componentType, new Set);
661
+ registerLifecycleHook(componentType, hook) {
662
+ if (!this.lifecycleHooks.has(componentType)) {
663
+ this.lifecycleHooks.set(componentType, new Set);
616
664
  }
617
- this.componentLifecycleHooks.get(componentType).add(hook);
665
+ this.lifecycleHooks.get(componentType).add(hook);
618
666
  }
619
- unregisterComponentLifecycleHook(componentType, hook) {
620
- const hooks = this.componentLifecycleHooks.get(componentType);
667
+ unregisterLifecycleHook(componentType, hook) {
668
+ const hooks = this.lifecycleHooks.get(componentType);
621
669
  if (hooks) {
622
670
  hooks.delete(hook);
623
671
  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);
672
+ this.lifecycleHooks.delete(componentType);
640
673
  }
641
674
  }
642
675
  }
@@ -758,8 +791,22 @@ class World {
758
791
  break;
759
792
  case "removeComponent":
760
793
  if (cmd.componentType) {
761
- removes.add(cmd.componentType);
762
- adds.delete(cmd.componentType);
794
+ const detailedType = getDetailedIdType(cmd.componentType);
795
+ if (detailedType.type === "wildcard-relation") {
796
+ const baseComponentId = detailedType.componentId;
797
+ for (const componentType of currentArchetype.componentTypes) {
798
+ const componentDetailedType = getDetailedIdType(componentType);
799
+ if (componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") {
800
+ if (componentDetailedType.componentId === baseComponentId) {
801
+ removes.add(componentType);
802
+ adds.delete(componentType);
803
+ }
804
+ }
805
+ }
806
+ } else {
807
+ removes.add(cmd.componentType);
808
+ adds.delete(cmd.componentType);
809
+ }
763
810
  }
764
811
  break;
765
812
  }
@@ -768,8 +815,8 @@ class World {
768
815
  for (const componentType of removes) {
769
816
  finalComponents.delete(componentType);
770
817
  }
771
- for (const [componentType, component] of adds) {
772
- finalComponents.set(componentType, component);
818
+ for (const [componentType, component2] of adds) {
819
+ finalComponents.set(componentType, component2);
773
820
  }
774
821
  const finalComponentTypes = Array.from(finalComponents.keys()).sort((a, b) => a - b);
775
822
  const currentComponentTypes = currentArchetype.componentTypes.sort((a, b) => a - b);
@@ -780,8 +827,8 @@ class World {
780
827
  newArchetype.addEntity(entityId, finalComponents);
781
828
  this.entityToArchetype.set(entityId, newArchetype);
782
829
  } else {
783
- for (const [componentType, component] of adds) {
784
- currentArchetype.setComponent(entityId, componentType, component);
830
+ for (const [componentType, component2] of adds) {
831
+ currentArchetype.setComponent(entityId, componentType, component2);
785
832
  }
786
833
  }
787
834
  for (const componentType of removes) {
@@ -793,7 +840,7 @@ class World {
793
840
  this.removeComponentReference(entityId, componentType, componentType);
794
841
  }
795
842
  }
796
- for (const [componentType, component] of adds) {
843
+ for (const [componentType, component2] of adds) {
797
844
  const detailedType = getDetailedIdType(componentType);
798
845
  if (detailedType.type === "entity-relation") {
799
846
  const targetEntityId = detailedType.targetId;
@@ -868,31 +915,32 @@ class World {
868
915
  }
869
916
  }
870
917
  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) {
918
+ for (const [componentType, component2] of addedComponents) {
919
+ const directHooks = this.lifecycleHooks.get(componentType);
920
+ if (directHooks) {
921
+ for (const hook of directHooks) {
875
922
  if (hook.onAdded) {
876
- hook.onAdded(entityId, componentType, component);
923
+ hook.onAdded(entityId, componentType, component2);
877
924
  }
878
925
  }
879
926
  }
880
927
  const detailedType = getDetailedIdType(componentType);
881
928
  if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
882
- const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId);
929
+ const wildcardRelationId = relation(detailedType.componentId, "*");
930
+ const wildcardHooks = this.lifecycleHooks.get(wildcardRelationId);
883
931
  if (wildcardHooks) {
884
932
  for (const hook of wildcardHooks) {
885
933
  if (hook.onAdded) {
886
- hook.onAdded(entityId, componentType, component);
934
+ hook.onAdded(entityId, componentType, component2);
887
935
  }
888
936
  }
889
937
  }
890
938
  }
891
939
  }
892
940
  for (const componentType of removedComponents) {
893
- const hooks = this.componentLifecycleHooks.get(componentType);
894
- if (hooks) {
895
- for (const hook of hooks) {
941
+ const directHooks = this.lifecycleHooks.get(componentType);
942
+ if (directHooks) {
943
+ for (const hook of directHooks) {
896
944
  if (hook.onRemoved) {
897
945
  hook.onRemoved(entityId, componentType);
898
946
  }
@@ -900,7 +948,8 @@ class World {
900
948
  }
901
949
  const detailedType = getDetailedIdType(componentType);
902
950
  if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
903
- const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId);
951
+ const wildcardRelationId = relation(detailedType.componentId, "*");
952
+ const wildcardHooks = this.lifecycleHooks.get(wildcardRelationId);
904
953
  if (wildcardHooks) {
905
954
  for (const hook of wildcardHooks) {
906
955
  if (hook.onRemoved) {
@@ -913,6 +962,8 @@ class World {
913
962
  }
914
963
  }
915
964
  export {
965
+ relation,
966
+ isWildcardRelationId,
916
967
  isRelationId,
917
968
  isEntityId,
918
969
  isComponentId,
@@ -920,9 +971,9 @@ export {
920
971
  getIdType,
921
972
  getDetailedIdType,
922
973
  decodeRelationId,
923
- createRelationId,
924
974
  createEntityId,
925
975
  createComponentId,
976
+ component,
926
977
  World,
927
978
  WILDCARD_TARGET_ID,
928
979
  RELATION_SHIFT,
@@ -930,7 +981,7 @@ export {
930
981
  INVALID_COMPONENT_ID,
931
982
  EntityIdManager,
932
983
  ENTITY_ID_START,
933
- ComponentIdManager,
984
+ ComponentIdAllocator,
934
985
  COMPONENT_ID_MAX,
935
986
  Archetype
936
987
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
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,37 +1,10 @@
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";
7
6
  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
- }
7
+ import type { ComponentTuple, LifecycleHook } from "./types";
35
8
  /**
36
9
  * World class for ECS architecture
37
10
  * Manages entities, components, and systems
@@ -46,14 +19,9 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
46
19
  private commandBuffer;
47
20
  private componentToArchetypes;
48
21
  /**
49
- * Hook storage for component lifecycle events
22
+ * Hook storage for component and wildcard relation lifecycle events
50
23
  */
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;
24
+ private lifecycleHooks;
57
25
  /**
58
26
  * Reverse index tracking which entities use each entity as a component type
59
27
  * Maps entity ID to set of {sourceEntityId, componentType} pairs where componentType uses this entity
@@ -93,6 +61,10 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
93
61
  * Check if an entity has a specific component
94
62
  */
95
63
  hasComponent<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
64
+ /**
65
+ * Get wildcard relations from an entity
66
+ */
67
+ getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][] | undefined;
96
68
  /**
97
69
  * Get a component from an entity
98
70
  */
@@ -106,22 +78,13 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
106
78
  */
107
79
  unregisterSystem(system: System<ExtraParams>): void;
108
80
  /**
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
81
+ * Register a lifecycle hook for component or wildcard relation events
119
82
  */
120
- registerWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: WildcardRelationLifecycleHook<T>): void;
83
+ registerLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
121
84
  /**
122
- * Unregister a lifecycle hook for wildcard relation events
85
+ * Unregister a lifecycle hook for component or wildcard relation events
123
86
  */
124
- unregisterWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: WildcardRelationLifecycleHook<T>): void;
87
+ unregisterLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
125
88
  /**
126
89
  * Update the world (run all systems)
127
90
  */