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 = `
+
+
实体控制
+
+
+
+
+
+
+
+
+
增量快照
+
+
+
+
+
+
+
+
+
+
+
场景数据控制
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
增量历史 (点击快照查看详情)
+
+ 暂无历史记录
+
+
+
+
+
快照详情
+
+ 点击历史记录查看详情...
+
+
+ `;
+
+ 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);
+ });
+ });
+});