优化EntitySystem初始化逻辑/防止多次初始化
增加matcher和entitysystem的测试
This commit is contained in:
@@ -527,7 +527,9 @@ export class Entity {
|
||||
}
|
||||
|
||||
// 调用组件的生命周期方法
|
||||
component.onRemovedFromEntity();
|
||||
if (component.onRemovedFromEntity) {
|
||||
component.onRemovedFromEntity();
|
||||
}
|
||||
|
||||
// 发射组件移除事件
|
||||
if (Entity.eventBus) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体的组件发生变化时调用
|
||||
*
|
||||
|
||||
@@ -50,12 +50,11 @@ export class EntityProcessorList {
|
||||
|
||||
/**
|
||||
* 开始处理
|
||||
*
|
||||
* 对所有处理器进行排序以确保正确的执行顺序。
|
||||
*/
|
||||
public begin(): void {
|
||||
this.sortProcessors();
|
||||
for (const processor of this._processors) {
|
||||
processor.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
416
tests/ECS/Core/DecoratorSystem.test.ts
Normal file
416
tests/ECS/Core/DecoratorSystem.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
299
tests/ECS/Core/MatcherTimingIntegration.test.ts
Normal file
299
tests/ECS/Core/MatcherTimingIntegration.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
484
tests/ECS/Core/SystemInitializeIssue.test.ts
Normal file
484
tests/ECS/Core/SystemInitializeIssue.test.ts
Normal file
@@ -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));
|
||||
});
|
||||
});
|
||||
129
tests/ECS/Core/SystemMultipleInitialize.test.ts
Normal file
129
tests/ECS/Core/SystemMultipleInitialize.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
359
tests/ECS/Core/SystemTimingIssue.test.ts
Normal file
359
tests/ECS/Core/SystemTimingIssue.test.ts
Normal file
@@ -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));
|
||||
});
|
||||
});
|
||||
180
tests/ECS/Utils/Matcher.basic.test.ts
Normal file
180
tests/ECS/Utils/Matcher.basic.test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
493
tests/ECS/Utils/Matcher.comprehensive.test.ts
Normal file
493
tests/ECS/Utils/Matcher.comprehensive.test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
273
tests/ECS/Utils/Matcher.integration.test.ts
Normal file
273
tests/ECS/Utils/Matcher.integration.test.ts
Normal file
@@ -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;
|
||||
});
|
||||
});
|
||||
26
tests/ECS/Utils/Matcher.minimal.test.ts
Normal file
26
tests/ECS/Utils/Matcher.minimal.test.ts
Normal file
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user