427 lines
14 KiB
TypeScript
427 lines
14 KiB
TypeScript
|
|
import { Scene } from '../../src/ECS/Scene';
|
||
|
|
import { EntitySystem } from '../../src/ECS/Systems/EntitySystem';
|
||
|
|
import { Entity } from '../../src/ECS/Entity';
|
||
|
|
import { Component } from '../../src/ECS/Component';
|
||
|
|
import { Matcher } from '../../src/ECS/Utils/Matcher';
|
||
|
|
import { Injectable, Inject } from '../../src/Core/DI';
|
||
|
|
import { Core } from '../../src/Core';
|
||
|
|
import type { IService } from '../../src/Core/ServiceContainer';
|
||
|
|
import { ECSSystem } from '../../src/ECS/Decorators';
|
||
|
|
|
||
|
|
class Transform extends Component {
|
||
|
|
constructor(public x: number = 0, public y: number = 0) {
|
||
|
|
super();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class Velocity extends Component {
|
||
|
|
constructor(public vx: number = 0, public vy: number = 0) {
|
||
|
|
super();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class Health extends Component {
|
||
|
|
constructor(public value: number = 100) {
|
||
|
|
super();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('EntitySystem - 依赖注入测试', () => {
|
||
|
|
let scene: Scene;
|
||
|
|
|
||
|
|
beforeAll(() => {
|
||
|
|
Core.create();
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
scene = new Scene();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
scene.end();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(() => {
|
||
|
|
Core.destroy();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('基本DI功能', () => {
|
||
|
|
test('应该支持无依赖的System通过类型添加', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Movement')
|
||
|
|
class MovementSystem extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty().all(Transform, Velocity));
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
const system = scene.addEntityProcessor(MovementSystem);
|
||
|
|
|
||
|
|
expect(system).toBeInstanceOf(MovementSystem);
|
||
|
|
expect(scene.systems.length).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('应该支持有依赖的System自动注入', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Collision')
|
||
|
|
class CollisionSystem extends EntitySystem implements IService {
|
||
|
|
public checkCount = 0;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty().all(Transform));
|
||
|
|
}
|
||
|
|
|
||
|
|
public checkCollisions() {
|
||
|
|
this.checkCount++;
|
||
|
|
}
|
||
|
|
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Physics')
|
||
|
|
class PhysicsSystem extends EntitySystem implements IService {
|
||
|
|
constructor(
|
||
|
|
@Inject(CollisionSystem) public collision: CollisionSystem
|
||
|
|
) {
|
||
|
|
super(Matcher.empty().all(Transform, Velocity));
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override process(entities: readonly Entity[]): void {
|
||
|
|
this.collision.checkCollisions();
|
||
|
|
}
|
||
|
|
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
scene.addEntityProcessor(CollisionSystem);
|
||
|
|
const physics = scene.addEntityProcessor(PhysicsSystem);
|
||
|
|
|
||
|
|
expect(physics).toBeInstanceOf(PhysicsSystem);
|
||
|
|
expect(physics.collision).toBeInstanceOf(CollisionSystem);
|
||
|
|
expect(scene.systems.length).toBe(2);
|
||
|
|
|
||
|
|
const entity = scene.createEntity('test');
|
||
|
|
entity.addComponent(new Transform());
|
||
|
|
entity.addComponent(new Velocity());
|
||
|
|
|
||
|
|
scene.update();
|
||
|
|
|
||
|
|
expect(physics.collision.checkCount).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('应该支持多层级依赖注入', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('A')
|
||
|
|
class SystemA extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('B')
|
||
|
|
class SystemB extends EntitySystem implements IService {
|
||
|
|
constructor(@Inject(SystemA) public systemA: SystemA) {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('C')
|
||
|
|
class SystemC extends EntitySystem implements IService {
|
||
|
|
constructor(
|
||
|
|
@Inject(SystemA) public systemA: SystemA,
|
||
|
|
@Inject(SystemB) public systemB: SystemB
|
||
|
|
) {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
scene.addEntityProcessor(SystemA);
|
||
|
|
scene.addEntityProcessor(SystemB);
|
||
|
|
const systemC = scene.addEntityProcessor(SystemC);
|
||
|
|
|
||
|
|
expect(systemC.systemA).toBeInstanceOf(SystemA);
|
||
|
|
expect(systemC.systemB).toBeInstanceOf(SystemB);
|
||
|
|
expect(systemC.systemB.systemA).toBe(systemC.systemA);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('批量注册', () => {
|
||
|
|
test('应该支持批量注册System并自动解析依赖', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Collision')
|
||
|
|
class CollisionSystem extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty().all(Transform));
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Physics', { updateOrder: 10 })
|
||
|
|
class PhysicsSystem extends EntitySystem implements IService {
|
||
|
|
constructor(@Inject(CollisionSystem) public collision: CollisionSystem) {
|
||
|
|
super(Matcher.empty().all(Transform, Velocity));
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Render', { updateOrder: 20 })
|
||
|
|
class RenderSystem extends EntitySystem implements IService {
|
||
|
|
constructor(@Inject(PhysicsSystem) public physics: PhysicsSystem) {
|
||
|
|
super(Matcher.empty().all(Transform));
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
const systems = scene.registerSystems([
|
||
|
|
CollisionSystem,
|
||
|
|
PhysicsSystem,
|
||
|
|
RenderSystem
|
||
|
|
]);
|
||
|
|
|
||
|
|
expect(systems.length).toBe(3);
|
||
|
|
expect(scene.systems.length).toBe(3);
|
||
|
|
|
||
|
|
const [collision, physics, render] = systems;
|
||
|
|
expect(collision).toBeInstanceOf(CollisionSystem);
|
||
|
|
expect(physics).toBeInstanceOf(PhysicsSystem);
|
||
|
|
expect(render).toBeInstanceOf(RenderSystem);
|
||
|
|
|
||
|
|
expect((physics as any).collision).toBe(collision);
|
||
|
|
expect((render as any).physics).toBe(physics);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('批量注册的System应该按updateOrder排序', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('C', { updateOrder: 30 })
|
||
|
|
class SystemC extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('A', { updateOrder: 10 })
|
||
|
|
class SystemA extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('B', { updateOrder: 20 })
|
||
|
|
class SystemB extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
scene.registerSystems([SystemC, SystemA, SystemB]);
|
||
|
|
|
||
|
|
const systems = scene.systems;
|
||
|
|
expect(systems[0]).toBeInstanceOf(SystemA);
|
||
|
|
expect(systems[1]).toBeInstanceOf(SystemB);
|
||
|
|
expect(systems[2]).toBeInstanceOf(SystemC);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('场景隔离', () => {
|
||
|
|
test('不同Scene的System实例应该相互独立', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Counter')
|
||
|
|
class CounterSystem extends EntitySystem implements IService {
|
||
|
|
public count = 0;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override process(): void {
|
||
|
|
this.count++;
|
||
|
|
}
|
||
|
|
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
const scene1 = new Scene();
|
||
|
|
const scene2 = new Scene();
|
||
|
|
|
||
|
|
const counter1 = scene1.addEntityProcessor(CounterSystem);
|
||
|
|
const counter2 = scene2.addEntityProcessor(CounterSystem);
|
||
|
|
|
||
|
|
expect(counter1).not.toBe(counter2);
|
||
|
|
|
||
|
|
scene1.update();
|
||
|
|
expect(counter1.count).toBe(1);
|
||
|
|
expect(counter2.count).toBe(0);
|
||
|
|
|
||
|
|
scene2.update();
|
||
|
|
expect(counter1.count).toBe(1);
|
||
|
|
expect(counter2.count).toBe(1);
|
||
|
|
|
||
|
|
scene1.end();
|
||
|
|
scene2.end();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getSystem方法', () => {
|
||
|
|
test('应该能通过getSystem获取已注册的System', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Test')
|
||
|
|
class TestSystem extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
scene.addEntityProcessor(TestSystem);
|
||
|
|
|
||
|
|
const system = scene.getSystem(TestSystem);
|
||
|
|
expect(system).toBeInstanceOf(TestSystem);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('获取未注册的System应该返回null', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Test')
|
||
|
|
class TestSystem extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
const system = scene.getSystem(TestSystem);
|
||
|
|
expect(system).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('向后兼容性', () => {
|
||
|
|
test('应该继续支持手动创建实例的方式', () => {
|
||
|
|
class LegacySystem extends EntitySystem {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty().all(Transform));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const system = new LegacySystem();
|
||
|
|
scene.addEntityProcessor(system);
|
||
|
|
|
||
|
|
expect(scene.systems.length).toBe(1);
|
||
|
|
expect(scene.systems[0]).toBe(system);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('混合使用DI和手动创建应该正常工作', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('DI')
|
||
|
|
class DISystem extends EntitySystem implements IService {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty().all(Transform));
|
||
|
|
}
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ManualSystem extends EntitySystem {
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty().all(Velocity));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
scene.addEntityProcessor(DISystem);
|
||
|
|
scene.addEntityProcessor(new ManualSystem());
|
||
|
|
|
||
|
|
expect(scene.systems.length).toBe(2);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Issue #76 场景验证', () => {
|
||
|
|
test('应该消除硬编码依赖,使用构造函数注入', () => {
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('TimeService')
|
||
|
|
class TimeService extends EntitySystem implements IService {
|
||
|
|
public getDeltaTime(): number {
|
||
|
|
return 0.016;
|
||
|
|
}
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('CollisionService')
|
||
|
|
class CollisionService extends EntitySystem implements IService {
|
||
|
|
public detectCollisions(): string[] {
|
||
|
|
return ['collision1', 'collision2'];
|
||
|
|
}
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super(Matcher.empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
@ECSSystem('Physics')
|
||
|
|
class PhysicsSystem extends EntitySystem implements IService {
|
||
|
|
constructor(
|
||
|
|
@Inject(TimeService) private time: TimeService,
|
||
|
|
@Inject(CollisionService) private collision: CollisionService
|
||
|
|
) {
|
||
|
|
super(Matcher.empty().all(Transform, Velocity));
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override process(entities: readonly Entity[]): void {
|
||
|
|
const dt = this.time.getDeltaTime();
|
||
|
|
const collisions = this.collision.detectCollisions();
|
||
|
|
|
||
|
|
for (const entity of entities) {
|
||
|
|
const transform = entity.getComponent(Transform)!;
|
||
|
|
const velocity = entity.getComponent(Velocity)!;
|
||
|
|
|
||
|
|
transform.x += velocity.vx * dt;
|
||
|
|
transform.y += velocity.vy * dt;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
override dispose() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
scene.registerSystems([
|
||
|
|
TimeService,
|
||
|
|
CollisionService,
|
||
|
|
PhysicsSystem
|
||
|
|
]);
|
||
|
|
|
||
|
|
const entity = scene.createEntity('player');
|
||
|
|
entity.addComponent(new Transform(0, 0));
|
||
|
|
entity.addComponent(new Velocity(100, 50));
|
||
|
|
|
||
|
|
const physics = scene.getSystem(PhysicsSystem);
|
||
|
|
expect(physics).not.toBeNull();
|
||
|
|
expect((physics as any).time).toBeInstanceOf(TimeService);
|
||
|
|
expect((physics as any).collision).toBeInstanceOf(CollisionService);
|
||
|
|
|
||
|
|
scene.update();
|
||
|
|
|
||
|
|
const transform = entity.getComponent(Transform)!;
|
||
|
|
expect(transform.x).toBeCloseTo(1.6, 1);
|
||
|
|
expect(transform.y).toBeCloseTo(0.8, 1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|