@codehz/ecs 0.1.0 → 0.1.2
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 +74 -0
- package/entity.d.ts +16 -0
- package/index.js +86 -16
- package/package.json +1 -1
- package/world.d.ts +18 -1
package/README.md
CHANGED
|
@@ -180,6 +180,80 @@ bun run examples/simple/demo.ts
|
|
|
180
180
|
- `update(...params)`: 更新世界(参数取决于泛型配置)
|
|
181
181
|
- `sync()`: 应用命令缓冲区
|
|
182
182
|
|
|
183
|
+
### 序列化(快照)
|
|
184
|
+
|
|
185
|
+
库提供了对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件的数据。注意关键点:
|
|
186
|
+
|
|
187
|
+
- `World.serialize()` 返回一个内存中的快照对象(snapshot),快照会按引用保存组件的实际值;它不会对数据做 JSON.stringify 操作,也不会尝试把组件值转换为可序列化格式。
|
|
188
|
+
- `World.deserialize(snapshot)` 接受由 `World.serialize()` 生成的快照对象并重建世界状态。它期望一个内存对象(非 JSON 字符串)。
|
|
189
|
+
|
|
190
|
+
为什么采用这种设计?很多情况下组件值可能包含函数、类实例、循环引用或其他无法用 JSON 表示的值。库不对组件值强行进行序列化/字符串化,以避免数据丢失或不可信的自动转换。
|
|
191
|
+
|
|
192
|
+
示例:内存回环(component 值可为任意对象)
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// 获取快照(内存对象)
|
|
196
|
+
const snapshot = world.serialize();
|
|
197
|
+
|
|
198
|
+
// 在同一进程内直接恢复
|
|
199
|
+
const restored = World.deserialize(snapshot);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
持久化到磁盘或跨进程传输
|
|
203
|
+
|
|
204
|
+
如果你需要把世界保存到文件或通过网络传输,需要自己实现组件值的编码/解码策略:
|
|
205
|
+
|
|
206
|
+
1. 使用 `World.serialize()` 得到 snapshot。
|
|
207
|
+
2. 对 snapshot 中的组件值逐项进行可自定义的编码(例如将类实例转成纯数据、把函数替换为标识符,或使用自定义二进制编码)。
|
|
208
|
+
3. 将编码后的对象字符串化并持久化。恢复时执行相反的解码步骤,得到与 `World.serialize()` 兼容的快照对象,然后调用 `World.deserialize(decodedSnapshot)`。
|
|
209
|
+
|
|
210
|
+
简单示例:当组件值都是 JSON-友好时
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const snapshot = world.serialize();
|
|
214
|
+
// 如果组件值都可 JSON 化,可以直接 stringify
|
|
215
|
+
const text = JSON.stringify(snapshot);
|
|
216
|
+
// 写入文件或发送到网络
|
|
217
|
+
|
|
218
|
+
// 恢复:parse -> deserialize
|
|
219
|
+
const parsed = JSON.parse(text);
|
|
220
|
+
const restored = World.deserialize(parsed);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
示例:带自定义编码的持久化(伪代码)
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const snapshot = world.serialize();
|
|
227
|
+
|
|
228
|
+
// 将组件值编码为可持久化格式
|
|
229
|
+
const encoded = {
|
|
230
|
+
...snapshot,
|
|
231
|
+
entities: snapshot.entities.map((e) => ({
|
|
232
|
+
id: e.id,
|
|
233
|
+
components: e.components.map((c) => ({ type: c.type, value: myEncode(c.value) })),
|
|
234
|
+
})),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// 持久化 encoded(JSON.stringify / 二进制写入等)
|
|
238
|
+
|
|
239
|
+
// 恢复时解码回原始组件值
|
|
240
|
+
const decoded = /* parse file and decode */ encoded;
|
|
241
|
+
const readySnapshot = {
|
|
242
|
+
...decoded,
|
|
243
|
+
entities: decoded.entities.map((e) => ({
|
|
244
|
+
id: e.id,
|
|
245
|
+
components: e.components.map((c) => ({ type: c.type, value: myDecode(c.value) })),
|
|
246
|
+
})),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const restored = World.deserialize(readySnapshot);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
注意事项
|
|
253
|
+
|
|
254
|
+
- 快照只包含实体、组件、以及 `EntityIdManager` 的分配器状态(用于保留下一次分配的 ID);并不会自动恢复已注册的系统、查询缓存或生命周期钩子。恢复后应由应用负责重新注册系统与钩子。
|
|
255
|
+
- 若需要跨版本兼容,建议在持久化格式中包含 `version` 字段,并在恢复时进行格式兼容性检查与迁移。
|
|
256
|
+
|
|
183
257
|
### Entity
|
|
184
258
|
|
|
185
259
|
- `component<T>(id)`: 分配类型安全的组件ID(上限:1022个)
|
package/entity.d.ts
CHANGED
|
@@ -129,6 +129,22 @@ export declare class EntityIdManager {
|
|
|
129
129
|
* Get the next ID that would be allocated (for debugging)
|
|
130
130
|
*/
|
|
131
131
|
getNextId(): number;
|
|
132
|
+
/**
|
|
133
|
+
* Serialize internal state for persistence.
|
|
134
|
+
* Returns a plain object representing allocator state. Values may be non-JSON-serializable.
|
|
135
|
+
*/
|
|
136
|
+
serializeState(): {
|
|
137
|
+
nextId: number;
|
|
138
|
+
freelist: number[];
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Restore internal state from a previously-serialized object.
|
|
142
|
+
* Overwrites the current nextId and freelist.
|
|
143
|
+
*/
|
|
144
|
+
deserializeState(state: {
|
|
145
|
+
nextId: number;
|
|
146
|
+
freelist?: number[];
|
|
147
|
+
}): void;
|
|
132
148
|
}
|
|
133
149
|
/**
|
|
134
150
|
* Component ID Manager for automatic allocation
|
package/index.js
CHANGED
|
@@ -184,6 +184,16 @@ class EntityIdManager {
|
|
|
184
184
|
getNextId() {
|
|
185
185
|
return this.nextId;
|
|
186
186
|
}
|
|
187
|
+
serializeState() {
|
|
188
|
+
return { nextId: this.nextId, freelist: Array.from(this.freelist) };
|
|
189
|
+
}
|
|
190
|
+
deserializeState(state) {
|
|
191
|
+
if (typeof state.nextId !== "number") {
|
|
192
|
+
throw new Error("Invalid state for EntityIdManager.deserializeState");
|
|
193
|
+
}
|
|
194
|
+
this.nextId = state.nextId;
|
|
195
|
+
this.freelist = new Set(state.freelist || []);
|
|
196
|
+
}
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
class ComponentIdAllocator {
|
|
@@ -259,7 +269,7 @@ class Archetype {
|
|
|
259
269
|
this.entityToIndex.set(entityId, index);
|
|
260
270
|
for (const componentType of this.componentTypes) {
|
|
261
271
|
const data = componentData.get(componentType);
|
|
262
|
-
this.
|
|
272
|
+
this.getComponentData(componentType).push(data === undefined ? MISSING_COMPONENT : data);
|
|
263
273
|
}
|
|
264
274
|
}
|
|
265
275
|
removeEntity(entityId) {
|
|
@@ -271,7 +281,7 @@ class Archetype {
|
|
|
271
281
|
this.entityToIndex.delete(entityId);
|
|
272
282
|
const removedData = new Map;
|
|
273
283
|
for (const componentType of this.componentTypes) {
|
|
274
|
-
const dataArray = this.
|
|
284
|
+
const dataArray = this.getComponentData(componentType);
|
|
275
285
|
removedData.set(componentType, dataArray[index]);
|
|
276
286
|
dataArray.splice(index, 1);
|
|
277
287
|
}
|
|
@@ -295,7 +305,7 @@ class Archetype {
|
|
|
295
305
|
for (const relType of this.componentTypes) {
|
|
296
306
|
const relDetailed = getDetailedIdType(relType);
|
|
297
307
|
if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) {
|
|
298
|
-
const dataArray = this.
|
|
308
|
+
const dataArray = this.getComponentData(relType);
|
|
299
309
|
if (dataArray && dataArray[index] !== undefined) {
|
|
300
310
|
const data = dataArray[index];
|
|
301
311
|
relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
@@ -304,7 +314,7 @@ class Archetype {
|
|
|
304
314
|
}
|
|
305
315
|
return relations;
|
|
306
316
|
} else {
|
|
307
|
-
const data = this.
|
|
317
|
+
const data = this.getComponentData(componentType)[index];
|
|
308
318
|
return data === MISSING_COMPONENT ? undefined : data;
|
|
309
319
|
}
|
|
310
320
|
}
|
|
@@ -316,14 +326,18 @@ class Archetype {
|
|
|
316
326
|
if (index === undefined) {
|
|
317
327
|
throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
318
328
|
}
|
|
319
|
-
const dataArray = this.
|
|
329
|
+
const dataArray = this.getComponentData(componentType);
|
|
320
330
|
dataArray[index] = data;
|
|
321
331
|
}
|
|
322
332
|
getEntities() {
|
|
323
|
-
return
|
|
333
|
+
return this.entities;
|
|
324
334
|
}
|
|
325
335
|
getComponentData(componentType) {
|
|
326
|
-
|
|
336
|
+
const data = this.componentData.get(componentType);
|
|
337
|
+
if (!data) {
|
|
338
|
+
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
339
|
+
}
|
|
340
|
+
return data;
|
|
327
341
|
}
|
|
328
342
|
getEntitiesWithComponents(componentTypes) {
|
|
329
343
|
const result = [];
|
|
@@ -347,7 +361,7 @@ class Archetype {
|
|
|
347
361
|
});
|
|
348
362
|
return matchingRelations;
|
|
349
363
|
} else {
|
|
350
|
-
return this.
|
|
364
|
+
return this.getComponentData(compType);
|
|
351
365
|
}
|
|
352
366
|
});
|
|
353
367
|
});
|
|
@@ -359,12 +373,10 @@ class Archetype {
|
|
|
359
373
|
const matchingRelations = dataSource;
|
|
360
374
|
const relations = [];
|
|
361
375
|
for (const relType of matchingRelations) {
|
|
362
|
-
const dataArray = this.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
367
|
-
}
|
|
376
|
+
const dataArray = this.getComponentData(relType);
|
|
377
|
+
const data = dataArray[entityIndex];
|
|
378
|
+
const decodedRel = decodeRelationId(relType);
|
|
379
|
+
relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
368
380
|
}
|
|
369
381
|
return relations;
|
|
370
382
|
} else {
|
|
@@ -380,7 +392,7 @@ class Archetype {
|
|
|
380
392
|
for (let i = 0;i < this.entities.length; i++) {
|
|
381
393
|
const components = new Map;
|
|
382
394
|
for (const componentType of this.componentTypes) {
|
|
383
|
-
const data = this.
|
|
395
|
+
const data = this.getComponentData(componentType)[i];
|
|
384
396
|
components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
385
397
|
}
|
|
386
398
|
callback(this.entities[i], components);
|
|
@@ -658,7 +670,8 @@ class World {
|
|
|
658
670
|
lifecycleHooks = new Map;
|
|
659
671
|
entityReverseIndex = new Map;
|
|
660
672
|
exclusiveComponents = new Set;
|
|
661
|
-
constructor() {
|
|
673
|
+
constructor(entityIdManager) {
|
|
674
|
+
this.entityIdManager = entityIdManager || new EntityIdManager;
|
|
662
675
|
this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
663
676
|
}
|
|
664
677
|
getComponentTypesHash(componentTypes) {
|
|
@@ -1089,6 +1102,63 @@ class World {
|
|
|
1089
1102
|
}
|
|
1090
1103
|
}
|
|
1091
1104
|
}
|
|
1105
|
+
serialize() {
|
|
1106
|
+
const entities = [];
|
|
1107
|
+
for (const [entityId, archetype] of this.entityToArchetype.entries()) {
|
|
1108
|
+
const compEntries = [];
|
|
1109
|
+
for (const compType of archetype.componentTypes) {
|
|
1110
|
+
const value = archetype.get(entityId, compType);
|
|
1111
|
+
compEntries.push({ type: compType, value });
|
|
1112
|
+
}
|
|
1113
|
+
entities.push({ id: entityId, components: compEntries });
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
version: 1,
|
|
1117
|
+
entityManager: this.entityIdManager.serializeState(),
|
|
1118
|
+
exclusiveComponents: Array.from(this.exclusiveComponents),
|
|
1119
|
+
entities
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
static deserialize(obj) {
|
|
1123
|
+
if (!obj || typeof obj !== "object") {
|
|
1124
|
+
throw new Error("World.deserialize expects a snapshot object (not a JSON string)");
|
|
1125
|
+
}
|
|
1126
|
+
const entityManager = new EntityIdManager;
|
|
1127
|
+
if (obj && obj.entityManager) {
|
|
1128
|
+
entityManager.deserializeState(obj.entityManager);
|
|
1129
|
+
}
|
|
1130
|
+
const world = new World(entityManager);
|
|
1131
|
+
if (obj && Array.isArray(obj.exclusiveComponents)) {
|
|
1132
|
+
for (const id of obj.exclusiveComponents) {
|
|
1133
|
+
world.exclusiveComponents.add(id);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (obj && Array.isArray(obj.entities)) {
|
|
1137
|
+
for (const entry of obj.entities) {
|
|
1138
|
+
const entityId = entry.id;
|
|
1139
|
+
const componentsArray = entry.components || [];
|
|
1140
|
+
const componentMap = new Map;
|
|
1141
|
+
const componentTypes = [];
|
|
1142
|
+
for (const c of componentsArray) {
|
|
1143
|
+
componentMap.set(c.type, c.value);
|
|
1144
|
+
componentTypes.push(c.type);
|
|
1145
|
+
}
|
|
1146
|
+
const archetype = world.getOrCreateArchetype(componentTypes);
|
|
1147
|
+
archetype.addEntity(entityId, componentMap);
|
|
1148
|
+
world.entityToArchetype.set(entityId, archetype);
|
|
1149
|
+
for (const compType of componentTypes) {
|
|
1150
|
+
const detailedType = getDetailedIdType(compType);
|
|
1151
|
+
if (detailedType.type === "entity-relation") {
|
|
1152
|
+
const targetEntityId = detailedType.targetId;
|
|
1153
|
+
world.addComponentReference(entityId, compType, targetEntityId);
|
|
1154
|
+
} else if (detailedType.type === "entity") {
|
|
1155
|
+
world.addComponentReference(entityId, compType, compType);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return world;
|
|
1161
|
+
}
|
|
1092
1162
|
}
|
|
1093
1163
|
export {
|
|
1094
1164
|
relation,
|
package/package.json
CHANGED
package/world.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Archetype } from "./archetype";
|
|
|
2
2
|
import { ComponentChangeset } from "./changeset";
|
|
3
3
|
import { type Command } from "./command-buffer";
|
|
4
4
|
import type { EntityId, WildcardRelationId } from "./entity";
|
|
5
|
+
import { EntityIdManager } from "./entity";
|
|
5
6
|
import { Query } from "./query";
|
|
6
7
|
import { type QueryFilter } from "./query-filter";
|
|
7
8
|
import type { System } from "./system";
|
|
@@ -35,7 +36,7 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
35
36
|
* For exclusive relations, an entity can have at most one relation per base component
|
|
36
37
|
*/
|
|
37
38
|
private exclusiveComponents;
|
|
38
|
-
constructor();
|
|
39
|
+
constructor(entityIdManager?: EntityIdManager);
|
|
39
40
|
/**
|
|
40
41
|
* Generate a hash key for component types array
|
|
41
42
|
*/
|
|
@@ -172,4 +173,20 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
172
173
|
* Execute component lifecycle hooks for added and removed components
|
|
173
174
|
*/
|
|
174
175
|
private executeComponentLifecycleHooks;
|
|
176
|
+
/**
|
|
177
|
+
* Convert the world into a plain JSON-serializable object.
|
|
178
|
+
* Note: component values must be JSON-serializable by the caller.
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* Convert the world into a plain snapshot object.
|
|
182
|
+
* This returns an in-memory structure and does not perform JSON stringification.
|
|
183
|
+
* Component values are stored as-is (they may be non-JSON-serializable).
|
|
184
|
+
*/
|
|
185
|
+
serialize(): any;
|
|
186
|
+
/**
|
|
187
|
+
* Deserialize a world from a previously-created snapshot object.
|
|
188
|
+
* The snapshot must have been produced by `world.serialize()` and may contain
|
|
189
|
+
* non-JSON values (they will be copied by reference).
|
|
190
|
+
*/
|
|
191
|
+
static deserialize<T extends any[] = []>(obj: any): World<T>;
|
|
175
192
|
}
|