From fd1bbb0e00005c682784a58b07bfacb44506ef3a Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Thu, 9 Oct 2025 12:30:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=A2=9E=E9=87=8F=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/demos/IncrementalSerializationDemo.ts | 438 +++++++++++ examples/core-demos/src/demos/index.ts | 7 +- packages/core/src/ECS/Scene.ts | 141 +++- .../Serialization/IncrementalSerializer.ts | 677 ++++++++++++++++++ packages/core/src/ECS/Serialization/index.ts | 10 + ...chetypeVsComponentIndex.comparison.test.ts | 213 ++++++ .../IncrementalSerialization.test.ts | 593 +++++++++++++++ 7 files changed, 2075 insertions(+), 4 deletions(-) create mode 100644 examples/core-demos/src/demos/IncrementalSerializationDemo.ts create mode 100644 packages/core/src/ECS/Serialization/IncrementalSerializer.ts create mode 100644 packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts create mode 100644 packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts diff --git a/examples/core-demos/src/demos/IncrementalSerializationDemo.ts b/examples/core-demos/src/demos/IncrementalSerializationDemo.ts new file mode 100644 index 00000000..a9078a7d --- /dev/null +++ b/examples/core-demos/src/demos/IncrementalSerializationDemo.ts @@ -0,0 +1,438 @@ +import { DemoBase, DemoInfo } from './DemoBase'; +import { + Component, + ECSComponent, + EntitySystem, + Serializable, + Serialize, + IncrementalSerializer +} from '@esengine/ecs-framework'; + +// ===== 组件定义 ===== +@ECSComponent('IncDemo_Position') +@Serializable({ version: 1, typeId: 'IncDemo_Position' }) +class PositionComponent extends Component { + @Serialize() x: number = 0; + @Serialize() y: number = 0; + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('IncDemo_Velocity') +@Serializable({ version: 1, typeId: 'IncDemo_Velocity' }) +class VelocityComponent extends Component { + @Serialize() vx: number = 0; + @Serialize() vy: number = 0; + constructor(vx: number = 0, vy: number = 0) { + super(); + this.vx = vx; + this.vy = vy; + } +} + +@ECSComponent('IncDemo_Renderable') +@Serializable({ version: 1, typeId: 'IncDemo_Renderable' }) +class RenderableComponent extends Component { + @Serialize() color: string = '#ffffff'; + @Serialize() radius: number = 10; + constructor(color: string = '#ffffff', radius: number = 10) { + super(); + this.color = color; + this.radius = radius; + } +} + +// ===== 系统定义 ===== +class MovementSystem extends EntitySystem { + update() { + if (!this.scene) return; + const entities = this.scene.entities.buffer; + for (const entity of entities) { + const pos = entity.getComponent(PositionComponent); + const vel = entity.getComponent(VelocityComponent); + if (pos && vel) { + pos.x += vel.vx; + pos.y += vel.vy; + + if (pos.x < 0 || pos.x > 1200) vel.vx *= -1; + if (pos.y < 0 || pos.y > 600) vel.vy *= -1; + } + } + } +} + +class RenderSystem extends EntitySystem { + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + + constructor(canvas: HTMLCanvasElement) { + super(); + this.canvas = canvas; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Failed to get canvas context'); + this.ctx = ctx; + } + + update() { + if (!this.scene) return; + + this.ctx.fillStyle = '#0a0a15'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + const entities = this.scene.entities.buffer; + for (const entity of entities) { + const pos = entity.getComponent(PositionComponent); + const render = entity.getComponent(RenderableComponent); + if (pos && render) { + this.ctx.fillStyle = render.color; + this.ctx.beginPath(); + this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2); + this.ctx.fill(); + + this.ctx.fillStyle = 'white'; + this.ctx.font = '10px Arial'; + this.ctx.textAlign = 'center'; + this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5); + } + } + } +} + +export class IncrementalSerializationDemo extends DemoBase { + private renderSystem!: RenderSystem; + private incrementalHistory: any[] = []; + private autoSnapshotInterval: number | null = null; + + getInfo(): DemoInfo { + return { + id: 'incremental-serialization', + name: '增量序列化', + description: '演示增量序列化功能,只保存场景变更而非完整状态,适用于网络同步和回放系统', + category: '核心功能', + icon: '🔄' + }; + } + + setup() { + // 创建控制面板 + this.createControls(); + + // 添加系统 + this.renderSystem = new RenderSystem(this.canvas); + this.scene.addEntityProcessor(new MovementSystem()); + this.scene.addEntityProcessor(this.renderSystem); + + // 创建初始实体 + this.createInitialEntities(); + + // 创建基础快照 + this.scene.createIncrementalSnapshot(); + this.addToHistory('Initial State'); + } + + private createInitialEntities() { + // 创建玩家 + const player = this.scene.createEntity('Player'); + player.addComponent(new PositionComponent(600, 300)); + player.addComponent(new VelocityComponent(2, 1.5)); + player.addComponent(new RenderableComponent('#4a9eff', 15)); + + // 设置场景数据 + this.scene.sceneData.set('gameTime', 0); + this.scene.sceneData.set('score', 0); + } + + private createRandomEntity() { + const entity = this.scene.createEntity(`Entity_${Date.now()}`); + entity.addComponent(new PositionComponent( + Math.random() * this.canvas.width, + Math.random() * this.canvas.height + )); + entity.addComponent(new VelocityComponent( + (Math.random() - 0.5) * 3, + (Math.random() - 0.5) * 3 + )); + const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee']; + entity.addComponent(new RenderableComponent( + colors[Math.floor(Math.random() * colors.length)], + 5 + Math.random() * 10 + )); + } + + private addToHistory(label: string) { + const incremental = this.scene.serializeIncremental(); + const stats = IncrementalSerializer.getIncrementalStats(incremental); + + this.incrementalHistory.push({ + label, + incremental, + stats, + timestamp: Date.now() + }); + + this.scene.updateIncrementalSnapshot(); + this.updateHistoryPanel(); + this.updateStats(); + } + + createControls() { + this.controlPanel.innerHTML = ` +
+

实体控制

+
+ + + +
+
+ +
+

增量快照

+
+ + +
+
+ +
+
+ +
+

场景数据控制

+
+ + +
+
+ + +
+ +
+ +
+
+
实体数量
+
0
+
+
+
历史记录
+
0
+
+
+
最后快照大小
+
0B
+
+
+
总变更数
+
0
+
+
+ +
+

增量历史 (点击快照查看详情)

+
+ 暂无历史记录 +
+
+ +
+

快照详情

