From 1361fd8a9054217eee2087fbd665d6d224ff08bc Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Mon, 11 Aug 2025 09:01:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=87=BAsoa=E8=A3=85=E9=A5=B0?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 25 +- package.json | 6 +- packages/core/src/ECS/Core/Storage/index.ts | 3 +- packages/core/tests/ECS/Utils/Matcher.test.ts | 617 ++++++++++++------ .../performance/Matcher.performance.test.ts | 538 ++++++++------- packages/math/package.json | 4 +- packages/network/package.json | 2 +- 7 files changed, 745 insertions(+), 450 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a73ce79..9f911b80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -564,6 +564,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@esengine/ecs-framework-math": { + "resolved": "packages/math", + "link": true + }, "node_modules/@esengine/ecs-framework-network": { "resolved": "packages/network", "link": true @@ -11527,9 +11531,28 @@ "typescript": "^5.8.3" } }, + "packages/math": { + "name": "@esengine/ecs-framework-math", + "version": "1.0.2", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "rimraf": "^5.0.0", + "rollup": "^4.42.0", + "rollup-plugin-dts": "^6.2.1", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + } + }, "packages/network": { "name": "@esengine/ecs-framework-network", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "isomorphic-ws": "^5.0.0", diff --git a/package.json b/package.json index 12b46427..2266d182 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "scripts": { "bootstrap": "lerna bootstrap", "clean": "lerna run clean", - "build": "npm run build:core && npm run build:network", + "build": "npm run build:core && npm run build:math && npm run build:network", "build:core": "cd packages/core && npm run build", + "build:math": "cd packages/math && npm run build", "build:network": "cd packages/network && npm run build", - "build:npm": "npm run build:npm:core && npm run build:npm:network", + "build:npm": "npm run build:npm:core && npm run build:npm:math && npm run build:npm:network", "build:npm:core": "cd packages/core && npm run build:npm", + "build:npm:math": "cd packages/math && npm run build:npm", "build:npm:network": "cd packages/network && npm run build:npm", "test": "lerna run test", "test:coverage": "lerna run test:coverage", diff --git a/packages/core/src/ECS/Core/Storage/index.ts b/packages/core/src/ECS/Core/Storage/index.ts index 2ad8f418..17eeb3ea 100644 --- a/packages/core/src/ECS/Core/Storage/index.ts +++ b/packages/core/src/ECS/Core/Storage/index.ts @@ -1,2 +1,3 @@ export { ComponentPool, ComponentPoolManager } from '../ComponentPool'; -export { ComponentStorage, ComponentRegistry } from '../ComponentStorage'; \ No newline at end of file +export { ComponentStorage, ComponentRegistry } from '../ComponentStorage'; +export { EnableSoA, HighPrecision, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage'; \ No newline at end of file diff --git a/packages/core/tests/ECS/Utils/Matcher.test.ts b/packages/core/tests/ECS/Utils/Matcher.test.ts index 117512ef..ddb05fd1 100644 --- a/packages/core/tests/ECS/Utils/Matcher.test.ts +++ b/packages/core/tests/ECS/Utils/Matcher.test.ts @@ -1,14 +1,7 @@ -/** - * Matcher完整测试套件 - * 测试新的Matcher条件构建功能和QuerySystem集成 - */ - -import { Scene } from '../../../src/ECS/Scene'; -import { Entity } from '../../../src/ECS/Entity'; import { Component } from '../../../src/ECS/Component'; import { Matcher } from '../../../src/ECS/Utils/Matcher'; +import { ComponentType } from '../../../src/ECS/Core/ComponentStorage'; -// 测试组件 class Position extends Component { public x: number = 0; public y: number = 0; @@ -43,246 +36,490 @@ class Health extends Component { } } +class Shield extends Component { + public value: number = 50; + + constructor(...args: unknown[]) { + super(); + const [value = 50] = args as [number?]; + this.value = value; + } +} + class Dead extends Component {} -describe('Matcher测试套件', () => { - let scene: Scene; - let entities: Entity[]; +class Weapon extends Component { + public damage: number = 10; - beforeEach(() => { - scene = new Scene(); - scene.begin(); - - // 创建测试实体 - entities = []; - - // 实体1: 移动的活体 - const entity1 = scene.createEntity('MovingAlive'); - entity1.addComponent(new Position(10, 20)); - entity1.addComponent(new Velocity(1, 0)); - entity1.addComponent(new Health(100)); - entities.push(entity1); - - // 实体2: 静止的活体 - const entity2 = scene.createEntity('StillAlive'); - entity2.addComponent(new Position(30, 40)); - entity2.addComponent(new Health(50)); - entities.push(entity2); - - // 实体3: 移动的死体 - const entity3 = scene.createEntity('MovingDead'); - entity3.addComponent(new Position(50, 60)); - entity3.addComponent(new Velocity(0, 1)); - entity3.addComponent(new Dead()); - entities.push(entity3); - - // 实体4: 静止的死体 - const entity4 = scene.createEntity('StillDead'); - entity4.addComponent(new Position(70, 80)); - entity4.addComponent(new Dead()); - entities.push(entity4); - }); - - afterEach(() => { - scene.end(); - }); + constructor(...args: unknown[]) { + super(); + const [damage = 10] = args as [number?]; + this.damage = damage; + } +} - describe('Matcher条件构建测试', () => { - test('Matcher.all()应该创建正确的查询条件', () => { - const matcher = Matcher.all(Position, Health); +describe('Matcher', () => { + describe('静态工厂方法', () => { + test('all() 应该创建包含所有指定组件的条件', () => { + const matcher = Matcher.all(Position, Velocity); const condition = matcher.getCondition(); + expect(condition.all).toHaveLength(2); expect(condition.all).toContain(Position); - expect(condition.all).toContain(Health); - expect(condition.all.length).toBe(2); + expect(condition.all).toContain(Velocity); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); }); - test('Matcher.any()应该创建正确的查询条件', () => { - const matcher = Matcher.any(Health, Dead); + test('any() 应该创建包含任一指定组件的条件', () => { + const matcher = Matcher.any(Health, Shield); const condition = matcher.getCondition(); + expect(condition.any).toHaveLength(2); expect(condition.any).toContain(Health); - expect(condition.any).toContain(Dead); - expect(condition.any.length).toBe(2); + expect(condition.any).toContain(Shield); + expect(condition.all).toHaveLength(0); + expect(condition.none).toHaveLength(0); }); - test('Matcher.none()应该创建正确的查询条件', () => { - const matcher = Matcher.none(Dead); + test('none() 应该创建排除指定组件的条件', () => { + const matcher = Matcher.none(Dead, Weapon); const condition = matcher.getCondition(); + expect(condition.none).toHaveLength(2); expect(condition.none).toContain(Dead); - expect(condition.none.length).toBe(1); + expect(condition.none).toContain(Weapon); + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); }); - test('链式调用应该正确工作', () => { - const matcher = Matcher.all(Position) - .any(Health, Velocity) - .none(Dead); - + test('byTag() 应该创建标签查询条件', () => { + const matcher = Matcher.byTag(123); const condition = matcher.getCondition(); - expect(condition.all).toContain(Position); - expect(condition.any).toContain(Health); - expect(condition.any).toContain(Velocity); - expect(condition.none).toContain(Dead); + + expect(condition.tag).toBe(123); + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); }); - test('byComponent()应该创建单组件查询条件', () => { + test('byName() 应该创建名称查询条件', () => { + const matcher = Matcher.byName('TestEntity'); + const condition = matcher.getCondition(); + + expect(condition.name).toBe('TestEntity'); + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); + }); + + test('byComponent() 应该创建单组件查询条件', () => { const matcher = Matcher.byComponent(Position); const condition = matcher.getCondition(); + expect(condition.component).toBe(Position); + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); + }); + + test('complex() 应该创建空的复杂查询构建器', () => { + const matcher = Matcher.complex(); + const condition = matcher.getCondition(); + + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); + expect(condition.tag).toBeUndefined(); + expect(condition.name).toBeUndefined(); + expect(condition.component).toBeUndefined(); + }); + + test('empty() 应该创建空匹配器', () => { + const matcher = Matcher.empty(); + const condition = matcher.getCondition(); + + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); + expect(condition.tag).toBeUndefined(); + expect(condition.name).toBeUndefined(); + expect(condition.component).toBeUndefined(); + }); + }); + + describe('实例方法', () => { + test('all() 应该添加到 all 条件数组', () => { + const matcher = Matcher.empty(); + matcher.all(Position, Velocity); + const condition = matcher.getCondition(); + + expect(condition.all).toHaveLength(2); + expect(condition.all).toContain(Position); + expect(condition.all).toContain(Velocity); + }); + + test('any() 应该添加到 any 条件数组', () => { + const matcher = Matcher.empty(); + matcher.any(Health, Shield); + const condition = matcher.getCondition(); + + expect(condition.any).toHaveLength(2); + expect(condition.any).toContain(Health); + expect(condition.any).toContain(Shield); + }); + + test('none() 应该添加到 none 条件数组', () => { + const matcher = Matcher.empty(); + matcher.none(Dead); + const condition = matcher.getCondition(); + + expect(condition.none).toHaveLength(1); + expect(condition.none).toContain(Dead); + }); + + test('exclude() 应该是 none() 的别名', () => { + const matcher = Matcher.empty(); + matcher.exclude(Dead, Weapon); + const condition = matcher.getCondition(); + + expect(condition.none).toHaveLength(2); + expect(condition.none).toContain(Dead); + expect(condition.none).toContain(Weapon); + }); + + test('one() 应该是 any() 的别名', () => { + const matcher = Matcher.empty(); + matcher.one(Health, Shield); + const condition = matcher.getCondition(); + + expect(condition.any).toHaveLength(2); + expect(condition.any).toContain(Health); + expect(condition.any).toContain(Shield); + }); + }); + + describe('链式调用', () => { + test('应该支持复杂的链式调用', () => { + const matcher = Matcher.all(Position) + .any(Health, Shield) + .none(Dead) + .withTag(100) + .withName('Player'); + + const condition = matcher.getCondition(); + + expect(condition.all).toContain(Position); + expect(condition.any).toContain(Health); + expect(condition.any).toContain(Shield); + expect(condition.none).toContain(Dead); + expect(condition.tag).toBe(100); + expect(condition.name).toBe('Player'); + }); + + test('多次调用同一方法应该累积组件', () => { + const matcher = Matcher.empty() + .all(Position) + .all(Velocity) + .any(Health) + .any(Shield) + .none(Dead) + .none(Weapon); + + const condition = matcher.getCondition(); + + expect(condition.all).toHaveLength(2); + expect(condition.any).toHaveLength(2); + expect(condition.none).toHaveLength(2); + }); + }); + + describe('条件管理方法', () => { + test('withTag() 应该设置标签条件', () => { + const matcher = Matcher.empty().withTag(42); + const condition = matcher.getCondition(); + + expect(condition.tag).toBe(42); + }); + + test('withName() 应该设置名称条件', () => { + const matcher = Matcher.empty().withName('Enemy'); + const condition = matcher.getCondition(); + + expect(condition.name).toBe('Enemy'); + }); + + test('withComponent() 应该设置单组件条件', () => { + const matcher = Matcher.empty().withComponent(Position); + const condition = matcher.getCondition(); + expect(condition.component).toBe(Position); }); - test('byTag()应该创建标签查询条件', () => { - const matcher = Matcher.byTag(123); + test('withoutTag() 应该移除标签条件', () => { + const matcher = Matcher.byTag(123).withoutTag(); const condition = matcher.getCondition(); - expect(condition.tag).toBe(123); + expect(condition.tag).toBeUndefined(); }); - test('byName()应该创建名称查询条件', () => { - const matcher = Matcher.byName('TestEntity'); + test('withoutName() 应该移除名称条件', () => { + const matcher = Matcher.byName('Test').withoutName(); const condition = matcher.getCondition(); - expect(condition.name).toBe('TestEntity'); + expect(condition.name).toBeUndefined(); + }); + + test('withoutComponent() 应该移除单组件条件', () => { + const matcher = Matcher.byComponent(Position).withoutComponent(); + const condition = matcher.getCondition(); + + expect(condition.component).toBeUndefined(); + }); + + test('覆盖条件应该替换之前的值', () => { + const matcher = Matcher.byTag(100) + .withTag(200) + .withName('First') + .withName('Second') + .withComponent(Position) + .withComponent(Velocity); + + const condition = matcher.getCondition(); + + expect(condition.tag).toBe(200); + expect(condition.name).toBe('Second'); + expect(condition.component).toBe(Velocity); }); }); - describe('QuerySystem集成测试', () => { - test('使用QuerySystem的queryAll()查询所有匹配实体', () => { - const result = scene.querySystem.queryAll(Position, Health); - expect(result.entities.map(e => e.name).sort()).toEqual(['MovingAlive', 'StillAlive']); - }); - - test('使用QuerySystem的queryAny()查询任一匹配实体', () => { - const result = scene.querySystem.queryAny(Health, Dead); - expect(result.entities.length).toBe(4); // 所有实体都有Health或Dead - }); - - test('使用QuerySystem的queryNone()查询排除实体', () => { - const result = scene.querySystem.queryNone(Dead); - const aliveEntities = result.entities.filter(e => e.hasComponent(Position)); - expect(aliveEntities.map(e => e.name).sort()).toEqual(['MovingAlive', 'StillAlive']); - }); - - test('QuerySystem查询性能统计', () => { - scene.querySystem.queryAll(Position, Velocity); - const stats = scene.querySystem.getStats(); + describe('工具方法', () => { + test('isEmpty() 应该正确判断空条件', () => { + const emptyMatcher = Matcher.empty(); + expect(emptyMatcher.isEmpty()).toBe(true); - expect(stats.queryStats.totalQueries).toBeGreaterThan(0); - expect(stats.queryStats.cacheHits).toBeGreaterThanOrEqual(0); + const nonEmptyMatcher = Matcher.all(Position); + expect(nonEmptyMatcher.isEmpty()).toBe(false); + }); + + test('isEmpty() 应该检查所有条件类型', () => { + expect(Matcher.all(Position).isEmpty()).toBe(false); + expect(Matcher.any(Health).isEmpty()).toBe(false); + expect(Matcher.none(Dead).isEmpty()).toBe(false); + expect(Matcher.byTag(1).isEmpty()).toBe(false); + expect(Matcher.byName('test').isEmpty()).toBe(false); + expect(Matcher.byComponent(Position).isEmpty()).toBe(false); + }); + + test('reset() 应该清空所有条件', () => { + const matcher = Matcher.all(Position, Velocity) + .any(Health, Shield) + .none(Dead) + .withTag(123) + .withName('Test') + .withComponent(Weapon); + + matcher.reset(); + const condition = matcher.getCondition(); + + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); + expect(condition.tag).toBeUndefined(); + expect(condition.name).toBeUndefined(); + expect(condition.component).toBeUndefined(); + expect(matcher.isEmpty()).toBe(true); + }); + + test('clone() 应该创建独立的副本', () => { + const original = Matcher.all(Position, Velocity) + .any(Health) + .none(Dead) + .withTag(100) + .withName('Original') + .withComponent(Weapon); + + const cloned = original.clone(); + const originalCondition = original.getCondition(); + const clonedCondition = cloned.getCondition(); + + expect(clonedCondition.all).toEqual(originalCondition.all); + expect(clonedCondition.any).toEqual(originalCondition.any); + expect(clonedCondition.none).toEqual(originalCondition.none); + expect(clonedCondition.tag).toBe(originalCondition.tag); + expect(clonedCondition.name).toBe(originalCondition.name); + expect(clonedCondition.component).toBe(originalCondition.component); + + // 修改克隆不应影响原对象 + cloned.all(Shield).withTag(200); + + expect(original.getCondition().all).not.toContain(Shield); + expect(original.getCondition().tag).toBe(100); + }); + + test('toString() 应该生成可读的字符串表示', () => { + const matcher = Matcher.all(Position, Velocity) + .any(Health, Shield) + .none(Dead) + .withTag(123) + .withName('TestEntity') + .withComponent(Weapon); + + const str = matcher.toString(); + + expect(str).toContain('all(Position, Velocity)'); + expect(str).toContain('any(Health, Shield)'); + expect(str).toContain('none(Dead)'); + expect(str).toContain('tag(123)'); + expect(str).toContain('name(TestEntity)'); + expect(str).toContain('component(Weapon)'); + expect(str).toContain('Matcher['); + expect(str).toContain(' & '); + }); + + test('toString() 应该处理空条件', () => { + const emptyMatcher = Matcher.empty(); + const str = emptyMatcher.toString(); + + expect(str).toBe('Matcher[]'); + }); + + test('toString() 应该处理部分条件', () => { + const matcher = Matcher.all(Position).withTag(42); + const str = matcher.toString(); + + expect(str).toContain('all(Position)'); + expect(str).toContain('tag(42)'); + expect(str).not.toContain('any('); + expect(str).not.toContain('none('); }); }); - describe('实际使用场景测试', () => { - test('游戏系统中的移动实体查询', () => { - // 查询所有可移动的实体(有位置和速度的) - const movableEntities = scene.querySystem.queryAll(Position, Velocity); - expect(movableEntities.entities.length).toBe(2); // MovingAlive, MovingDead + describe('getCondition() 返回值', () => { + test('应该返回只读的条件副本', () => { + const matcher = Matcher.all(Position, Velocity); + const condition1 = matcher.getCondition(); + const condition2 = matcher.getCondition(); - movableEntities.entities.forEach(entity => { - const pos = entity.getComponent(Position)!; - const vel = entity.getComponent(Velocity)!; - expect(pos).toBeDefined(); - expect(vel).toBeDefined(); - }); + // 应该是不同的对象实例 + expect(condition1).not.toBe(condition2); + + // 但内容应该相同 + expect(condition1.all).toEqual(condition2.all); }); - test('游戏系统中的活体实体查询', () => { - // 查询所有活体实体(有血量,没有死亡标记的) - const aliveEntitiesAll = scene.querySystem.queryAll(Health); - const deadEntitiesAll = scene.querySystem.queryAll(Dead); - - expect(aliveEntitiesAll.entities.length).toBe(2); // MovingAlive, StillAlive - expect(deadEntitiesAll.entities.length).toBe(2); // MovingDead, StillDead - }); - - test('复杂查询:查找活着的移动实体', () => { - // 首先获取所有有位置和速度的实体 - const movableEntities = scene.querySystem.queryAll(Position, Velocity); - - // 然后过滤出活着的(有血量的) - const aliveMovableEntities = movableEntities.entities.filter(entity => - entity.hasComponent(Health) - ); - - expect(aliveMovableEntities.length).toBe(1); // 只有MovingAlive - expect(aliveMovableEntities[0].name).toBe('MovingAlive'); - }); - - test('复合查询条件应用', () => { - // 使用Matcher建立复杂条件,然后用QuerySystem执行 - const matcher = Matcher.all(Position).any(Health, Dead); + test('修改返回的条件不应影响原 Matcher', () => { + const matcher = Matcher.all(Position); + const condition = matcher.getCondition(); + + // 尝试修改返回的条件 + condition.all.push(Velocity as ComponentType); + + // 原 Matcher 不应被影响 + const freshCondition = matcher.getCondition(); + expect(freshCondition.all).toHaveLength(1); + expect(freshCondition.all).toContain(Position); + expect(freshCondition.all).not.toContain(Velocity); + }); + }); + + describe('边界情况', () => { + test('应该处理空参数调用', () => { + const matcher = Matcher.empty(); + + matcher.all(); + matcher.any(); + matcher.none(); + + const condition = matcher.getCondition(); + expect(condition.all).toHaveLength(0); + expect(condition.any).toHaveLength(0); + expect(condition.none).toHaveLength(0); + }); + + test('应该处理重复的组件类型', () => { + const matcher = Matcher.all(Position, Position, Velocity); + const condition = matcher.getCondition(); + + expect(condition.all).toHaveLength(3); + expect(condition.all.filter(t => t === Position)).toHaveLength(2); + }); + + test('应该处理标签值为0的情况', () => { + const matcher = Matcher.byTag(0); + const condition = matcher.getCondition(); + + expect(condition.tag).toBe(0); + expect(matcher.isEmpty()).toBe(false); + }); + + test('应该处理空字符串名称', () => { + const matcher = Matcher.byName(''); + const condition = matcher.getCondition(); + + expect(condition.name).toBe(''); + expect(matcher.isEmpty()).toBe(false); + }); + + test('reset() 应该返回自身以支持链式调用', () => { + const matcher = Matcher.all(Position); + const result = matcher.reset(); + + expect(result).toBe(matcher); + }); + + test('所有实例方法都应该返回自身以支持链式调用', () => { + const matcher = Matcher.empty(); + + expect(matcher.all(Position)).toBe(matcher); + expect(matcher.any(Health)).toBe(matcher); + expect(matcher.none(Dead)).toBe(matcher); + expect(matcher.exclude(Weapon)).toBe(matcher); + expect(matcher.one(Shield)).toBe(matcher); + expect(matcher.withTag(1)).toBe(matcher); + expect(matcher.withName('test')).toBe(matcher); + expect(matcher.withComponent(Position)).toBe(matcher); + expect(matcher.withoutTag()).toBe(matcher); + expect(matcher.withoutName()).toBe(matcher); + expect(matcher.withoutComponent()).toBe(matcher); + }); + }); + + describe('类型安全性', () => { + test('ComponentType 应该正确工作', () => { + // 这个测试主要是确保类型编译正确 + const matcher = Matcher.all(Position as ComponentType); const condition = matcher.getCondition(); - // 这里演示如何用条件,实际执行需要QuerySystem支持复合条件 expect(condition.all).toContain(Position); - expect(condition.any).toContain(Health); - expect(condition.any).toContain(Dead); - }); - }); - - describe('性能测试', () => { - test('大量简单查询的性能', () => { - const startTime = performance.now(); - - for (let i = 0; i < 1000; i++) { - scene.querySystem.queryAll(Position); - } - - const executionTime = performance.now() - startTime; - expect(executionTime).toBeLessThan(100); // 应该在100ms内完成 }); - test('复杂查询的性能', () => { - const startTime = performance.now(); - - for (let i = 0; i < 100; i++) { - scene.querySystem.queryAll(Position, Health); - scene.querySystem.queryAny(Health, Dead); - scene.querySystem.queryNone(Dead); - } - - const executionTime = performance.now() - startTime; - expect(executionTime).toBeLessThan(50); - }); - - test('不存在组件的查询性能', () => { - class NonExistentComponent extends Component { - constructor(...args: unknown[]) { + test('应该支持泛型组件类型', () => { + class GenericComponent extends Component { + public data: T; + constructor(data: T, ...args: unknown[]) { super(); + this.data = data; } } - const result = scene.querySystem.queryAll(NonExistentComponent); - expect(result.entities.length).toBe(0); - }); - }); - - describe('边界情况测试', () => { - test('空查询应该返回所有实体', () => { - const result = scene.querySystem.queryAll(); - expect(result.entities.length).toBe(entities.length); - }); - - test('查询不存在的组件应该返回空结果', () => { - class NonExistentComponent extends Component { + class StringComponent extends GenericComponent { constructor(...args: unknown[]) { - super(); + super(args[0] as string || 'default'); } } - const result = scene.querySystem.queryAll(NonExistentComponent); - expect(result.entities.length).toBe(0); - }); - - test('Matcher条件构建的边界情况', () => { - const emptyMatcher = Matcher.complex(); - const condition = emptyMatcher.getCondition(); + class NumberComponent extends GenericComponent { + constructor(...args: unknown[]) { + super(args[0] as number || 0); + } + } - expect(condition.all.length).toBe(0); - expect(condition.any.length).toBe(0); - expect(condition.none.length).toBe(0); + const matcher = Matcher.all(StringComponent, NumberComponent); + const condition = matcher.getCondition(); + + expect(condition.all).toContain(StringComponent); + expect(condition.all).toContain(NumberComponent); }); }); }); \ No newline at end of file diff --git a/packages/core/tests/performance/Matcher.performance.test.ts b/packages/core/tests/performance/Matcher.performance.test.ts index 0bd50828..dafed869 100644 --- a/packages/core/tests/performance/Matcher.performance.test.ts +++ b/packages/core/tests/performance/Matcher.performance.test.ts @@ -1,15 +1,6 @@ -/** - * Matcher性能测试 - * - * 注意:性能测试结果可能因平台而异,主要用于性能回归检测 - */ - -import { Scene } from '../../src/ECS/Scene'; -import { Entity } from '../../src/ECS/Entity'; import { Component } from '../../src/ECS/Component'; import { Matcher } from '../../src/ECS/Utils/Matcher'; -// 测试组件 class Position extends Component { public x: number = 0; public y: number = 0; @@ -50,260 +41,301 @@ class Weapon extends Component { } } -describe('Matcher性能测试', () => { - let scene: Scene; - - beforeEach(() => { - scene = new Scene(); - scene.begin(); +describe('Matcher 性能测试', () => { + test('大量 Matcher 创建性能', () => { + console.log('\n=== Matcher 创建性能测试 ==='); + + const iterationCount = 10000; + + const staticStart = performance.now(); + for (let i = 0; i < iterationCount; i++) { + Matcher.all(Position, Velocity); + } + const staticTime = performance.now() - staticStart; + + const complexStart = performance.now(); + for (let i = 0; i < iterationCount; i++) { + Matcher.complex() + .all(Position, Velocity) + .any(Health, Weapon) + .none(Weapon); + } + const complexTime = performance.now() - complexStart; + + console.log(`静态方法创建: ${staticTime.toFixed(3)}ms (${(staticTime/iterationCount*1000).toFixed(3)}μs/次)`); + console.log(`复杂链式创建: ${complexTime.toFixed(3)}ms (${(complexTime/iterationCount*1000).toFixed(3)}μs/次)`); + + expect(staticTime).toBeLessThan(1000); + expect(complexTime).toBeLessThan(2000); }); - afterEach(() => { - scene.end(); + test('Matcher getCondition() 性能', () => { + console.log('\n=== getCondition() 性能测试 ==='); + + const matcher = Matcher.all(Position, Velocity, Health) + .any(Weapon) + .none(Health) + .withTag(123) + .withName('TestEntity') + .withComponent(Position); + + const iterationCount = 50000; + + const start = performance.now(); + for (let i = 0; i < iterationCount; i++) { + matcher.getCondition(); + } + const time = performance.now() - start; + + console.log(`getCondition() 调用: ${time.toFixed(3)}ms (${(time/iterationCount*1000).toFixed(3)}μs/次)`); + + expect(time).toBeLessThan(500); }); - const createTestEntities = (count: number): Entity[] => { - const entities: Entity[] = []; - for (let i = 0; i < count; i++) { - const entity = scene.createEntity(`Entity${i}`); + test('Matcher clone() 性能', () => { + console.log('\n=== clone() 性能测试 ==='); + + const originalMatcher = Matcher.all(Position, Velocity, Health) + .any(Weapon) + .none(Health) + .withTag(123) + .withName('TestEntity') + .withComponent(Position); + + const iterationCount = 10000; + + const start = performance.now(); + for (let i = 0; i < iterationCount; i++) { + originalMatcher.clone(); + } + const time = performance.now() - start; + + console.log(`clone() 调用: ${time.toFixed(3)}ms (${(time/iterationCount*1000).toFixed(3)}μs/次)`); + + expect(time).toBeLessThan(1000); + }); + + test('Matcher toString() 性能', () => { + console.log('\n=== toString() 性能测试 ==='); + + const simpleMatcherStart = performance.now(); + const simpleMatcher = Matcher.all(Position); + for (let i = 0; i < 10000; i++) { + simpleMatcher.toString(); + } + const simpleTime = performance.now() - simpleMatcherStart; + + const complexMatcherStart = performance.now(); + const complexMatcher = Matcher.all(Position, Velocity, Health) + .any(Weapon) + .none(Health) + .withTag(123) + .withName('TestEntity') + .withComponent(Position); + for (let i = 0; i < 10000; i++) { + complexMatcher.toString(); + } + const complexTime = performance.now() - complexMatcherStart; + + console.log(`简单 toString(): ${simpleTime.toFixed(3)}ms (${(simpleTime/10000*1000).toFixed(3)}μs/次)`); + console.log(`复杂 toString(): ${complexTime.toFixed(3)}ms (${(complexTime/10000*1000).toFixed(3)}μs/次)`); + + expect(simpleTime).toBeLessThan(200); + expect(complexTime).toBeLessThan(500); + }); + + test('Matcher isEmpty() 性能', () => { + console.log('\n=== isEmpty() 性能测试 ==='); + + const emptyMatcher = Matcher.empty(); + const fullMatcher = Matcher.all(Position, Velocity) + .any(Health) + .none(Weapon) + .withTag(123); + + const iterationCount = 100000; + + const emptyStart = performance.now(); + for (let i = 0; i < iterationCount; i++) { + emptyMatcher.isEmpty(); + } + const emptyTime = performance.now() - emptyStart; + + const fullStart = performance.now(); + for (let i = 0; i < iterationCount; i++) { + fullMatcher.isEmpty(); + } + const fullTime = performance.now() - fullStart; + + console.log(`空匹配器 isEmpty(): ${emptyTime.toFixed(3)}ms (${(emptyTime/iterationCount*1000).toFixed(3)}μs/次)`); + console.log(`复杂匹配器 isEmpty(): ${fullTime.toFixed(3)}ms (${(fullTime/iterationCount*1000).toFixed(3)}μs/次)`); + + expect(emptyTime).toBeLessThan(100); + expect(fullTime).toBeLessThan(200); + }); + + test('Matcher reset() 性能', () => { + console.log('\n=== reset() 性能测试 ==='); + + const iterationCount = 50000; + + const start = performance.now(); + for (let i = 0; i < iterationCount; i++) { + const matcher = Matcher.all(Position, Velocity, Health) + .any(Weapon) + .none(Health) + .withTag(123) + .withName('TestEntity') + .withComponent(Position); + matcher.reset(); + } + const time = performance.now() - start; + + console.log(`reset() 调用: ${time.toFixed(3)}ms (${(time/iterationCount*1000).toFixed(3)}μs/次)`); + + expect(time).toBeLessThan(1000); + }); + + test('大规模链式调用性能', () => { + console.log('\n=== 大规模链式调用性能测试 ==='); + + const iterationCount = 5000; + + const start = performance.now(); + for (let i = 0; i < iterationCount; i++) { + Matcher.empty() + .all(Position) + .all(Velocity) + .all(Health) + .any(Weapon) + .any(Health) + .none(Weapon) + .none(Health) + .withTag(i) + .withName(`Entity${i}`) + .withComponent(Position) + .withoutTag() + .withoutName() + .withoutComponent() + .withTag(i * 2) + .withName(`NewEntity${i}`) + .withComponent(Velocity); + } + const time = performance.now() - start; + + console.log(`大规模链式调用: ${time.toFixed(3)}ms (${(time/iterationCount*1000).toFixed(3)}μs/次)`); + + expect(time).toBeLessThan(2000); + }); + + test('内存分配性能测试', () => { + console.log('\n=== 内存分配性能测试 ==='); + + const iterationCount = 10000; + const matchers: Matcher[] = []; + + const start = performance.now(); + for (let i = 0; i < iterationCount; i++) { + const matcher = Matcher.all(Position, Velocity) + .any(Health, Weapon) + .none(Health) + .withTag(i) + .withName(`Entity${i}`); + matchers.push(matcher); + } + const allocationTime = performance.now() - start; + + const cloneStart = performance.now(); + const clonedMatchers = matchers.map(m => m.clone()); + const cloneTime = performance.now() - cloneStart; + + const conditionStart = performance.now(); + const conditions = matchers.map(m => m.getCondition()); + const conditionTime = performance.now() - conditionStart; + + console.log(`创建 ${iterationCount} 个 Matcher: ${allocationTime.toFixed(3)}ms`); + console.log(`克隆 ${iterationCount} 个 Matcher: ${cloneTime.toFixed(3)}ms`); + console.log(`获取 ${iterationCount} 个条件: ${conditionTime.toFixed(3)}ms`); + + expect(allocationTime).toBeLessThan(1500); + expect(cloneTime).toBeLessThan(1000); + expect(conditionTime).toBeLessThan(500); + + expect(matchers.length).toBe(iterationCount); + expect(clonedMatchers.length).toBe(iterationCount); + expect(conditions.length).toBe(iterationCount); + }); + + test('字符串操作性能对比', () => { + console.log('\n=== 字符串操作性能对比 ==='); + + const simpleMatcher = Matcher.all(Position); + const complexMatcher = Matcher.all(Position, Velocity, Health, Weapon) + .any(Health, Weapon) + .none(Position, Velocity) + .withTag(123456) + .withName('VeryLongEntityNameForPerformanceTesting') + .withComponent(Health); + + const iterationCount = 10000; + + const simpleStart = performance.now(); + for (let i = 0; i < iterationCount; i++) { + simpleMatcher.toString(); + } + const simpleTime = performance.now() - simpleStart; + + const complexStart = performance.now(); + for (let i = 0; i < iterationCount; i++) { + complexMatcher.toString(); + } + const complexTime = performance.now() - complexStart; + + console.log(`简单匹配器字符串化: ${simpleTime.toFixed(3)}ms`); + console.log(`复杂匹配器字符串化: ${complexTime.toFixed(3)}ms`); + console.log(`复杂度影响: ${(complexTime/simpleTime).toFixed(2)}x`); + + expect(simpleTime).toBeLessThan(200); + expect(complexTime).toBeLessThan(800); + }); + + test('批量操作性能基准', () => { + console.log('\n=== 批量操作性能基准 ==='); + + const batchSize = 1000; + const operationCount = 10; + + const totalStart = performance.now(); + + for (let batch = 0; batch < operationCount; batch++) { + const matchers: Matcher[] = []; - // 确定性的组件分配 - if (i % 3 !== 0) entity.addComponent(new Position(i, i)); - if (i % 2 === 0) entity.addComponent(new Velocity(i % 10, i % 10)); - if (i % 4 !== 0) entity.addComponent(new Health(100 - (i % 50))); - if (i % 5 === 0) entity.addComponent(new Weapon(i % 20)); + for (let i = 0; i < batchSize; i++) { + const matcher = Matcher.complex() + .all(Position, Velocity) + .any(Health, Weapon) + .withTag(batch * batchSize + i); + + matchers.push(matcher); + } - entities.push(entity); - } - return entities; - }; - - test('小规模性能测试 (100个实体)', () => { - const entities = createTestEntities(100); - const matcher = Matcher.create(scene.querySystem) - .all(Position, Velocity); - - console.log('\n=== 小规模测试 (100个实体) ==='); - - // 传统逐个检查 - const matcherStart = performance.now(); - let matcherCount = 0; - for (const entity of entities) { - if (matcher.matches(entity)) { - matcherCount++; - } - } - const matcherTime = performance.now() - matcherStart; - - // QuerySystem批量查询 - const queryStart = performance.now(); - const queryResult = scene.querySystem.queryAll(Position, Velocity); - const queryTime = performance.now() - queryStart; - - // Matcher批量查询 - const batchStart = performance.now(); - const batchResult = matcher.query(); - const batchTime = performance.now() - batchStart; - - console.log(`传统逐个: ${matcherTime.toFixed(3)}ms (${matcherCount}个匹配)`); - console.log(`QuerySystem: ${queryTime.toFixed(3)}ms (${queryResult.count}个匹配)`); - console.log(`Matcher批量: ${batchTime.toFixed(3)}ms (${batchResult.length}个匹配)`); - console.log(`性能提升: ${(matcherTime/batchTime).toFixed(1)}x`); - - // 验证结果一致性 - expect(matcherCount).toBe(queryResult.count); - expect(batchResult.length).toBe(queryResult.count); - - // 性能断言(宽松,避免平台差异) - expect(batchTime).toBeLessThan(matcherTime * 2); - }); - - test('中等规模性能测试 (1000个实体)', () => { - const entities = createTestEntities(1000); - const matcher = Matcher.create(scene.querySystem) - .all(Position, Velocity); - - console.log('\n=== 中等规模测试 (1000个实体) ==='); - - // 传统方式 - const matcherStart = performance.now(); - let matcherCount = 0; - for (const entity of entities) { - if (matcher.matches(entity)) { - matcherCount++; - } - } - const matcherTime = performance.now() - matcherStart; - - // QuerySystem方式 - const queryStart = performance.now(); - const queryResult = scene.querySystem.queryAll(Position, Velocity); - const queryTime = performance.now() - queryStart; - - console.log(`传统逐个: ${matcherTime.toFixed(3)}ms (${matcherCount}个匹配)`); - console.log(`QuerySystem: ${queryTime.toFixed(3)}ms (${queryResult.count}个匹配)`); - console.log(`性能提升: ${(matcherTime/queryTime).toFixed(1)}x`); - - expect(matcherCount).toBe(queryResult.count); - expect(queryTime).toBeLessThan(matcherTime); - }); - - test('大规模性能测试 (5000个实体)', () => { - const entities = createTestEntities(5000); - const matcher = Matcher.create(scene.querySystem) - .all(Position, Velocity); - - console.log('\n=== 大规模测试 (5000个实体) ==='); - - // 传统方式 - const matcherStart = performance.now(); - let matcherCount = 0; - for (const entity of entities) { - if (matcher.matches(entity)) { - matcherCount++; - } - } - const matcherTime = performance.now() - matcherStart; - - // QuerySystem方式 - const queryStart = performance.now(); - const queryResult = scene.querySystem.queryAll(Position, Velocity); - const queryTime = performance.now() - queryStart; - - console.log(`传统逐个: ${matcherTime.toFixed(3)}ms (${matcherCount}个匹配)`); - console.log(`QuerySystem: ${queryTime.toFixed(3)}ms (${queryResult.count}个匹配)`); - console.log(`性能提升: ${(matcherTime/queryTime).toFixed(1)}x`); - - expect(matcherCount).toBe(queryResult.count); - expect(queryTime).toBeLessThan(matcherTime); - }); - - test('重复查询缓存性能', () => { - createTestEntities(2000); - const matcher = Matcher.create(scene.querySystem) - .all(Position, Velocity); - const repeatCount = 10; - - console.log('\n=== 重复查询缓存测试 (2000个实体, 10次查询) ==='); - - // 首次查询(建立缓存) - const firstResult = matcher.query(); - - // 重复查询测试 - const repeatStart = performance.now(); - for (let i = 0; i < repeatCount; i++) { - const result = matcher.query(); - expect(result.length).toBe(firstResult.length); - } - const repeatTime = performance.now() - repeatStart; - - // QuerySystem重复查询对比 - const queryStart = performance.now(); - for (let i = 0; i < repeatCount; i++) { - scene.querySystem.queryAll(Position, Velocity); - } - const queryTime = performance.now() - queryStart; - - console.log(`Matcher重复: ${repeatTime.toFixed(3)}ms (${(repeatTime/repeatCount).toFixed(3)}ms/次)`); - console.log(`QuerySystem重复: ${queryTime.toFixed(3)}ms (${(queryTime/repeatCount).toFixed(3)}ms/次)`); - console.log(`缓存优势: ${(queryTime/repeatTime).toFixed(1)}x`); - - // 宽松的性能断言(允许平台差异) - expect(repeatTime).toBeLessThan(queryTime * 5); - }); - - test('复杂查询性能测试', () => { - const entities = createTestEntities(1000); - - console.log('\n=== 复杂查询测试 (1000个实体) ==='); - console.log('查询条件: all(Position) + any(Velocity, Weapon) + none(Health)'); - - // Matcher复杂查询 - const matcher = Matcher.create(scene.querySystem) - .all(Position) - .any(Velocity, Weapon) - .none(Health); - - const matcherStart = performance.now(); - let matcherCount = 0; - for (const entity of entities) { - if (matcher.matches(entity)) { - matcherCount++; - } - } - const matcherTime = performance.now() - matcherStart; - - // QuerySystem分步查询 - const queryStart = performance.now(); - const posResult = scene.querySystem.queryAll(Position); - const velResult = scene.querySystem.queryAll(Velocity); - const weaponResult = scene.querySystem.queryAll(Weapon); - const healthResult = scene.querySystem.queryAll(Health); - - const posSet = new Set(posResult.entities); - const velSet = new Set(velResult.entities); - const weaponSet = new Set(weaponResult.entities); - const healthSet = new Set(healthResult.entities); - - let queryCount = 0; - for (const entity of entities) { - const hasPos = posSet.has(entity); - const hasVelOrWeapon = velSet.has(entity) || weaponSet.has(entity); - const hasHealth = healthSet.has(entity); + matchers.forEach(m => { + m.getCondition(); + m.toString(); + m.isEmpty(); + }); - if (hasPos && hasVelOrWeapon && !hasHealth) { - queryCount++; - } + const cloned = matchers.map(m => m.clone()); + cloned.forEach(m => m.reset()); } - const queryTime = performance.now() - queryStart; - console.log(`Matcher复杂: ${matcherTime.toFixed(3)}ms (${matcherCount}个匹配)`); - console.log(`分步QuerySystem: ${queryTime.toFixed(3)}ms (${queryCount}个匹配)`); - console.log(`性能比: ${(matcherTime/queryTime).toFixed(2)}x`); + const totalTime = performance.now() - totalStart; + const totalOperations = batchSize * operationCount * 5; // 每个matcher执行5个操作 - // 验证结果一致性 - expect(matcherCount).toBe(queryCount); + console.log(`批量操作总时间: ${totalTime.toFixed(3)}ms`); + console.log(`总操作数: ${totalOperations}`); + console.log(`平均每操作: ${(totalTime/totalOperations*1000).toFixed(3)}μs`); - // 宽松的性能断言(复杂查询可能有差异) - expect(matcherTime).toBeLessThan(queryTime * 10); - }); - - test('缓存失效性能影响', () => { - createTestEntities(1000); - const matcher = Matcher.create(scene.querySystem) - .all(Position); - - console.log('\n=== 缓存失效性能测试 ==='); - - // 首次查询 - const firstStart = performance.now(); - const firstResult = matcher.query(); - const firstTime = performance.now() - firstStart; - - // 重复查询(缓存命中) - const cachedStart = performance.now(); - const cachedResult = matcher.query(); - const cachedTime = performance.now() - cachedStart; - - // 添加新实体(使缓存失效) - const newEntity = scene.createEntity('CacheInvalidator'); - newEntity.addComponent(new Position(999, 999)); - - // 缓存失效后的查询 - const invalidatedStart = performance.now(); - const invalidatedResult = matcher.query(); - const invalidatedTime = performance.now() - invalidatedStart; - - console.log(`首次查询: ${firstTime.toFixed(3)}ms (${firstResult.length}个结果)`); - console.log(`缓存查询: ${cachedTime.toFixed(3)}ms (${cachedResult.length}个结果)`); - console.log(`失效查询: ${invalidatedTime.toFixed(3)}ms (${invalidatedResult.length}个结果)`); - - // 验证功能正确性 - expect(cachedResult.length).toBe(firstResult.length); - expect(invalidatedResult.length).toBe(firstResult.length + 1); - - // 性能验证 - expect(cachedTime).toBeLessThan(firstTime); - expect(invalidatedTime).toBeGreaterThan(cachedTime); + expect(totalTime).toBeLessThan(5000); }); }); \ No newline at end of file diff --git a/packages/math/package.json b/packages/math/package.json index fd09a9e1..d211a5fe 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@esengine/ecs-framework-math", - "version": "1.0.0", + "version": "1.0.2", "description": "ECS框架2D数学库 - 提供向量、矩阵、几何形状和碰撞检测功能", "type": "module", "main": "bin/index.js", @@ -63,4 +63,4 @@ "url": "https://github.com/esengine/ecs-framework.git", "directory": "packages/math" } -} \ No newline at end of file +} diff --git a/packages/network/package.json b/packages/network/package.json index 2cf21af6..37cd3d58 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -1,6 +1,6 @@ { "name": "@esengine/ecs-framework-network", - "version": "1.0.0", + "version": "1.0.1", "description": "ECS框架网络插件 - 提供TSRPC网络通信、帧同步和快照功能", "type": "module", "main": "bin/index.js",