diff --git a/docs/guide/serialization.md b/docs/guide/serialization.md index b72e62ad..4b8c7781 100644 --- a/docs/guide/serialization.md +++ b/docs/guide/serialization.md @@ -251,13 +251,25 @@ console.log('更新组件:', stats.updatedComponents); #### 4. 序列化增量数据 ```typescript -// 转换为JSON字符串 -const json = IncrementalSerializer.serializeIncremental(incremental); +// JSON格式(默认) +const jsonData = IncrementalSerializer.serializeIncremental(incremental, { + format: 'json' +}); -// 发送到服务器或保存 -socket.send(json); -// 或 -localStorage.setItem('changes', json); +// 二进制格式(更小的体积,更高性能) +const binaryData = IncrementalSerializer.serializeIncremental(incremental, { + format: 'binary' +}); + +// 美化JSON输出(便于调试) +const prettyJson = IncrementalSerializer.serializeIncremental(incremental, { + format: 'json', + pretty: true +}); + +// 发送或保存 +socket.send(binaryData); // 网络传输使用二进制 +localStorage.setItem('changes', jsonData); // 本地存储可用JSON ``` #### 5. 应用增量变更 @@ -266,11 +278,16 @@ localStorage.setItem('changes', json); // 在另一个场景应用变更 const otherScene = new Scene(); -// 从JSON字符串应用 -otherScene.applyIncremental(json); - -// 或直接应用增量对象 +// 直接应用增量对象 otherScene.applyIncremental(incremental); + +// 从JSON字符串应用 +const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' }); +otherScene.applyIncremental(jsonData); + +// 从二进制Buffer应用 +const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' }); +otherScene.applyIncremental(binaryData); ``` ### 增量快照管理 @@ -322,6 +339,16 @@ interface IncrementalSerializationOptions { // 是否压缩快照(使用JSON序列化) // 默认false compressSnapshot?: boolean; + + // 序列化格式 + // 'json': JSON格式(可读性好,方便调试) + // 'binary': MessagePack二进制格式(体积小,性能高) + // 默认 'json' + format?: 'json' | 'binary'; + + // 是否美化JSON输出(仅在format='json'时有效) + // 默认false + pretty?: boolean; } // 使用选项 @@ -513,17 +540,21 @@ class NetworkSync { // 只在有变更时发送 if (stats.totalChanges > 0) { - const json = IncrementalSerializer.serializeIncremental(incremental); - this.socket.send(json); + // 使用二进制格式减少网络传输量 + const binaryData = IncrementalSerializer.serializeIncremental(incremental, { + format: 'binary' + }); + this.socket.send(binaryData); // 更新基准 this.scene.updateIncrementalSnapshot(); } } - private receiveIncremental(data: string): void { - const incremental = IncrementalSerializer.deserializeIncremental(data); - this.scene.applyIncremental(incremental); + private receiveIncremental(data: ArrayBuffer): void { + // 直接应用二进制数据 + const buffer = Buffer.from(data); + this.scene.applyIncremental(buffer); } } ``` @@ -750,23 +781,27 @@ class LargeDataComponent extends Component { ### 全量序列化API -- `scene.serialize(options?): string | Buffer` - 序列化场景 -- `scene.deserialize(data, options?)` - 反序列化场景 -- `SceneSerializer.validate(data)` - 验证序列化数据 -- `SceneSerializer.getInfo(data)` - 获取序列化数据信息 +- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景 +- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景 +- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器 +- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器 ### 增量序列化API -- `scene.createIncrementalSnapshot(options?)` - 创建基础快照 -- `scene.serializeIncremental(options?)` - 获取增量变更 -- `scene.applyIncremental(incremental)` - 应用增量变更 -- `scene.updateIncrementalSnapshot(options?)` - 更新快照基准 -- `scene.clearIncrementalSnapshot()` - 清除快照 -- `scene.hasIncrementalSnapshot()` - 检查是否有快照 -- `IncrementalSerializer.getIncrementalStats(incremental)` - 获取统计信息 +- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照 +- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更 +- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Buffer) +- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准 +- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照 +- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照 +- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器 +- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口 +- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项 +- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型 ### 版本迁移API +- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器 - `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移 - `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移 - `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移 diff --git a/examples/core-demos/src/demos/IncrementalSerializationDemo.ts b/examples/core-demos/src/demos/IncrementalSerializationDemo.ts index a9078a7d..0b3c3331 100644 --- a/examples/core-demos/src/demos/IncrementalSerializationDemo.ts +++ b/examples/core-demos/src/demos/IncrementalSerializationDemo.ts @@ -166,11 +166,17 @@ export class IncrementalSerializationDemo extends DemoBase { const incremental = this.scene.serializeIncremental(); const stats = IncrementalSerializer.getIncrementalStats(incremental); + // 计算JSON和二进制格式的大小 + const jsonSize = IncrementalSerializer.getIncrementalSize(incremental, 'json'); + const binarySize = IncrementalSerializer.getIncrementalSize(incremental, 'binary'); + this.incrementalHistory.push({ label, incremental, stats, - timestamp: Date.now() + timestamp: Date.now(), + jsonSize, + binarySize }); this.scene.updateIncrementalSnapshot(); @@ -226,8 +232,16 @@ export class IncrementalSerializationDemo extends DemoBase {
0
-
最后快照大小
-
0B
+
JSON大小
+
0B
+
+
+
二进制大小
+
0B
+
+
+
压缩率
+
0%
总变更数
@@ -358,8 +372,12 @@ export class IncrementalSerializationDemo extends DemoBase {
实体: +${item.stats.addedEntities} -${item.stats.removedEntities} ~${item.stats.updatedEntities} | - 组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents} | - 总变更: ${item.stats.totalChanges} + 组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents} +
+
+ JSON: ${this.formatBytes(item.jsonSize)} | + Binary: ${this.formatBytes(item.binarySize)} | + 节省: ${((1 - item.binarySize / item.jsonSize) * 100).toFixed(1)}%
`; @@ -381,11 +399,19 @@ export class IncrementalSerializationDemo extends DemoBase { const item = this.incrementalHistory[index]; const detailsPanel = document.getElementById('snapshotDetails')!; + const compressionRatio = ((1 - item.binarySize / item.jsonSize) * 100).toFixed(1); + const details = { 版本: item.incremental.version, 基础版本: item.incremental.baseVersion, 时间戳: new Date(item.incremental.timestamp).toLocaleString(), 场景名称: item.incremental.sceneName, + 格式对比: { + JSON大小: this.formatBytes(item.jsonSize), + 二进制大小: this.formatBytes(item.binarySize), + 压缩率: `${compressionRatio}%`, + 节省字节: this.formatBytes(item.jsonSize - item.binarySize) + }, 统计: item.stats, 实体变更: item.incremental.entityChanges.map((c: any) => ({ 操作: c.operation, @@ -413,8 +439,15 @@ export class IncrementalSerializationDemo extends DemoBase { if (this.incrementalHistory.length > 0) { const lastItem = this.incrementalHistory[this.incrementalHistory.length - 1]; - const size = IncrementalSerializer.getIncrementalSize(lastItem.incremental); - document.getElementById('snapshotSize')!.textContent = this.formatBytes(size); + + document.getElementById('jsonSize')!.textContent = this.formatBytes(lastItem.jsonSize); + document.getElementById('binarySize')!.textContent = this.formatBytes(lastItem.binarySize); + + const compressionRatio = ((1 - lastItem.binarySize / lastItem.jsonSize) * 100).toFixed(1); + const ratioElement = document.getElementById('compressionRatio')!; + ratioElement.textContent = `${compressionRatio}%`; + ratioElement.style.color = parseFloat(compressionRatio) > 30 ? '#4ecdc4' : '#ffe66d'; + document.getElementById('totalChanges')!.textContent = lastItem.stats.totalChanges.toString(); } } diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index ae0356cc..40865517 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -618,24 +618,28 @@ export class Scene implements IScene { /** * 应用增量变更到场景 * - * @param incremental 增量快照数据(JSON字符串或对象) + * @param incremental 增量快照数据(IncrementalSnapshot对象、JSON字符串或二进制Buffer) * @param componentRegistry 组件类型注册表(可选,默认使用全局注册表) * * @example * ```typescript - * // 应用增量变更 + * // 应用增量变更对象 * scene.applyIncremental(incrementalSnapshot); * - * // 或从JSON字符串应用 - * const incremental = IncrementalSerializer.deserializeIncremental(jsonString); - * scene.applyIncremental(incremental); + * // 从JSON字符串应用 + * const jsonData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'json' }); + * scene.applyIncremental(jsonData); + * + * // 从二进制Buffer应用 + * const binaryData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'binary' }); + * scene.applyIncremental(binaryData); * ``` */ public applyIncremental( - incremental: IncrementalSnapshot | string, + incremental: IncrementalSnapshot | string | Buffer, componentRegistry?: Map ): void { - const snapshot = typeof incremental === 'string' + const snapshot = (typeof incremental === 'string' || Buffer.isBuffer(incremental)) ? IncrementalSerializer.deserializeIncremental(incremental) : incremental; diff --git a/packages/core/src/ECS/Serialization/IncrementalSerializer.ts b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts index 0e5c964f..95012d5e 100644 --- a/packages/core/src/ECS/Serialization/IncrementalSerializer.ts +++ b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts @@ -11,6 +11,7 @@ import { Component } from '../Component'; import { ComponentSerializer, SerializedComponent } from './ComponentSerializer'; import { SerializedEntity } from './EntitySerializer'; import { ComponentType } from '../Core/ComponentStorage'; +import * as msgpack from 'msgpack-lite'; /** * 变更操作类型 @@ -117,6 +118,11 @@ interface SceneSnapshot { sceneData: Map; // 使用JSON字符串存储场景数据 } +/** + * 增量序列化格式 + */ +export type IncrementalSerializationFormat = 'json' | 'binary'; + /** * 增量序列化选项 */ @@ -138,6 +144,20 @@ export interface IncrementalSerializationOptions { * 默认false,设为true可减少内存占用但增加CPU开销 */ compressSnapshot?: boolean; + + /** + * 序列化格式 + * - 'json': JSON格式(可读性好,方便调试) + * - 'binary': MessagePack二进制格式(体积小,性能高) + * 默认 'json' + */ + format?: IncrementalSerializationFormat; + + /** + * 是否美化JSON输出(仅在format='json'时有效) + * 默认false + */ + pretty?: boolean; } /** @@ -585,40 +605,93 @@ export class IncrementalSerializer { } /** - * 序列化增量快照为JSON + * 序列化增量快照 * * @param incremental 增量快照 - * @param pretty 是否美化输出 - * @returns JSON字符串 + * @param options 序列化选项 + * @returns 序列化后的数据(JSON字符串或二进制Buffer) + * + * @example + * ```typescript + * // JSON格式(默认) + * const jsonData = IncrementalSerializer.serializeIncremental(snapshot); + * + * // 二进制格式 + * const binaryData = IncrementalSerializer.serializeIncremental(snapshot, { + * format: 'binary' + * }); + * + * // 美化JSON + * const prettyJson = IncrementalSerializer.serializeIncremental(snapshot, { + * format: 'json', + * pretty: true + * }); + * ``` */ public static serializeIncremental( incremental: IncrementalSnapshot, - pretty: boolean = false - ): string { - return pretty - ? JSON.stringify(incremental, null, 2) - : JSON.stringify(incremental); + options?: { format?: IncrementalSerializationFormat; pretty?: boolean } + ): string | Buffer { + const opts = { + format: 'json' as IncrementalSerializationFormat, + pretty: false, + ...options + }; + + if (opts.format === 'binary') { + return msgpack.encode(incremental); + } else { + return opts.pretty + ? JSON.stringify(incremental, null, 2) + : JSON.stringify(incremental); + } } /** - * 从JSON反序列化增量快照 + * 反序列化增量快照 * - * @param json JSON字符串 + * @param data 序列化的数据(JSON字符串或二进制Buffer) * @returns 增量快照 + * + * @example + * ```typescript + * // 从JSON反序列化 + * const snapshot = IncrementalSerializer.deserializeIncremental(jsonString); + * + * // 从二进制反序列化 + * const snapshot = IncrementalSerializer.deserializeIncremental(buffer); + * ``` */ - public static deserializeIncremental(json: string): IncrementalSnapshot { - return JSON.parse(json); + public static deserializeIncremental(data: string | Buffer): IncrementalSnapshot { + if (typeof data === 'string') { + // JSON格式 + return JSON.parse(data); + } else { + // 二进制格式(MessagePack) + return msgpack.decode(data); + } } /** * 计算增量快照的大小(字节) * * @param incremental 增量快照 + * @param format 序列化格式,默认为 'json' * @returns 字节数 */ - public static getIncrementalSize(incremental: IncrementalSnapshot): number { - const json = this.serializeIncremental(incremental); - return new Blob([json]).size; + public static getIncrementalSize( + incremental: IncrementalSnapshot, + format: IncrementalSerializationFormat = 'json' + ): number { + const data = this.serializeIncremental(incremental, { format }); + + if (typeof data === 'string') { + // JSON格式:计算UTF-8编码后的字节数 + return Buffer.byteLength(data, 'utf8'); + } else { + // 二进制格式:直接返回Buffer长度 + return data.length; + } } /** diff --git a/packages/core/src/ECS/Serialization/index.ts b/packages/core/src/ECS/Serialization/index.ts index fff7789f..154c9df1 100644 --- a/packages/core/src/ECS/Serialization/index.ts +++ b/packages/core/src/ECS/Serialization/index.ts @@ -55,6 +55,7 @@ export { IncrementalSerializer, ChangeOperation } from './IncrementalSerializer' export type { IncrementalSnapshot, IncrementalSerializationOptions, + IncrementalSerializationFormat, EntityChange, ComponentChange, SceneDataChange diff --git a/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts b/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts index e2f52851..53ae0fdf 100644 --- a/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts +++ b/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts @@ -436,20 +436,42 @@ describe('Incremental Serialization System', () => { }); describe('Incremental Serialization', () => { - it('应该序列化和反序列化增量快照', () => { + it('应该序列化和反序列化增量快照(JSON格式)', () => { scene.createIncrementalSnapshot(); const entity = scene.createEntity('Entity'); entity.addComponent(new PositionComponent(50, 100)); const incremental = scene.serializeIncremental(); - const json = IncrementalSerializer.serializeIncremental(incremental); + const json = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' }); expect(typeof json).toBe('string'); const deserialized = IncrementalSerializer.deserializeIncremental(json); expect(deserialized.version).toBe(incremental.version); expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length); + expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length); + }); + + it('应该序列化和反序列化增量快照(二进制格式)', () => { + scene.createIncrementalSnapshot(); + + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(50, 100)); + entity.tag = 42; + scene.sceneData.set('weather', 'sunny'); + + const incremental = scene.serializeIncremental(); + const binary = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' }); + + expect(Buffer.isBuffer(binary)).toBe(true); + + const deserialized = IncrementalSerializer.deserializeIncremental(binary); + expect(deserialized.version).toBe(incremental.version); + expect(deserialized.sceneName).toBe(incremental.sceneName); + expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length); + expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length); + expect(deserialized.sceneDataChanges.length).toBe(incremental.sceneDataChanges.length); }); it('应该美化JSON输出', () => { @@ -458,11 +480,158 @@ describe('Incremental Serialization System', () => { entity.addComponent(new PositionComponent(10, 20)); const incremental = scene.serializeIncremental(); - const prettyJson = IncrementalSerializer.serializeIncremental(incremental, true); + const prettyJson = IncrementalSerializer.serializeIncremental(incremental, { format: 'json', pretty: true }); + expect(typeof prettyJson).toBe('string'); expect(prettyJson).toContain('\n'); expect(prettyJson).toContain(' '); }); + + it('二进制格式应该比JSON格式更小', () => { + const entities = []; + for (let i = 0; i < 50; i++) { + const entity = scene.createEntity(`Entity_${i}`); + entity.addComponent(new PositionComponent(i * 10, i * 20)); + entity.addComponent(new VelocityComponent()); + entities.push(entity); + } + + scene.createIncrementalSnapshot(); + + for (const entity of entities) { + const pos = entity.getComponent(PositionComponent)!; + pos.x += 100; + pos.y += 200; + } + + const incremental = scene.serializeIncremental(); + + const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' }); + const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' }); + + const jsonSize = Buffer.byteLength(jsonData as string); + const binarySize = (binaryData as Buffer).length; + + expect(binarySize).toBeLessThan(jsonSize); + }); + + it('二进制和JSON格式应该包含相同的数据', () => { + scene.createIncrementalSnapshot(); + + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + entity1.addComponent(new VelocityComponent()); + entity1.tag = 99; + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new HealthComponent()); + + scene.sceneData.set('level', 5); + scene.sceneData.set('score', 1000); + + const incremental = scene.serializeIncremental(); + + const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' }); + const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' }); + + const fromJson = IncrementalSerializer.deserializeIncremental(jsonData); + const fromBinary = IncrementalSerializer.deserializeIncremental(binaryData); + + expect(fromJson.version).toBe(fromBinary.version); + expect(fromJson.timestamp).toBe(fromBinary.timestamp); + expect(fromJson.sceneName).toBe(fromBinary.sceneName); + expect(fromJson.entityChanges.length).toBe(fromBinary.entityChanges.length); + expect(fromJson.componentChanges.length).toBe(fromBinary.componentChanges.length); + expect(fromJson.sceneDataChanges.length).toBe(fromBinary.sceneDataChanges.length); + + expect(fromJson.entityChanges[0].entityName).toBe(fromBinary.entityChanges[0].entityName); + expect(fromJson.entityChanges[0].entityData?.tag).toBe(fromBinary.entityChanges[0].entityData?.tag); + }); + + it('应该正确应用二进制格式反序列化的增量快照', () => { + const scene1 = new Scene({ name: 'Scene1' }); + scene1.createIncrementalSnapshot(); + + const entity = scene1.createEntity('TestEntity'); + entity.addComponent(new PositionComponent(100, 200)); + entity.tag = 77; + + const incremental = scene1.serializeIncremental(); + const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' }); + + const deserializedIncremental = IncrementalSerializer.deserializeIncremental(binaryData); + + const scene2 = new Scene({ name: 'Scene2' }); + scene2.applyIncremental(deserializedIncremental); + + expect(scene2.entities.count).toBe(1); + const restoredEntity = scene2.findEntity('TestEntity'); + expect(restoredEntity).not.toBeNull(); + expect(restoredEntity!.tag).toBe(77); + expect(restoredEntity!.hasComponent(PositionComponent)).toBe(true); + + const pos = restoredEntity!.getComponent(PositionComponent)!; + expect(pos.x).toBe(100); + expect(pos.y).toBe(200); + + scene1.end(); + scene2.end(); + }); + + it('Scene.applyIncremental应该直接支持二进制Buffer', () => { + const scene1 = new Scene({ name: 'Scene1' }); + scene1.createIncrementalSnapshot(); + + const entity1 = scene1.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + entity1.addComponent(new VelocityComponent()); + + const entity2 = scene1.createEntity('Entity2'); + entity2.addComponent(new HealthComponent()); + + const incremental = scene1.serializeIncremental(); + const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' }); + + const scene2 = new Scene({ name: 'Scene2' }); + scene2.applyIncremental(binaryData); + + expect(scene2.entities.count).toBe(2); + + const e1 = scene2.findEntity('Entity1'); + expect(e1).not.toBeNull(); + expect(e1!.hasComponent(PositionComponent)).toBe(true); + expect(e1!.hasComponent(VelocityComponent)).toBe(true); + + const e2 = scene2.findEntity('Entity2'); + expect(e2).not.toBeNull(); + expect(e2!.hasComponent(HealthComponent)).toBe(true); + + scene1.end(); + scene2.end(); + }); + + it('Scene.applyIncremental应该直接支持JSON字符串', () => { + const scene1 = new Scene({ name: 'Scene1' }); + scene1.createIncrementalSnapshot(); + + const entity = scene1.createEntity('TestEntity'); + entity.addComponent(new PositionComponent(50, 100)); + entity.tag = 99; + + const incremental = scene1.serializeIncremental(); + const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' }); + + const scene2 = new Scene({ name: 'Scene2' }); + scene2.applyIncremental(jsonData); + + expect(scene2.entities.count).toBe(1); + const restoredEntity = scene2.findEntity('TestEntity'); + expect(restoredEntity).not.toBeNull(); + expect(restoredEntity!.tag).toBe(99); + + scene1.end(); + scene2.end(); + }); }); describe('Snapshot Management', () => {