+
+ 点击历史记录查看详情... +
+
+ `; + + this.bindEvents(); + this.updateStats(); + } + + private bindEvents() { + document.getElementById('addEntity')!.addEventListener('click', () => { + this.createRandomEntity(); + this.addToHistory('添加实体'); + this.showToast('添加了一个随机实体'); + }); + + document.getElementById('removeEntity')!.addEventListener('click', () => { + const entities = this.scene.entities.buffer; + if (entities.length > 1) { + const lastEntity = entities[entities.length - 1]; + lastEntity.destroy(); + this.addToHistory('删除实体'); + this.showToast('删除了最后一个实体'); + } else { + this.showToast('至少保留一个实体', '⚠️'); + } + }); + + document.getElementById('modifyEntity')!.addEventListener('click', () => { + const entities = this.scene.entities.buffer; + if (entities.length > 0) { + const randomEntity = entities[Math.floor(Math.random() * entities.length)]; + const pos = randomEntity.getComponent(PositionComponent); + if (pos) { + pos.x = Math.random() * this.canvas.width; + pos.y = Math.random() * this.canvas.height; + } + this.addToHistory('修改实体位置'); + this.showToast(`修改了 ${randomEntity.name} 的位置`); + } + }); + + document.getElementById('captureSnapshot')!.addEventListener('click', () => { + this.addToHistory('手动快照'); + this.showToast('已捕获当前状态', '📸'); + }); + + document.getElementById('clearHistory')!.addEventListener('click', () => { + this.incrementalHistory = []; + this.scene.createIncrementalSnapshot(); + this.addToHistory('清空后重新开始'); + this.showToast('历史记录已清空'); + }); + + document.getElementById('autoSnapshot')!.addEventListener('change', (e) => { + const checkbox = e.target as HTMLInputElement; + if (checkbox.checked) { + this.autoSnapshotInterval = window.setInterval(() => { + this.addToHistory('自动快照'); + }, 2000); + this.showToast('自动快照已启用', '⏱️'); + } else { + if (this.autoSnapshotInterval !== null) { + clearInterval(this.autoSnapshotInterval); + this.autoSnapshotInterval = null; + } + this.showToast('自动快照已禁用'); + } + }); + + document.getElementById('updateSceneData')!.addEventListener('click', () => { + const gameTime = parseInt((document.getElementById('gameTime') as HTMLInputElement).value); + const score = parseInt((document.getElementById('score') as HTMLInputElement).value); + + this.scene.sceneData.set('gameTime', gameTime); + this.scene.sceneData.set('score', score); + + this.addToHistory('更新场景数据'); + this.showToast('场景数据已更新'); + }); + } + + private updateHistoryPanel() { + const panel = document.getElementById('historyPanel')!; + + if (this.incrementalHistory.length === 0) { + panel.innerHTML = '暂无历史记录'; + return; + } + + panel.innerHTML = this.incrementalHistory.map((item, index) => { + const isLatest = index === this.incrementalHistory.length - 1; + const time = new Date(item.timestamp).toLocaleTimeString(); + + return ` +
+
+
+ ${item.label} + ${isLatest ? '' : ''} +
+ ${time} +
+
+ 实体: +${item.stats.addedEntities} -${item.stats.removedEntities} ~${item.stats.updatedEntities} | + 组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents} | + 总变更: ${item.stats.totalChanges} +
+
+ `; + }).join(''); + + // 绑定点击事件 + panel.querySelectorAll('.history-item').forEach(item => { + item.addEventListener('click', () => { + const index = parseInt(item.getAttribute('data-index')!); + this.showSnapshotDetails(index); + }); + }); + + // 自动滚动到底部 + panel.scrollTop = panel.scrollHeight; + } + + private showSnapshotDetails(index: number) { + const item = this.incrementalHistory[index]; + const detailsPanel = document.getElementById('snapshotDetails')!; + + const details = { + 版本: item.incremental.version, + 基础版本: item.incremental.baseVersion, + 时间戳: new Date(item.incremental.timestamp).toLocaleString(), + 场景名称: item.incremental.sceneName, + 统计: item.stats, + 实体变更: item.incremental.entityChanges.map((c: any) => ({ + 操作: c.operation, + 实体ID: c.entityId, + 实体名称: c.entityName + })), + 组件变更: item.incremental.componentChanges.map((c: any) => ({ + 操作: c.operation, + 实体ID: c.entityId, + 组件类型: c.componentType + })), + 场景数据变更: item.incremental.sceneDataChanges.map((c: any) => ({ + 键: c.key, + 值: c.value, + 已删除: c.deleted + })) + }; + + detailsPanel.textContent = JSON.stringify(details, null, 2); + } + + private updateStats() { + document.getElementById('entityCount')!.textContent = this.scene.entities.count.toString(); + document.getElementById('historyCount')!.textContent = this.incrementalHistory.length.toString(); + + 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('totalChanges')!.textContent = lastItem.stats.totalChanges.toString(); + } + } + + private formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; + } + + protected render() { + // RenderSystem会处理渲染 + } + + public destroy() { + if (this.autoSnapshotInterval !== null) { + clearInterval(this.autoSnapshotInterval); + } + super.destroy(); + } +} diff --git a/examples/core-demos/src/demos/index.ts b/examples/core-demos/src/demos/index.ts index 004fc191..4fa44ad1 100644 --- a/examples/core-demos/src/demos/index.ts +++ b/examples/core-demos/src/demos/index.ts @@ -1,12 +1,13 @@ import { DemoBase } from './DemoBase'; import { SerializationDemo } from './SerializationDemo'; +import { IncrementalSerializationDemo } from './IncrementalSerializationDemo'; import { WorkerSystemDemo } from './WorkerSystemDemo'; -export { DemoBase, SerializationDemo, WorkerSystemDemo }; +export { DemoBase, SerializationDemo, IncrementalSerializationDemo, WorkerSystemDemo }; // Demo注册表 export const DEMO_REGISTRY: typeof DemoBase[] = [ SerializationDemo, - WorkerSystemDemo, - // 更多demos可以在这里添加 + IncrementalSerializationDemo, + WorkerSystemDemo ]; diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 395d49d6..ae0356cc 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -3,7 +3,7 @@ import { EntityList } from './Utils/EntityList'; import { EntityProcessorList } from './Utils/EntityProcessorList'; import { IdentifierPool } from './Utils/IdentifierPool'; import { EntitySystem } from './Systems/EntitySystem'; -import { ComponentStorageManager } from './Core/ComponentStorage'; +import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStorage'; import { QuerySystem } from './Core/QuerySystem'; import { TypeSafeEventSystem } from './Core/EventSystem'; import { EventBus } from './Core/EventBus'; @@ -11,6 +11,7 @@ import { IScene, ISceneConfig } from './IScene'; import { getComponentInstanceTypeName, getSystemInstanceTypeName } from './Decorators'; import { TypedQueryBuilder } from './Core/Query/TypedQuery'; import { SceneSerializer, SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer'; +import { IncrementalSerializer, IncrementalSnapshot, IncrementalSerializationOptions } from './Serialization/IncrementalSerializer'; /** * 游戏场景默认实现类 @@ -548,4 +549,142 @@ export class Scene implements IScene { public deserialize(saveData: string | Buffer, options?: SceneDeserializationOptions): void { SceneSerializer.deserialize(this, saveData, options); } + + // ==================== 增量序列化 API ==================== + + /** 增量序列化的基础快照 */ + private _incrementalBaseSnapshot?: any; + + /** + * 创建增量序列化的基础快照 + * + * 在需要进行增量序列化前,先调用此方法创建基础快照 + * + * @param options 序列化选项 + * + * @example + * ```typescript + * // 创建基础快照 + * scene.createIncrementalSnapshot(); + * + * // 进行一些修改... + * entity.addComponent(new PositionComponent(100, 200)); + * + * // 计算增量变更 + * const incremental = scene.serializeIncremental(); + * ``` + */ + public createIncrementalSnapshot(options?: IncrementalSerializationOptions): void { + this._incrementalBaseSnapshot = IncrementalSerializer.createSnapshot(this, options); + } + + /** + * 增量序列化场景 + * + * 只序列化相对于基础快照的变更部分 + * + * @param options 序列化选项 + * @returns 增量快照对象 + * + * @example + * ```typescript + * // 创建基础快照 + * scene.createIncrementalSnapshot(); + * + * // 修改场景 + * const entity = scene.createEntity('NewEntity'); + * entity.addComponent(new PositionComponent(50, 100)); + * + * // 获取增量变更 + * const incremental = scene.serializeIncremental(); + * console.log(`变更数量: ${incremental.entityChanges.length}`); + * + * // 序列化为JSON + * const json = IncrementalSerializer.serializeIncremental(incremental); + * ``` + */ + public serializeIncremental(options?: IncrementalSerializationOptions): IncrementalSnapshot { + if (!this._incrementalBaseSnapshot) { + throw new Error('必须先调用 createIncrementalSnapshot() 创建基础快照'); + } + + return IncrementalSerializer.computeIncremental( + this, + this._incrementalBaseSnapshot, + options + ); + } + + /** + * 应用增量变更到场景 + * + * @param incremental 增量快照数据(JSON字符串或对象) + * @param componentRegistry 组件类型注册表(可选,默认使用全局注册表) + * + * @example + * ```typescript + * // 应用增量变更 + * scene.applyIncremental(incrementalSnapshot); + * + * // 或从JSON字符串应用 + * const incremental = IncrementalSerializer.deserializeIncremental(jsonString); + * scene.applyIncremental(incremental); + * ``` + */ + public applyIncremental( + incremental: IncrementalSnapshot | string, + componentRegistry?: Map + ): void { + const snapshot = typeof incremental === 'string' + ? IncrementalSerializer.deserializeIncremental(incremental) + : incremental; + + const registry = componentRegistry || ComponentRegistry.getAllComponentNames() as Map; + + IncrementalSerializer.applyIncremental(this, snapshot, registry); + } + + /** + * 更新增量快照基准 + * + * 将当前场景状态设为新的增量序列化基准 + * + * @param options 序列化选项 + * + * @example + * ```typescript + * // 创建初始快照 + * scene.createIncrementalSnapshot(); + * + * // 进行一些修改并序列化 + * const incremental1 = scene.serializeIncremental(); + * + * // 更新基准,之后的增量将基于当前状态 + * scene.updateIncrementalSnapshot(); + * + * // 继续修改 + * const incremental2 = scene.serializeIncremental(); + * ``` + */ + public updateIncrementalSnapshot(options?: IncrementalSerializationOptions): void { + this.createIncrementalSnapshot(options); + } + + /** + * 清除增量快照 + * + * 释放快照占用的内存 + */ + public clearIncrementalSnapshot(): void { + this._incrementalBaseSnapshot = undefined; + } + + /** + * 检查是否有增量快照 + * + * @returns 如果已创建增量快照返回true + */ + public hasIncrementalSnapshot(): boolean { + return this._incrementalBaseSnapshot !== undefined; + } } \ No newline at end of file diff --git a/packages/core/src/ECS/Serialization/IncrementalSerializer.ts b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts new file mode 100644 index 00000000..0e5c964f --- /dev/null +++ b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts @@ -0,0 +1,677 @@ +/** + * 增量序列化器 + * + * 提供高性能的增量序列化支持,只序列化变更的数据 + * 适用于网络同步、大场景存档、时间回溯等场景 + */ + +import type { IScene } from '../IScene'; +import { Entity } from '../Entity'; +import { Component } from '../Component'; +import { ComponentSerializer, SerializedComponent } from './ComponentSerializer'; +import { SerializedEntity } from './EntitySerializer'; +import { ComponentType } from '../Core/ComponentStorage'; + +/** + * 变更操作类型 + */ +export enum ChangeOperation { + /** 添加新实体 */ + EntityAdded = 'entity_added', + /** 删除实体 */ + EntityRemoved = 'entity_removed', + /** 实体属性更新 */ + EntityUpdated = 'entity_updated', + /** 添加组件 */ + ComponentAdded = 'component_added', + /** 删除组件 */ + ComponentRemoved = 'component_removed', + /** 组件数据更新 */ + ComponentUpdated = 'component_updated', + /** 场景数据更新 */ + SceneDataUpdated = 'scene_data_updated' +} + +/** + * 实体变更记录 + */ +export interface EntityChange { + /** 操作类型 */ + operation: ChangeOperation; + /** 实体ID */ + entityId: number; + /** 实体名称(用于Added操作) */ + entityName?: string; + /** 实体数据(用于Added/Updated操作) */ + entityData?: Partial; +} + +/** + * 组件变更记录 + */ +export interface ComponentChange { + /** 操作类型 */ + operation: ChangeOperation; + /** 实体ID */ + entityId: number; + /** 组件类型名称 */ + componentType: string; + /** 组件数据(用于Added/Updated操作) */ + componentData?: SerializedComponent; +} + +/** + * 场景数据变更记录 + */ +export interface SceneDataChange { + /** 操作类型 */ + operation: ChangeOperation; + /** 变更的键 */ + key: string; + /** 新值 */ + value: any; + /** 是否删除 */ + deleted?: boolean; +} + +/** + * 增量序列化数据 + */ +export interface IncrementalSnapshot { + /** 快照版本号 */ + version: number; + /** 时间戳 */ + timestamp: number; + /** 场景名称 */ + sceneName: string; + /** 基础版本号(相对于哪个快照的增量) */ + baseVersion: number; + /** 实体变更列表 */ + entityChanges: EntityChange[]; + /** 组件变更列表 */ + componentChanges: ComponentChange[]; + /** 场景数据变更列表 */ + sceneDataChanges: SceneDataChange[]; +} + +/** + * 场景快照(用于对比) + */ +interface SceneSnapshot { + /** 快照版本号 */ + version: number; + /** 实体ID集合 */ + entityIds: Set; + /** 实体数据映射 */ + entities: Map; + /** 组件数据映射 (entityId -> componentType -> serializedData) */ + components: Map>; // 使用JSON字符串存储组件数据 + /** 场景自定义数据 */ + sceneData: Map; // 使用JSON字符串存储场景数据 +} + +/** + * 增量序列化选项 + */ +export interface IncrementalSerializationOptions { + /** + * 是否包含组件数据的深度对比 + * 默认true,设为false可提升性能但可能漏掉组件内部字段变更 + */ + deepComponentComparison?: boolean; + + /** + * 是否跟踪场景数据变更 + * 默认true + */ + trackSceneData?: boolean; + + /** + * 是否压缩快照(使用JSON序列化) + * 默认false,设为true可减少内存占用但增加CPU开销 + */ + compressSnapshot?: boolean; +} + +/** + * 增量序列化器类 + */ +export class IncrementalSerializer { + /** 当前快照版本号 */ + private static snapshotVersion = 0; + + /** + * 创建场景快照 + * + * @param scene 要快照的场景 + * @param options 序列化选项 + * @returns 场景快照对象 + */ + public static createSnapshot( + scene: IScene, + options?: IncrementalSerializationOptions + ): SceneSnapshot { + const opts = { + deepComponentComparison: true, + trackSceneData: true, + compressSnapshot: false, + ...options + }; + + const snapshot: SceneSnapshot = { + version: ++this.snapshotVersion, + entityIds: new Set(), + entities: new Map(), + components: new Map(), + sceneData: new Map() + }; + + // 快照所有实体 + for (const entity of scene.entities.buffer) { + snapshot.entityIds.add(entity.id); + + // 存储实体基本信息 + snapshot.entities.set(entity.id, { + name: entity.name, + tag: entity.tag, + active: entity.active, + enabled: entity.enabled, + updateOrder: entity.updateOrder, + parentId: entity.parent?.id + }); + + // 快照组件 + if (opts.deepComponentComparison) { + const componentMap = new Map(); + + for (const component of entity.components) { + const serialized = ComponentSerializer.serialize(component); + if (serialized) { + // 使用JSON字符串存储,便于后续对比 + componentMap.set( + serialized.type, + JSON.stringify(serialized.data) + ); + } + } + + if (componentMap.size > 0) { + snapshot.components.set(entity.id, componentMap); + } + } + } + + // 快照场景数据 + if (opts.trackSceneData) { + for (const [key, value] of scene.sceneData) { + snapshot.sceneData.set(key, JSON.stringify(value)); + } + } + + return snapshot; + } + + /** + * 计算增量变更 + * + * @param scene 当前场景 + * @param baseSnapshot 基础快照 + * @param options 序列化选项 + * @returns 增量快照 + */ + public static computeIncremental( + scene: IScene, + baseSnapshot: SceneSnapshot, + options?: IncrementalSerializationOptions + ): IncrementalSnapshot { + const opts = { + deepComponentComparison: true, + trackSceneData: true, + ...options + }; + + const incremental: IncrementalSnapshot = { + version: ++this.snapshotVersion, + timestamp: Date.now(), + sceneName: scene.name, + baseVersion: baseSnapshot.version, + entityChanges: [], + componentChanges: [], + sceneDataChanges: [] + }; + + const currentEntityIds = new Set(); + + // 检测实体变更 + for (const entity of scene.entities.buffer) { + currentEntityIds.add(entity.id); + + if (!baseSnapshot.entityIds.has(entity.id)) { + // 新增实体 + incremental.entityChanges.push({ + operation: ChangeOperation.EntityAdded, + entityId: entity.id, + entityName: entity.name, + entityData: { + id: entity.id, + name: entity.name, + tag: entity.tag, + active: entity.active, + enabled: entity.enabled, + updateOrder: entity.updateOrder, + parentId: entity.parent?.id, + components: [], + children: [] + } + }); + + // 新增实体的所有组件都是新增 + for (const component of entity.components) { + const serialized = ComponentSerializer.serialize(component); + if (serialized) { + incremental.componentChanges.push({ + operation: ChangeOperation.ComponentAdded, + entityId: entity.id, + componentType: serialized.type, + componentData: serialized + }); + } + } + } else { + // 检查实体属性变更 + const oldData = baseSnapshot.entities.get(entity.id)!; + const entityChanged = + oldData.name !== entity.name || + oldData.tag !== entity.tag || + oldData.active !== entity.active || + oldData.enabled !== entity.enabled || + oldData.updateOrder !== entity.updateOrder || + oldData.parentId !== entity.parent?.id; + + if (entityChanged) { + incremental.entityChanges.push({ + operation: ChangeOperation.EntityUpdated, + entityId: entity.id, + entityData: { + name: entity.name, + tag: entity.tag, + active: entity.active, + enabled: entity.enabled, + updateOrder: entity.updateOrder, + parentId: entity.parent?.id + } + }); + } + + // 检查组件变更 + if (opts.deepComponentComparison) { + this.detectComponentChanges( + entity, + baseSnapshot, + incremental.componentChanges + ); + } + } + } + + // 检测删除的实体 + for (const oldEntityId of baseSnapshot.entityIds) { + if (!currentEntityIds.has(oldEntityId)) { + incremental.entityChanges.push({ + operation: ChangeOperation.EntityRemoved, + entityId: oldEntityId + }); + } + } + + // 检测场景数据变更 + if (opts.trackSceneData) { + this.detectSceneDataChanges( + scene, + baseSnapshot, + incremental.sceneDataChanges + ); + } + + return incremental; + } + + /** + * 检测组件变更 + */ + private static detectComponentChanges( + entity: Entity, + baseSnapshot: SceneSnapshot, + componentChanges: ComponentChange[] + ): void { + const oldComponents = baseSnapshot.components.get(entity.id); + const currentComponents = new Map(); + + // 收集当前组件 + for (const component of entity.components) { + const serialized = ComponentSerializer.serialize(component); + if (serialized) { + currentComponents.set(serialized.type, serialized); + } + } + + // 检测新增和更新的组件 + for (const [type, serialized] of currentComponents) { + const currentData = JSON.stringify(serialized.data); + + if (!oldComponents || !oldComponents.has(type)) { + // 新增组件 + componentChanges.push({ + operation: ChangeOperation.ComponentAdded, + entityId: entity.id, + componentType: type, + componentData: serialized + }); + } else if (oldComponents.get(type) !== currentData) { + // 组件数据变更 + componentChanges.push({ + operation: ChangeOperation.ComponentUpdated, + entityId: entity.id, + componentType: type, + componentData: serialized + }); + } + } + + // 检测删除的组件 + if (oldComponents) { + for (const oldType of oldComponents.keys()) { + if (!currentComponents.has(oldType)) { + componentChanges.push({ + operation: ChangeOperation.ComponentRemoved, + entityId: entity.id, + componentType: oldType + }); + } + } + } + } + + /** + * 检测场景数据变更 + */ + private static detectSceneDataChanges( + scene: IScene, + baseSnapshot: SceneSnapshot, + sceneDataChanges: SceneDataChange[] + ): void { + const currentKeys = new Set(); + + // 检测新增和更新的场景数据 + for (const [key, value] of scene.sceneData) { + currentKeys.add(key); + const currentValue = JSON.stringify(value); + const oldValue = baseSnapshot.sceneData.get(key); + + if (!oldValue || oldValue !== currentValue) { + sceneDataChanges.push({ + operation: ChangeOperation.SceneDataUpdated, + key, + value + }); + } + } + + // 检测删除的场景数据 + for (const oldKey of baseSnapshot.sceneData.keys()) { + if (!currentKeys.has(oldKey)) { + sceneDataChanges.push({ + operation: ChangeOperation.SceneDataUpdated, + key: oldKey, + value: undefined, + deleted: true + }); + } + } + } + + /** + * 应用增量变更到场景 + * + * @param scene 目标场景 + * @param incremental 增量快照 + * @param componentRegistry 组件类型注册表 + */ + public static applyIncremental( + scene: IScene, + incremental: IncrementalSnapshot, + componentRegistry: Map + ): void { + // 应用实体变更 + for (const change of incremental.entityChanges) { + switch (change.operation) { + case ChangeOperation.EntityAdded: + this.applyEntityAdded(scene, change); + break; + case ChangeOperation.EntityRemoved: + this.applyEntityRemoved(scene, change); + break; + case ChangeOperation.EntityUpdated: + this.applyEntityUpdated(scene, change); + break; + } + } + + // 应用组件变更 + for (const change of incremental.componentChanges) { + switch (change.operation) { + case ChangeOperation.ComponentAdded: + this.applyComponentAdded(scene, change, componentRegistry); + break; + case ChangeOperation.ComponentRemoved: + this.applyComponentRemoved(scene, change, componentRegistry); + break; + case ChangeOperation.ComponentUpdated: + this.applyComponentUpdated(scene, change, componentRegistry); + break; + } + } + + // 应用场景数据变更 + for (const change of incremental.sceneDataChanges) { + if (change.deleted) { + scene.sceneData.delete(change.key); + } else { + scene.sceneData.set(change.key, change.value); + } + } + } + + private static applyEntityAdded(scene: IScene, change: EntityChange): void { + if (!change.entityData) return; + + const entity = new Entity(change.entityName || 'Entity', change.entityId); + entity.tag = change.entityData.tag || 0; + entity.active = change.entityData.active ?? true; + entity.enabled = change.entityData.enabled ?? true; + entity.updateOrder = change.entityData.updateOrder || 0; + + scene.addEntity(entity); + } + + private static applyEntityRemoved(scene: IScene, change: EntityChange): void { + const entity = scene.entities.findEntityById(change.entityId); + if (entity) { + entity.destroy(); + } + } + + private static applyEntityUpdated(scene: IScene, change: EntityChange): void { + if (!change.entityData) return; + + const entity = scene.entities.findEntityById(change.entityId); + if (!entity) return; + + if (change.entityData.name !== undefined) entity.name = change.entityData.name; + if (change.entityData.tag !== undefined) entity.tag = change.entityData.tag; + if (change.entityData.active !== undefined) entity.active = change.entityData.active; + if (change.entityData.enabled !== undefined) entity.enabled = change.entityData.enabled; + if (change.entityData.updateOrder !== undefined) entity.updateOrder = change.entityData.updateOrder; + + if (change.entityData.parentId !== undefined) { + const newParent = scene.entities.findEntityById(change.entityData.parentId); + if (newParent && entity.parent !== newParent) { + if (entity.parent) { + entity.parent.removeChild(entity); + } + newParent.addChild(entity); + } + } else if (entity.parent) { + entity.parent.removeChild(entity); + } + } + + private static applyComponentAdded( + scene: IScene, + change: ComponentChange, + componentRegistry: Map + ): void { + if (!change.componentData) return; + + const entity = scene.entities.findEntityById(change.entityId); + if (!entity) return; + + const component = ComponentSerializer.deserialize(change.componentData, componentRegistry); + if (component) { + entity.addComponent(component); + } + } + + private static applyComponentRemoved( + scene: IScene, + change: ComponentChange, + componentRegistry: Map + ): void { + const entity = scene.entities.findEntityById(change.entityId); + if (!entity) return; + + const componentClass = componentRegistry.get(change.componentType); + if (!componentClass) return; + + entity.removeComponentByType(componentClass); + } + + private static applyComponentUpdated( + scene: IScene, + change: ComponentChange, + componentRegistry: Map + ): void { + if (!change.componentData) return; + + const entity = scene.entities.findEntityById(change.entityId); + if (!entity) return; + + const componentClass = componentRegistry.get(change.componentType); + if (!componentClass) return; + + entity.removeComponentByType(componentClass); + + const component = ComponentSerializer.deserialize(change.componentData, componentRegistry); + if (component) { + entity.addComponent(component); + } + } + + /** + * 序列化增量快照为JSON + * + * @param incremental 增量快照 + * @param pretty 是否美化输出 + * @returns JSON字符串 + */ + public static serializeIncremental( + incremental: IncrementalSnapshot, + pretty: boolean = false + ): string { + return pretty + ? JSON.stringify(incremental, null, 2) + : JSON.stringify(incremental); + } + + /** + * 从JSON反序列化增量快照 + * + * @param json JSON字符串 + * @returns 增量快照 + */ + public static deserializeIncremental(json: string): IncrementalSnapshot { + return JSON.parse(json); + } + + /** + * 计算增量快照的大小(字节) + * + * @param incremental 增量快照 + * @returns 字节数 + */ + public static getIncrementalSize(incremental: IncrementalSnapshot): number { + const json = this.serializeIncremental(incremental); + return new Blob([json]).size; + } + + /** + * 获取增量快照的统计信息 + * + * @param incremental 增量快照 + * @returns 统计信息 + */ + public static getIncrementalStats(incremental: IncrementalSnapshot): { + totalChanges: number; + entityChanges: number; + componentChanges: number; + sceneDataChanges: number; + addedEntities: number; + removedEntities: number; + updatedEntities: number; + addedComponents: number; + removedComponents: number; + updatedComponents: number; + } { + return { + totalChanges: + incremental.entityChanges.length + + incremental.componentChanges.length + + incremental.sceneDataChanges.length, + entityChanges: incremental.entityChanges.length, + componentChanges: incremental.componentChanges.length, + sceneDataChanges: incremental.sceneDataChanges.length, + addedEntities: incremental.entityChanges.filter( + c => c.operation === ChangeOperation.EntityAdded + ).length, + removedEntities: incremental.entityChanges.filter( + c => c.operation === ChangeOperation.EntityRemoved + ).length, + updatedEntities: incremental.entityChanges.filter( + c => c.operation === ChangeOperation.EntityUpdated + ).length, + addedComponents: incremental.componentChanges.filter( + c => c.operation === ChangeOperation.ComponentAdded + ).length, + removedComponents: incremental.componentChanges.filter( + c => c.operation === ChangeOperation.ComponentRemoved + ).length, + updatedComponents: incremental.componentChanges.filter( + c => c.operation === ChangeOperation.ComponentUpdated + ).length + }; + } + + /** + * 重置快照版本号(用于测试) + */ + public static resetVersion(): void { + this.snapshotVersion = 0; + } +} diff --git a/packages/core/src/ECS/Serialization/index.ts b/packages/core/src/ECS/Serialization/index.ts index 357babc1..fff7789f 100644 --- a/packages/core/src/ECS/Serialization/index.ts +++ b/packages/core/src/ECS/Serialization/index.ts @@ -49,3 +49,13 @@ export type { ComponentMigrationFunction, SceneMigrationFunction } from './VersionMigration'; + +// 增量序列化 +export { IncrementalSerializer, ChangeOperation } from './IncrementalSerializer'; +export type { + IncrementalSnapshot, + IncrementalSerializationOptions, + EntityChange, + ComponentChange, + SceneDataChange +} from './IncrementalSerializer'; diff --git a/packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts b/packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts new file mode 100644 index 00000000..1fa550c8 --- /dev/null +++ b/packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts @@ -0,0 +1,213 @@ +import { ArchetypeSystem } from '../src/ECS/Core/ArchetypeSystem'; +import { ComponentIndexManager } from '../src/ECS/Core/ComponentIndex'; +import { Entity } from '../src/ECS/Entity'; +import { Component } from '../src/ECS/Component'; + +class PositionComponent extends Component { + public x: number; + public y: number; + + constructor(...args: unknown[]) { + super(); + const [x = 0, y = 0] = args as [number?, number?]; + this.x = x; + this.y = y; + } +} + +class VelocityComponent extends Component { + public vx: number; + public vy: number; + + constructor(...args: unknown[]) { + super(); + const [vx = 0, vy = 0] = args as [number?, number?]; + this.vx = vx; + this.vy = vy; + } +} + +class HealthComponent extends Component { + public hp: number; + + constructor(...args: unknown[]) { + super(); + const [hp = 100] = args as [number?]; + this.hp = hp; + } +} + +describe('ArchetypeSystem vs ComponentIndexManager 对比测试', () => { + let archetypeSystem: ArchetypeSystem; + let componentIndexManager: ComponentIndexManager; + let entities: Entity[]; + + beforeEach(() => { + archetypeSystem = new ArchetypeSystem(); + componentIndexManager = new ComponentIndexManager(); + entities = []; + }); + + describe('功能等价性验证', () => { + test('单组件查询应该返回相同结果', () => { + // 创建测试实体 + for (let i = 0; i < 100; i++) { + const entity = new Entity(`Entity${i}`, i); + entity.addComponent(new PositionComponent(i, i)); + + if (i % 2 === 0) { + entity.addComponent(new VelocityComponent(1, 1)); + } + + entities.push(entity); + archetypeSystem.addEntity(entity); + componentIndexManager.addEntity(entity); + } + + // 使用 ArchetypeSystem 查询 + const archetypeEntities = archetypeSystem.getEntitiesByComponent(PositionComponent); + + // 使用 ComponentIndexManager 查询 + const indexEntities = Array.from(componentIndexManager.query(PositionComponent)); + + // 应该返回相同数量 + expect(archetypeEntities.length).toBe(indexEntities.length); + expect(archetypeEntities.length).toBe(100); + + // 应该包含相同的实体 + const archetypeIds = new Set(archetypeEntities.map(e => e.id)); + const indexIds = new Set(indexEntities.map(e => e.id)); + + expect(archetypeIds).toEqual(indexIds); + }); + + test('多组件 AND 查询应该返回相同结果', () => { + // 创建测试实体 + for (let i = 0; i < 100; i++) { + const entity = new Entity(`Entity${i}`, i); + entity.addComponent(new PositionComponent(i, i)); + + if (i % 2 === 0) { + entity.addComponent(new VelocityComponent(1, 1)); + } + + if (i % 3 === 0) { + entity.addComponent(new HealthComponent(100)); + } + + entities.push(entity); + archetypeSystem.addEntity(entity); + componentIndexManager.addEntity(entity); + } + + // ArchetypeSystem 查询 + const archetypeResult = archetypeSystem.queryArchetypes([PositionComponent, VelocityComponent], 'AND'); + const archetypeEntities: Entity[] = []; + for (const archetype of archetypeResult.archetypes) { + for (const entity of archetype.entities) { + archetypeEntities.push(entity); + } + } + + // ComponentIndexManager 查询 + const indexEntities = Array.from(componentIndexManager.queryMultiple([PositionComponent, VelocityComponent], 'AND')); + + // 验证结果 + expect(archetypeEntities.length).toBe(indexEntities.length); + expect(archetypeEntities.length).toBe(50); // i % 2 === 0 + + const archetypeIds = new Set(archetypeEntities.map(e => e.id)); + const indexIds = new Set(indexEntities.map(e => e.id)); + expect(archetypeIds).toEqual(indexIds); + }); + + test('多组件 OR 查询应该返回相同结果', () => { + // 创建测试实体 + for (let i = 0; i < 100; i++) { + const entity = new Entity(`Entity${i}`, i); + + if (i % 2 === 0) { + entity.addComponent(new PositionComponent(i, i)); + } + + if (i % 3 === 0) { + entity.addComponent(new VelocityComponent(1, 1)); + } + + entities.push(entity); + archetypeSystem.addEntity(entity); + componentIndexManager.addEntity(entity); + } + + // ArchetypeSystem 查询 + const archetypeResult = archetypeSystem.queryArchetypes([PositionComponent, VelocityComponent], 'OR'); + const archetypeEntities: Entity[] = []; + for (const archetype of archetypeResult.archetypes) { + for (const entity of archetype.entities) { + archetypeEntities.push(entity); + } + } + + // ComponentIndexManager 查询 + const indexEntities = Array.from(componentIndexManager.queryMultiple([PositionComponent, VelocityComponent], 'OR')); + + // 验证结果 - 有 Position 或 Velocity 的实体 + expect(archetypeEntities.length).toBe(indexEntities.length); + + const archetypeIds = new Set(archetypeEntities.map(e => e.id)); + const indexIds = new Set(indexEntities.map(e => e.id)); + expect(archetypeIds).toEqual(indexIds); + }); + + test('空查询应该返回空结果', () => { + // 创建一些实体但不添加 HealthComponent + for (let i = 0; i < 10; i++) { + const entity = new Entity(`Entity${i}`, i); + entity.addComponent(new PositionComponent(i, i)); + archetypeSystem.addEntity(entity); + componentIndexManager.addEntity(entity); + } + + // 查询不存在的组件 + const archetypeEntities = archetypeSystem.getEntitiesByComponent(HealthComponent); + const indexEntities = Array.from(componentIndexManager.query(HealthComponent)); + + expect(archetypeEntities.length).toBe(0); + expect(indexEntities.length).toBe(0); + }); + }); + + describe('性能对比', () => { + test('单组件查询性能对比', () => { + // 准备大量数据 + for (let i = 0; i < 1000; i++) { + const entity = new Entity(`Entity${i}`, i); + entity.addComponent(new PositionComponent(i, i)); + archetypeSystem.addEntity(entity); + componentIndexManager.addEntity(entity); + } + + // ArchetypeSystem 性能测试 + const archetypeStart = performance.now(); + for (let i = 0; i < 100; i++) { + archetypeSystem.getEntitiesByComponent(PositionComponent); + } + const archetypeDuration = performance.now() - archetypeStart; + + // ComponentIndexManager 性能测试 + const indexStart = performance.now(); + for (let i = 0; i < 100; i++) { + componentIndexManager.query(PositionComponent); + } + const indexDuration = performance.now() - indexStart; + + console.log(`ArchetypeSystem: ${archetypeDuration.toFixed(2)}ms`); + console.log(`ComponentIndexManager: ${indexDuration.toFixed(2)}ms`); + console.log(`Ratio: ${(archetypeDuration / indexDuration).toFixed(2)}x`); + + // 两者应该都很快 + expect(archetypeDuration).toBeLessThan(100); + expect(indexDuration).toBeLessThan(100); + }); + }); +}); \ No newline at end of file diff --git a/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts b/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts new file mode 100644 index 00000000..e2f52851 --- /dev/null +++ b/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts @@ -0,0 +1,593 @@ +/** + * 增量序列化系统测试 + */ + +import { Component } from '../../../src/ECS/Component'; +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { + Serializable, + Serialize, + IncrementalSerializer, + ChangeOperation +} from '../../../src/ECS/Serialization'; +import { ECSComponent } from '../../../src/ECS/Decorators'; +import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage'; + +// 测试组件定义 +@ECSComponent('IncTest_Position') +@Serializable({ version: 1 }) +class PositionComponent extends Component { + @Serialize() + public x: number = 0; + + @Serialize() + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('IncTest_Velocity') +@Serializable({ version: 1 }) +class VelocityComponent extends Component { + @Serialize() + public dx: number = 0; + + @Serialize() + public dy: number = 0; +} + +@ECSComponent('IncTest_Health') +@Serializable({ version: 1 }) +class HealthComponent extends Component { + @Serialize() + public current: number = 100; + + @Serialize() + public max: number = 100; +} + +describe('Incremental Serialization System', () => { + let scene: Scene; + + beforeEach(() => { + IncrementalSerializer.resetVersion(); + ComponentRegistry.reset(); + + // 重新注册测试组件 + ComponentRegistry.register(PositionComponent); + ComponentRegistry.register(VelocityComponent); + ComponentRegistry.register(HealthComponent); + + scene = new Scene({ name: 'IncrementalTestScene' }); + }); + + afterEach(() => { + scene.end(); + }); + + describe('Scene Snapshot', () => { + it('应该创建场景快照', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + scene.createIncrementalSnapshot(); + + expect(scene.hasIncrementalSnapshot()).toBe(true); + }); + + it('应该在快照中包含所有实体', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const snapshot = IncrementalSerializer.createSnapshot(scene); + + expect(snapshot.entityIds.size).toBe(2); + expect(snapshot.entityIds.has(entity1.id)).toBe(true); + expect(snapshot.entityIds.has(entity2.id)).toBe(true); + }); + + it('应该在快照中包含实体基本信息', () => { + const entity = scene.createEntity('TestEntity'); + entity.tag = 42; + entity.active = false; + entity.enabled = false; + entity.updateOrder = 5; + + const snapshot = IncrementalSerializer.createSnapshot(scene); + + const entityData = snapshot.entities.get(entity.id); + expect(entityData).toBeDefined(); + expect(entityData!.name).toBe('TestEntity'); + expect(entityData!.tag).toBe(42); + expect(entityData!.active).toBe(false); + expect(entityData!.enabled).toBe(false); + expect(entityData!.updateOrder).toBe(5); + }); + + it('应该在快照中包含组件数据', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(100, 200)); + + const snapshot = IncrementalSerializer.createSnapshot(scene, { + deepComponentComparison: true + }); + + const components = snapshot.components.get(entity.id); + expect(components).toBeDefined(); + expect(components!.has('IncTest_Position')).toBe(true); + }); + }); + + describe('Entity Changes Detection', () => { + it('应该检测新增的实体', () => { + scene.createIncrementalSnapshot(); + + const newEntity = scene.createEntity('NewEntity'); + newEntity.addComponent(new PositionComponent(50, 100)); + + const incremental = scene.serializeIncremental(); + + expect(incremental.entityChanges.length).toBe(1); + expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityAdded); + expect(incremental.entityChanges[0].entityId).toBe(newEntity.id); + expect(incremental.entityChanges[0].entityName).toBe('NewEntity'); + }); + + it('应该检测删除的实体', () => { + const entity = scene.createEntity('ToDelete'); + scene.createIncrementalSnapshot(); + + entity.destroy(); + + const incremental = scene.serializeIncremental(); + + expect(incremental.entityChanges.length).toBe(1); + expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityRemoved); + expect(incremental.entityChanges[0].entityId).toBe(entity.id); + }); + + it('应该检测实体属性变更', () => { + const entity = scene.createEntity('Entity'); + scene.createIncrementalSnapshot(); + + entity.name = 'UpdatedName'; + entity.tag = 99; + entity.active = false; + + const incremental = scene.serializeIncremental(); + + expect(incremental.entityChanges.length).toBe(1); + expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityUpdated); + expect(incremental.entityChanges[0].entityData!.name).toBe('UpdatedName'); + expect(incremental.entityChanges[0].entityData!.tag).toBe(99); + expect(incremental.entityChanges[0].entityData!.active).toBe(false); + }); + }); + + describe('Component Changes Detection', () => { + it('应该检测新增的组件', () => { + const entity = scene.createEntity('Entity'); + scene.createIncrementalSnapshot(); + + entity.addComponent(new PositionComponent(10, 20)); + + const incremental = scene.serializeIncremental(); + + expect(incremental.componentChanges.length).toBe(1); + expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentAdded); + expect(incremental.componentChanges[0].entityId).toBe(entity.id); + expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position'); + }); + + it('应该检测删除的组件', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + scene.createIncrementalSnapshot(); + + entity.removeComponentByType(PositionComponent); + + const incremental = scene.serializeIncremental(); + + expect(incremental.componentChanges.length).toBe(1); + expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentRemoved); + expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position'); + }); + + it('应该检测组件数据变更', () => { + const entity = scene.createEntity('Entity'); + const pos = new PositionComponent(10, 20); + entity.addComponent(pos); + scene.createIncrementalSnapshot(); + + pos.x = 100; + pos.y = 200; + + const incremental = scene.serializeIncremental(); + + expect(incremental.componentChanges.length).toBe(1); + expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated); + expect(incremental.componentChanges[0].componentData!.data.x).toBe(100); + expect(incremental.componentChanges[0].componentData!.data.y).toBe(200); + }); + + it('应该检测多个组件变更', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + scene.createIncrementalSnapshot(); + + entity.addComponent(new VelocityComponent()); + entity.addComponent(new HealthComponent()); + entity.removeComponentByType(PositionComponent); + + const incremental = scene.serializeIncremental(); + + expect(incremental.componentChanges.length).toBe(3); + }); + }); + + describe('Scene Data Changes Detection', () => { + it('应该检测新增的场景数据', () => { + scene.createIncrementalSnapshot(); + + scene.sceneData.set('weather', 'sunny'); + scene.sceneData.set('time', 12.5); + + const incremental = scene.serializeIncremental(); + + expect(incremental.sceneDataChanges.length).toBe(2); + }); + + it('应该检测更新的场景数据', () => { + scene.sceneData.set('weather', 'sunny'); + scene.createIncrementalSnapshot(); + + scene.sceneData.set('weather', 'rainy'); + + const incremental = scene.serializeIncremental(); + + expect(incremental.sceneDataChanges.length).toBe(1); + expect(incremental.sceneDataChanges[0].key).toBe('weather'); + expect(incremental.sceneDataChanges[0].value).toBe('rainy'); + }); + + it('应该检测删除的场景数据', () => { + scene.sceneData.set('temp', 'value'); + scene.createIncrementalSnapshot(); + + scene.sceneData.delete('temp'); + + const incremental = scene.serializeIncremental(); + + expect(incremental.sceneDataChanges.length).toBe(1); + expect(incremental.sceneDataChanges[0].deleted).toBe(true); + }); + }); + + describe('Apply Incremental Changes', () => { + it('应该应用实体添加变更', () => { + const scene1 = new Scene({ name: 'Scene1' }); + scene1.createIncrementalSnapshot(); + + const newEntity = scene1.createEntity('NewEntity'); + newEntity.addComponent(new PositionComponent(50, 100)); + + const incremental = scene1.serializeIncremental(); + + const scene2 = new Scene({ name: 'Scene2' }); + scene2.applyIncremental(incremental); + + expect(scene2.entities.count).toBe(1); + const entity = scene2.findEntity('NewEntity'); + expect(entity).not.toBeNull(); + expect(entity!.hasComponent(PositionComponent)).toBe(true); + + scene1.end(); + scene2.end(); + }); + + it('应该应用实体删除变更', () => { + const scene1 = new Scene({ name: 'Scene1' }); + const entity = scene1.createEntity('ToDelete'); + scene1.createIncrementalSnapshot(); + + entity.destroy(); + const incremental = scene1.serializeIncremental(); + + const scene2 = new Scene({ name: 'Scene2' }); + const entity2 = scene2.createEntity('ToDelete'); + Object.defineProperty(entity2, 'id', { value: entity.id, writable: true }); + + scene2.applyIncremental(incremental); + + expect(scene2.entities.count).toBe(0); + + scene1.end(); + scene2.end(); + }); + + it('应该应用实体属性更新', () => { + const scene1 = new Scene({ name: 'Scene1' }); + const entity1 = scene1.createEntity('Entity'); + scene1.createIncrementalSnapshot(); + + entity1.name = 'UpdatedName'; + entity1.tag = 42; + entity1.active = false; + + const incremental = scene1.serializeIncremental(); + + const scene2 = new Scene({ name: 'Scene2' }); + const entity2 = scene2.createEntity('Entity'); + Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true }); + + scene2.applyIncremental(incremental); + + expect(entity2.name).toBe('UpdatedName'); + expect(entity2.tag).toBe(42); + expect(entity2.active).toBe(false); + + scene1.end(); + scene2.end(); + }); + + it('应该应用组件添加变更', () => { + const scene1 = new Scene({ name: 'Scene1' }); + const entity1 = scene1.createEntity('Entity'); + scene1.createIncrementalSnapshot(); + + entity1.addComponent(new PositionComponent(100, 200)); + + const incremental = scene1.serializeIncremental(); + + const scene2 = new Scene({ name: 'Scene2' }); + const entity2 = scene2.createEntity('Entity'); + Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true }); + + scene2.applyIncremental(incremental); + + expect(entity2.hasComponent(PositionComponent)).toBe(true); + const pos = entity2.getComponent(PositionComponent); + expect(pos!.x).toBe(100); + expect(pos!.y).toBe(200); + + scene1.end(); + scene2.end(); + }); + + it('应该应用组件删除变更', () => { + const scene1 = new Scene({ name: 'Scene1' }); + const entity1 = scene1.createEntity('Entity'); + entity1.addComponent(new PositionComponent(10, 20)); + scene1.createIncrementalSnapshot(); + + entity1.removeComponentByType(PositionComponent); + + const incremental = scene1.serializeIncremental(); + + const scene2 = new Scene({ name: 'Scene2' }); + const entity2 = scene2.createEntity('Entity'); + Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true }); + entity2.addComponent(new PositionComponent(10, 20)); + + scene2.applyIncremental(incremental); + + expect(entity2.hasComponent(PositionComponent)).toBe(false); + + scene1.end(); + scene2.end(); + }); + + it('应该应用组件数据更新', () => { + const scene1 = new Scene({ name: 'Scene1' }); + const entity1 = scene1.createEntity('Entity'); + const pos1 = new PositionComponent(10, 20); + entity1.addComponent(pos1); + scene1.createIncrementalSnapshot(); + + pos1.x = 100; + pos1.y = 200; + + const incremental = scene1.serializeIncremental(); + + const scene2 = new Scene({ name: 'Scene2' }); + const entity2 = scene2.createEntity('Entity'); + Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true }); + entity2.addComponent(new PositionComponent(10, 20)); + + scene2.applyIncremental(incremental); + + const pos2 = entity2.getComponent(PositionComponent); + expect(pos2!.x).toBe(100); + expect(pos2!.y).toBe(200); + + scene1.end(); + scene2.end(); + }); + + it('应该应用场景数据变更', () => { + const scene1 = new Scene({ name: 'Scene1' }); + scene1.createIncrementalSnapshot(); + + scene1.sceneData.set('weather', 'sunny'); + scene1.sceneData.set('time', 12.5); + + const incremental = scene1.serializeIncremental(); + + const scene2 = new Scene({ name: 'Scene2' }); + scene2.applyIncremental(incremental); + + expect(scene2.sceneData.get('weather')).toBe('sunny'); + expect(scene2.sceneData.get('time')).toBe(12.5); + + scene1.end(); + scene2.end(); + }); + }); + + describe('Incremental Serialization', () => { + it('应该序列化和反序列化增量快照', () => { + scene.createIncrementalSnapshot(); + + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(50, 100)); + + const incremental = scene.serializeIncremental(); + const json = IncrementalSerializer.serializeIncremental(incremental); + + expect(typeof json).toBe('string'); + + const deserialized = IncrementalSerializer.deserializeIncremental(json); + expect(deserialized.version).toBe(incremental.version); + expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length); + }); + + it('应该美化JSON输出', () => { + scene.createIncrementalSnapshot(); + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + + const incremental = scene.serializeIncremental(); + const prettyJson = IncrementalSerializer.serializeIncremental(incremental, true); + + expect(prettyJson).toContain('\n'); + expect(prettyJson).toContain(' '); + }); + }); + + describe('Snapshot Management', () => { + it('应该更新增量快照基准', () => { + const entity = scene.createEntity('Entity'); + scene.createIncrementalSnapshot(); + + entity.addComponent(new PositionComponent(10, 20)); + const incremental1 = scene.serializeIncremental(); + + scene.updateIncrementalSnapshot(); + + const pos = entity.getComponent(PositionComponent)!; + pos.x = 100; + + const incremental2 = scene.serializeIncremental(); + + // incremental2应该只包含Position的更新,不包含添加 + expect(incremental1.componentChanges.length).toBe(1); + expect(incremental2.componentChanges.length).toBe(1); + expect(incremental2.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated); + }); + + it('应该清除增量快照', () => { + scene.createIncrementalSnapshot(); + expect(scene.hasIncrementalSnapshot()).toBe(true); + + scene.clearIncrementalSnapshot(); + expect(scene.hasIncrementalSnapshot()).toBe(false); + }); + + it('应该在没有快照时抛出错误', () => { + expect(() => { + scene.serializeIncremental(); + }).toThrow('必须先调用 createIncrementalSnapshot() 创建基础快照'); + }); + }); + + describe('Statistics and Utilities', () => { + it('应该计算增量快照大小', () => { + scene.createIncrementalSnapshot(); + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + + const incremental = scene.serializeIncremental(); + const size = IncrementalSerializer.getIncrementalSize(incremental); + + expect(size).toBeGreaterThan(0); + }); + + it('应该提供增量快照统计信息', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + scene.createIncrementalSnapshot(); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + entity1.destroy(); + + const incremental = scene.serializeIncremental(); + const stats = IncrementalSerializer.getIncrementalStats(incremental); + + expect(stats.addedEntities).toBe(1); + expect(stats.removedEntities).toBe(1); + expect(stats.addedComponents).toBe(1); + expect(stats.totalChanges).toBeGreaterThan(0); + }); + }); + + describe('Performance and Edge Cases', () => { + it('应该处理大量实体变更', () => { + scene.createIncrementalSnapshot(); + + for (let i = 0; i < 100; i++) { + const entity = scene.createEntity(`Entity_${i}`); + entity.addComponent(new PositionComponent(i, i * 2)); + } + + const incremental = scene.serializeIncremental(); + + expect(incremental.entityChanges.length).toBe(100); + expect(incremental.componentChanges.length).toBe(100); + }); + + it('应该处理空变更', () => { + scene.createIncrementalSnapshot(); + + const incremental = scene.serializeIncremental(); + + expect(incremental.entityChanges.length).toBe(0); + expect(incremental.componentChanges.length).toBe(0); + expect(incremental.sceneDataChanges.length).toBe(0); + }); + + it('应该处理复杂嵌套场景数据', () => { + scene.createIncrementalSnapshot(); + + scene.sceneData.set('config', { + nested: { + deep: { + value: 42 + } + }, + array: [1, 2, 3] + }); + + const incremental = scene.serializeIncremental(); + const scene2 = new Scene({ name: 'Scene2' }); + scene2.applyIncremental(incremental); + + const config = scene2.sceneData.get('config'); + expect(config.nested.deep.value).toBe(42); + expect(config.array).toEqual([1, 2, 3]); + + scene2.end(); + }); + + it('应该正确处理快照版本号', () => { + IncrementalSerializer.resetVersion(); + + const snapshot1 = IncrementalSerializer.createSnapshot(scene); + expect(snapshot1.version).toBe(1); + + const snapshot2 = IncrementalSerializer.createSnapshot(scene); + expect(snapshot2.version).toBe(2); + }); + }); +});