From 959879440d99bd37c5420bd3bddf5bae305bfdb9 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Thu, 9 Oct 2025 14:18:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide/index.md | 3 + docs/guide/serialization.md | 775 ++++++++++++++++++ ...chetypeVsComponentIndex.comparison.test.ts | 213 ----- 3 files changed, 778 insertions(+), 213 deletions(-) create mode 100644 docs/guide/serialization.md delete mode 100644 packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts diff --git a/docs/guide/index.md b/docs/guide/index.md index 5df11794..bb49bd2f 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -19,6 +19,9 @@ ### [事件系统 (Event)](./event-system.md) 掌握类型安全的事件系统,实现组件间通信和系统协作。 +### [序列化系统 (Serialization)](./serialization.md) +掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。 + ### [时间和定时器 (Time)](./time-and-timers.md) 学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。 diff --git a/docs/guide/serialization.md b/docs/guide/serialization.md new file mode 100644 index 00000000..b72e62ad --- /dev/null +++ b/docs/guide/serialization.md @@ -0,0 +1,775 @@ +# 序列化系统 + +序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。 + +## 基本概念 + +序列化系统分为两个层次: + +- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据 +- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量 + +### 支持的数据格式 + +- **JSON格式**:人类可读,便于调试和编辑 +- **Binary格式**:使用MessagePack,体积更小,性能更高 + +## 全量序列化 + +### 基础用法 + +#### 1. 标记可序列化组件 + +使用 `@Serializable` 和 `@Serialize` 装饰器标记需要序列化的组件和字段: + +```typescript +import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework'; + +@ECSComponent('Player') +@Serializable({ version: 1 }) +class PlayerComponent extends Component { + @Serialize() + public name: string = ''; + + @Serialize() + public level: number = 1; + + @Serialize() + public experience: number = 0; + + @Serialize() + public position: { x: number; y: number } = { x: 0, y: 0 }; + + // 不使用 @Serialize() 的字段不会被序列化 + private tempData: any = null; +} +``` + +#### 2. 序列化场景 + +```typescript +// JSON格式序列化 +const jsonData = scene.serialize({ + format: 'json', + pretty: true // 美化输出 +}); + +// 保存到本地存储 +localStorage.setItem('gameSave', jsonData); + +// Binary格式序列化(更小的体积) +const binaryData = scene.serialize({ + format: 'binary' +}); + +// 保存为文件(Node.js环境) +fs.writeFileSync('save.bin', binaryData); +``` + +#### 3. 反序列化场景 + +```typescript +// 从JSON恢复 +const saveData = localStorage.getItem('gameSave'); +if (saveData) { + scene.deserialize(saveData, { + strategy: 'replace' // 替换当前场景内容 + }); +} + +// 从Binary恢复 +const binaryData = fs.readFileSync('save.bin'); +scene.deserialize(binaryData, { + strategy: 'merge' // 合并到现有场景 +}); +``` + +### 序列化选项 + +#### SerializationOptions + +```typescript +interface SceneSerializationOptions { + // 指定要序列化的组件类型(可选) + components?: ComponentType[]; + + // 序列化格式:'json' 或 'binary' + format?: 'json' | 'binary'; + + // JSON美化输出 + pretty?: boolean; + + // 包含元数据 + includeMetadata?: boolean; +} +``` + +示例: + +```typescript +// 只序列化特定组件类型 +const saveData = scene.serialize({ + format: 'json', + components: [PlayerComponent, InventoryComponent], + pretty: true, + includeMetadata: true +}); +``` + +#### DeserializationOptions + +```typescript +interface SceneDeserializationOptions { + // 反序列化策略 + strategy?: 'merge' | 'replace'; + + // 组件类型注册表(可选,默认使用全局注册表) + componentRegistry?: Map; +} +``` + +### 高级装饰器 + +#### 字段序列化选项 + +```typescript +@ECSComponent('Advanced') +@Serializable({ version: 1 }) +class AdvancedComponent extends Component { + // 使用别名 + @Serialize({ alias: 'playerName' }) + public name: string = ''; + + // 自定义序列化器 + @Serialize({ + serializer: (value: Date) => value.toISOString(), + deserializer: (value: string) => new Date(value) + }) + public createdAt: Date = new Date(); + + // 忽略序列化 + @IgnoreSerialization() + public cachedData: any = null; +} +``` + +#### 集合类型序列化 + +```typescript +@ECSComponent('Collections') +@Serializable({ version: 1 }) +class CollectionsComponent extends Component { + // Map序列化 + @SerializeAsMap() + public inventory: Map = new Map(); + + // Set序列化 + @SerializeAsSet() + public acquiredSkills: Set = new Set(); + + constructor() { + super(); + this.inventory.set('gold', 100); + this.inventory.set('silver', 50); + this.acquiredSkills.add('attack'); + this.acquiredSkills.add('defense'); + } +} +``` + +### 场景自定义数据 + +除了实体和组件,还可以序列化场景级别的配置数据: + +```typescript +// 设置场景数据 +scene.sceneData.set('weather', 'rainy'); +scene.sceneData.set('difficulty', 'hard'); +scene.sceneData.set('checkpoint', { x: 100, y: 200 }); + +// 序列化时会自动包含场景数据 +const saveData = scene.serialize({ format: 'json' }); + +// 反序列化后场景数据会恢复 +scene.deserialize(saveData); +console.log(scene.sceneData.get('weather')); // 'rainy' +``` + +## 增量序列化 + +增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。 + +### 基础用法 + +#### 1. 创建基础快照 + +```typescript +// 在需要开始记录变更前创建基础快照 +scene.createIncrementalSnapshot(); +``` + +#### 2. 修改场景 + +```typescript +// 添加实体 +const enemy = scene.createEntity('Enemy'); +enemy.addComponent(new PositionComponent(100, 200)); +enemy.addComponent(new HealthComponent(50)); + +// 修改组件 +const player = scene.findEntity('Player'); +const pos = player.getComponent(PositionComponent); +pos.x = 300; +pos.y = 400; + +// 删除组件 +player.removeComponentByType(BuffComponent); + +// 删除实体 +const oldEntity = scene.findEntity('ToDelete'); +oldEntity.destroy(); + +// 修改场景数据 +scene.sceneData.set('score', 1000); +``` + +#### 3. 获取增量变更 + +```typescript +// 获取相对于基础快照的所有变更 +const incremental = scene.serializeIncremental(); + +// 查看变更统计 +const stats = IncrementalSerializer.getIncrementalStats(incremental); +console.log('总变更数:', stats.totalChanges); +console.log('新增实体:', stats.addedEntities); +console.log('删除实体:', stats.removedEntities); +console.log('新增组件:', stats.addedComponents); +console.log('更新组件:', stats.updatedComponents); +``` + +#### 4. 序列化增量数据 + +```typescript +// 转换为JSON字符串 +const json = IncrementalSerializer.serializeIncremental(incremental); + +// 发送到服务器或保存 +socket.send(json); +// 或 +localStorage.setItem('changes', json); +``` + +#### 5. 应用增量变更 + +```typescript +// 在另一个场景应用变更 +const otherScene = new Scene(); + +// 从JSON字符串应用 +otherScene.applyIncremental(json); + +// 或直接应用增量对象 +otherScene.applyIncremental(incremental); +``` + +### 增量快照管理 + +#### 更新快照基准 + +在应用增量变更后,可以更新快照基准: + +```typescript +// 创建初始快照 +scene.createIncrementalSnapshot(); + +// 第一次修改 +entity.addComponent(new VelocityComponent(5, 0)); +const incremental1 = scene.serializeIncremental(); + +// 更新基准(将当前状态设为新的基准) +scene.updateIncrementalSnapshot(); + +// 第二次修改(增量将基于更新后的基准) +entity.getComponent(VelocityComponent).dx = 10; +const incremental2 = scene.serializeIncremental(); +``` + +#### 清除快照 + +```typescript +// 释放快照占用的内存 +scene.clearIncrementalSnapshot(); + +// 检查是否有快照 +if (scene.hasIncrementalSnapshot()) { + console.log('存在增量快照'); +} +``` + +### 增量序列化选项 + +```typescript +interface IncrementalSerializationOptions { + // 是否进行组件数据的深度对比 + // 默认true,设为false可提升性能但可能漏掉组件内部字段变更 + deepComponentComparison?: boolean; + + // 是否跟踪场景数据变更 + // 默认true + trackSceneData?: boolean; + + // 是否压缩快照(使用JSON序列化) + // 默认false + compressSnapshot?: boolean; +} + +// 使用选项 +scene.createIncrementalSnapshot({ + deepComponentComparison: true, + trackSceneData: true +}); +``` + +### 增量数据结构 + +增量快照包含以下变更类型: + +```typescript +interface IncrementalSnapshot { + version: number; // 快照版本号 + timestamp: number; // 时间戳 + sceneName: string; // 场景名称 + baseVersion: number; // 基础版本号 + entityChanges: EntityChange[]; // 实体变更 + componentChanges: ComponentChange[]; // 组件变更 + sceneDataChanges: SceneDataChange[]; // 场景数据变更 +} + +// 变更操作类型 +enum ChangeOperation { + EntityAdded = 'entity_added', + EntityRemoved = 'entity_removed', + EntityUpdated = 'entity_updated', + ComponentAdded = 'component_added', + ComponentRemoved = 'component_removed', + ComponentUpdated = 'component_updated', + SceneDataUpdated = 'scene_data_updated' +} +``` + +## 版本迁移 + +当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。 + +### 注册迁移函数 + +```typescript +import { VersionMigrationManager } from '@esengine/ecs-framework'; + +// 假设 PlayerComponent v1 有 hp 字段 +// v2 改为 health 和 maxHealth 字段 + +// 注册从版本1到版本2的迁移 +VersionMigrationManager.registerComponentMigration( + 'Player', + 1, // 从版本 + 2, // 到版本 + (data) => { + // 迁移逻辑 + const newData = { + ...data, + health: data.hp, + maxHealth: data.hp, + }; + delete newData.hp; + return newData; + } +); +``` + +### 使用迁移构建器 + +```typescript +import { MigrationBuilder } from '@esengine/ecs-framework'; + +new MigrationBuilder() + .forComponent('Player') + .fromVersionToVersion(2, 3) + .migrate((data) => { + // 从版本2迁移到版本3 + data.experience = data.exp || 0; + delete data.exp; + return data; + }); +``` + +### 场景级迁移 + +```typescript +// 注册场景级迁移 +VersionMigrationManager.registerSceneMigration( + 1, // 从版本 + 2, // 到版本 + (scene) => { + // 迁移场景结构 + scene.metadata = { + ...scene.metadata, + migratedFrom: 1 + }; + return scene; + } +); +``` + +### 检查迁移路径 + +```typescript +// 检查是否可以迁移 +const canMigrate = VersionMigrationManager.canMigrateComponent( + 'Player', + 1, // 从版本 + 3 // 到版本 +); + +if (canMigrate) { + // 可以安全迁移 + scene.deserialize(oldSaveData); +} + +// 获取迁移路径 +const path = VersionMigrationManager.getComponentMigrationPath('Player'); +console.log('可用迁移版本:', path); // [1, 2, 3] +``` + +## 使用场景 + +### 游戏存档系统 + +```typescript +class SaveSystem { + private static SAVE_KEY = 'game_save'; + + // 保存游戏 + public static saveGame(scene: Scene): void { + const saveData = scene.serialize({ + format: 'json', + pretty: false + }); + + localStorage.setItem(this.SAVE_KEY, saveData); + console.log('游戏已保存'); + } + + // 加载游戏 + public static loadGame(scene: Scene): boolean { + const saveData = localStorage.getItem(this.SAVE_KEY); + if (saveData) { + scene.deserialize(saveData, { + strategy: 'replace' + }); + console.log('游戏已加载'); + return true; + } + return false; + } + + // 检查是否有存档 + public static hasSave(): boolean { + return localStorage.getItem(this.SAVE_KEY) !== null; + } +} +``` + +### 网络同步 + +```typescript +class NetworkSync { + private baseSnapshot?: any; + private syncInterval: number = 100; // 100ms同步一次 + + constructor(private scene: Scene, private socket: WebSocket) { + this.setupSync(); + } + + private setupSync(): void { + // 创建基础快照 + this.scene.createIncrementalSnapshot(); + + // 定期发送增量 + setInterval(() => { + this.sendIncremental(); + }, this.syncInterval); + + // 接收远程增量 + this.socket.onmessage = (event) => { + this.receiveIncremental(event.data); + }; + } + + private sendIncremental(): void { + const incremental = this.scene.serializeIncremental(); + const stats = IncrementalSerializer.getIncrementalStats(incremental); + + // 只在有变更时发送 + if (stats.totalChanges > 0) { + const json = IncrementalSerializer.serializeIncremental(incremental); + this.socket.send(json); + + // 更新基准 + this.scene.updateIncrementalSnapshot(); + } + } + + private receiveIncremental(data: string): void { + const incremental = IncrementalSerializer.deserializeIncremental(data); + this.scene.applyIncremental(incremental); + } +} +``` + +### 撤销/重做系统 + +```typescript +class UndoRedoSystem { + private history: IncrementalSnapshot[] = []; + private currentIndex: number = -1; + private maxHistory: number = 50; + + constructor(private scene: Scene) { + // 创建初始快照 + this.scene.createIncrementalSnapshot(); + this.saveState('Initial'); + } + + // 保存当前状态 + public saveState(label: string): void { + const incremental = this.scene.serializeIncremental(); + + // 删除当前位置之后的历史 + this.history = this.history.slice(0, this.currentIndex + 1); + + // 添加新状态 + this.history.push(incremental); + this.currentIndex++; + + // 限制历史记录数量 + if (this.history.length > this.maxHistory) { + this.history.shift(); + this.currentIndex--; + } + + // 更新快照基准 + this.scene.updateIncrementalSnapshot(); + } + + // 撤销 + public undo(): boolean { + if (this.currentIndex > 0) { + this.currentIndex--; + const incremental = this.history[this.currentIndex]; + this.scene.applyIncremental(incremental); + return true; + } + return false; + } + + // 重做 + public redo(): boolean { + if (this.currentIndex < this.history.length - 1) { + this.currentIndex++; + const incremental = this.history[this.currentIndex]; + this.scene.applyIncremental(incremental); + return true; + } + return false; + } + + public canUndo(): boolean { + return this.currentIndex > 0; + } + + public canRedo(): boolean { + return this.currentIndex < this.history.length - 1; + } +} +``` + +### 关卡编辑器 + +```typescript +class LevelEditor { + // 导出关卡 + public exportLevel(scene: Scene, filename: string): void { + const levelData = scene.serialize({ + format: 'json', + pretty: true, + includeMetadata: true + }); + + // 浏览器环境 + const blob = new Blob([levelData], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + // 导入关卡 + public importLevel(scene: Scene, fileContent: string): void { + scene.deserialize(fileContent, { + strategy: 'replace' + }); + } + + // 验证关卡数据 + public validateLevel(saveData: string): boolean { + const validation = SceneSerializer.validate(saveData); + if (!validation.valid) { + console.error('关卡数据无效:', validation.errors); + return false; + } + return true; + } + + // 获取关卡信息(不完全反序列化) + public getLevelInfo(saveData: string): any { + const info = SceneSerializer.getInfo(saveData); + return info; + } +} +``` + +## 性能优化建议 + +### 1. 选择合适的格式 + +- **开发阶段**:使用JSON格式,便于调试和查看 +- **生产环境**:使用Binary格式,减少30-50%的数据大小 + +### 2. 按需序列化 + +```typescript +// 只序列化需要持久化的组件 +const saveData = scene.serialize({ + format: 'binary', + components: [PlayerComponent, InventoryComponent, QuestComponent] +}); +``` + +### 3. 增量序列化优化 + +```typescript +// 对于高频同步,关闭深度对比以提升性能 +scene.createIncrementalSnapshot({ + deepComponentComparison: false // 只检测组件的添加/删除 +}); +``` + +### 4. 批量操作 + +```typescript +// 批量修改后再序列化 +scene.entities.buffer.forEach(entity => { + // 批量修改 +}); + +// 一次性序列化所有变更 +const incremental = scene.serializeIncremental(); +``` + +## 最佳实践 + +### 1. 明确序列化字段 + +```typescript +// 明确标记需要序列化的字段 +@ECSComponent('Player') +@Serializable({ version: 1 }) +class PlayerComponent extends Component { + @Serialize() + public name: string = ''; + + @Serialize() + public level: number = 1; + + // 运行时数据不序列化 + private _cachedSprite: any = null; +} +``` + +### 2. 使用版本控制 + +```typescript +// 为组件指定版本 +@Serializable({ version: 2 }) +class PlayerComponent extends Component { + // 版本2的字段 +} + +// 注册迁移函数确保兼容性 +VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2); +``` + +### 3. 避免循环引用 + +```typescript +// 不要在组件中直接引用其他实体 +@ECSComponent('Follower') +@Serializable({ version: 1 }) +class FollowerComponent extends Component { + // 存储实体ID而不是实体引用 + @Serialize() + public targetId: number = 0; + + // 通过场景查找目标实体 + public getTarget(scene: Scene): Entity | null { + return scene.entities.findEntityById(this.targetId); + } +} +``` + +### 4. 压缩大数据 + +```typescript +// 对于大型数据结构,使用自定义序列化 +@ECSComponent('LargeData') +@Serializable({ version: 1 }) +class LargeDataComponent extends Component { + @Serialize({ + serializer: (data: LargeObject) => compressData(data), + deserializer: (data: CompressedData) => decompressData(data) + }) + public data: LargeObject; +} +``` + +## API参考 + +### 全量序列化API + +- `scene.serialize(options?): string | Buffer` - 序列化场景 +- `scene.deserialize(data, options?)` - 反序列化场景 +- `SceneSerializer.validate(data)` - 验证序列化数据 +- `SceneSerializer.getInfo(data)` - 获取序列化数据信息 + +### 增量序列化API + +- `scene.createIncrementalSnapshot(options?)` - 创建基础快照 +- `scene.serializeIncremental(options?)` - 获取增量变更 +- `scene.applyIncremental(incremental)` - 应用增量变更 +- `scene.updateIncrementalSnapshot(options?)` - 更新快照基准 +- `scene.clearIncrementalSnapshot()` - 清除快照 +- `scene.hasIncrementalSnapshot()` - 检查是否有快照 +- `IncrementalSerializer.getIncrementalStats(incremental)` - 获取统计信息 + +### 版本迁移API + +- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移 +- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移 +- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移 +- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径 + +序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。 diff --git a/packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts b/packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts deleted file mode 100644 index 1fa550c8..00000000 --- a/packages/core/tests/ArchetypeVsComponentIndex.comparison.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -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