@codehz/ecs 0.1.1 → 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 +69 -1
- 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 {
|
|
@@ -660,7 +670,8 @@ class World {
|
|
|
660
670
|
lifecycleHooks = new Map;
|
|
661
671
|
entityReverseIndex = new Map;
|
|
662
672
|
exclusiveComponents = new Set;
|
|
663
|
-
constructor() {
|
|
673
|
+
constructor(entityIdManager) {
|
|
674
|
+
this.entityIdManager = entityIdManager || new EntityIdManager;
|
|
664
675
|
this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
665
676
|
}
|
|
666
677
|
getComponentTypesHash(componentTypes) {
|
|
@@ -1091,6 +1102,63 @@ class World {
|
|
|
1091
1102
|
}
|
|
1092
1103
|
}
|
|
1093
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
|
+
}
|
|
1094
1162
|
}
|
|
1095
1163
|
export {
|
|
1096
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
|
}
|