@codehz/ecs 0.6.10 → 0.7.0
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.en.md +440 -0
- package/README.md +248 -269
- package/builder.d.mts +667 -196
- package/index.d.mts +2 -2
- package/package.json +1 -1
- package/testing.d.mts +2 -2
- package/testing.mjs.map +1 -1
- package/world.mjs +1457 -1137
- package/world.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @codehz/ecs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **English version:** [README.en.md](./README.en.md)
|
|
4
|
+
|
|
5
|
+
一个高性能的 Entity Component System (ECS) 库,使用 TypeScript 和 Bun 运行时构建。
|
|
4
6
|
|
|
5
7
|
## 特性
|
|
6
8
|
|
|
@@ -9,7 +11,7 @@
|
|
|
9
11
|
- 🏗️ 模块化:清晰的架构,支持自定义组件
|
|
10
12
|
- 📦 轻量级:零依赖,易于集成
|
|
11
13
|
- ⚡ 内存高效:连续内存布局,优化的迭代性能
|
|
12
|
-
- 🎣
|
|
14
|
+
- 🎣 生命周期钩子:支持多组件和通配符关系的事件监听
|
|
13
15
|
|
|
14
16
|
## 安装
|
|
15
17
|
|
|
@@ -22,260 +24,307 @@ bun install
|
|
|
22
24
|
### 基本示例
|
|
23
25
|
|
|
24
26
|
```typescript
|
|
25
|
-
import { World } from "@codehz/ecs";
|
|
26
|
-
import { component } from "@codehz/ecs";
|
|
27
|
+
import { World, component } from "@codehz/ecs";
|
|
27
28
|
|
|
28
29
|
// 定义组件类型
|
|
29
30
|
type Position = { x: number; y: number };
|
|
30
31
|
type Velocity = { x: number; y: number };
|
|
31
32
|
|
|
32
|
-
// 定义组件ID
|
|
33
|
-
const PositionId = component<Position>(
|
|
34
|
-
const VelocityId = component<Velocity>(
|
|
33
|
+
// 定义组件 ID(自动分配)
|
|
34
|
+
const PositionId = component<Position>();
|
|
35
|
+
const VelocityId = component<Velocity>();
|
|
35
36
|
|
|
36
37
|
// 创建世界
|
|
37
38
|
const world = new World();
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
+
// 创建实体并设置组件(所有更改缓冲到 sync() 时应用)
|
|
40
41
|
const entity = world.new();
|
|
41
42
|
world.set(entity, PositionId, { x: 0, y: 0 });
|
|
42
43
|
world.set(entity, VelocityId, { x: 1, y: 0.5 });
|
|
43
|
-
|
|
44
|
-
// 应用更改
|
|
45
44
|
world.sync();
|
|
46
45
|
|
|
47
|
-
//
|
|
46
|
+
// 创建可重用的查询
|
|
48
47
|
const query = world.createQuery([PositionId, VelocityId]);
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
// 更新循环
|
|
50
|
+
const deltaTime = 1.0 / 60.0;
|
|
50
51
|
query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
|
|
51
52
|
position.x += velocity.x * deltaTime;
|
|
52
53
|
position.y += velocity.y * deltaTime;
|
|
53
54
|
});
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
###
|
|
57
|
+
### 定义组件(ID 自动分配)
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
`component()` 自动从全局分配器中分配一个唯一 ID,也可以指定名称或选项:
|
|
59
60
|
|
|
60
61
|
```typescript
|
|
61
|
-
|
|
62
|
-
type Position = { x: number; y: number };
|
|
63
|
-
type Velocity = { x: number; y: number };
|
|
62
|
+
import { component } from "@codehz/ecs";
|
|
64
63
|
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
|
|
64
|
+
// 无参自动分配 ID
|
|
65
|
+
const Position = component<Position>();
|
|
66
|
+
|
|
67
|
+
// 指定名称(序列化时可读)
|
|
68
|
+
const Velocity = component<Velocity>("Velocity");
|
|
69
|
+
|
|
70
|
+
// 带选项的组件(关系专用)
|
|
71
|
+
const ChildOf = component({ exclusive: true, name: "ChildOf" });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**`ComponentOptions` 选项:**
|
|
75
|
+
|
|
76
|
+
| 选项 | 类型 | 说明 |
|
|
77
|
+
| --------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
78
|
+
| `name` | `string` | 组件名称,用于序列化/调试 |
|
|
79
|
+
| `exclusive` | `boolean` | 仅关系组件:同一实体对同一基础组件最多只能有一个关系 |
|
|
80
|
+
| `cascadeDelete` | `boolean` | 仅实体关系:删除目标实体时,持有该关系的**整个实体**也会被删除。区别于默认行为(默认仅清理关系组件,实体保留)。支持传递级联。 |
|
|
81
|
+
| `dontFragment` | `boolean` | 仅关系组件:不同目标实体的关系存放在同一 Archetype,防止因目标不同而过度碎片化 |
|
|
82
|
+
| `merge` | `(prev, next) => T` | 在同一 sync 批次中对同一组件反复 `set()` 时的合并策略 |
|
|
83
|
+
|
|
84
|
+
### 生命周期钩子
|
|
85
|
+
|
|
86
|
+
`world.hook()` 使用组件数组注册多组件生命周期钩子:
|
|
68
87
|
|
|
69
|
-
|
|
88
|
+
```typescript
|
|
89
|
+
// 返回卸载函数
|
|
70
90
|
const unhook = world.hook([PositionId, VelocityId], {
|
|
71
91
|
on_init: (entityId, position, velocity) => {
|
|
72
|
-
//
|
|
73
|
-
console.log(`实体 ${entityId} 同时拥有 Position 和 Velocity 组件`);
|
|
92
|
+
// 钩子注册时,为每个已同时满足条件的实体调用
|
|
74
93
|
},
|
|
75
94
|
on_set: (entityId, position, velocity) => {
|
|
76
|
-
//
|
|
77
|
-
console.log(
|
|
78
|
-
`实体 ${entityId} 现在同时拥有 Position (${position.x}, ${position.y}) 和 Velocity (${velocity.x}, ${velocity.y})`,
|
|
79
|
-
);
|
|
95
|
+
// 当实体「进入」匹配集合时调用(添加/更新组件后)
|
|
80
96
|
},
|
|
81
97
|
on_remove: (entityId, position, velocity) => {
|
|
82
|
-
//
|
|
83
|
-
console.log(`实体 ${entityId} 失去了 Position 或 Velocity 组件`);
|
|
98
|
+
// 当实体「退出」匹配集合时调用(移除组件或删除实体后)
|
|
84
99
|
},
|
|
85
100
|
});
|
|
86
|
-
|
|
87
|
-
// 添加组件
|
|
88
|
-
const entity = world.new();
|
|
89
|
-
world.set(entity, PositionId, { x: 0, y: 0 });
|
|
90
|
-
world.set(entity, VelocityId, { x: 1, y: 0.5 });
|
|
91
|
-
world.sync(); // 钩子在这里被调用
|
|
92
|
-
|
|
93
|
-
// 不再需要时,调用卸载函数移除钩子
|
|
101
|
+
// 卸载钩子
|
|
94
102
|
unhook();
|
|
95
103
|
```
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
也支持回调简写形式:
|
|
98
106
|
|
|
99
107
|
```typescript
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
},
|
|
105
|
-
on_remove: (entityId, position) => {
|
|
106
|
-
console.log(`组件 Position 被从实体 ${entityId} 移除`);
|
|
107
|
-
},
|
|
108
|
+
const unhook = world.hook([PositionId, VelocityId], (type, entityId, position, velocity) => {
|
|
109
|
+
if (type === "init") console.log("初始化");
|
|
110
|
+
if (type === "set") console.log("设置");
|
|
111
|
+
if (type === "remove") console.log("移除");
|
|
108
112
|
});
|
|
109
113
|
```
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
可选组件与过滤器:
|
|
112
116
|
|
|
113
117
|
```typescript
|
|
114
|
-
//
|
|
115
|
-
|
|
118
|
+
// 可选组件:即使 Velocity 不存在也会触发钩子
|
|
119
|
+
world.hook([PositionId, { optional: VelocityId }], {
|
|
116
120
|
on_set: (entityId, position, velocity) => {
|
|
117
|
-
// 当实体拥有 Position 组件时调用,Velocity 组件可选
|
|
118
121
|
if (velocity !== undefined) {
|
|
119
|
-
console.log(
|
|
122
|
+
console.log("拥有速度和位置");
|
|
120
123
|
} else {
|
|
121
|
-
console.log(
|
|
124
|
+
console.log("仅拥有位置");
|
|
122
125
|
}
|
|
123
126
|
},
|
|
124
127
|
});
|
|
125
|
-
```
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
// 过滤器:排除带有指定负面组件的实体
|
|
130
|
+
const DisabledId = component<void>();
|
|
131
|
+
world.hook(
|
|
132
|
+
[PositionId, VelocityId],
|
|
133
|
+
{
|
|
134
|
+
on_set: (entityId, position, velocity) => console.log("进入匹配集合"),
|
|
135
|
+
on_remove: (entityId, position, velocity) => console.log("退出匹配集合"),
|
|
136
|
+
},
|
|
137
|
+
{ negativeComponentTypes: [DisabledId] },
|
|
138
|
+
);
|
|
139
|
+
```
|
|
128
140
|
|
|
129
|
-
|
|
141
|
+
### 关系组件
|
|
130
142
|
|
|
131
143
|
```typescript
|
|
132
144
|
import { World, component, relation } from "@codehz/ecs";
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
const ChildOf = component<void>({ exclusive: true });
|
|
147
|
+
const world = new World();
|
|
148
|
+
const child = world.new();
|
|
149
|
+
const parent1 = world.new();
|
|
150
|
+
const parent2 = world.new();
|
|
151
|
+
|
|
152
|
+
// 添加关系
|
|
153
|
+
world.set(child, relation(ChildOf, parent1));
|
|
154
|
+
world.sync();
|
|
136
155
|
|
|
137
|
-
//
|
|
138
|
-
|
|
156
|
+
// 独占关系:添加新关系时自动移除旧关系
|
|
157
|
+
world.set(child, relation(ChildOf, parent2));
|
|
158
|
+
world.sync();
|
|
159
|
+
console.log(world.has(child, relation(ChildOf, parent1))); // false
|
|
160
|
+
console.log(world.has(child, relation(ChildOf, parent2))); // true
|
|
161
|
+
```
|
|
139
162
|
|
|
140
|
-
|
|
141
|
-
const world = new World();
|
|
163
|
+
### 通配符关系钩子
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
```typescript
|
|
166
|
+
import { World, component, relation } from "@codehz/ecs";
|
|
167
|
+
const PositionId = component<Position>();
|
|
145
168
|
|
|
146
|
-
|
|
147
|
-
const
|
|
169
|
+
const world = new World();
|
|
170
|
+
const wildcardPos = relation(PositionId, "*");
|
|
148
171
|
|
|
149
|
-
//
|
|
150
|
-
|
|
172
|
+
// 监听所有该类型关系的变动
|
|
173
|
+
world.hook([wildcardPos], {
|
|
151
174
|
on_set: (entityId, relations) => {
|
|
152
|
-
console.log(`实体 ${entityId} 添加了 Position 关系`);
|
|
153
175
|
for (const [targetId, position] of relations) {
|
|
154
|
-
console.log(
|
|
176
|
+
console.log(`实体 ${entityId} -> 目标 ${targetId}:`, position);
|
|
155
177
|
}
|
|
156
178
|
},
|
|
157
179
|
on_remove: (entityId, relations) => {
|
|
158
|
-
console.log(`实体 ${entityId}
|
|
180
|
+
console.log(`实体 ${entityId} 移除了所有 Position 关系`);
|
|
159
181
|
},
|
|
160
182
|
});
|
|
161
|
-
|
|
162
|
-
// 创建实体间的关系
|
|
163
|
-
const entity2 = world.new();
|
|
164
|
-
const positionRelation = relation(PositionId, entity2);
|
|
165
|
-
world.set(entity, positionRelation, { x: 10, y: 20 });
|
|
166
|
-
world.sync(); // 通配符钩子会被触发
|
|
167
|
-
|
|
168
|
-
// 不再需要时移除钩子
|
|
169
|
-
unhook();
|
|
170
183
|
```
|
|
171
184
|
|
|
172
|
-
###
|
|
173
|
-
|
|
174
|
-
ECS 支持 Exclusive Relations,确保实体对于指定的组件类型最多只能有一个关系。当添加新的关系时,会自动移除之前的所有同类型关系:
|
|
185
|
+
### EntityBuilder 流式创建
|
|
175
186
|
|
|
176
187
|
```typescript
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
const entity = world
|
|
189
|
+
.spawn()
|
|
190
|
+
.with(Position, { x: 0, y: 0 })
|
|
191
|
+
.with(Marker) // void 组件无需传值
|
|
192
|
+
.withRelation(ChildOf, parentEntity)
|
|
193
|
+
.build();
|
|
194
|
+
world.sync(); // 统一应用
|
|
195
|
+
```
|
|
184
196
|
|
|
185
|
-
|
|
186
|
-
const child = world.new();
|
|
187
|
-
const parent1 = world.new();
|
|
188
|
-
const parent2 = world.new();
|
|
197
|
+
### 批量创建
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
world.
|
|
192
|
-
world.sync();
|
|
193
|
-
console.log(world.has(child, relation(ChildOf, parent1))); // true
|
|
194
|
-
|
|
195
|
-
// 添加第二个关系 - 会自动移除第一个
|
|
196
|
-
world.set(child, relation(ChildOf, parent2));
|
|
199
|
+
```typescript
|
|
200
|
+
const entities = world.spawnMany(100, (builder, index) => builder.with(Position, { x: index * 10, y: 0 }));
|
|
197
201
|
world.sync();
|
|
198
|
-
console.log(world.has(child, relation(ChildOf, parent1))); // false
|
|
199
|
-
console.log(world.has(child, relation(ChildOf, parent2))); // true
|
|
200
202
|
```
|
|
201
203
|
|
|
202
204
|
### 运行示例
|
|
203
205
|
|
|
204
|
-
```bash
|
|
205
|
-
bun run demo
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
或者直接运行:
|
|
209
|
-
|
|
210
206
|
```bash
|
|
211
207
|
bun run examples/simple/demo.ts
|
|
208
|
+
bun run examples/advanced-scheduling/demo.ts
|
|
212
209
|
```
|
|
213
210
|
|
|
214
211
|
## API 概述
|
|
215
212
|
|
|
216
213
|
### World
|
|
217
214
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
| 方法 | 说明 |
|
|
216
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
217
|
+
| `new<T>()` | 创建新实体,返回 `EntityId<T>` |
|
|
218
|
+
| `create<T>()` | `new()` 的语义别名 |
|
|
219
|
+
| `spawn()` | 返回 `EntityBuilder` 用于流式创建 |
|
|
220
|
+
| `spawnMany(count, configure)` | 批量创建多个实体 |
|
|
221
|
+
| `exists(entity)` | 检查实体是否存在 |
|
|
222
|
+
| `set(entity, componentId, data?)` | 添加/更新组件(缓冲,`sync()` 后生效)。对 `void` 组件可不传 data |
|
|
223
|
+
| `set(componentId, data)` | 单例组件简写:`world.set(GlobalConfig, { ... })` |
|
|
224
|
+
| `get(entity, componentId?)` | 获取组件数据。**若组件不存在会抛出异常**,请先用 `has()` 检查或使用 `getOptional()` |
|
|
225
|
+
| `getOptional(entity, componentId?)` | 安全获取组件,返回 `{ value: T } \| undefined` |
|
|
226
|
+
| `has(entity, componentId?)` | 检查组件是否存在 |
|
|
227
|
+
| `remove(entity, componentId?)` | 移除组件(缓冲),也有单例简写 |
|
|
228
|
+
| `delete(entity)` | 销毁实体及其所有组件(缓冲) |
|
|
229
|
+
| `query(componentIds)` | 快速查询(不缓存) |
|
|
230
|
+
| `query(componentIds, true)` | 快速查询并返回实体及组件数据 |
|
|
231
|
+
| `createQuery(componentIds, filter?)` | 创建可重用的缓存查询 |
|
|
232
|
+
| `releaseQuery(query)` | 释放查询(可选清理) |
|
|
233
|
+
| `hook(componentTypes, hook, filter?)` | 注册生命周期钩子,返回卸载函数 |
|
|
234
|
+
| `serialize()` | 序列化世界状态为快照对象 |
|
|
235
|
+
| `sync()` | 执行所有延迟命令 |
|
|
236
|
+
|
|
237
|
+
### Query
|
|
238
|
+
|
|
239
|
+
查询通过 `world.createQuery()` 创建,应**跨帧复用**以获得最佳性能。
|
|
240
|
+
|
|
241
|
+
| 方法 | 说明 |
|
|
242
|
+
| ----------------------------------- | ---------------------------------------- |
|
|
243
|
+
| `forEach(componentTypes, callback)` | 遍历匹配实体 |
|
|
244
|
+
| `getEntities()` | 获取所有匹配实体的 ID 列表 |
|
|
245
|
+
| `getEntitiesWithComponents(types)` | 获取实体及组件数据的对象数组 |
|
|
246
|
+
| `iterate(types)` | 返回生成器,用于 `for...of` 遍历 |
|
|
247
|
+
| `getComponentData(type)` | 获取所有匹配实体的单组件数据数组 |
|
|
248
|
+
| `dispose()` | 释放查询(引用计数减一,归零时完全释放) |
|
|
249
|
+
| `get disposed()` | 检查查询是否已释放 |
|
|
250
|
+
|
|
251
|
+
### QueryFilter
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
interface QueryFilter {
|
|
255
|
+
negativeComponentTypes?: EntityId<any>[]; // 排除的组件
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### EntityBuilder
|
|
260
|
+
|
|
261
|
+
| 方法 | 说明 |
|
|
262
|
+
| -------------------------------------------- | -------------------------------------------- |
|
|
263
|
+
| `with(componentId, ...args)` | 添加普通组件。`void` 类型不传值 |
|
|
264
|
+
| `withRelation(componentId, target, ...args)` | 添加关系组件。`void` 类型不传值 |
|
|
265
|
+
| `build()` | 创建实体并返回 `EntityId`(仍需要 `sync()`) |
|
|
266
|
+
|
|
267
|
+
### component()
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// 自动分配 ID
|
|
271
|
+
component<T>();
|
|
272
|
+
// 指定名称
|
|
273
|
+
component<T>("Name");
|
|
274
|
+
// 带选项
|
|
275
|
+
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, dontFragment?: boolean, merge?: (prev, next) => T });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### relation()
|
|
232
279
|
|
|
233
|
-
|
|
280
|
+
```typescript
|
|
281
|
+
// 创建关系 ID
|
|
282
|
+
relation(componentId, targetEntity);
|
|
283
|
+
// 通配符(查询所有目标)
|
|
284
|
+
relation(componentId, "*");
|
|
285
|
+
// 单例目标(关联到另一个组件)
|
|
286
|
+
relation(componentId, otherComponentId);
|
|
287
|
+
```
|
|
234
288
|
|
|
235
|
-
|
|
289
|
+
### 组件 / 实体 ID 规则
|
|
236
290
|
|
|
237
|
-
- `
|
|
238
|
-
-
|
|
291
|
+
- 组件 ID:`1` ~ `1023`
|
|
292
|
+
- 实体 ID:`1024+`
|
|
293
|
+
- 关系 ID:负数编码 `-(componentId * 2^42 + targetId)`
|
|
239
294
|
|
|
240
|
-
|
|
295
|
+
## 序列化(快照)
|
|
241
296
|
|
|
242
|
-
|
|
297
|
+
库提供对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件数据。
|
|
243
298
|
|
|
244
|
-
```
|
|
245
|
-
//
|
|
299
|
+
```typescript
|
|
300
|
+
// 创建快照(内存对象)
|
|
246
301
|
const snapshot = world.serialize();
|
|
247
302
|
|
|
248
303
|
// 在同一进程内直接恢复
|
|
249
304
|
const restored = new World(snapshot);
|
|
250
305
|
```
|
|
251
306
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
如果你需要把世界保存到文件或通过网络传输,需要自己实现组件值的编码/解码策略:
|
|
307
|
+
**设计要点:**
|
|
255
308
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
309
|
+
- `world.serialize()` 返回内存快照对象,**不会**对组件值执行 `JSON.stringify`,也不会尝试将组件值转换为可序列化格式。
|
|
310
|
+
- `new World(snapshot)` 是反序列化的唯一入口(没有 `World.deserialize()` 静态方法)。
|
|
311
|
+
- 快照包含实体、组件以及 `EntityIdManager` 分配器状态(保留下一次分配的 ID);**不会**自动恢复查询缓存或生命周期钩子。
|
|
259
312
|
|
|
260
|
-
|
|
313
|
+
**持久化示例(组件值为 JSON 友好时):**
|
|
261
314
|
|
|
262
|
-
```
|
|
315
|
+
```typescript
|
|
263
316
|
const snapshot = world.serialize();
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// 写入文件或发送到网络
|
|
317
|
+
const json = JSON.stringify(snapshot);
|
|
318
|
+
// 写入文件或发送到网络 ...
|
|
267
319
|
|
|
268
|
-
|
|
269
|
-
const parsed = JSON.parse(text);
|
|
320
|
+
const parsed = JSON.parse(json);
|
|
270
321
|
const restored = new World(parsed);
|
|
271
322
|
```
|
|
272
323
|
|
|
273
|
-
|
|
324
|
+
**自定义编码示例:**
|
|
274
325
|
|
|
275
|
-
```
|
|
326
|
+
```typescript
|
|
276
327
|
const snapshot = world.serialize();
|
|
277
|
-
|
|
278
|
-
// 将组件值编码为可持久化格式
|
|
279
328
|
const encoded = {
|
|
280
329
|
...snapshot,
|
|
281
330
|
entities: snapshot.entities.map((e) => ({
|
|
@@ -283,89 +332,29 @@ const encoded = {
|
|
|
283
332
|
components: e.components.map((c) => ({ type: c.type, value: myEncode(c.value) })),
|
|
284
333
|
})),
|
|
285
334
|
};
|
|
335
|
+
// 持久化 encoded ...
|
|
286
336
|
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
// 恢复时解码回原始组件值
|
|
290
|
-
const decoded = /* parse file and decode */ encoded;
|
|
291
|
-
const readySnapshot = {
|
|
337
|
+
// 恢复时反向解码
|
|
338
|
+
const decodedSnapshot = {
|
|
292
339
|
...decoded,
|
|
293
340
|
entities: decoded.entities.map((e) => ({
|
|
294
341
|
id: e.id,
|
|
295
342
|
components: e.components.map((c) => ({ type: c.type, value: myDecode(c.value) })),
|
|
296
343
|
})),
|
|
297
344
|
};
|
|
298
|
-
|
|
299
|
-
const restored = new World(readySnapshot);
|
|
345
|
+
const restored = new World(decodedSnapshot);
|
|
300
346
|
```
|
|
301
347
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
- **重要警告**:`get()` 方法只能获取实体已设置的组件。如果尝试获取不存在的组件,会抛出错误。由于 `undefined` 是组件的有效值,不能使用 `get()` 的返回值是否为 `undefined` 来判断组件是否存在。请在使用 `get()` 之前先用 `has()` 方法检查组件是否存在。
|
|
305
|
-
- 快照只包含实体、组件、以及 `EntityIdManager` 的分配器状态(用于保留下一次分配的 ID);并不会自动恢复查询缓存或生命周期钩子。恢复后应由应用负责重新注册钩子。
|
|
306
|
-
- 若需要跨版本兼容,建议在持久化格式中包含 `version` 字段,并在恢复时进行格式兼容性检查与迁移。
|
|
307
|
-
|
|
308
|
-
### Entity
|
|
309
|
-
|
|
310
|
-
- `component<T>(id)`: 分配类型安全的组件ID(上限:1022个)
|
|
311
|
-
|
|
312
|
-
### Query
|
|
313
|
-
|
|
314
|
-
- `forEach(componentIds, callback)`: 遍历匹配的实体,为每个实体调用回调函数
|
|
315
|
-
- `getEntities()`: 获取所有匹配实体的ID列表
|
|
316
|
-
- `getEntitiesWithComponents(componentIds)`: 获取实体及其组件数据的对象数组
|
|
317
|
-
- `iterate(componentIds)`: 返回一个生成器,用于遍历匹配的实体及其组件数据
|
|
318
|
-
- `getComponentData(componentType)`: 获取指定组件类型的所有匹配实体的数据数组
|
|
319
|
-
- `dispose()`: 释放查询资源,停止接收世界更新通知
|
|
320
|
-
|
|
321
|
-
### EntityBuilder
|
|
322
|
-
|
|
323
|
-
EntityBuilder 提供流式 API 用于便捷的实体创建:
|
|
324
|
-
|
|
325
|
-
- `with(componentId, value?)`: 添加组件到构建器(对于 `void` 类型组件,value 参数可省略)
|
|
326
|
-
- `withRelation(componentId, targetEntity, value?)`: 添加关系组件到构建器(对于 `void` 类型关系,value 参数可省略)
|
|
327
|
-
- `build()`: 创建实体并应用所有组件(需要手动调用 `world.sync()`)
|
|
328
|
-
|
|
329
|
-
### World
|
|
330
|
-
|
|
331
|
-
从 v0.4.0 开始,本库移除了内置的 `System` 和 `SystemScheduler` 功能。推荐使用 `@codehz/pipeline` 作为替代方案来组织游戏循环逻辑。
|
|
332
|
-
|
|
333
|
-
### 为什么移除 System?
|
|
334
|
-
|
|
335
|
-
- **简化库的维护**:System 调度器增加了代码复杂度,但其功能可以通过更通用的 pipeline 模式实现
|
|
336
|
-
- **更灵活的执行控制**:Pipeline 模式允许更细粒度的控制,支持异步操作和条件执行
|
|
337
|
-
- **更好的关注点分离**:ECS 库专注于实体和组件管理,系统调度由外部库处理
|
|
338
|
-
|
|
339
|
-
### 迁移示例
|
|
340
|
-
|
|
341
|
-
**旧代码(使用 System)**:
|
|
348
|
+
**重要:** `get()` 在组件不存在时会抛出异常。由于 `undefined` 是组件的有效值,不能用 `get()` 的返回值是否为 `undefined` 来判断组件是否存在。请使用 `has()` 或 `getOptional()`。
|
|
342
349
|
|
|
343
|
-
|
|
344
|
-
import { World, component } from "@codehz/ecs";
|
|
345
|
-
import type { System } from "@codehz/ecs";
|
|
346
|
-
|
|
347
|
-
class MovementSystem implements System<[deltaTime: number]> {
|
|
348
|
-
private query: Query;
|
|
350
|
+
## System / Pipeline 集成
|
|
349
351
|
|
|
350
|
-
|
|
351
|
-
this.query = world.createQuery([PositionId, VelocityId]);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
update(deltaTime: number): void {
|
|
355
|
-
this.query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
|
|
356
|
-
position.x += velocity.x * deltaTime;
|
|
357
|
-
position.y += velocity.y * deltaTime;
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
352
|
+
从 v0.4.0 开始,库移除了内置的 `System` 和 `SystemScheduler`。推荐使用 `@codehz/pipeline` 来组织游戏循环,**务必在最后一个 pass 调用 `world.sync()`**。
|
|
361
353
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
world.update(0.016); // 自动调用 sync()
|
|
354
|
+
```bash
|
|
355
|
+
bun add @codehz/pipeline
|
|
365
356
|
```
|
|
366
357
|
|
|
367
|
-
**新代码(使用 Pipeline)**:
|
|
368
|
-
|
|
369
358
|
```typescript
|
|
370
359
|
import { pipeline } from "@codehz/pipeline";
|
|
371
360
|
import { World, component } from "@codehz/ecs";
|
|
@@ -380,76 +369,66 @@ const gameLoop = pipeline<{ deltaTime: number }>()
|
|
|
380
369
|
position.y += velocity.y * env.deltaTime;
|
|
381
370
|
});
|
|
382
371
|
})
|
|
383
|
-
// 重要:world.sync() 必须作为最后一个 pass 调用,以还原之前 world.update() 的自动提交行为
|
|
384
372
|
.addPass(() => {
|
|
385
|
-
world.sync();
|
|
373
|
+
world.sync(); // 必须作为最后一个 pass
|
|
386
374
|
})
|
|
387
375
|
.build();
|
|
388
376
|
|
|
389
377
|
gameLoop({ deltaTime: 0.016 });
|
|
390
378
|
```
|
|
391
379
|
|
|
392
|
-
### 关键变化
|
|
393
|
-
|
|
394
|
-
1. **移除泛型参数**:`World` 不再需要 `UpdateParams` 泛型参数
|
|
395
|
-
2. **移除的方法**:`registerSystem()` 和 `update()` 方法已移除
|
|
396
|
-
3. **手动调用 sync()**:之前 `world.update()` 会自动调用 `sync()`,现在需要在 pipeline 末尾显式调用
|
|
397
|
-
4. **执行顺序**:Pass 的执行顺序由添加顺序决定,无需手动声明依赖关系
|
|
398
|
-
|
|
399
|
-
### 安装 Pipeline
|
|
400
|
-
|
|
401
|
-
```bash
|
|
402
|
-
bun add @codehz/pipeline
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
## 性能特点
|
|
406
|
-
|
|
407
|
-
- **Archetype 系统**:实体按组件组合分组,实现连续内存访问
|
|
408
|
-
- **缓存查询**:查询结果自动缓存,减少重复计算
|
|
409
|
-
- **命令缓冲区**:延迟执行组件添加/移除,提高批处理效率
|
|
410
|
-
- **类型安全**:编译时类型检查,无运行时开销
|
|
411
|
-
|
|
412
|
-
## 开发
|
|
413
|
-
|
|
414
|
-
### 运行测试
|
|
415
|
-
|
|
416
|
-
```bash
|
|
417
|
-
bun test
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
### 类型检查
|
|
421
|
-
|
|
422
|
-
```bash
|
|
423
|
-
bunx tsc --noEmit
|
|
424
|
-
```
|
|
425
|
-
|
|
426
380
|
## 项目结构
|
|
427
381
|
|
|
428
382
|
```
|
|
429
383
|
src/
|
|
430
|
-
├── index.ts
|
|
431
|
-
├──
|
|
432
|
-
├── world.ts
|
|
433
|
-
├── archetype.ts
|
|
434
|
-
├──
|
|
435
|
-
├──
|
|
436
|
-
├──
|
|
437
|
-
├──
|
|
438
|
-
├──
|
|
439
|
-
├──
|
|
440
|
-
├──
|
|
441
|
-
|
|
384
|
+
├── index.ts # 入口文件(统一导出)
|
|
385
|
+
├── core/ # 核心实现
|
|
386
|
+
│ ├── world.ts # 世界管理
|
|
387
|
+
│ ├── archetype.ts # Archetype 系统(高效组件存储)
|
|
388
|
+
│ ├── builder.ts # EntityBuilder 流式创建
|
|
389
|
+
│ ├── component-registry.ts # 组件注册表
|
|
390
|
+
│ ├── component-entity-store.ts # 单例组件存储
|
|
391
|
+
│ ├── component-type-utils.ts # 组件类型工具
|
|
392
|
+
│ ├── dont-fragment-store.ts # DontFragment 存储
|
|
393
|
+
│ ├── entity.ts # 实体/组件/关系类型导出(聚合)
|
|
394
|
+
│ ├── entity-types.ts # 实体 ID 类型定义与常量
|
|
395
|
+
│ ├── entity-relation.ts # 关系 ID 编码/解码
|
|
396
|
+
│ ├── entity-manager.ts # ID 分配器
|
|
397
|
+
│ ├── query-registry.ts # 查询注册表
|
|
398
|
+
│ ├── serialization.ts # 序列化 ID 编解码
|
|
399
|
+
│ ├── world-serialization.ts # 世界序列化/反序列化
|
|
400
|
+
│ ├── world-commands.ts # 世界命令
|
|
401
|
+
│ ├── world-hooks.ts # 钩子执行逻辑
|
|
402
|
+
│ ├── world-references.ts # 实体引用追踪
|
|
403
|
+
│ └── types.ts # 类型定义
|
|
404
|
+
├── query/ # 查询系统
|
|
405
|
+
│ ├── query.ts # Query 类
|
|
406
|
+
│ └── filter.ts # 查询过滤器
|
|
407
|
+
├── commands/ # 命令缓冲区
|
|
408
|
+
├── utils/ # 工具函数
|
|
409
|
+
├── testing/ # 测试工具
|
|
410
|
+
└── __tests__/ # 单元测试 & 性能测试
|
|
442
411
|
|
|
443
412
|
examples/
|
|
444
413
|
├── simple/
|
|
445
|
-
│ ├── demo.ts
|
|
446
|
-
│ └── README.md
|
|
414
|
+
│ ├── demo.ts # 基本示例
|
|
415
|
+
│ └── README.md # 示例说明
|
|
447
416
|
└── advanced-scheduling/
|
|
448
|
-
└── demo.ts
|
|
417
|
+
└── demo.ts # Pipeline 调度示例
|
|
449
418
|
|
|
450
419
|
scripts/
|
|
451
|
-
├── build.ts
|
|
452
|
-
└── release.ts
|
|
420
|
+
├── build.ts # 构建脚本
|
|
421
|
+
└── release.ts # 发布脚本
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## 开发
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
bun install
|
|
428
|
+
bun test # 运行测试
|
|
429
|
+
bunx tsc --noEmit # 类型检查
|
|
430
|
+
bun run examples/simple/demo.ts # 运行示例
|
|
431
|
+
bun run scripts/build.ts # 构建
|
|
453
432
|
```
|
|
454
433
|
|
|
455
434
|
## 许可证
|