Files
esengine/tests/ECS/Utils/Matcher.comprehensive.test.ts
YHH 65386ff731 优化EntitySystem初始化逻辑/防止多次初始化
增加matcher和entitysystem的测试
2025-07-30 15:42:19 +08:00

493 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { ComponentTypeManager } from '../../../src/ECS/Utils/ComponentTypeManager';
// 测试组件定义
class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
class VelocityComponent extends Component {
constructor(public vx: number = 0, public vy: number = 0) {
super();
}
}
class HealthComponent extends Component {
constructor(public health: number = 100, public maxHealth: number = 100) {
super();
}
}
class RenderComponent extends Component {
constructor(public visible: boolean = true, public layer: number = 0) {
super();
}
}
class AIComponent extends Component {
constructor(public behavior: string = 'idle') {
super();
}
}
class PlayerComponent extends Component {
constructor(public playerId: number = 0) {
super();
}
}
class WeaponComponent extends Component {
constructor(public damage: number = 10, public range: number = 100) {
super();
}
}
class ArmorComponent extends Component {
constructor(public defense: number = 5) {
super();
}
}
describe('Matcher综合测试', () => {
let typeManager: ComponentTypeManager;
beforeEach(() => {
// 重置组件类型管理器以确保测试隔离
typeManager = ComponentTypeManager.instance;
typeManager.reset();
});
describe('基础匹配器创建和配置', () => {
test('空匹配器应该匹配所有实体', () => {
const matcher = Matcher.empty();
// 创建具有不同组件的实体
const entity1 = new Entity('Entity1', 1);
const entity2 = new Entity('Entity2', 2);
entity2.addComponent(new PositionComponent(10, 20));
const entity3 = new Entity('Entity3', 3);
entity3.addComponent(new PositionComponent(0, 0));
entity3.addComponent(new VelocityComponent(1, 1));
// 空匹配器应该匹配所有实体
expect(matcher.isInterestedEntity(entity1)).toBe(true);
expect(matcher.isInterestedEntity(entity2)).toBe(true);
expect(matcher.isInterestedEntity(entity3)).toBe(true);
});
test('单一all条件匹配器', () => {
const matcher = Matcher.empty().all(PositionComponent);
const entityWithPosition = new Entity('WithPosition', 1);
entityWithPosition.addComponent(new PositionComponent(10, 20));
const entityWithoutPosition = new Entity('WithoutPosition', 2);
entityWithoutPosition.addComponent(new VelocityComponent(1, 1));
expect(matcher.isInterestedEntity(entityWithPosition)).toBe(true);
expect(matcher.isInterestedEntity(entityWithoutPosition)).toBe(false);
});
test('多个all条件匹配器', () => {
const matcher = Matcher.empty().all(PositionComponent, VelocityComponent);
const completeEntity = new Entity('Complete', 1);
completeEntity.addComponent(new PositionComponent(10, 20));
completeEntity.addComponent(new VelocityComponent(1, 1));
const partialEntity1 = new Entity('Partial1', 2);
partialEntity1.addComponent(new PositionComponent(0, 0));
const partialEntity2 = new Entity('Partial2', 3);
partialEntity2.addComponent(new VelocityComponent(1, 1));
const emptyEntity = new Entity('Empty', 4);
expect(matcher.isInterestedEntity(completeEntity)).toBe(true);
expect(matcher.isInterestedEntity(partialEntity1)).toBe(false);
expect(matcher.isInterestedEntity(partialEntity2)).toBe(false);
expect(matcher.isInterestedEntity(emptyEntity)).toBe(false);
});
test('exclude条件匹配器', () => {
const matcher = Matcher.empty()
.all(PositionComponent)
.exclude(AIComponent);
const playerEntity = new Entity('Player', 1);
playerEntity.addComponent(new PositionComponent(10, 20));
playerEntity.addComponent(new PlayerComponent(1));
const aiEntity = new Entity('AI', 2);
aiEntity.addComponent(new PositionComponent(50, 60));
aiEntity.addComponent(new AIComponent('patrol'));
const staticEntity = new Entity('Static', 3);
staticEntity.addComponent(new RenderComponent());
expect(matcher.isInterestedEntity(playerEntity)).toBe(true);
expect(matcher.isInterestedEntity(aiEntity)).toBe(false); // 被exclude排除
expect(matcher.isInterestedEntity(staticEntity)).toBe(false); // 缺少required组件
});
test('one条件匹配器', () => {
const matcher = Matcher.empty().one(WeaponComponent, ArmorComponent);
const weaponEntity = new Entity('Weapon', 1);
weaponEntity.addComponent(new WeaponComponent(15, 150));
const armorEntity = new Entity('Armor', 2);
armorEntity.addComponent(new ArmorComponent(8));
const bothEntity = new Entity('Both', 3);
bothEntity.addComponent(new WeaponComponent(20, 200));
bothEntity.addComponent(new ArmorComponent(10));
const neitherEntity = new Entity('Neither', 4);
neitherEntity.addComponent(new PositionComponent(0, 0));
expect(matcher.isInterestedEntity(weaponEntity)).toBe(true);
expect(matcher.isInterestedEntity(armorEntity)).toBe(true);
expect(matcher.isInterestedEntity(bothEntity)).toBe(true);
expect(matcher.isInterestedEntity(neitherEntity)).toBe(false);
});
});
describe('复杂匹配器组合', () => {
test('all + exclude组合', () => {
// 匹配有位置和速度但不是AI的实体
const matcher = Matcher.empty()
.all(PositionComponent, VelocityComponent)
.exclude(AIComponent);
const playerEntity = new Entity('Player', 1);
playerEntity.addComponent(new PositionComponent(10, 20));
playerEntity.addComponent(new VelocityComponent(2, 2));
playerEntity.addComponent(new PlayerComponent(1));
const aiEntity = new Entity('AI', 2);
aiEntity.addComponent(new PositionComponent(50, 60));
aiEntity.addComponent(new VelocityComponent(1, 0));
aiEntity.addComponent(new AIComponent('chase'));
const incompleteEntity = new Entity('Incomplete', 3);
incompleteEntity.addComponent(new PositionComponent(0, 0));
expect(matcher.isInterestedEntity(playerEntity)).toBe(true);
expect(matcher.isInterestedEntity(aiEntity)).toBe(false);
expect(matcher.isInterestedEntity(incompleteEntity)).toBe(false);
});
test('all + one组合', () => {
// 匹配有位置,且有武器或护甲的实体
const matcher = Matcher.empty()
.all(PositionComponent)
.one(WeaponComponent, ArmorComponent);
const warriorEntity = new Entity('Warrior', 1);
warriorEntity.addComponent(new PositionComponent(10, 20));
warriorEntity.addComponent(new WeaponComponent(25, 180));
const guardEntity = new Entity('Guard', 2);
guardEntity.addComponent(new PositionComponent(30, 40));
guardEntity.addComponent(new ArmorComponent(12));
const knightEntity = new Entity('Knight', 3);
knightEntity.addComponent(new PositionComponent(50, 60));
knightEntity.addComponent(new WeaponComponent(30, 200));
knightEntity.addComponent(new ArmorComponent(15));
const civilianEntity = new Entity('Civilian', 4);
civilianEntity.addComponent(new PositionComponent(70, 80));
civilianEntity.addComponent(new HealthComponent(80));
const weaponNoPositionEntity = new Entity('WeaponNoPos', 5);
weaponNoPositionEntity.addComponent(new WeaponComponent(20, 160));
expect(matcher.isInterestedEntity(warriorEntity)).toBe(true);
expect(matcher.isInterestedEntity(guardEntity)).toBe(true);
expect(matcher.isInterestedEntity(knightEntity)).toBe(true);
expect(matcher.isInterestedEntity(civilianEntity)).toBe(false);
expect(matcher.isInterestedEntity(weaponNoPositionEntity)).toBe(false);
});
test('all + exclude + one组合', () => {
// 匹配有位置和健康有武器或护甲但不是AI的实体
const matcher = Matcher.empty()
.all(PositionComponent, HealthComponent)
.exclude(AIComponent)
.one(WeaponComponent, ArmorComponent);
const playerWarriorEntity = new Entity('PlayerWarrior', 1);
playerWarriorEntity.addComponent(new PositionComponent(10, 20));
playerWarriorEntity.addComponent(new HealthComponent(120));
playerWarriorEntity.addComponent(new WeaponComponent(25, 180));
playerWarriorEntity.addComponent(new PlayerComponent(1));
const aiWarriorEntity = new Entity('AIWarrior', 2);
aiWarriorEntity.addComponent(new PositionComponent(30, 40));
aiWarriorEntity.addComponent(new HealthComponent(100));
aiWarriorEntity.addComponent(new WeaponComponent(20, 160));
aiWarriorEntity.addComponent(new AIComponent('attack'));
const civilianEntity = new Entity('Civilian', 3);
civilianEntity.addComponent(new PositionComponent(50, 60));
civilianEntity.addComponent(new HealthComponent(80));
// 没有武器或护甲
const incompleteEntity = new Entity('Incomplete', 4);
incompleteEntity.addComponent(new PositionComponent(70, 80));
incompleteEntity.addComponent(new WeaponComponent(15, 140));
// 没有健康组件
expect(matcher.isInterestedEntity(playerWarriorEntity)).toBe(true);
expect(matcher.isInterestedEntity(aiWarriorEntity)).toBe(false); // 被AI排除
expect(matcher.isInterestedEntity(civilianEntity)).toBe(false); // 缺少武器/护甲
expect(matcher.isInterestedEntity(incompleteEntity)).toBe(false); // 缺少健康组件
});
});
describe('匹配器性能和缓存测试', () => {
test('位掩码缓存应该正确工作', () => {
const matcher = Matcher.empty().all(PositionComponent, VelocityComponent);
// 创建测试实体
const entity = new Entity('TestEntity', 1);
entity.addComponent(new PositionComponent(10, 20));
entity.addComponent(new VelocityComponent(1, 1));
// 第一次匹配会构建缓存
const result1 = matcher.isInterestedEntity(entity);
// 再次匹配应该使用缓存
const result2 = matcher.isInterestedEntity(entity);
const result3 = matcher.isInterestedEntity(entity);
expect(result1).toBe(true);
expect(result2).toBe(true);
expect(result3).toBe(true);
});
test('修改匹配器后应该重新构建缓存', () => {
const matcher = Matcher.empty().all(PositionComponent);
const entity = new Entity('TestEntity', 1);
entity.addComponent(new PositionComponent(10, 20));
entity.addComponent(new VelocityComponent(1, 1));
// 初始匹配
expect(matcher.isInterestedEntity(entity)).toBe(true);
// 修改匹配器
matcher.all(HealthComponent);
// 应该重新计算匹配结果
expect(matcher.isInterestedEntity(entity)).toBe(false);
// 添加健康组件后应该匹配
entity.addComponent(new HealthComponent(100));
expect(matcher.isInterestedEntity(entity)).toBe(true);
});
test('适量实体匹配性能测试', () => {
const matcher = Matcher.empty()
.all(PositionComponent, VelocityComponent)
.exclude(AIComponent);
// 创建适量测试实体
const entities: Entity[] = [];
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 % 5 === 0) {
entity.addComponent(new AIComponent('patrol'));
}
entities.push(entity);
}
// 测试匹配性能
const startTime = performance.now();
let matchCount = 0;
for (const entity of entities) {
if (matcher.isInterestedEntity(entity)) {
matchCount++;
}
}
const endTime = performance.now();
const executionTime = endTime - startTime;
// 性能验证100个实体的匹配应该在合理时间内完成
expect(executionTime).toBeLessThan(50); // 50ms内完成
// 逻辑验证只有偶数索引且不是5的倍数的实体应该匹配
// 偶数50个5的倍数20个重叠的偶数且是5倍数10个
// 所以匹配的应该是50 - 10 = 40个
expect(matchCount).toBe(40);
});
});
describe('边界情况和错误处理', () => {
test('重复添加同一组件类型', () => {
const matcher = Matcher.empty()
.all(PositionComponent)
.all(PositionComponent); // 重复添加
const entity = new Entity('TestEntity', 1);
entity.addComponent(new PositionComponent(10, 20));
// 应该仍然正常工作
expect(matcher.isInterestedEntity(entity)).toBe(true);
// 检查内部状态
expect(matcher.getAllSet().length).toBe(2); // 会有重复
});
test('空实体匹配测试', () => {
const matchers = [
Matcher.empty().all(PositionComponent),
Matcher.empty().exclude(PositionComponent),
Matcher.empty().one(PositionComponent, VelocityComponent)
];
const emptyEntity = new Entity('EmptyEntity', 1);
expect(matchers[0].isInterestedEntity(emptyEntity)).toBe(false); // all
expect(matchers[1].isInterestedEntity(emptyEntity)).toBe(true); // exclude
expect(matchers[2].isInterestedEntity(emptyEntity)).toBe(false); // one
});
test('实体组件动态变化', () => {
const matcher = Matcher.empty()
.all(PositionComponent, VelocityComponent)
.exclude(AIComponent);
const entity = new Entity('DynamicEntity', 1);
// 初始状态:无组件
expect(matcher.isInterestedEntity(entity)).toBe(false);
// 添加位置组件
entity.addComponent(new PositionComponent(10, 20));
expect(matcher.isInterestedEntity(entity)).toBe(false);
// 添加速度组件
entity.addComponent(new VelocityComponent(1, 1));
expect(matcher.isInterestedEntity(entity)).toBe(true);
// 添加AI组件被排除
entity.addComponent(new AIComponent('idle'));
expect(matcher.isInterestedEntity(entity)).toBe(false);
// 移除AI组件
const aiComponent = entity.getComponent(AIComponent);
if (aiComponent) {
entity.removeComponent(aiComponent);
}
expect(matcher.isInterestedEntity(entity)).toBe(true);
});
test('链式调用应该返回同一个匹配器实例', () => {
const matcher = Matcher.empty();
const result = matcher
.all(PositionComponent)
.exclude(AIComponent)
.one(WeaponComponent, ArmorComponent);
expect(result).toBe(matcher);
});
});
describe('匹配器调试和工具方法', () => {
test('toString方法应该返回有意义的描述', () => {
const emptyMatcher = Matcher.empty();
expect(emptyMatcher.toString()).toBe('Matcher()');
const simpleMatcher = Matcher.empty().all(PositionComponent);
expect(simpleMatcher.toString()).toContain('all: [PositionComponent]');
const complexMatcher = Matcher.empty()
.all(PositionComponent, VelocityComponent)
.exclude(AIComponent)
.one(WeaponComponent, ArmorComponent);
const str = complexMatcher.toString();
expect(str).toContain('all: [PositionComponent, VelocityComponent]');
expect(str).toContain('exclude: [AIComponent]');
expect(str).toContain('one: [WeaponComponent, ArmorComponent]');
});
test('获取匹配器配置', () => {
const matcher = Matcher.empty()
.all(PositionComponent, VelocityComponent)
.exclude(AIComponent, PlayerComponent)
.one(WeaponComponent);
expect(matcher.getAllSet()).toEqual([PositionComponent, VelocityComponent]);
expect(matcher.getExclusionSet()).toEqual([AIComponent, PlayerComponent]);
expect(matcher.getOneSet()).toEqual([WeaponComponent]);
});
});
describe('位掩码直接匹配测试', () => {
test('isInterested方法应该正确处理Bits对象', () => {
const matcher = Matcher.empty().all(PositionComponent, VelocityComponent);
// 创建包含Position和Velocity的位掩码
const matchingBits = typeManager.createBits(PositionComponent, VelocityComponent);
expect(matcher.isInterested(matchingBits)).toBe(true);
// 创建只包含Position的位掩码
const partialBits = typeManager.createBits(PositionComponent);
expect(matcher.isInterested(partialBits)).toBe(false);
// 创建包含Position、Velocity和Health的位掩码
const extraBits = typeManager.createBits(PositionComponent, VelocityComponent, HealthComponent);
expect(matcher.isInterested(extraBits)).toBe(true);
});
test('复杂位掩码匹配测试', () => {
const matcher = Matcher.empty()
.all(PositionComponent)
.exclude(AIComponent)
.one(WeaponComponent, ArmorComponent);
// 匹配情况Position + Weapon
const bits1 = typeManager.createBits(PositionComponent, WeaponComponent);
expect(matcher.isInterested(bits1)).toBe(true);
// 匹配情况Position + Armor
const bits2 = typeManager.createBits(PositionComponent, ArmorComponent);
expect(matcher.isInterested(bits2)).toBe(true);
// 不匹配Position + AI + Weapon被排除
const bits3 = typeManager.createBits(PositionComponent, AIComponent, WeaponComponent);
expect(matcher.isInterested(bits3)).toBe(false);
// 不匹配Position only缺少one条件
const bits4 = typeManager.createBits(PositionComponent);
expect(matcher.isInterested(bits4)).toBe(false);
// 不匹配Weapon only缺少all条件
const bits5 = typeManager.createBits(WeaponComponent);
expect(matcher.isInterested(bits5)).toBe(false);
});
});
afterEach(() => {
// 清理组件类型管理器
typeManager.reset();
});
});