From 65386ff731751ccd19fdb4c7f2316ec1189e977f Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 30 Jul 2025 15:42:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96EntitySystem=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E9=80=BB=E8=BE=91/=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=E5=A4=9A=E6=AC=A1=E5=88=9D=E5=A7=8B=E5=8C=96=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0matcher=E5=92=8Centitysystem=E7=9A=84=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ECS/Entity.ts | 4 +- src/ECS/Scene.ts | 3 +- src/ECS/Systems/EntitySystem.ts | 19 + src/ECS/Utils/EntityProcessorList.ts | 5 +- tests/ECS/Core/DecoratorSystem.test.ts | 416 +++++++++++++++ .../ECS/Core/MatcherTimingIntegration.test.ts | 299 +++++++++++ tests/ECS/Core/SystemInitializeIssue.test.ts | 484 +++++++++++++++++ .../ECS/Core/SystemMultipleInitialize.test.ts | 129 +++++ tests/ECS/Core/SystemTimingIssue.test.ts | 359 +++++++++++++ tests/ECS/Utils/Matcher.basic.test.ts | 180 +++++++ tests/ECS/Utils/Matcher.comprehensive.test.ts | 493 ++++++++++++++++++ tests/ECS/Utils/Matcher.integration.test.ts | 273 ++++++++++ tests/ECS/Utils/Matcher.minimal.test.ts | 26 + 13 files changed, 2685 insertions(+), 5 deletions(-) create mode 100644 tests/ECS/Core/DecoratorSystem.test.ts create mode 100644 tests/ECS/Core/MatcherTimingIntegration.test.ts create mode 100644 tests/ECS/Core/SystemInitializeIssue.test.ts create mode 100644 tests/ECS/Core/SystemMultipleInitialize.test.ts create mode 100644 tests/ECS/Core/SystemTimingIssue.test.ts create mode 100644 tests/ECS/Utils/Matcher.basic.test.ts create mode 100644 tests/ECS/Utils/Matcher.comprehensive.test.ts create mode 100644 tests/ECS/Utils/Matcher.integration.test.ts create mode 100644 tests/ECS/Utils/Matcher.minimal.test.ts diff --git a/src/ECS/Entity.ts b/src/ECS/Entity.ts index de431bcd..41debd3a 100644 --- a/src/ECS/Entity.ts +++ b/src/ECS/Entity.ts @@ -527,7 +527,9 @@ export class Entity { } // 调用组件的生命周期方法 - component.onRemovedFromEntity(); + if (component.onRemovedFromEntity) { + component.onRemovedFromEntity(); + } // 发射组件移除事件 if (Entity.eventBus) { diff --git a/src/ECS/Scene.ts b/src/ECS/Scene.ts index 85d7247b..4f40d8ba 100644 --- a/src/ECS/Scene.ts +++ b/src/ECS/Scene.ts @@ -340,7 +340,7 @@ export class Scene { processor.scene = this; this.entityProcessors.add(processor); - + processor.initialize(); processor.setUpdateOrder(this.entityProcessors.count - 1); return processor; } @@ -359,6 +359,7 @@ export class Scene { */ public removeEntityProcessor(processor: EntitySystem) { this.entityProcessors.remove(processor); + processor.reset(); processor.scene = null; } diff --git a/src/ECS/Systems/EntitySystem.ts b/src/ECS/Systems/EntitySystem.ts index f189b8b0..bda0d527 100644 --- a/src/ECS/Systems/EntitySystem.ts +++ b/src/ECS/Systems/EntitySystem.ts @@ -34,6 +34,7 @@ export abstract class EntitySystem implements ISystemBase { private _enabled: boolean = true; private _performanceMonitor = PerformanceMonitor.instance; private _systemName: string; + private _initialized: boolean = false; /** * 获取系统处理的实体列表 @@ -117,9 +118,17 @@ export abstract class EntitySystem implements ISystemBase { * 系统初始化 * * 在系统创建时调用,自动检查场景中已存在的实体是否匹配此系统。 + * 防止重复初始化以避免实体被重复处理。 * 子类可以重写此方法进行额外的初始化操作。 */ public initialize(): void { + // 防止重复初始化 + if (this._initialized) { + return; + } + + this._initialized = true; + if (this.scene?.entities?.buffer) { for (const entity of this.scene.entities.buffer) { this.onChanged(entity); @@ -129,6 +138,16 @@ export abstract class EntitySystem implements ISystemBase { // 子类可以重写此方法进行额外初始化 } + /** + * 重置系统状态 + * + * 当系统从场景中移除时调用,重置初始化状态以便重新添加时能正确初始化。 + */ + public reset(): void { + this._initialized = false; + this._entities.length = 0; + } + /** * 当实体的组件发生变化时调用 * diff --git a/src/ECS/Utils/EntityProcessorList.ts b/src/ECS/Utils/EntityProcessorList.ts index 00ab3082..da35171c 100644 --- a/src/ECS/Utils/EntityProcessorList.ts +++ b/src/ECS/Utils/EntityProcessorList.ts @@ -50,12 +50,11 @@ export class EntityProcessorList { /** * 开始处理 + * + * 对所有处理器进行排序以确保正确的执行顺序。 */ public begin(): void { this.sortProcessors(); - for (const processor of this._processors) { - processor.initialize(); - } } /** diff --git a/tests/ECS/Core/DecoratorSystem.test.ts b/tests/ECS/Core/DecoratorSystem.test.ts new file mode 100644 index 00000000..4e2df6e5 --- /dev/null +++ b/tests/ECS/Core/DecoratorSystem.test.ts @@ -0,0 +1,416 @@ +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; +import { EventBus } from '../../../src/ECS/Core/EventBus'; + +// 测试组件 +class TransformComponent extends Component { + constructor(public x: number = 0, public y: number = 0, public rotation: 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(); + } +} + +// 简单的事件装饰器实现(用于测试) +function EventHandler(eventType: string, priority: number = 0) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + // 在原型上标记事件处理器信息 + if (!target.constructor._eventHandlers) { + target.constructor._eventHandlers = []; + } + target.constructor._eventHandlers.push({ + eventType, + methodName: propertyKey, + priority, + handler: originalMethod + }); + + return descriptor; + }; +} + +// 自动初始化事件监听器的基类 +class EventAwareSystem extends EntitySystem { + private eventListenerIds: string[] = []; + + constructor(matcher: Matcher) { + super(matcher); + } + + public override initialize(): void { + super.initialize(); + this.initializeEventHandlers(); + } + + private initializeEventHandlers(): void { + const eventHandlers = (this.constructor as any)._eventHandlers; + if (!eventHandlers || !this.scene?.eventSystem) { + return; + } + + // 按优先级排序并注册事件处理器 + eventHandlers + .sort((a: any, b: any) => b.priority - a.priority) + .forEach((handlerInfo: any) => { + const listenerId = this.scene!.eventSystem.on( + handlerInfo.eventType, + handlerInfo.handler.bind(this), + { priority: handlerInfo.priority } + ); + this.eventListenerIds.push(listenerId); + }); + } + + public cleanup(): void { + // 清理事件监听器 + if (this.scene?.eventSystem) { + this.eventListenerIds.forEach(id => { + // 注意:这里需要修改EventSystem来支持通过ID移除监听器 + // this.scene!.eventSystem.removeListener(id); + }); + } + this.eventListenerIds = []; + } +} + +// 使用装饰器的测试系统 +class DecoratedMovementSystem extends EventAwareSystem { + public processedEntities: Entity[] = []; + public receivedEvents: any[] = []; + public entityMovedEvents: any[] = []; + + constructor() { + super(Matcher.empty().all(TransformComponent, VelocityComponent)); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + + for (const entity of entities) { + const transform = entity.getComponent(TransformComponent)!; + const velocity = entity.getComponent(VelocityComponent)!; + + const oldX = transform.x; + const oldY = transform.y; + + // 更新位置 + transform.x += velocity.vx; + transform.y += velocity.vy; + + // 发射实体移动事件 + if (this.scene?.eventSystem) { + this.scene.eventSystem.emit('entity:moved', { + entityId: entity.id, + entityName: entity.name, + oldPosition: { x: oldX, y: oldY }, + newPosition: { x: transform.x, y: transform.y } + }); + } + } + } + + @EventHandler('entity:moved', 10) + onEntityMoved(data: any): void { + this.entityMovedEvents.push(data); + } + + @EventHandler('entity:health_changed', 5) + onHealthChanged(data: any): void { + this.receivedEvents.push({ type: 'health_changed', data }); + } + + @EventHandler('system:initialized', 15) + onSystemInitialized(data: any): void { + this.receivedEvents.push({ type: 'system_initialized', data }); + } +} + +class HealthSystem extends EventAwareSystem { + public processedEntities: Entity[] = []; + public receivedEvents: any[] = []; + + constructor() { + super(Matcher.empty().all(HealthComponent)); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + + for (const entity of entities) { + const health = entity.getComponent(HealthComponent)!; + + // 模拟健康值变化 + if (health.health > 0) { + const oldHealth = health.health; + health.health = Math.max(0, health.health - 1); + + // 发射健康值变化事件 + if (this.scene?.eventSystem && oldHealth !== health.health) { + this.scene.eventSystem.emit('entity:health_changed', { + entityId: entity.id, + entityName: entity.name, + oldHealth, + newHealth: health.health, + isDead: health.health <= 0 + }); + } + } + } + } + + @EventHandler('entity:health_changed', 8) + onHealthChanged(data: any): void { + this.receivedEvents.push(data); + + // 如果实体死亡,禁用它 + if (data.isDead) { + const entity = this.scene?.findEntityById(data.entityId); + if (entity) { + entity.enabled = false; + } + } + } +} + +describe('装饰器系统测试', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + scene.name = "DecoratorTestScene"; + }); + + describe('事件装饰器基础功能', () => { + test('装饰器应该正确注册事件处理器', () => { + const movementSystem = new DecoratedMovementSystem(); + + // 检查装饰器是否正确注册了事件处理器信息 + const eventHandlers = (DecoratedMovementSystem as any)._eventHandlers; + expect(eventHandlers).toBeDefined(); + expect(eventHandlers.length).toBe(3); + + // 检查事件处理器信息 + const entityMovedHandler = eventHandlers.find((h: any) => h.eventType === 'entity:moved'); + expect(entityMovedHandler).toBeDefined(); + expect(entityMovedHandler.priority).toBe(10); + expect(entityMovedHandler.methodName).toBe('onEntityMoved'); + }); + + test('系统初始化时应该自动注册事件监听器', () => { + const entity = scene.createEntity("TestEntity"); + entity.addComponent(new TransformComponent(0, 0)); + entity.addComponent(new VelocityComponent(1, 1)); + + const movementSystem = new DecoratedMovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 验证系统正确初始化 + expect(movementSystem.entities.length).toBe(1); + + // 运行一次更新,应该触发entity:moved事件 + scene.update(); + + // 检查事件是否被正确处理 + expect(movementSystem.entityMovedEvents.length).toBe(1); + expect(movementSystem.entityMovedEvents[0].entityId).toBe(entity.id); + expect(movementSystem.entityMovedEvents[0].newPosition.x).toBe(1); + expect(movementSystem.entityMovedEvents[0].newPosition.y).toBe(1); + }); + }); + + describe('多系统事件交互', () => { + test('多个系统应该能够响应同一事件', () => { + const entity = scene.createEntity("TestEntity"); + entity.addComponent(new TransformComponent(0, 0)); + entity.addComponent(new VelocityComponent(1, 1)); + entity.addComponent(new HealthComponent(10)); + + const movementSystem = new DecoratedMovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 运行几次更新 + scene.update(); + scene.update(); + scene.update(); + + // 检查健康系统是否处理了健康变化事件 + expect(healthSystem.receivedEvents.length).toBeGreaterThan(0); + + // 检查移动系统是否也接收到了健康变化事件 + const healthChangedEvents = movementSystem.receivedEvents.filter(e => e.type === 'health_changed'); + expect(healthChangedEvents.length).toBeGreaterThan(0); + }); + + test('事件优先级应该正确工作', () => { + const entity = scene.createEntity("TestEntity"); + entity.addComponent(new TransformComponent(0, 0)); + entity.addComponent(new VelocityComponent(1, 1)); + + const movementSystem = new DecoratedMovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 发射系统初始化事件(如果有的话) + if (scene.eventSystem) { + scene.eventSystem.emit('system:initialized', { + systemName: 'DecoratedMovementSystem', + timestamp: Date.now() + }); + } + + // 检查事件是否被接收 + scene.update(); + + // 验证不同优先级的事件都被处理了 + const systemInitEvents = movementSystem.receivedEvents.filter(e => e.type === 'system_initialized'); + expect(systemInitEvents.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe('装饰器系统的时序问题', () => { + test('先添加实体再添加装饰器系统 - 事件应该正常工作', () => { + // 先创建实体 + const entity = scene.createEntity("TestEntity"); + entity.addComponent(new TransformComponent(5, 5)); + entity.addComponent(new VelocityComponent(2, 3)); + + // 然后添加装饰器系统 + const movementSystem = new DecoratedMovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 验证系统正确识别了实体 + expect(movementSystem.entities.length).toBe(1); + + // 运行更新,应该触发事件 + scene.update(); + + // 检查事件装饰器是否正常工作 + expect(movementSystem.entityMovedEvents.length).toBe(1); + expect(movementSystem.entityMovedEvents[0].oldPosition.x).toBe(5); + expect(movementSystem.entityMovedEvents[0].newPosition.x).toBe(7); + }); + + test('动态添加组件后装饰器事件应该正常', () => { + const movementSystem = new DecoratedMovementSystem(); + scene.addEntityProcessor(movementSystem); + + const entity = scene.createEntity("DynamicEntity"); + entity.addComponent(new TransformComponent(0, 0)); + + // 初始状态:系统不匹配 + expect(movementSystem.entities.length).toBe(0); + + // 动态添加速度组件 + entity.addComponent(new VelocityComponent(1, 1)); + + // 系统应该匹配 + expect(movementSystem.entities.length).toBe(1); + + // 运行更新,事件应该正常触发 + scene.update(); + expect(movementSystem.entityMovedEvents.length).toBe(1); + }); + }); + + describe('装饰器系统的清理', () => { + test('系统移除时应该清理事件监听器', () => { + const movementSystem = new DecoratedMovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 验证系统已添加 + expect(scene.entityProcessors.count).toBe(1); + + // 移除系统 + scene.removeEntityProcessor(movementSystem); + expect(scene.entityProcessors.count).toBe(0); + + // 清理事件监听器 + movementSystem.cleanup(); + + // 验证事件监听器已清理(这里主要是检查不抛出异常) + expect(() => { + if (scene.eventSystem) { + scene.eventSystem.emit('entity:moved', { test: true }); + } + }).not.toThrow(); + }); + }); + + describe('边界情况测试', () => { + test('没有装饰器的系统应该正常工作', () => { + class SimpleSystem extends EventAwareSystem { + public processedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty().all(TransformComponent)); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + } + } + + const entity = scene.createEntity("SimpleEntity"); + entity.addComponent(new TransformComponent(1, 1)); + + const simpleSystem = new SimpleSystem(); + + expect(() => { + scene.addEntityProcessor(simpleSystem); + }).not.toThrow(); + + expect(simpleSystem.entities.length).toBe(1); + + scene.update(); + expect(simpleSystem.processedEntities.length).toBe(1); + }); + + test('空事件数据应该正常处理', () => { + const movementSystem = new DecoratedMovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 发射空事件 + if (scene.eventSystem) { + scene.eventSystem.emit('entity:health_changed', null); + scene.eventSystem.emit('entity:health_changed', undefined); + scene.eventSystem.emit('entity:health_changed', {}); + } + + // 系统应该能够处理空数据而不崩溃 + expect(() => { + scene.update(); + }).not.toThrow(); + }); + }); + + afterEach(() => { + // 清理场景 + scene.destroyAllEntities(); + + // 清理系统并清理它们的事件监听器 + const processors = [...scene.entityProcessors.processors]; + processors.forEach(processor => { + if (processor instanceof EventAwareSystem) { + processor.cleanup(); + } + scene.removeEntityProcessor(processor); + }); + }); +}); \ No newline at end of file diff --git a/tests/ECS/Core/MatcherTimingIntegration.test.ts b/tests/ECS/Core/MatcherTimingIntegration.test.ts new file mode 100644 index 00000000..0a8fece7 --- /dev/null +++ b/tests/ECS/Core/MatcherTimingIntegration.test.ts @@ -0,0 +1,299 @@ +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; +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) { + super(); + } +} + +class MovementSystem extends EntitySystem { + public processedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty().all(PositionComponent, VelocityComponent)); + } + + public override onChanged(entity: Entity): void { + if (this.matcher.isInterestedEntity(entity)) { + if (!this.processedEntities.includes(entity)) { + this.processedEntities.push(entity); + } + } else { + const index = this.processedEntities.indexOf(entity); + if (index !== -1) { + this.processedEntities.splice(index, 1); + } + } + } +} + +class HealthSystem extends EntitySystem { + public processedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty().all(HealthComponent).exclude(VelocityComponent)); + } + + public override onChanged(entity: Entity): void { + if (this.matcher.isInterestedEntity(entity)) { + if (!this.processedEntities.includes(entity)) { + this.processedEntities.push(entity); + } + } else { + const index = this.processedEntities.indexOf(entity); + if (index !== -1) { + this.processedEntities.splice(index, 1); + } + } + } +} + +class CombatSystem extends EntitySystem { + public processedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty() + .all(PositionComponent) + .one(VelocityComponent, HealthComponent)); + } + + public override onChanged(entity: Entity): void { + if (this.matcher.isInterestedEntity(entity)) { + if (!this.processedEntities.includes(entity)) { + this.processedEntities.push(entity); + } + } else { + const index = this.processedEntities.indexOf(entity); + if (index !== -1) { + this.processedEntities.splice(index, 1); + } + } + } +} + +describe('Matcher时序集成测试', () => { + let scene: Scene; + let movementSystem: MovementSystem; + let healthSystem: HealthSystem; + let combatSystem: CombatSystem; + + beforeEach(() => { + ComponentTypeManager.instance.reset(); + scene = new Scene(); + movementSystem = new MovementSystem(); + healthSystem = new HealthSystem(); + combatSystem = new CombatSystem(); + }); + + describe('先添加实体,再添加系统', () => { + test('MovementSystem应该正确过滤实体', () => { + // 创建各种类型的实体 + const movingEntity = scene.createEntity('MovingEntity'); + movingEntity.addComponent(new PositionComponent(10, 20)); + movingEntity.addComponent(new VelocityComponent(1, 1)); + + const staticEntity = scene.createEntity('StaticEntity'); + staticEntity.addComponent(new PositionComponent(30, 40)); + + const healthEntity = scene.createEntity('HealthEntity'); + healthEntity.addComponent(new HealthComponent(80)); + + const complexEntity = scene.createEntity('ComplexEntity'); + complexEntity.addComponent(new PositionComponent(50, 60)); + complexEntity.addComponent(new VelocityComponent(2, 2)); + complexEntity.addComponent(new HealthComponent(120)); + + // 添加系统 - 应该发现已存在的匹配实体 + scene.addEntityProcessor(movementSystem); + + // MovementSystem需要Position + Velocity + expect(movementSystem.processedEntities).toHaveLength(2); + expect(movementSystem.processedEntities).toContain(movingEntity); + expect(movementSystem.processedEntities).toContain(complexEntity); + expect(movementSystem.processedEntities).not.toContain(staticEntity); + expect(movementSystem.processedEntities).not.toContain(healthEntity); + }); + + test('HealthSystem应该正确过滤实体', () => { + // 创建实体 + const healthOnlyEntity = scene.createEntity('HealthOnly'); + healthOnlyEntity.addComponent(new HealthComponent(100)); + + const movingHealthEntity = scene.createEntity('MovingHealth'); + movingHealthEntity.addComponent(new HealthComponent(80)); + movingHealthEntity.addComponent(new VelocityComponent(1, 1)); + + const positionHealthEntity = scene.createEntity('PositionHealth'); + positionHealthEntity.addComponent(new PositionComponent(10, 20)); + positionHealthEntity.addComponent(new HealthComponent(90)); + + // 添加系统 + scene.addEntityProcessor(healthSystem); + + // HealthSystem需要Health但不要Velocity + expect(healthSystem.processedEntities).toHaveLength(2); + expect(healthSystem.processedEntities).toContain(healthOnlyEntity); + expect(healthSystem.processedEntities).toContain(positionHealthEntity); + expect(healthSystem.processedEntities).not.toContain(movingHealthEntity); // 被exclude排除 + }); + + test('CombatSystem复杂匹配应该正确工作', () => { + // 创建实体 + const warriorEntity = scene.createEntity('Warrior'); + warriorEntity.addComponent(new PositionComponent(10, 20)); + warriorEntity.addComponent(new VelocityComponent(1, 1)); + + const guardEntity = scene.createEntity('Guard'); + guardEntity.addComponent(new PositionComponent(30, 40)); + guardEntity.addComponent(new HealthComponent(100)); + + const archerEntity = scene.createEntity('Archer'); + archerEntity.addComponent(new PositionComponent(50, 60)); + archerEntity.addComponent(new VelocityComponent(2, 2)); + archerEntity.addComponent(new HealthComponent(80)); + + const structureEntity = scene.createEntity('Structure'); + structureEntity.addComponent(new PositionComponent(70, 80)); + + // 添加系统 + scene.addEntityProcessor(combatSystem); + + // CombatSystem需要Position + (Velocity OR Health) + expect(combatSystem.processedEntities).toHaveLength(3); + expect(combatSystem.processedEntities).toContain(warriorEntity); // Position + Velocity + expect(combatSystem.processedEntities).toContain(guardEntity); // Position + Health + expect(combatSystem.processedEntities).toContain(archerEntity); // Position + Both + expect(combatSystem.processedEntities).not.toContain(structureEntity); // 只有Position + }); + }); + + describe('先添加系统,再添加实体', () => { + test('系统应该动态发现新添加的实体', () => { + // 先添加系统 + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + scene.addEntityProcessor(combatSystem); + + expect(movementSystem.processedEntities).toHaveLength(0); + expect(healthSystem.processedEntities).toHaveLength(0); + expect(combatSystem.processedEntities).toHaveLength(0); + + // 添加匹配MovementSystem的实体 + const movingEntity = scene.createEntity('MovingEntity'); + movingEntity.addComponent(new PositionComponent(10, 20)); + movingEntity.addComponent(new VelocityComponent(1, 1)); + + expect(movementSystem.processedEntities).toHaveLength(1); + expect(movementSystem.processedEntities).toContain(movingEntity); + expect(combatSystem.processedEntities).toContain(movingEntity); // 也匹配CombatSystem + + // 添加只匹配HealthSystem的实体 + const healthEntity = scene.createEntity('HealthEntity'); + healthEntity.addComponent(new HealthComponent(100)); + + expect(healthSystem.processedEntities).toHaveLength(1); + expect(healthSystem.processedEntities).toContain(healthEntity); + expect(movementSystem.processedEntities).toHaveLength(1); // 不变 + + // 添加匹配CombatSystem但不匹配其他的实体 + const guardEntity = scene.createEntity('GuardEntity'); + guardEntity.addComponent(new PositionComponent(30, 40)); + guardEntity.addComponent(new HealthComponent(120)); + + expect(combatSystem.processedEntities).toHaveLength(2); + expect(combatSystem.processedEntities).toContain(guardEntity); + expect(movementSystem.processedEntities).toHaveLength(1); // 不变 + expect(healthSystem.processedEntities).toHaveLength(2); // 增加 + }); + }); + + describe('混合时序和动态组件变化', () => { + test('实体组件的动态添加移除应该正确更新所有系统', () => { + // 先添加一些系统 + scene.addEntityProcessor(movementSystem); + + // 创建一个只有位置的实体 + const entity = scene.createEntity('DynamicEntity'); + entity.addComponent(new PositionComponent(10, 20)); + + expect(movementSystem.processedEntities).toHaveLength(0); // 缺少Velocity + + // 后添加健康系统 + scene.addEntityProcessor(healthSystem); + expect(healthSystem.processedEntities).toHaveLength(0); // 实体没有Health + + // 添加速度组件 - 应该被MovementSystem发现 + entity.addComponent(new VelocityComponent(1, 1)); + expect(movementSystem.processedEntities).toHaveLength(1); + expect(movementSystem.processedEntities).toContain(entity); + + // 添加战斗系统 + scene.addEntityProcessor(combatSystem); + expect(combatSystem.processedEntities).toContain(entity); // Position + Velocity + + // 添加健康组件 + entity.addComponent(new HealthComponent(100)); + expect(healthSystem.processedEntities).toHaveLength(0); // 被Velocity排除 + expect(combatSystem.processedEntities).toContain(entity); // 仍然匹配 + + // 移除速度组件 + const velocityComponent = entity.getComponent(VelocityComponent); + if (velocityComponent) { + entity.removeComponent(velocityComponent); + } + + expect(movementSystem.processedEntities).toHaveLength(0); // 不再匹配 + expect(healthSystem.processedEntities).toContain(entity); // 现在匹配了 + expect(combatSystem.processedEntities).toContain(entity); // Position + Health + }); + }); + + describe('场景生命周期测试', () => { + test('场景begin()后系统过滤仍然正常工作', () => { + // 添加实体和系统 + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + entity1.addComponent(new VelocityComponent(1, 1)); + + scene.addEntityProcessor(movementSystem); + + expect(movementSystem.processedEntities).toContain(entity1); + + // 调用场景begin + scene.begin(); + + // 添加新实体应该仍然正常工作 + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + entity2.addComponent(new VelocityComponent(2, 2)); + + expect(movementSystem.processedEntities).toHaveLength(2); + expect(movementSystem.processedEntities).toContain(entity2); + + // 动态添加系统也应该正常工作 + scene.addEntityProcessor(healthSystem); + + const healthEntity = scene.createEntity('HealthEntity'); + healthEntity.addComponent(new HealthComponent(100)); + + expect(healthSystem.processedEntities).toContain(healthEntity); + }); + }); +}); \ No newline at end of file diff --git a/tests/ECS/Core/SystemInitializeIssue.test.ts b/tests/ECS/Core/SystemInitializeIssue.test.ts new file mode 100644 index 00000000..fb3cc5f1 --- /dev/null +++ b/tests/ECS/Core/SystemInitializeIssue.test.ts @@ -0,0 +1,484 @@ +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; + +// 测试组件 +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) { + super(); + } +} + +class TagComponent extends Component { + constructor(public tag: string = '') { + super(); + } +} + +// 测试系统 +class MovementSystem extends EntitySystem { + public processedEntities: Entity[] = []; + public initializeCalled = false; + public onAddedEntities: Entity[] = []; + public onRemovedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty().all(PositionComponent, VelocityComponent)); + } + + public override initialize(): void { + this.initializeCalled = true; + super.initialize(); + } + + protected override onAdded(entity: Entity): void { + this.onAddedEntities.push(entity); + } + + protected override onRemoved(entity: Entity): void { + this.onRemovedEntities.push(entity); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + for (const entity of entities) { + const position = entity.getComponent(PositionComponent)!; + const velocity = entity.getComponent(VelocityComponent)!; + position.x += velocity.vx; + position.y += velocity.vy; + } + } +} + +class HealthSystem extends EntitySystem { + public processedEntities: Entity[] = []; + public initializeCalled = false; + public onAddedEntities: Entity[] = []; + public onRemovedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty().all(HealthComponent)); + } + + public override initialize(): void { + this.initializeCalled = true; + super.initialize(); + } + + protected override onAdded(entity: Entity): void { + this.onAddedEntities.push(entity); + } + + protected override onRemoved(entity: Entity): void { + this.onRemovedEntities.push(entity); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + for (const entity of entities) { + const health = entity.getComponent(HealthComponent)!; + if (health.health <= 0) { + entity.enabled = false; + } + } + } +} + +class MultiComponentSystem extends EntitySystem { + public processedEntities: Entity[] = []; + public initializeCalled = false; + + constructor() { + super(Matcher.empty().all(PositionComponent, HealthComponent, TagComponent)); + } + + public override initialize(): void { + this.initializeCalled = true; + super.initialize(); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + } +} + +describe('ECS系统初始化时序问题深度测试', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + scene.name = "InitializeTestScene"; + }); + + describe('基础时序问题测试', () => { + test('先添加实体再添加系统 - 系统应该正确初始化', () => { + // 创建实体并添加组件 + const player = scene.createEntity("Player"); + player.addComponent(new PositionComponent(10, 20)); + player.addComponent(new VelocityComponent(1, 1)); + player.addComponent(new HealthComponent(100)); + + const enemy = scene.createEntity("Enemy"); + enemy.addComponent(new PositionComponent(50, 60)); + enemy.addComponent(new VelocityComponent(-1, 0)); + enemy.addComponent(new HealthComponent(80)); + + // 验证实体已创建 + expect(scene.entities.count).toBe(2); + + // 添加系统 + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 验证initialize方法被调用 + expect(movementSystem.initializeCalled).toBe(true); + expect(healthSystem.initializeCalled).toBe(true); + + // 验证系统正确识别已存在的实体 + expect(movementSystem.entities.length).toBe(2); + expect(healthSystem.entities.length).toBe(2); + + // 验证onAdded回调被正确调用 + expect(movementSystem.onAddedEntities.length).toBe(2); + expect(movementSystem.onAddedEntities).toContain(player); + expect(movementSystem.onAddedEntities).toContain(enemy); + + // 运行更新确认处理 + scene.update(); + expect(movementSystem.processedEntities.length).toBe(2); + expect(healthSystem.processedEntities.length).toBe(2); + + // 检查移动逻辑是否生效 + const playerPos = player.getComponent(PositionComponent)!; + expect(playerPos.x).toBe(11); + expect(playerPos.y).toBe(21); + }); + + test('先添加系统再添加实体 - 正常工作', () => { + // 先添加系统 + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 验证initialize被调用,但没有发现实体 + expect(movementSystem.initializeCalled).toBe(true); + expect(healthSystem.initializeCalled).toBe(true); + expect(movementSystem.entities.length).toBe(0); + expect(healthSystem.entities.length).toBe(0); + + // 创建实体 + const player = scene.createEntity("Player"); + player.addComponent(new PositionComponent(10, 20)); + player.addComponent(new VelocityComponent(1, 1)); + player.addComponent(new HealthComponent(100)); + + // 系统应该自动识别新实体 + expect(movementSystem.entities.length).toBe(1); + expect(healthSystem.entities.length).toBe(1); + expect(movementSystem.onAddedEntities.length).toBe(1); + }); + }); + + describe('复杂场景的时序测试', () => { + test('部分匹配实体的初始化', () => { + // 创建不同类型的实体 + const fullEntity = scene.createEntity("FullEntity"); + fullEntity.addComponent(new PositionComponent(0, 0)); + fullEntity.addComponent(new VelocityComponent(1, 1)); + fullEntity.addComponent(new HealthComponent(100)); + + const partialEntity1 = scene.createEntity("PartialEntity1"); + partialEntity1.addComponent(new PositionComponent(10, 10)); + partialEntity1.addComponent(new HealthComponent(50)); + // 缺少VelocityComponent + + const partialEntity2 = scene.createEntity("PartialEntity2"); + partialEntity2.addComponent(new PositionComponent(20, 20)); + partialEntity2.addComponent(new VelocityComponent(2, 2)); + // 缺少HealthComponent + + // 添加系统 + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 验证选择性匹配 + expect(movementSystem.entities).toContain(fullEntity); + expect(movementSystem.entities).not.toContain(partialEntity1); + expect(movementSystem.entities).toContain(partialEntity2); + expect(movementSystem.entities.length).toBe(2); + + expect(healthSystem.entities).toContain(fullEntity); + expect(healthSystem.entities).toContain(partialEntity1); + expect(healthSystem.entities).not.toContain(partialEntity2); + expect(healthSystem.entities.length).toBe(2); + }); + + test('多组件要求系统的初始化', () => { + // 创建具有不同组件组合的实体 + const entity1 = scene.createEntity("Entity1"); + entity1.addComponent(new PositionComponent(0, 0)); + entity1.addComponent(new HealthComponent(100)); + entity1.addComponent(new TagComponent("player")); + + const entity2 = scene.createEntity("Entity2"); + entity2.addComponent(new PositionComponent(10, 10)); + entity2.addComponent(new HealthComponent(80)); + // 缺少TagComponent + + const entity3 = scene.createEntity("Entity3"); + entity3.addComponent(new PositionComponent(20, 20)); + entity3.addComponent(new TagComponent("enemy")); + // 缺少HealthComponent + + // 添加要求三个组件的系统 + const multiSystem = new MultiComponentSystem(); + scene.addEntityProcessor(multiSystem); + + // 只有entity1应该匹配 + expect(multiSystem.entities.length).toBe(1); + expect(multiSystem.entities).toContain(entity1); + expect(multiSystem.entities).not.toContain(entity2); + expect(multiSystem.entities).not.toContain(entity3); + }); + + test('批量实体创建后的系统初始化', () => { + // 批量创建实体 + const entities = scene.createEntities(10, "BatchEntity"); + + // 为所有实体添加组件 + entities.forEach((entity, index) => { + entity.addComponent(new PositionComponent(index * 10, index * 10)); + entity.addComponent(new VelocityComponent(index, index)); + if (index % 2 === 0) { + entity.addComponent(new HealthComponent(100 - index * 10)); + } + }); + + // 添加系统 + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 验证系统正确处理批量实体 + expect(movementSystem.entities.length).toBe(10); // 所有实体都有Position+Velocity + expect(healthSystem.entities.length).toBe(5); // 只有偶数索引的实体有Health + + // 验证onAdded回调被正确调用 + expect(movementSystem.onAddedEntities.length).toBe(10); + expect(healthSystem.onAddedEntities.length).toBe(5); + }); + }); + + describe('动态组件修改后的系统响应', () => { + test('运行时添加组件 - 系统自动响应', () => { + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 创建只有位置组件的实体 + const entity = scene.createEntity("TestEntity"); + entity.addComponent(new PositionComponent(0, 0)); + + // 系统不应该匹配 + expect(movementSystem.entities.length).toBe(0); + + // 添加速度组件 + entity.addComponent(new VelocityComponent(5, 5)); + + // 系统应该立即识别 + expect(movementSystem.entities.length).toBe(1); + expect(movementSystem.entities).toContain(entity); + expect(movementSystem.onAddedEntities).toContain(entity); + }); + + test('运行时移除组件 - 系统自动响应', () => { + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 创建完整的可移动实体 + const entity = scene.createEntity("MovableEntity"); + entity.addComponent(new PositionComponent(0, 0)); + entity.addComponent(new VelocityComponent(5, 5)); + + // 系统应该识别 + expect(movementSystem.entities.length).toBe(1); + + // 移除速度组件 + const velocityComponent = entity.getComponent(VelocityComponent); + if (velocityComponent) { + entity.removeComponent(velocityComponent); + } + + // 系统应该移除实体 + expect(movementSystem.entities.length).toBe(0); + expect(movementSystem.onRemovedEntities).toContain(entity); + }); + + test('复杂的组件添加移除序列', () => { + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + const entity = scene.createEntity("ComplexEntity"); + + // 初始状态:无组件 + expect(movementSystem.entities.length).toBe(0); + expect(healthSystem.entities.length).toBe(0); + + // 添加位置组件 + entity.addComponent(new PositionComponent(0, 0)); + expect(movementSystem.entities.length).toBe(0); + expect(healthSystem.entities.length).toBe(0); + + // 添加健康组件 + entity.addComponent(new HealthComponent(100)); + expect(movementSystem.entities.length).toBe(0); + expect(healthSystem.entities.length).toBe(1); + + // 添加速度组件 + entity.addComponent(new VelocityComponent(1, 1)); + expect(movementSystem.entities.length).toBe(1); + expect(healthSystem.entities.length).toBe(1); + + // 移除健康组件 + const healthComponent = entity.getComponent(HealthComponent); + if (healthComponent) { + entity.removeComponent(healthComponent); + } + expect(movementSystem.entities.length).toBe(1); + expect(healthSystem.entities.length).toBe(0); + + // 移除位置组件 + const positionComponent = entity.getComponent(PositionComponent); + if (positionComponent) { + entity.removeComponent(positionComponent); + } + expect(movementSystem.entities.length).toBe(0); + expect(healthSystem.entities.length).toBe(0); + }); + }); + + describe('系统重复添加和移除测试', () => { + test('重复添加同一个系统 - 应该忽略', () => { + const movementSystem = new MovementSystem(); + + // 第一次添加 + scene.addEntityProcessor(movementSystem); + expect(scene.entityProcessors.count).toBe(1); + expect(movementSystem.initializeCalled).toBe(true); + + // 重置标志 + movementSystem.initializeCalled = false; + + // 第二次添加同一个系统 + scene.addEntityProcessor(movementSystem); + expect(scene.entityProcessors.count).toBe(1); // 没有增加 + expect(movementSystem.initializeCalled).toBe(false); // initialize不应该再次调用 + }); + + test('添加后移除再添加 - 应该重新初始化', () => { + const entity = scene.createEntity("TestEntity"); + entity.addComponent(new PositionComponent(0, 0)); + entity.addComponent(new VelocityComponent(1, 1)); + + const movementSystem = new MovementSystem(); + + // 第一次添加 + scene.addEntityProcessor(movementSystem); + expect(movementSystem.entities.length).toBe(1); + expect(movementSystem.initializeCalled).toBe(true); + + // 移除系统 + scene.removeEntityProcessor(movementSystem); + expect(scene.entityProcessors.count).toBe(0); + + // 重置状态 + movementSystem.initializeCalled = false; + movementSystem.onAddedEntities = []; + + // 重新添加 + scene.addEntityProcessor(movementSystem); + expect(movementSystem.entities.length).toBe(1); + expect(movementSystem.initializeCalled).toBe(true); + expect(movementSystem.onAddedEntities.length).toBe(1); + }); + }); + + describe('空场景和空系统的边界情况', () => { + test('空场景添加系统 - 不应该出错', () => { + const movementSystem = new MovementSystem(); + + expect(() => { + scene.addEntityProcessor(movementSystem); + }).not.toThrow(); + + expect(movementSystem.initializeCalled).toBe(true); + expect(movementSystem.entities.length).toBe(0); + }); + + test('有实体但没有匹配组件 - 系统应该为空', () => { + // 创建只有健康组件的实体 + const entity = scene.createEntity("HealthOnlyEntity"); + entity.addComponent(new HealthComponent(100)); + + // 添加移动系统(需要Position+Velocity) + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + expect(movementSystem.entities.length).toBe(0); + expect(movementSystem.onAddedEntities.length).toBe(0); + }); + + test('实体被禁用 - 系统仍应包含但不处理', () => { + const entity = scene.createEntity("DisabledEntity"); + entity.addComponent(new PositionComponent(0, 0)); + entity.addComponent(new VelocityComponent(1, 1)); + + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + expect(movementSystem.entities.length).toBe(1); + + // 禁用实体 + entity.enabled = false; + + // 系统仍然包含实体,但处理时应该跳过 + expect(movementSystem.entities.length).toBe(1); + + scene.update(); + // 处理逻辑中应该检查enabled状态 + // 由于实体被禁用,位置不应该改变(这取决于系统实现) + }); + }); + + afterEach(() => { + scene.destroyAllEntities(); + const processors = [...scene.entityProcessors.processors]; + processors.forEach(processor => scene.removeEntityProcessor(processor)); + }); +}); \ No newline at end of file diff --git a/tests/ECS/Core/SystemMultipleInitialize.test.ts b/tests/ECS/Core/SystemMultipleInitialize.test.ts new file mode 100644 index 00000000..de60c61d --- /dev/null +++ b/tests/ECS/Core/SystemMultipleInitialize.test.ts @@ -0,0 +1,129 @@ +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { ComponentTypeManager } from '../../../src/ECS/Utils/ComponentTypeManager'; + +class TestComponent extends Component { + constructor(public value: number = 0) { + super(); + } +} + +class TrackingSystem extends EntitySystem { + public initializeCallCount = 0; + public onChangedCallCount = 0; + public trackedEntities: Entity[] = []; + + public override initialize(): void { + // 必须先调用父类的initialize来检查防重复逻辑 + const wasInitialized = (this as any)._initialized; + super.initialize(); + + // 只有在真正执行初始化时才增加计数 + if (!wasInitialized) { + this.initializeCallCount++; + } + } + + public override onChanged(entity: Entity): void { + this.onChangedCallCount++; + if (this.isInterestedEntity(entity)) { + if (!this.trackedEntities.includes(entity)) { + this.trackedEntities.push(entity); + } + } else { + const index = this.trackedEntities.indexOf(entity); + if (index !== -1) { + this.trackedEntities.splice(index, 1); + } + } + } + + public isInterestedEntity(entity: Entity): boolean { + return entity.hasComponent(TestComponent); + } +} + +describe('系统多次初始化问题测试', () => { + let scene: Scene; + let system: TrackingSystem; + + beforeEach(() => { + ComponentTypeManager.instance.reset(); + scene = new Scene(); + system = new TrackingSystem(); + }); + + test('系统被多次添加到场景 - 应该防止重复初始化', () => { + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new TestComponent(10)); + + // 第一次添加系统 + scene.addEntityProcessor(system); + expect(system.initializeCallCount).toBe(1); + expect(system.trackedEntities.length).toBe(1); + expect(system.onChangedCallCount).toBe(1); + + // 再次添加同一个系统 - 应该被忽略 + scene.addEntityProcessor(system); + expect(system.initializeCallCount).toBe(1); // 不应该增加 + expect(system.trackedEntities.length).toBe(1); // 实体不应该重复 + expect(system.onChangedCallCount).toBe(1); // onChanged不应该重复调用 + }); + + test('手动多次调用initialize - 应该防止重复处理', () => { + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new TestComponent(10)); + + scene.addEntityProcessor(system); + expect(system.initializeCallCount).toBe(1); + expect(system.trackedEntities.length).toBe(1); + expect(system.onChangedCallCount).toBe(1); + + // 手动再次调用initialize - 应该被防止 + system.initialize(); + expect(system.initializeCallCount).toBe(1); // 不应该增加 + expect(system.onChangedCallCount).toBe(1); // onChanged不应该重复调用 + expect(system.trackedEntities.length).toBe(1); + }); + + test('系统被移除后重新添加 - 应该重新初始化', () => { + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new TestComponent(10)); + + // 添加系统 + scene.addEntityProcessor(system); + expect(system.initializeCallCount).toBe(1); + expect(system.trackedEntities.length).toBe(1); + + // 移除系统 + scene.removeEntityProcessor(system); + + // 重新添加系统 - 应该重新初始化 + scene.addEntityProcessor(system); + expect(system.initializeCallCount).toBe(2); // 应该重新初始化 + expect(system.trackedEntities.length).toBe(1); + }); + + test('多个实体的重复初始化应该被防止', () => { + // 创建多个实体 + const entities = []; + for (let i = 0; i < 5; i++) { + const entity = scene.createEntity(`Entity${i}`); + entity.addComponent(new TestComponent(i)); + entities.push(entity); + } + + scene.addEntityProcessor(system); + expect(system.initializeCallCount).toBe(1); + expect(system.trackedEntities.length).toBe(5); + expect(system.onChangedCallCount).toBe(5); + + // 手动再次初始化 - 应该被防止 + system.initialize(); + expect(system.initializeCallCount).toBe(1); // 不应该增加 + expect(system.onChangedCallCount).toBe(5); // 不应该重复处理 + expect(system.trackedEntities.length).toBe(5); + }); +}); \ No newline at end of file diff --git a/tests/ECS/Core/SystemTimingIssue.test.ts b/tests/ECS/Core/SystemTimingIssue.test.ts new file mode 100644 index 00000000..3ea2cd71 --- /dev/null +++ b/tests/ECS/Core/SystemTimingIssue.test.ts @@ -0,0 +1,359 @@ +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; + +// 测试组件 +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) { + super(); + } +} + +// 测试系统 +class MovementSystem extends EntitySystem { + public processedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty().all(PositionComponent, VelocityComponent)); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + // 简单的移动逻辑 + for (const entity of entities) { + const position = entity.getComponent(PositionComponent)!; + const velocity = entity.getComponent(VelocityComponent)!; + position.x += velocity.vx; + position.y += velocity.vy; + } + } +} + +class HealthSystem extends EntitySystem { + public processedEntities: Entity[] = []; + + constructor() { + super(Matcher.empty().all(HealthComponent)); + } + + protected override process(entities: Entity[]): void { + this.processedEntities = [...entities]; + // 简单的健康检查逻辑 + for (const entity of entities) { + const health = entity.getComponent(HealthComponent)!; + if (health.health <= 0) { + // 标记为死亡,但不在这里销毁实体 + entity.enabled = false; + } + } + } +} + +describe('ECS系统时序问题测试', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + scene.name = "TimingTestScene"; + }); + + describe('实体添加时序问题', () => { + test(' 先添加实体再添加系统 - 暴露时序问题', () => { + // 第一步:先创建并添加实体 + const player = scene.createEntity("Player"); + player.addComponent(new PositionComponent(10, 20)); + player.addComponent(new VelocityComponent(1, 1)); + player.addComponent(new HealthComponent(100)); + + const enemy = scene.createEntity("Enemy"); + enemy.addComponent(new PositionComponent(50, 60)); + enemy.addComponent(new VelocityComponent(-1, 0)); + enemy.addComponent(new HealthComponent(80)); + + // 验证实体已创建 + expect(scene.entities.count).toBe(2); + + // 第二步:然后添加系统(这里可能出现时序问题) + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 验证系统已添加 + expect(scene.entityProcessors.count).toBe(2); + + // 关键测试:检查系统是否正确识别了已存在的实体 + console.log("MovementSystem匹配的实体数量:", movementSystem.entities.length); + console.log("HealthSystem匹配的实体数量:", healthSystem.entities.length); + console.log("Player组件:", player.components.map(c => c.constructor.name)); + console.log("Enemy组件:", enemy.components.map(c => c.constructor.name)); + + // 预期结果:移动系统应该匹配到2个实体(都有Position+Velocity) + expect(movementSystem.entities.length).toBe(2); + // 预期结果:健康系统应该匹配到2个实体(都有Health) + expect(healthSystem.entities.length).toBe(2); + + // 运行一次更新看看 + scene.update(); + + // 检查系统是否处理了实体 + expect(movementSystem.processedEntities.length).toBe(2); + expect(healthSystem.processedEntities.length).toBe(2); + + // 检查移动逻辑是否生效 + const playerPos = player.getComponent(PositionComponent)!; + expect(playerPos.x).toBe(11); // 10 + 1 + expect(playerPos.y).toBe(21); // 20 + 1 + }); + + test(' 先添加系统再添加实体 - 正常工作的情况', () => { + // 第一步:先添加系统 + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 验证系统已添加但没有实体 + expect(scene.entityProcessors.count).toBe(2); + expect(movementSystem.entities.length).toBe(0); + expect(healthSystem.entities.length).toBe(0); + + // 第二步:然后创建并添加实体 + const player = scene.createEntity("Player"); + player.addComponent(new PositionComponent(10, 20)); + player.addComponent(new VelocityComponent(1, 1)); + player.addComponent(new HealthComponent(100)); + + const enemy = scene.createEntity("Enemy"); + enemy.addComponent(new PositionComponent(50, 60)); + enemy.addComponent(new VelocityComponent(-1, 0)); + enemy.addComponent(new HealthComponent(80)); + + // 验证实体已创建 + expect(scene.entities.count).toBe(2); + + // 关键测试:检查系统是否正确识别了新添加的实体 + console.log("MovementSystem匹配的实体数量:", movementSystem.entities.length); + console.log("HealthSystem匹配的实体数量:", healthSystem.entities.length); + + // 预期结果:系统应该自动匹配到新实体 + expect(movementSystem.entities.length).toBe(2); + expect(healthSystem.entities.length).toBe(2); + + // 运行一次更新 + scene.update(); + + // 检查系统是否处理了实体 + expect(movementSystem.processedEntities.length).toBe(2); + expect(healthSystem.processedEntities.length).toBe(2); + }); + }); + + describe('组件动态修改时序问题', () => { + test(' 运行时动态添加组件 - 检查系统响应', () => { + // 先设置好系统 + const movementSystem = new MovementSystem(); + const healthSystem = new HealthSystem(); + scene.addEntityProcessor(movementSystem); + scene.addEntityProcessor(healthSystem); + + // 创建只有位置组件的实体 + const entity = scene.createEntity("TestEntity"); + entity.addComponent(new PositionComponent(0, 0)); + + // 初始状态:只有健康系统不匹配,移动系统不匹配 + expect(movementSystem.entities.length).toBe(0); + expect(healthSystem.entities.length).toBe(0); + + // 动态添加速度组件 + entity.addComponent(new VelocityComponent(5, 5)); + + // 关键测试:移动系统是否立即识别到这个实体 + console.log("添加VelocityComponent后MovementSystem实体数:", movementSystem.entities.length); + expect(movementSystem.entities.length).toBe(1); + + // 动态添加健康组件 + entity.addComponent(new HealthComponent(50)); + + // 关键测试:健康系统是否立即识别到这个实体 + console.log("添加HealthComponent后HealthSystem实体数:", healthSystem.entities.length); + expect(healthSystem.entities.length).toBe(1); + + // 运行更新确认处理 + scene.update(); + expect(movementSystem.processedEntities.length).toBe(1); + expect(healthSystem.processedEntities.length).toBe(1); + }); + + test(' 运行时动态移除组件 - 检查系统响应', () => { + // 先设置好系统 + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 创建完整的可移动实体 + const entity = scene.createEntity("MovableEntity"); + entity.addComponent(new PositionComponent(0, 0)); + entity.addComponent(new VelocityComponent(5, 5)); + + // 确认系统识别了实体 + expect(movementSystem.entities.length).toBe(1); + + // 动态移除速度组件 + const velocityComponent = entity.getComponent(VelocityComponent); + if (velocityComponent) { + entity.removeComponent(velocityComponent); + } + + // 关键测试:移动系统是否立即移除了这个实体 + console.log("移除VelocityComponent后MovementSystem实体数:", movementSystem.entities.length); + expect(movementSystem.entities.length).toBe(0); + + // 重新添加速度组件 + entity.addComponent(new VelocityComponent(3, 3)); + + // 关键测试:移动系统是否重新识别到这个实体 + console.log("重新添加VelocityComponent后MovementSystem实体数:", movementSystem.entities.length); + expect(movementSystem.entities.length).toBe(1); + }); + }); + + describe('系统初始化时序问题', () => { + test(' 系统initialize方法调用时机', () => { + // 创建实体 + const entity1 = scene.createEntity("Entity1"); + entity1.addComponent(new PositionComponent(10, 10)); + entity1.addComponent(new VelocityComponent(1, 1)); + + const entity2 = scene.createEntity("Entity2"); + entity2.addComponent(new PositionComponent(20, 20)); + entity2.addComponent(new VelocityComponent(2, 2)); + + // 创建系统但先不添加到场景 + const movementSystem = new MovementSystem(); + + // 检查系统初始状态 + expect(movementSystem.entities.length).toBe(0); + + // 添加系统到场景 + scene.addEntityProcessor(movementSystem); + + // 关键测试:系统是否在添加时正确初始化并发现已存在的实体 + console.log("系统添加后发现的实体数:", movementSystem.entities.length); + + // 这个测试会暴露initialize方法是否被正确调用 + expect(movementSystem.entities.length).toBe(2); + }); + + test(' 批量实体操作的时序问题', () => { + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + // 批量创建实体 + const entities = scene.createEntities(5, "BatchEntity"); + + // 为所有实体添加组件 + entities.forEach((entity, index) => { + entity.addComponent(new PositionComponent(index * 10, index * 10)); + entity.addComponent(new VelocityComponent(index, index)); + }); + + // 关键测试:系统是否识别了所有批量创建的实体 + console.log("批量操作后系统实体数:", movementSystem.entities.length); + expect(movementSystem.entities.length).toBe(5); + + // 运行更新确认处理 + scene.update(); + expect(movementSystem.processedEntities.length).toBe(5); + }); + }); + + describe('事件装饰器时序问题', () => { + // 模拟使用事件装饰器的系统 + class EventDecoratedSystem extends EntitySystem { + public receivedEvents: any[] = []; + + constructor() { + super(Matcher.empty().all(PositionComponent)); + // 模拟装饰器初始化 + this.initEventListeners(); + } + + // 模拟 @EventListener('entity:moved') 装饰器 + private initEventListeners() { + // 这里应该通过装饰器自动完成 + scene.eventSystem?.on('entity:moved', (data) => { + this.onEntityMoved(data); + }); + } + + public onEntityMoved(data: any) { + this.receivedEvents.push(data); + } + + protected override process(entities: Entity[]): void { + // 处理实体移动并发射事件 + for (const entity of entities) { + const pos = entity.getComponent(PositionComponent)!; + // 模拟移动 + pos.x += 1; + + // 发射移动事件 + scene.eventSystem?.emit('entity:moved', { + entityId: entity.id, + position: { x: pos.x, y: pos.y } + }); + } + } + } + + test(' 事件装饰器系统的初始化时序', () => { + // 先创建实体 + const entity = scene.createEntity("EventTestEntity"); + entity.addComponent(new PositionComponent(0, 0)); + + // 然后添加使用事件装饰器的系统 + const eventSystem = new EventDecoratedSystem(); + scene.addEntityProcessor(eventSystem); + + // 验证系统正确识别了实体 + expect(eventSystem.entities.length).toBe(1); + + // 运行更新,应该触发事件 + scene.update(); + + // 关键测试:事件装饰器是否正确工作 + console.log("接收到的事件数量:", eventSystem.receivedEvents.length); + expect(eventSystem.receivedEvents.length).toBe(1); + + // 验证事件数据 + const event = eventSystem.receivedEvents[0]; + expect(event.entityId).toBe(entity.id); + expect(event.position.x).toBe(1); // 移动后的位置 + }); + }); + + afterEach(() => { + // 清理场景 + scene.destroyAllEntities(); + + // 手动清理系统 + const processors = [...scene.entityProcessors.processors]; + processors.forEach(processor => scene.removeEntityProcessor(processor)); + }); +}); \ No newline at end of file diff --git a/tests/ECS/Utils/Matcher.basic.test.ts b/tests/ECS/Utils/Matcher.basic.test.ts new file mode 100644 index 00000000..78cd8b02 --- /dev/null +++ b/tests/ECS/Utils/Matcher.basic.test.ts @@ -0,0 +1,180 @@ +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 TestPositionComponent extends Component { + constructor(public x: number = 0, public y: number = 0) { + super(); + } +} + +class TestVelocityComponent extends Component { + constructor(public vx: number = 0, public vy: number = 0) { + super(); + } +} + +class TestHealthComponent extends Component { + constructor(public health: number = 100) { + 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 TestPositionComponent(10, 20)); + + expect(matcher.isInterestedEntity(entity1)).toBe(true); + expect(matcher.isInterestedEntity(entity2)).toBe(true); + }); + + test('单一all条件匹配', () => { + const matcher = Matcher.empty().all(TestPositionComponent); + + const entityWith = new Entity('With', 1); + entityWith.addComponent(new TestPositionComponent(10, 20)); + + const entityWithout = new Entity('Without', 2); + + expect(matcher.isInterestedEntity(entityWith)).toBe(true); + expect(matcher.isInterestedEntity(entityWithout)).toBe(false); + }); + + test('多个all条件匹配', () => { + const matcher = Matcher.empty().all(TestPositionComponent, TestVelocityComponent); + + const completeEntity = new Entity('Complete', 1); + completeEntity.addComponent(new TestPositionComponent(10, 20)); + completeEntity.addComponent(new TestVelocityComponent(1, 1)); + + const partialEntity = new Entity('Partial', 2); + partialEntity.addComponent(new TestPositionComponent(0, 0)); + + expect(matcher.isInterestedEntity(completeEntity)).toBe(true); + expect(matcher.isInterestedEntity(partialEntity)).toBe(false); + }); + + test('exclude条件匹配', () => { + const matcher = Matcher.empty() + .all(TestPositionComponent) + .exclude(TestHealthComponent); + + const normalEntity = new Entity('Normal', 1); + normalEntity.addComponent(new TestPositionComponent(10, 20)); + + const excludedEntity = new Entity('Excluded', 2); + excludedEntity.addComponent(new TestPositionComponent(50, 60)); + excludedEntity.addComponent(new TestHealthComponent(100)); + + expect(matcher.isInterestedEntity(normalEntity)).toBe(true); + expect(matcher.isInterestedEntity(excludedEntity)).toBe(false); + }); + + test('one条件匹配', () => { + const matcher = Matcher.empty().one(TestVelocityComponent, TestHealthComponent); + + const velocityEntity = new Entity('Velocity', 1); + velocityEntity.addComponent(new TestVelocityComponent(1, 1)); + + const healthEntity = new Entity('Health', 2); + healthEntity.addComponent(new TestHealthComponent(100)); + + const bothEntity = new Entity('Both', 3); + bothEntity.addComponent(new TestVelocityComponent(2, 2)); + bothEntity.addComponent(new TestHealthComponent(80)); + + const neitherEntity = new Entity('Neither', 4); + neitherEntity.addComponent(new TestPositionComponent(0, 0)); + + expect(matcher.isInterestedEntity(velocityEntity)).toBe(true); + expect(matcher.isInterestedEntity(healthEntity)).toBe(true); + expect(matcher.isInterestedEntity(bothEntity)).toBe(true); + expect(matcher.isInterestedEntity(neitherEntity)).toBe(false); + }); + }); + + describe('复杂组合测试', () => { + test('all + exclude组合', () => { + const matcher = Matcher.empty() + .all(TestPositionComponent, TestVelocityComponent) + .exclude(TestHealthComponent); + + const validEntity = new Entity('Valid', 1); + validEntity.addComponent(new TestPositionComponent(10, 20)); + validEntity.addComponent(new TestVelocityComponent(1, 1)); + + const excludedEntity = new Entity('Excluded', 2); + excludedEntity.addComponent(new TestPositionComponent(30, 40)); + excludedEntity.addComponent(new TestVelocityComponent(2, 2)); + excludedEntity.addComponent(new TestHealthComponent(100)); + + expect(matcher.isInterestedEntity(validEntity)).toBe(true); + expect(matcher.isInterestedEntity(excludedEntity)).toBe(false); + }); + + test('all + one组合', () => { + const matcher = Matcher.empty() + .all(TestPositionComponent) + .one(TestVelocityComponent, TestHealthComponent); + + const velocityEntity = new Entity('Velocity', 1); + velocityEntity.addComponent(new TestPositionComponent(10, 20)); + velocityEntity.addComponent(new TestVelocityComponent(1, 1)); + + const healthEntity = new Entity('Health', 2); + healthEntity.addComponent(new TestPositionComponent(30, 40)); + healthEntity.addComponent(new TestHealthComponent(100)); + + const invalidEntity = new Entity('Invalid', 3); + invalidEntity.addComponent(new TestPositionComponent(50, 60)); + + expect(matcher.isInterestedEntity(velocityEntity)).toBe(true); + expect(matcher.isInterestedEntity(healthEntity)).toBe(true); + expect(matcher.isInterestedEntity(invalidEntity)).toBe(false); + }); + }); + + describe('匹配器属性测试', () => { + test('获取匹配器配置', () => { + const matcher = Matcher.empty() + .all(TestPositionComponent, TestVelocityComponent) + .exclude(TestHealthComponent); + + expect(matcher.getAllSet()).toEqual([TestPositionComponent, TestVelocityComponent]); + expect(matcher.getExclusionSet()).toEqual([TestHealthComponent]); + expect(matcher.getOneSet()).toEqual([]); + }); + + test('toString方法', () => { + const emptyMatcher = Matcher.empty(); + expect(emptyMatcher.toString()).toBe('Matcher()'); + + const simpleMatcher = Matcher.empty().all(TestPositionComponent); + expect(simpleMatcher.toString()).toContain('all: [TestPositionComponent]'); + }); + + test('链式调用返回同一实例', () => { + const matcher = Matcher.empty(); + const result = matcher.all(TestPositionComponent).exclude(TestHealthComponent); + expect(result).toBe(matcher); + }); + }); + + afterEach(() => { + typeManager.reset(); + }); +}); \ No newline at end of file diff --git a/tests/ECS/Utils/Matcher.comprehensive.test.ts b/tests/ECS/Utils/Matcher.comprehensive.test.ts new file mode 100644 index 00000000..d5af52e1 --- /dev/null +++ b/tests/ECS/Utils/Matcher.comprehensive.test.ts @@ -0,0 +1,493 @@ +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(); + }); +}); \ No newline at end of file diff --git a/tests/ECS/Utils/Matcher.integration.test.ts b/tests/ECS/Utils/Matcher.integration.test.ts new file mode 100644 index 00000000..6d210582 --- /dev/null +++ b/tests/ECS/Utils/Matcher.integration.test.ts @@ -0,0 +1,273 @@ +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 SimplePositionComponent extends Component { + constructor(public x: number = 0, public y: number = 0) { + super(); + } +} + +class SimpleVelocityComponent extends Component { + constructor(public vx: number = 0, public vy: number = 0) { + super(); + } +} + +class SimpleHealthComponent extends Component { + constructor(public health: number = 100) { + super(); + } +} + +describe('Matcher集成测试', () => { + beforeEach(() => { + // 重置组件类型管理器 + ComponentTypeManager.instance.reset(); + // 重置Entity的静态eventBus以避免副作用 + Entity.eventBus = null; + }); + + describe('基础实体匹配测试', () => { + test('空匹配器匹配所有实体', () => { + const matcher = Matcher.empty(); + + const entity1 = new Entity('Entity1', 1); + const entity2 = new Entity('Entity2', 2); + + expect(matcher.isInterestedEntity(entity1)).toBe(true); + expect(matcher.isInterestedEntity(entity2)).toBe(true); + }); + + test('单一组件匹配测试', () => { + const matcher = Matcher.empty().all(SimplePositionComponent); + + const entityWithoutComponents = new Entity('Empty', 1); + expect(matcher.isInterestedEntity(entityWithoutComponents)).toBe(false); + + // 小心添加组件,这里可能是问题所在 + const entityWithPosition = new Entity('WithPosition', 2); + try { + entityWithPosition.addComponent(new SimplePositionComponent(10, 20)); + expect(matcher.isInterestedEntity(entityWithPosition)).toBe(true); + } catch (error) { + console.error('Error adding component:', error); + throw error; + } + }); + + test('多组件匹配测试', () => { + const matcher = Matcher.empty().all(SimplePositionComponent, SimpleVelocityComponent); + + const completeEntity = new Entity('Complete', 1); + completeEntity.addComponent(new SimplePositionComponent(10, 20)); + completeEntity.addComponent(new SimpleVelocityComponent(1, 1)); + + const partialEntity = new Entity('Partial', 2); + partialEntity.addComponent(new SimplePositionComponent(0, 0)); + + expect(matcher.isInterestedEntity(completeEntity)).toBe(true); + expect(matcher.isInterestedEntity(partialEntity)).toBe(false); + }); + + test('排除组件匹配测试', () => { + const matcher = Matcher.empty() + .all(SimplePositionComponent) + .exclude(SimpleHealthComponent); + + const normalEntity = new Entity('Normal', 1); + normalEntity.addComponent(new SimplePositionComponent(10, 20)); + + const excludedEntity = new Entity('Excluded', 2); + excludedEntity.addComponent(new SimplePositionComponent(50, 60)); + excludedEntity.addComponent(new SimpleHealthComponent(100)); + + expect(matcher.isInterestedEntity(normalEntity)).toBe(true); + expect(matcher.isInterestedEntity(excludedEntity)).toBe(false); + }); + + test('任一组件匹配测试', () => { + const matcher = Matcher.empty().one(SimpleVelocityComponent, SimpleHealthComponent); + + const velocityEntity = new Entity('Velocity', 1); + velocityEntity.addComponent(new SimpleVelocityComponent(1, 1)); + + const healthEntity = new Entity('Health', 2); + healthEntity.addComponent(new SimpleHealthComponent(100)); + + const neitherEntity = new Entity('Neither', 3); + neitherEntity.addComponent(new SimplePositionComponent(0, 0)); + + expect(matcher.isInterestedEntity(velocityEntity)).toBe(true); + expect(matcher.isInterestedEntity(healthEntity)).toBe(true); + expect(matcher.isInterestedEntity(neitherEntity)).toBe(false); + }); + }); + + describe('复杂匹配组合测试', () => { + test('all + exclude组合', () => { + const matcher = Matcher.empty() + .all(SimplePositionComponent, SimpleVelocityComponent) + .exclude(SimpleHealthComponent); + + const validEntity = new Entity('Valid', 1); + validEntity.addComponent(new SimplePositionComponent(10, 20)); + validEntity.addComponent(new SimpleVelocityComponent(1, 1)); + + const excludedEntity = new Entity('Excluded', 2); + excludedEntity.addComponent(new SimplePositionComponent(30, 40)); + excludedEntity.addComponent(new SimpleVelocityComponent(2, 2)); + excludedEntity.addComponent(new SimpleHealthComponent(100)); + + const incompleteEntity = new Entity('Incomplete', 3); + incompleteEntity.addComponent(new SimplePositionComponent(50, 60)); + + expect(matcher.isInterestedEntity(validEntity)).toBe(true); + expect(matcher.isInterestedEntity(excludedEntity)).toBe(false); + expect(matcher.isInterestedEntity(incompleteEntity)).toBe(false); + }); + + test('all + one组合', () => { + const matcher = Matcher.empty() + .all(SimplePositionComponent) + .one(SimpleVelocityComponent, SimpleHealthComponent); + + const velocityEntity = new Entity('Velocity', 1); + velocityEntity.addComponent(new SimplePositionComponent(10, 20)); + velocityEntity.addComponent(new SimpleVelocityComponent(1, 1)); + + const healthEntity = new Entity('Health', 2); + healthEntity.addComponent(new SimplePositionComponent(30, 40)); + healthEntity.addComponent(new SimpleHealthComponent(100)); + + const bothEntity = new Entity('Both', 3); + bothEntity.addComponent(new SimplePositionComponent(50, 60)); + bothEntity.addComponent(new SimpleVelocityComponent(2, 2)); + bothEntity.addComponent(new SimpleHealthComponent(80)); + + const invalidEntity = new Entity('Invalid', 4); + invalidEntity.addComponent(new SimplePositionComponent(70, 80)); + + expect(matcher.isInterestedEntity(velocityEntity)).toBe(true); + expect(matcher.isInterestedEntity(healthEntity)).toBe(true); + expect(matcher.isInterestedEntity(bothEntity)).toBe(true); + expect(matcher.isInterestedEntity(invalidEntity)).toBe(false); + }); + }); + + describe('动态组件变化测试', () => { + test('实体组件动态添加', () => { + const matcher = Matcher.empty().all(SimplePositionComponent, SimpleVelocityComponent); + + const entity = new Entity('Dynamic', 1); + + // 初始状态:不匹配 + expect(matcher.isInterestedEntity(entity)).toBe(false); + + // 添加位置组件:仍不匹配 + entity.addComponent(new SimplePositionComponent(10, 20)); + expect(matcher.isInterestedEntity(entity)).toBe(false); + + // 添加速度组件:现在匹配 + entity.addComponent(new SimpleVelocityComponent(1, 1)); + expect(matcher.isInterestedEntity(entity)).toBe(true); + }); + + test('匹配器配置修改', () => { + const matcher = Matcher.empty().all(SimplePositionComponent); + + const entity = new Entity('Test', 1); + entity.addComponent(new SimplePositionComponent(10, 20)); + entity.addComponent(new SimpleVelocityComponent(1, 1)); + + // 初始匹配 + expect(matcher.isInterestedEntity(entity)).toBe(true); + + // 修改匹配器,添加更多要求 + matcher.all(SimpleHealthComponent); + + // 现在应该不匹配(缺少健康组件) + expect(matcher.isInterestedEntity(entity)).toBe(false); + + // 添加健康组件后应该匹配 + entity.addComponent(new SimpleHealthComponent(100)); + expect(matcher.isInterestedEntity(entity)).toBe(true); + }); + }); + + describe('边界情况测试', () => { + test('空实体测试', () => { + const allMatcher = Matcher.empty().all(SimplePositionComponent); + const excludeMatcher = Matcher.empty().exclude(SimplePositionComponent); + const oneMatcher = Matcher.empty().one(SimplePositionComponent, SimpleVelocityComponent); + + const emptyEntity = new Entity('Empty', 1); + + expect(allMatcher.isInterestedEntity(emptyEntity)).toBe(false); + expect(excludeMatcher.isInterestedEntity(emptyEntity)).toBe(true); + expect(oneMatcher.isInterestedEntity(emptyEntity)).toBe(false); + }); + + test('重复组件类型配置', () => { + const matcher = Matcher.empty() + .all(SimplePositionComponent) + .all(SimplePositionComponent); // 重复添加 + + const entity = new Entity('Test', 1); + entity.addComponent(new SimplePositionComponent(10, 20)); + + // 应该仍然正常工作 + expect(matcher.isInterestedEntity(entity)).toBe(true); + + // 内部应该有重复的组件类型 + expect(matcher.getAllSet().length).toBe(2); + }); + }); + + describe('匹配器工具方法测试', () => { + test('toString方法', () => { + const emptyMatcher = Matcher.empty(); + expect(emptyMatcher.toString()).toBe('Matcher()'); + + const simpleMatcher = Matcher.empty().all(SimplePositionComponent); + const str = simpleMatcher.toString(); + expect(str).toContain('all: [SimplePositionComponent]'); + + const complexMatcher = Matcher.empty() + .all(SimplePositionComponent, SimpleVelocityComponent) + .exclude(SimpleHealthComponent); + + const complexStr = complexMatcher.toString(); + expect(complexStr).toContain('all: [SimplePositionComponent, SimpleVelocityComponent]'); + expect(complexStr).toContain('exclude: [SimpleHealthComponent]'); + }); + + test('配置获取方法', () => { + const matcher = Matcher.empty() + .all(SimplePositionComponent, SimpleVelocityComponent) + .exclude(SimpleHealthComponent); + + expect(matcher.getAllSet()).toEqual([SimplePositionComponent, SimpleVelocityComponent]); + expect(matcher.getExclusionSet()).toEqual([SimpleHealthComponent]); + expect(matcher.getOneSet()).toEqual([]); + }); + + test('链式调用', () => { + const matcher = Matcher.empty(); + const result = matcher + .all(SimplePositionComponent) + .exclude(SimpleHealthComponent) + .one(SimpleVelocityComponent); + + expect(result).toBe(matcher); + }); + }); + + afterEach(() => { + // 清理 + ComponentTypeManager.instance.reset(); + Entity.eventBus = null; + }); +}); \ No newline at end of file diff --git a/tests/ECS/Utils/Matcher.minimal.test.ts b/tests/ECS/Utils/Matcher.minimal.test.ts new file mode 100644 index 00000000..47f70e64 --- /dev/null +++ b/tests/ECS/Utils/Matcher.minimal.test.ts @@ -0,0 +1,26 @@ +import { Matcher } from '../../../src/ECS/Utils/Matcher'; + +describe('Matcher最小测试', () => { + test('创建空匹配器', () => { + const matcher = Matcher.empty(); + expect(matcher).toBeDefined(); + expect(matcher.toString()).toBe('Matcher()'); + }); + + test('匹配器配置方法', () => { + const matcher = Matcher.empty(); + + // 这些方法应该返回匹配器本身 + expect(matcher.all()).toBe(matcher); + expect(matcher.exclude()).toBe(matcher); + expect(matcher.one()).toBe(matcher); + }); + + test('获取匹配器配置', () => { + const matcher = Matcher.empty(); + + expect(matcher.getAllSet()).toEqual([]); + expect(matcher.getExclusionSet()).toEqual([]); + expect(matcher.getOneSet()).toEqual([]); + }); +}); \ No newline at end of file