@codehz/ecs 0.6.11 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +446 -0
- package/README.md +250 -288
- package/builder.d.mts +660 -195
- 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 +1532 -1215
- 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,283 +24,309 @@ 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
|
-
|
|
127
|
-
多组件 `hook()` 还支持第三个可选参数 `filter`(与 `createQuery()` 的过滤语义一致),可用于排除带有某些负面组件的实体:
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
// 过滤器:排除带有指定负面组件的实体
|
|
130
130
|
const DisabledId = component<void>();
|
|
131
|
-
|
|
132
|
-
const unhook = world.hook(
|
|
131
|
+
world.hook(
|
|
133
132
|
[PositionId, VelocityId],
|
|
134
133
|
{
|
|
135
|
-
on_set: (entityId, position, velocity) =>
|
|
136
|
-
|
|
137
|
-
console.log("active", entityId, position, velocity);
|
|
138
|
-
},
|
|
139
|
-
on_remove: (entityId, position, velocity) => {
|
|
140
|
-
// 实体退出匹配集合时触发(包括新增 Disabled 后退出)
|
|
141
|
-
console.log("inactive", entityId, position, velocity);
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
negativeComponentTypes: [DisabledId],
|
|
134
|
+
on_set: (entityId, position, velocity) => console.log("进入匹配集合"),
|
|
135
|
+
on_remove: (entityId, position, velocity) => console.log("退出匹配集合"),
|
|
146
136
|
},
|
|
137
|
+
{ negativeComponentTypes: [DisabledId] },
|
|
147
138
|
);
|
|
148
139
|
```
|
|
149
140
|
|
|
150
|
-
###
|
|
151
|
-
|
|
152
|
-
ECS 支持通配符关系钩子,可以监听特定组件的所有关系变化:
|
|
141
|
+
### 关系组件
|
|
153
142
|
|
|
154
143
|
```typescript
|
|
155
144
|
import { World, component, relation } from "@codehz/ecs";
|
|
156
145
|
|
|
157
|
-
|
|
158
|
-
|
|
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();
|
|
159
151
|
|
|
160
|
-
//
|
|
161
|
-
|
|
152
|
+
// 添加关系
|
|
153
|
+
world.set(child, relation(ChildOf, parent1));
|
|
154
|
+
world.sync();
|
|
162
155
|
|
|
163
|
-
//
|
|
164
|
-
|
|
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
|
+
```
|
|
165
162
|
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
### 通配符关系钩子
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { World, component, relation } from "@codehz/ecs";
|
|
167
|
+
const PositionId = component<Position>();
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
const
|
|
169
|
+
const world = new World();
|
|
170
|
+
const wildcardPos = relation(PositionId, "*");
|
|
171
171
|
|
|
172
|
-
//
|
|
173
|
-
|
|
172
|
+
// 监听所有该类型关系的变动
|
|
173
|
+
world.hook([wildcardPos], {
|
|
174
174
|
on_set: (entityId, relations) => {
|
|
175
|
-
console.log(`实体 ${entityId} 添加了 Position 关系`);
|
|
176
175
|
for (const [targetId, position] of relations) {
|
|
177
|
-
console.log(
|
|
176
|
+
console.log(`实体 ${entityId} -> 目标 ${targetId}:`, position);
|
|
178
177
|
}
|
|
179
178
|
},
|
|
180
179
|
on_remove: (entityId, relations) => {
|
|
181
|
-
console.log(`实体 ${entityId}
|
|
180
|
+
console.log(`实体 ${entityId} 移除了所有 Position 关系`);
|
|
182
181
|
},
|
|
183
182
|
});
|
|
184
|
-
|
|
185
|
-
// 创建实体间的关系
|
|
186
|
-
const entity2 = world.new();
|
|
187
|
-
const positionRelation = relation(PositionId, entity2);
|
|
188
|
-
world.set(entity, positionRelation, { x: 10, y: 20 });
|
|
189
|
-
world.sync(); // 通配符钩子会被触发
|
|
190
|
-
|
|
191
|
-
// 不再需要时移除钩子
|
|
192
|
-
unhook();
|
|
193
183
|
```
|
|
194
184
|
|
|
195
|
-
###
|
|
196
|
-
|
|
197
|
-
ECS 支持 Exclusive Relations,确保实体对于指定的组件类型最多只能有一个关系。当添加新的关系时,会自动移除之前的所有同类型关系:
|
|
185
|
+
### EntityBuilder 流式创建
|
|
198
186
|
|
|
199
187
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// 创建实体
|
|
209
|
-
const child = world.new();
|
|
210
|
-
const parent1 = world.new();
|
|
211
|
-
const parent2 = world.new();
|
|
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
|
+
```
|
|
212
196
|
|
|
213
|
-
|
|
214
|
-
world.set(child, relation(ChildOf, parent1));
|
|
215
|
-
world.sync();
|
|
216
|
-
console.log(world.has(child, relation(ChildOf, parent1))); // true
|
|
197
|
+
### 批量创建
|
|
217
198
|
|
|
218
|
-
|
|
219
|
-
world.
|
|
199
|
+
```typescript
|
|
200
|
+
const entities = world.spawnMany(100, (builder, index) => builder.with(Position, { x: index * 10, y: 0 }));
|
|
220
201
|
world.sync();
|
|
221
|
-
console.log(world.has(child, relation(ChildOf, parent1))); // false
|
|
222
|
-
console.log(world.has(child, relation(ChildOf, parent2))); // true
|
|
223
202
|
```
|
|
224
203
|
|
|
225
204
|
### 运行示例
|
|
226
205
|
|
|
227
206
|
```bash
|
|
228
|
-
bun run
|
|
207
|
+
bun run examples/simple.ts
|
|
208
|
+
bun run examples/advanced-scheduling.ts
|
|
209
|
+
bun run examples/parent-child-hierarchy.ts
|
|
210
|
+
bun run examples/inventory-system-relations.ts
|
|
229
211
|
```
|
|
230
212
|
|
|
231
|
-
|
|
213
|
+
## API 概述
|
|
232
214
|
|
|
233
|
-
|
|
234
|
-
|
|
215
|
+
### World
|
|
216
|
+
|
|
217
|
+
| 方法 | 说明 |
|
|
218
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
219
|
+
| `new<T>()` | 创建新实体,返回 `EntityId<T>` |
|
|
220
|
+
| `create<T>()` | `new()` 的语义别名 |
|
|
221
|
+
| `spawn()` | 返回 `EntityBuilder` 用于流式创建 |
|
|
222
|
+
| `spawnMany(count, configure)` | 批量创建多个实体 |
|
|
223
|
+
| `exists(entity)` | 检查实体是否存在 |
|
|
224
|
+
| `set(entity, componentId, data?)` | 添加/更新组件(缓冲,`sync()` 后生效)。对 `void` 组件可不传 data |
|
|
225
|
+
| `set(componentId, data)` | 单例组件简写:`world.set(GlobalConfig, { ... })` |
|
|
226
|
+
| `get(entity, componentId?)` | 获取组件数据。**若组件不存在会抛出异常**,请先用 `has()` 检查或使用 `getOptional()` |
|
|
227
|
+
| `getOptional(entity, componentId?)` | 安全获取组件,返回 `{ value: T } \| undefined` |
|
|
228
|
+
| `has(entity, componentId?)` | 检查组件是否存在 |
|
|
229
|
+
| `remove(entity, componentId?)` | 移除组件(缓冲),也有单例简写 |
|
|
230
|
+
| `delete(entity)` | 销毁实体及其所有组件(缓冲) |
|
|
231
|
+
| `query(componentIds)` | 快速查询(不缓存) |
|
|
232
|
+
| `query(componentIds, true)` | 快速查询并返回实体及组件数据 |
|
|
233
|
+
| `createQuery(componentIds, filter?)` | 创建可重用的缓存查询 |
|
|
234
|
+
| `releaseQuery(query)` | 释放查询(可选清理) |
|
|
235
|
+
| `hook(componentTypes, hook, filter?)` | 注册生命周期钩子,返回卸载函数 |
|
|
236
|
+
| `serialize()` | 序列化世界状态为快照对象 |
|
|
237
|
+
| `sync()` | 执行所有延迟命令 |
|
|
238
|
+
|
|
239
|
+
### Query
|
|
240
|
+
|
|
241
|
+
查询通过 `world.createQuery()` 创建,应**跨帧复用**以获得最佳性能。
|
|
242
|
+
|
|
243
|
+
| 方法 | 说明 |
|
|
244
|
+
| ----------------------------------- | ---------------------------------------- |
|
|
245
|
+
| `forEach(componentTypes, callback)` | 遍历匹配实体 |
|
|
246
|
+
| `getEntities()` | 获取所有匹配实体的 ID 列表 |
|
|
247
|
+
| `getEntitiesWithComponents(types)` | 获取实体及组件数据的对象数组 |
|
|
248
|
+
| `iterate(types)` | 返回生成器,用于 `for...of` 遍历 |
|
|
249
|
+
| `getComponentData(type)` | 获取所有匹配实体的单组件数据数组 |
|
|
250
|
+
| `dispose()` | 释放查询(引用计数减一,归零时完全释放) |
|
|
251
|
+
| `get disposed()` | 检查查询是否已释放 |
|
|
252
|
+
|
|
253
|
+
### QueryFilter
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
interface QueryFilter {
|
|
257
|
+
negativeComponentTypes?: EntityId<any>[]; // 排除的组件
|
|
258
|
+
}
|
|
235
259
|
```
|
|
236
260
|
|
|
237
|
-
|
|
261
|
+
### EntityBuilder
|
|
238
262
|
|
|
239
|
-
|
|
263
|
+
| 方法 | 说明 |
|
|
264
|
+
| -------------------------------------------- | -------------------------------------------- |
|
|
265
|
+
| `with(componentId, ...args)` | 添加普通组件。`void` 类型不传值 |
|
|
266
|
+
| `withRelation(componentId, target, ...args)` | 添加关系组件。`void` 类型不传值 |
|
|
267
|
+
| `build()` | 创建实体并返回 `EntityId`(仍需要 `sync()`) |
|
|
240
268
|
|
|
241
|
-
|
|
242
|
-
- `spawn()`: 创建 EntityBuilder 用于流式实体创建
|
|
243
|
-
- `spawnMany(count, configure)`: 批量创建多个实体
|
|
244
|
-
- `exists(entity)`: 检查实体是否存在
|
|
245
|
-
- `set(entity, componentId, data)`: 向实体添加组件
|
|
246
|
-
- `get(entity, componentId)`: 获取实体的组件数据(注意:只能获取已设置的组件,使用前请先用 `has()` 检查组件是否存在)
|
|
247
|
-
- `has(entity, componentId)`: 检查实体是否拥有指定组件
|
|
248
|
-
- `remove(entity, componentId)`: 从实体移除组件
|
|
249
|
-
- `delete(entity)`: 销毁实体及其所有组件
|
|
250
|
-
- `query(componentIds)`: 快速查询具有指定组件的实体
|
|
251
|
-
- `createQuery(componentIds)`: 创建可重用的查询对象
|
|
252
|
-
- `hook(componentIds, hook, filter?)`: 注册生命周期钩子,返回卸载函数(数组形式支持可选 filter)
|
|
253
|
-
- `serialize()`: 序列化世界状态为快照对象
|
|
254
|
-
- `sync()`: 执行所有延迟命令
|
|
269
|
+
### component()
|
|
255
270
|
|
|
256
|
-
|
|
271
|
+
```typescript
|
|
272
|
+
// 自动分配 ID
|
|
273
|
+
component<T>();
|
|
274
|
+
// 指定名称
|
|
275
|
+
component<T>("Name");
|
|
276
|
+
// 带选项
|
|
277
|
+
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, dontFragment?: boolean, merge?: (prev, next) => T });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### relation()
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// 创建关系 ID
|
|
284
|
+
relation(componentId, targetEntity);
|
|
285
|
+
// 通配符(查询所有目标)
|
|
286
|
+
relation(componentId, "*");
|
|
287
|
+
// 单例目标(关联到另一个组件)
|
|
288
|
+
relation(componentId, otherComponentId);
|
|
289
|
+
```
|
|
257
290
|
|
|
258
|
-
|
|
291
|
+
### 组件 / 实体 ID 规则
|
|
259
292
|
|
|
260
|
-
- `
|
|
261
|
-
-
|
|
293
|
+
- 组件 ID:`1` ~ `1023`
|
|
294
|
+
- 实体 ID:`1024+`
|
|
295
|
+
- 关系 ID:负数编码 `-(componentId * 2^42 + targetId)`
|
|
262
296
|
|
|
263
|
-
|
|
297
|
+
## 序列化(快照)
|
|
264
298
|
|
|
265
|
-
|
|
299
|
+
库提供对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件数据。
|
|
266
300
|
|
|
267
|
-
```
|
|
268
|
-
//
|
|
301
|
+
```typescript
|
|
302
|
+
// 创建快照(内存对象)
|
|
269
303
|
const snapshot = world.serialize();
|
|
270
304
|
|
|
271
305
|
// 在同一进程内直接恢复
|
|
272
306
|
const restored = new World(snapshot);
|
|
273
307
|
```
|
|
274
308
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
如果你需要把世界保存到文件或通过网络传输,需要自己实现组件值的编码/解码策略:
|
|
309
|
+
**设计要点:**
|
|
278
310
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
311
|
+
- `world.serialize()` 返回内存快照对象,**不会**对组件值执行 `JSON.stringify`,也不会尝试将组件值转换为可序列化格式。
|
|
312
|
+
- `new World(snapshot)` 是反序列化的唯一入口(没有 `World.deserialize()` 静态方法)。
|
|
313
|
+
- 快照包含实体、组件以及 `EntityIdManager` 分配器状态(保留下一次分配的 ID);**不会**自动恢复查询缓存或生命周期钩子。
|
|
282
314
|
|
|
283
|
-
|
|
315
|
+
**持久化示例(组件值为 JSON 友好时):**
|
|
284
316
|
|
|
285
|
-
```
|
|
317
|
+
```typescript
|
|
286
318
|
const snapshot = world.serialize();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// 写入文件或发送到网络
|
|
319
|
+
const json = JSON.stringify(snapshot);
|
|
320
|
+
// 写入文件或发送到网络 ...
|
|
290
321
|
|
|
291
|
-
|
|
292
|
-
const parsed = JSON.parse(text);
|
|
322
|
+
const parsed = JSON.parse(json);
|
|
293
323
|
const restored = new World(parsed);
|
|
294
324
|
```
|
|
295
325
|
|
|
296
|
-
|
|
326
|
+
**自定义编码示例:**
|
|
297
327
|
|
|
298
|
-
```
|
|
328
|
+
```typescript
|
|
299
329
|
const snapshot = world.serialize();
|
|
300
|
-
|
|
301
|
-
// 将组件值编码为可持久化格式
|
|
302
330
|
const encoded = {
|
|
303
331
|
...snapshot,
|
|
304
332
|
entities: snapshot.entities.map((e) => ({
|
|
@@ -306,89 +334,29 @@ const encoded = {
|
|
|
306
334
|
components: e.components.map((c) => ({ type: c.type, value: myEncode(c.value) })),
|
|
307
335
|
})),
|
|
308
336
|
};
|
|
337
|
+
// 持久化 encoded ...
|
|
309
338
|
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
// 恢复时解码回原始组件值
|
|
313
|
-
const decoded = /* parse file and decode */ encoded;
|
|
314
|
-
const readySnapshot = {
|
|
339
|
+
// 恢复时反向解码
|
|
340
|
+
const decodedSnapshot = {
|
|
315
341
|
...decoded,
|
|
316
342
|
entities: decoded.entities.map((e) => ({
|
|
317
343
|
id: e.id,
|
|
318
344
|
components: e.components.map((c) => ({ type: c.type, value: myDecode(c.value) })),
|
|
319
345
|
})),
|
|
320
346
|
};
|
|
321
|
-
|
|
322
|
-
const restored = new World(readySnapshot);
|
|
347
|
+
const restored = new World(decodedSnapshot);
|
|
323
348
|
```
|
|
324
349
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
- **重要警告**:`get()` 方法只能获取实体已设置的组件。如果尝试获取不存在的组件,会抛出错误。由于 `undefined` 是组件的有效值,不能使用 `get()` 的返回值是否为 `undefined` 来判断组件是否存在。请在使用 `get()` 之前先用 `has()` 方法检查组件是否存在。
|
|
328
|
-
- 快照只包含实体、组件、以及 `EntityIdManager` 的分配器状态(用于保留下一次分配的 ID);并不会自动恢复查询缓存或生命周期钩子。恢复后应由应用负责重新注册钩子。
|
|
329
|
-
- 若需要跨版本兼容,建议在持久化格式中包含 `version` 字段,并在恢复时进行格式兼容性检查与迁移。
|
|
330
|
-
|
|
331
|
-
### Entity
|
|
332
|
-
|
|
333
|
-
- `component<T>(id)`: 分配类型安全的组件ID(上限:1022个)
|
|
334
|
-
|
|
335
|
-
### Query
|
|
336
|
-
|
|
337
|
-
- `forEach(componentIds, callback)`: 遍历匹配的实体,为每个实体调用回调函数
|
|
338
|
-
- `getEntities()`: 获取所有匹配实体的ID列表
|
|
339
|
-
- `getEntitiesWithComponents(componentIds)`: 获取实体及其组件数据的对象数组
|
|
340
|
-
- `iterate(componentIds)`: 返回一个生成器,用于遍历匹配的实体及其组件数据
|
|
341
|
-
- `getComponentData(componentType)`: 获取指定组件类型的所有匹配实体的数据数组
|
|
342
|
-
- `dispose()`: 释放查询资源,停止接收世界更新通知
|
|
343
|
-
|
|
344
|
-
### EntityBuilder
|
|
345
|
-
|
|
346
|
-
EntityBuilder 提供流式 API 用于便捷的实体创建:
|
|
347
|
-
|
|
348
|
-
- `with(componentId, value?)`: 添加组件到构建器(对于 `void` 类型组件,value 参数可省略)
|
|
349
|
-
- `withRelation(componentId, targetEntity, value?)`: 添加关系组件到构建器(对于 `void` 类型关系,value 参数可省略)
|
|
350
|
-
- `build()`: 创建实体并应用所有组件(需要手动调用 `world.sync()`)
|
|
351
|
-
|
|
352
|
-
### World
|
|
350
|
+
**重要:** `get()` 在组件不存在时会抛出异常。由于 `undefined` 是组件的有效值,不能用 `get()` 的返回值是否为 `undefined` 来判断组件是否存在。请使用 `has()` 或 `getOptional()`。
|
|
353
351
|
|
|
354
|
-
|
|
352
|
+
## System / Pipeline 集成
|
|
355
353
|
|
|
356
|
-
|
|
354
|
+
从 v0.4.0 开始,库移除了内置的 `System` 和 `SystemScheduler`。推荐使用 `@codehz/pipeline` 来组织游戏循环,**务必在最后一个 pass 调用 `world.sync()`**。
|
|
357
355
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
- **更好的关注点分离**:ECS 库专注于实体和组件管理,系统调度由外部库处理
|
|
361
|
-
|
|
362
|
-
### 迁移示例
|
|
363
|
-
|
|
364
|
-
**旧代码(使用 System)**:
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
import { World, component } from "@codehz/ecs";
|
|
368
|
-
import type { System } from "@codehz/ecs";
|
|
369
|
-
|
|
370
|
-
class MovementSystem implements System<[deltaTime: number]> {
|
|
371
|
-
private query: Query;
|
|
372
|
-
|
|
373
|
-
constructor(world: World<[deltaTime: number]>) {
|
|
374
|
-
this.query = world.createQuery([PositionId, VelocityId]);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
update(deltaTime: number): void {
|
|
378
|
-
this.query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
|
|
379
|
-
position.x += velocity.x * deltaTime;
|
|
380
|
-
position.y += velocity.y * deltaTime;
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const world = new World<[deltaTime: number]>();
|
|
386
|
-
world.registerSystem(new MovementSystem(world));
|
|
387
|
-
world.update(0.016); // 自动调用 sync()
|
|
356
|
+
```bash
|
|
357
|
+
bun add @codehz/pipeline
|
|
388
358
|
```
|
|
389
359
|
|
|
390
|
-
**新代码(使用 Pipeline)**:
|
|
391
|
-
|
|
392
360
|
```typescript
|
|
393
361
|
import { pipeline } from "@codehz/pipeline";
|
|
394
362
|
import { World, component } from "@codehz/ecs";
|
|
@@ -403,76 +371,70 @@ const gameLoop = pipeline<{ deltaTime: number }>()
|
|
|
403
371
|
position.y += velocity.y * env.deltaTime;
|
|
404
372
|
});
|
|
405
373
|
})
|
|
406
|
-
// 重要:world.sync() 必须作为最后一个 pass 调用,以还原之前 world.update() 的自动提交行为
|
|
407
374
|
.addPass(() => {
|
|
408
|
-
world.sync();
|
|
375
|
+
world.sync(); // 必须作为最后一个 pass
|
|
409
376
|
})
|
|
410
377
|
.build();
|
|
411
378
|
|
|
412
379
|
gameLoop({ deltaTime: 0.016 });
|
|
413
380
|
```
|
|
414
381
|
|
|
415
|
-
### 关键变化
|
|
416
|
-
|
|
417
|
-
1. **移除泛型参数**:`World` 不再需要 `UpdateParams` 泛型参数
|
|
418
|
-
2. **移除的方法**:`registerSystem()` 和 `update()` 方法已移除
|
|
419
|
-
3. **手动调用 sync()**:之前 `world.update()` 会自动调用 `sync()`,现在需要在 pipeline 末尾显式调用
|
|
420
|
-
4. **执行顺序**:Pass 的执行顺序由添加顺序决定,无需手动声明依赖关系
|
|
421
|
-
|
|
422
|
-
### 安装 Pipeline
|
|
423
|
-
|
|
424
|
-
```bash
|
|
425
|
-
bun add @codehz/pipeline
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
## 性能特点
|
|
429
|
-
|
|
430
|
-
- **Archetype 系统**:实体按组件组合分组,实现连续内存访问
|
|
431
|
-
- **缓存查询**:查询结果自动缓存,减少重复计算
|
|
432
|
-
- **命令缓冲区**:延迟执行组件添加/移除,提高批处理效率
|
|
433
|
-
- **类型安全**:编译时类型检查,无运行时开销
|
|
434
|
-
|
|
435
|
-
## 开发
|
|
436
|
-
|
|
437
|
-
### 运行测试
|
|
438
|
-
|
|
439
|
-
```bash
|
|
440
|
-
bun test
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### 类型检查
|
|
444
|
-
|
|
445
|
-
```bash
|
|
446
|
-
bunx tsc --noEmit
|
|
447
|
-
```
|
|
448
|
-
|
|
449
382
|
## 项目结构
|
|
450
383
|
|
|
451
384
|
```
|
|
452
385
|
src/
|
|
453
|
-
├── index.ts
|
|
454
|
-
├──
|
|
455
|
-
├── world.ts
|
|
456
|
-
├── archetype.ts
|
|
457
|
-
├──
|
|
458
|
-
├──
|
|
459
|
-
├──
|
|
460
|
-
├──
|
|
461
|
-
├──
|
|
462
|
-
├──
|
|
463
|
-
├──
|
|
464
|
-
|
|
386
|
+
├── index.ts # 入口文件(统一导出)
|
|
387
|
+
├── core/ # 核心实现
|
|
388
|
+
│ ├── world.ts # 世界管理
|
|
389
|
+
│ ├── archetype.ts # Archetype 系统(高效组件存储)
|
|
390
|
+
│ ├── builder.ts # EntityBuilder 流式创建
|
|
391
|
+
│ ├── component-registry.ts # 组件注册表
|
|
392
|
+
│ ├── component-entity-store.ts # 单例组件存储
|
|
393
|
+
│ ├── component-type-utils.ts # 组件类型工具
|
|
394
|
+
│ ├── dont-fragment-store.ts # DontFragment 存储
|
|
395
|
+
│ ├── entity.ts # 实体/组件/关系类型导出(聚合)
|
|
396
|
+
│ ├── entity-types.ts # 实体 ID 类型定义与常量
|
|
397
|
+
│ ├── entity-relation.ts # 关系 ID 编码/解码
|
|
398
|
+
│ ├── entity-manager.ts # ID 分配器
|
|
399
|
+
│ ├── query-registry.ts # 查询注册表
|
|
400
|
+
│ ├── serialization.ts # 序列化 ID 编解码
|
|
401
|
+
│ ├── world-serialization.ts # 世界序列化/反序列化
|
|
402
|
+
│ ├── world-commands.ts # 世界命令
|
|
403
|
+
│ ├── world-hooks.ts # 钩子执行逻辑
|
|
404
|
+
│ ├── world-references.ts # 实体引用追踪
|
|
405
|
+
│ └── types.ts # 类型定义
|
|
406
|
+
├── query/ # 查询系统
|
|
407
|
+
│ ├── query.ts # Query 类
|
|
408
|
+
│ └── filter.ts # 查询过滤器
|
|
409
|
+
├── commands/ # 命令缓冲区
|
|
410
|
+
├── utils/ # 工具函数
|
|
411
|
+
├── testing/ # 测试工具
|
|
412
|
+
└── __tests__/ # 单元测试 & 性能测试
|
|
465
413
|
|
|
466
414
|
examples/
|
|
467
|
-
├──
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
415
|
+
├── advanced-scheduling.ts # Pipeline 调度示例
|
|
416
|
+
├── collision-detection.ts # 碰撞检测示例
|
|
417
|
+
├── parent-child-hierarchy.ts # 父子层级与 Transform 传播示例
|
|
418
|
+
├── serialization.ts # 序列化示例
|
|
419
|
+
├── simple.ts # 基本示例
|
|
420
|
+
├── spatial-grid.ts # 空间网格示例
|
|
421
|
+
├── state-machine.ts # 状态机示例
|
|
422
|
+
└── tag-filtering.ts # 标签过滤示例
|
|
472
423
|
|
|
473
424
|
scripts/
|
|
474
|
-
├── build.ts
|
|
475
|
-
└── release.ts
|
|
425
|
+
├── build.ts # 构建脚本
|
|
426
|
+
└── release.ts # 发布脚本
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## 开发
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
bun install
|
|
433
|
+
bun test # 运行测试
|
|
434
|
+
bunx tsc --noEmit # 类型检查
|
|
435
|
+
bun run examples/simple.ts # 运行示例
|
|
436
|
+
bun run examples/parent-child-hierarchy.ts
|
|
437
|
+
bun run scripts/build.ts # 构建
|
|
476
438
|
```
|
|
477
439
|
|
|
478
440
|
## 许可证
|