@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.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @codehz/ecs
2
2
 
3
- 一个高性能的Entity Component System (ECS) 库,使用 TypeScript 和 Bun 运行时构建。
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>(1);
34
- const VelocityId = component<Velocity>(2);
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
- const deltaTime = 1.0 / 60.0; // 假设60FPS
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
- ECS 支持监听组件的生命周期事件。可以监听单个组件或多个组件同时存在于实体时的事件。
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
- // 定义组件ID
66
- const PositionId = component<Position>();
67
- const VelocityId = component<Velocity>();
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
- // 当钩子注册时,为已同时拥有 Position 和 Velocity 组件的实体调用
73
- console.log(`实体 ${entityId} 同时拥有 Position 和 Velocity 组件`);
92
+ // 钩子注册时,为每个已同时满足条件的实体调用
74
93
  },
75
94
  on_set: (entityId, position, velocity) => {
76
- // 当实体同时拥有 Position 和 Velocity 组件时调用
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
- // 当实体失去 Position 或 Velocity 组件之一时调用(如果之前同时拥有两者)
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
- `hook()` 也支持只监听单个组件:
105
+ 也支持回调简写形式:
98
106
 
99
107
  ```typescript
100
- // 监听单个组件
101
- const unhook = world.hook([PositionId], {
102
- on_set: (entityId, position) => {
103
- console.log(`组件 Position 被添加到实体 ${entityId}`);
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
- const unhook = world.hook([PositionId, { optional: VelocityId }], {
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(`实体 ${entityId} 拥有 Position 和 Velocity 组件`);
122
+ console.log("拥有速度和位置");
120
123
  } else {
121
- console.log(`实体 ${entityId} 仅拥有 Position 组件`);
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
- ECS 支持通配符关系钩子,可以监听特定组件的所有关系变化:
141
+ ### 关系组件
130
142
 
131
143
  ```typescript
132
144
  import { World, component, relation } from "@codehz/ecs";
133
145
 
134
- // 定义组件类型
135
- type Position = { x: number; y: number };
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
- // 定义组件ID
138
- const PositionId = component<Position>(1);
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
- const entity = world.new();
165
+ ```typescript
166
+ import { World, component, relation } from "@codehz/ecs";
167
+ const PositionId = component<Position>();
145
168
 
146
- // 创建通配符关系ID,用于监听所有 Position 相关的关系
147
- const wildcardPositionRelation = relation(PositionId, "*");
169
+ const world = new World();
170
+ const wildcardPos = relation(PositionId, "*");
148
171
 
149
- // 注册通配符关系钩子,返回卸载函数
150
- const unhook = world.hook([wildcardPositionRelation], {
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(` -> 目标实体 ${targetId}:`, position);
176
+ console.log(`实体 ${entityId} -> 目标 ${targetId}:`, position);
155
177
  }
156
178
  },
157
179
  on_remove: (entityId, relations) => {
158
- console.log(`实体 ${entityId} 移除了 Position 关系`);
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
- ### Exclusive Relations
173
-
174
- ECS 支持 Exclusive Relations,确保实体对于指定的组件类型最多只能有一个关系。当添加新的关系时,会自动移除之前的所有同类型关系:
185
+ ### EntityBuilder 流式创建
175
186
 
176
187
  ```typescript
177
- import { World, component, relation } from "@codehz/ecs";
178
-
179
- // 定义组件ID,设置为独占关系
180
- const ChildOf = component({ exclusive: true }); // 空组件,用于关系
181
-
182
- // 创建世界
183
- const world = new World();
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.set(child, relation(ChildOf, parent1));
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
- - `new()`: 创建新实体
219
- - `spawn()`: 创建 EntityBuilder 用于流式实体创建
220
- - `spawnMany(count, configure)`: 批量创建多个实体
221
- - `exists(entity)`: 检查实体是否存在
222
- - `set(entity, componentId, data)`: 向实体添加组件
223
- - `get(entity, componentId)`: 获取实体的组件数据(注意:只能获取已设置的组件,使用前请先用 `has()` 检查组件是否存在)
224
- - `has(entity, componentId)`: 检查实体是否拥有指定组件
225
- - `remove(entity, componentId)`: 从实体移除组件
226
- - `delete(entity)`: 销毁实体及其所有组件
227
- - `query(componentIds)`: 快速查询具有指定组件的实体
228
- - `createQuery(componentIds)`: 创建可重用的查询对象
229
- - `hook(componentIds, hook)`: 注册生命周期钩子,返回卸载函数
230
- - `serialize()`: 序列化世界状态为快照对象
231
- - `sync()`: 执行所有延迟命令
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
- - `world.serialize()` 返回一个内存中的快照对象(snapshot),快照会按引用保存组件的实际值;它不会对数据做 JSON.stringify 操作,也不会尝试把组件值转换为可序列化格式。
238
- - `new World(snapshot)` 通过构造函数接受由 `world.serialize()` 生成的快照对象并重建世界状态。它期望一个内存对象(非 JSON 字符串)。
291
+ - 组件 ID:`1` ~ `1023`
292
+ - 实体 ID:`1024+`
293
+ - 关系 ID:负数编码 `-(componentId * 2^42 + targetId)`
239
294
 
240
- 为什么采用这种设计?很多情况下组件值可能包含函数、类实例、循环引用或其他无法用 JSON 表示的值。库不对组件值强行进行序列化/字符串化,以避免数据丢失或不可信的自动转换。
295
+ ## 序列化(快照)
241
296
 
242
- 示例:内存回环(component 值可为任意对象)
297
+ 库提供对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件数据。
243
298
 
244
- ```ts
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
- 1. 使用 `World.serialize()` 得到 snapshot。
257
- 2. snapshot 中的组件值逐项进行可自定义的编码(例如将类实例转成纯数据、把函数替换为标识符,或使用自定义二进制编码)。
258
- 3. 将编码后的对象字符串化并持久化。恢复时执行相反的解码步骤,得到与 `World.serialize()` 兼容的快照对象,然后调用 `World.deserialize(decodedSnapshot)`。
309
+ - `world.serialize()` 返回内存快照对象,**不会**对组件值执行 `JSON.stringify`,也不会尝试将组件值转换为可序列化格式。
310
+ - `new World(snapshot)` 是反序列化的唯一入口(没有 `World.deserialize()` 静态方法)。
311
+ - 快照包含实体、组件以及 `EntityIdManager` 分配器状态(保留下一次分配的 ID);**不会**自动恢复查询缓存或生命周期钩子。
259
312
 
260
- 简单示例:当组件值都是 JSON-友好时
313
+ **持久化示例(组件值为 JSON 友好时):**
261
314
 
262
- ```ts
315
+ ```typescript
263
316
  const snapshot = world.serialize();
264
- // 如果组件值都可 JSON 化,可以直接 stringify
265
- const text = JSON.stringify(snapshot);
266
- // 写入文件或发送到网络
317
+ const json = JSON.stringify(snapshot);
318
+ // 写入文件或发送到网络 ...
267
319
 
268
- // 恢复:parse -> deserialize
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
- ```ts
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
- // 持久化 encoded(JSON.stringify / 二进制写入等)
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
- ```typescript
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
- constructor(world: World<[deltaTime: number]>) {
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
- const world = new World<[deltaTime: number]>();
363
- world.registerSystem(new MovementSystem(world));
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
- ├── entity.ts # 实体和组件管理
432
- ├── world.ts # 世界管理
433
- ├── archetype.ts # Archetype 系统(高效组件存储)
434
- ├── query.ts # 查询系统
435
- ├── query-filter.ts # 查询过滤器
436
- ├── command-buffer.ts # 命令缓冲区
437
- ├── types.ts # 类型定义
438
- ├── utils.ts # 工具函数
439
- ├── *.test.ts # 单元测试
440
- ├── query.example.ts # 查询示例
441
- └── *.perf.test.ts # 性能测试
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 # Pipeline 调度示例
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
  ## 许可证