@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 +87 -22
- package/archetype.d.ts +8 -1
- package/entity.d.ts +39 -23
- package/index.d.ts +1 -0
- package/index.js +111 -60
- package/package.json +1 -1
- package/types.d.ts +13 -0
- package/world.d.ts +12 -49
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 {
|
|
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 =
|
|
32
|
-
const VelocityId =
|
|
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.
|
|
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.
|
|
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
|
-
- `
|
|
96
|
-
- `
|
|
143
|
+
- `registerLifecycleHook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
|
|
144
|
+
- `unregisterLifecycleHook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
|
|
97
145
|
- `update(deltaTime)`: 更新世界
|
|
98
146
|
- `flushCommands()`: 应用命令缓冲区
|
|
99
147
|
|
|
100
148
|
### Entity
|
|
101
149
|
|
|
102
|
-
- `
|
|
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
|
-
├──
|
|
144
|
-
├──
|
|
145
|
-
├──
|
|
146
|
-
|
|
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
|
-
*
|
|
2
|
+
* Unique symbol brand for associating component type information with EntityId
|
|
3
3
|
*/
|
|
4
|
-
declare const
|
|
4
|
+
declare const __componentTypeMarker: unique symbol;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Unique symbol brand for tagging the kind of EntityId (e.g., 'component', 'entity-relation')
|
|
7
7
|
*/
|
|
8
|
-
declare const
|
|
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 [
|
|
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):
|
|
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,
|
|
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
|
|
53
|
-
export declare function
|
|
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<
|
|
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<
|
|
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):
|
|
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:
|
|
72
|
-
componentId:
|
|
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" | "
|
|
87
|
-
componentId?:
|
|
88
|
-
targetId?:
|
|
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
|
|
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>():
|
|
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
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ function createEntityId(id) {
|
|
|
16
16
|
}
|
|
17
17
|
return id;
|
|
18
18
|
}
|
|
19
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
const 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
|
|
315
|
-
if (
|
|
346
|
+
const detailedCt = getDetailedIdType(ct);
|
|
347
|
+
if (detailedCt.type !== "entity-relation" && detailedCt.type !== "component-relation")
|
|
316
348
|
return false;
|
|
317
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
611
|
+
addComponent(entityId, componentType, component2) {
|
|
582
612
|
if (!this.hasEntity(entityId)) {
|
|
583
613
|
throw new Error(`Entity ${entityId} does not exist`);
|
|
584
614
|
}
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
if (!this.
|
|
615
|
-
this.
|
|
661
|
+
registerLifecycleHook(componentType, hook) {
|
|
662
|
+
if (!this.lifecycleHooks.has(componentType)) {
|
|
663
|
+
this.lifecycleHooks.set(componentType, new Set);
|
|
616
664
|
}
|
|
617
|
-
this.
|
|
665
|
+
this.lifecycleHooks.get(componentType).add(hook);
|
|
618
666
|
}
|
|
619
|
-
|
|
620
|
-
const hooks = this.
|
|
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.
|
|
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
|
-
|
|
762
|
-
|
|
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,
|
|
772
|
-
finalComponents.set(componentType,
|
|
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,
|
|
784
|
-
currentArchetype.setComponent(entityId, componentType,
|
|
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,
|
|
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,
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
874
|
-
for (const hook of
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
894
|
-
if (
|
|
895
|
-
for (const hook of
|
|
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
|
|
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
|
-
|
|
984
|
+
ComponentIdAllocator,
|
|
934
985
|
COMPONENT_ID_MAX,
|
|
935
986
|
Archetype
|
|
936
987
|
};
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
unregisterLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
|
|
125
88
|
/**
|
|
126
89
|
* Update the world (run all systems)
|
|
127
90
|
*/
|