feat(ecs): 核心系统改进 - 句柄、调度、变更检测与查询编译 (#304)

新增功能:
- EntityHandle: 轻量级实体句柄 (28位索引 + 20位代数)
- SystemScheduler: 声明式系统调度,支持 @Stage/@Before/@After/@InSet 装饰器
- EpochManager: 帧级变更检测
- CompiledQuery: 预编译类型安全查询

API 改进:
- EntitySystem 添加 getBefore()/getAfter()/getSets() getter 方法
- Entity 添加 markDirty() 辅助方法
- IScene 添加 epochManager 属性
- CommandBuffer.pendingCount 修正为返回实际操作数

文档更新:
- 更新系统调度和查询相关文档
This commit is contained in:
YHH
2025-12-15 09:17:00 +08:00
committed by GitHub
parent b5158b6ac6
commit cd6ef222d1
27 changed files with 5233 additions and 43 deletions

View File

@@ -282,7 +282,9 @@ describe('CommandBuffer', () => {
commandBuffer.addComponent(entity, new MarkerComponent());
commandBuffer.destroyEntity(entity);
expect(commandBuffer.pendingCount).toBe(2);
// 由于去重逻辑destroyEntity 会清除同一实体的其他操作
// Due to deduplication, destroyEntity clears other operations for the same entity
expect(commandBuffer.pendingCount).toBe(1);
commandBuffer.clear();

View File

@@ -0,0 +1,373 @@
import { CompiledQuery } from '../../../src/ECS/Core/Query/CompiledQuery';
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件
@ECSComponent('CompiledQuery_Position')
class PositionComponent extends Component {
public x: number;
public y: number;
constructor(...args: unknown[]) {
super();
const [x = 0, y = 0] = args as [number?, number?];
this.x = x;
this.y = y;
}
}
@ECSComponent('CompiledQuery_Velocity')
class VelocityComponent extends Component {
public vx: number;
public vy: number;
constructor(...args: unknown[]) {
super();
const [vx = 0, vy = 0] = args as [number?, number?];
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('CompiledQuery_Health')
class HealthComponent extends Component {
public current: number;
public max: number;
constructor(...args: unknown[]) {
super();
const [current = 100, max = 100] = args as [number?, number?];
this.current = current;
this.max = max;
}
}
describe('CompiledQuery', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
afterEach(() => {
scene.end();
});
describe('创建和基本属性', () => {
it('应该能通过 QuerySystem.compile 创建', () => {
const query = scene.querySystem.compile(PositionComponent);
expect(query).toBeInstanceOf(CompiledQuery);
});
it('应该保存组件类型列表', () => {
const query = scene.querySystem.compile(PositionComponent, VelocityComponent);
expect(query.componentTypes).toContain(PositionComponent);
expect(query.componentTypes).toContain(VelocityComponent);
});
});
describe('entities 属性', () => {
it('初始应该返回空数组', () => {
const query = scene.querySystem.compile(PositionComponent);
expect(query.entities).toHaveLength(0);
});
it('应该返回匹配的实体', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent(10, 20));
const query = scene.querySystem.compile(PositionComponent);
expect(query.entities).toHaveLength(1);
expect(query.entities[0]).toBe(entity);
});
it('应该只返回拥有所有组件的实体', () => {
const entity1 = scene.createEntity('entity1');
entity1.addComponent(new PositionComponent());
entity1.addComponent(new VelocityComponent());
const entity2 = scene.createEntity('entity2');
entity2.addComponent(new PositionComponent());
// entity2 没有 VelocityComponent
const query = scene.querySystem.compile(PositionComponent, VelocityComponent);
expect(query.entities).toHaveLength(1);
expect(query.entities[0]).toBe(entity1);
});
});
describe('count 属性', () => {
it('应该返回匹配实体的数量', () => {
const query = scene.querySystem.compile(PositionComponent);
expect(query.count).toBe(0);
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent());
expect(query.count).toBe(1);
});
});
describe('forEach', () => {
it('应该遍历所有匹配的实体', () => {
const entity1 = scene.createEntity('entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('entity2');
entity2.addComponent(new PositionComponent(30, 40));
const query = scene.querySystem.compile(PositionComponent);
const visited: Entity[] = [];
query.forEach((entity, pos) => {
visited.push(entity);
});
expect(visited).toHaveLength(2);
expect(visited).toContain(entity1);
expect(visited).toContain(entity2);
});
it('应该提供类型安全的组件参数', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent(10, 20));
entity.addComponent(new VelocityComponent(1, 2));
const query = scene.querySystem.compile(PositionComponent, VelocityComponent);
query.forEach((entity, pos, vel) => {
expect(pos.x).toBe(10);
expect(pos.y).toBe(20);
expect(vel.vx).toBe(1);
expect(vel.vy).toBe(2);
});
});
});
describe('forEachChanged', () => {
it('应该只遍历变更的实体', () => {
const entity1 = scene.createEntity('entity1');
const pos1 = entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('entity2');
const pos2 = entity2.addComponent(new PositionComponent(30, 40));
const query = scene.querySystem.compile(PositionComponent);
// 获取当前 epoch
const epoch = scene.epochManager.current;
// 递增 epoch
scene.epochManager.increment();
// 只标记 entity1 的组件为已修改
pos1.markDirty(scene.epochManager.current);
const changed: Entity[] = [];
query.forEachChanged(epoch, (entity, pos) => {
changed.push(entity);
});
expect(changed).toHaveLength(1);
expect(changed[0]).toBe(entity1);
});
it('当所有组件都未变更时应该不遍历任何实体', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent(10, 20));
const query = scene.querySystem.compile(PositionComponent);
// 使用当前 epoch 检查 - 组件的 epoch 应该小于等于当前
const futureEpoch = scene.epochManager.current + 100;
const changed: Entity[] = [];
query.forEachChanged(futureEpoch, (entity, pos) => {
changed.push(entity);
});
expect(changed).toHaveLength(0);
});
});
describe('first', () => {
it('应该返回第一个匹配的实体和组件', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent(10, 20));
const query = scene.querySystem.compile(PositionComponent);
const result = query.first();
expect(result).not.toBeNull();
expect(result![0]).toBe(entity);
expect(result![1].x).toBe(10);
expect(result![1].y).toBe(20);
});
it('没有匹配实体时应该返回 null', () => {
const query = scene.querySystem.compile(PositionComponent);
const result = query.first();
expect(result).toBeNull();
});
});
describe('toArray', () => {
it('应该返回实体和组件的数组', () => {
const entity1 = scene.createEntity('entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('entity2');
entity2.addComponent(new PositionComponent(30, 40));
const query = scene.querySystem.compile(PositionComponent);
const result = query.toArray();
expect(result).toHaveLength(2);
expect(result[0]![0]).toBe(entity1);
expect(result[0]![1].x).toBe(10);
});
});
describe('map', () => {
it('应该映射转换实体数据', () => {
const entity1 = scene.createEntity('entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('entity2');
entity2.addComponent(new PositionComponent(30, 40));
const query = scene.querySystem.compile(PositionComponent);
const result = query.map((entity, pos) => pos.x + pos.y);
expect(result).toHaveLength(2);
expect(result).toContain(30); // 10 + 20
expect(result).toContain(70); // 30 + 40
});
});
describe('filter', () => {
it('应该过滤实体', () => {
const entity1 = scene.createEntity('entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('entity2');
entity2.addComponent(new PositionComponent(30, 40));
const query = scene.querySystem.compile(PositionComponent);
const result = query.filter((entity, pos) => pos.x > 20);
expect(result).toHaveLength(1);
expect(result[0]).toBe(entity2);
});
});
describe('find', () => {
it('应该找到第一个满足条件的实体', () => {
const entity1 = scene.createEntity('entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('entity2');
entity2.addComponent(new PositionComponent(30, 40));
const query = scene.querySystem.compile(PositionComponent);
const result = query.find((entity, pos) => pos.x > 20);
expect(result).toBe(entity2);
});
it('找不到时应该返回 undefined', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent(10, 20));
const query = scene.querySystem.compile(PositionComponent);
const result = query.find((entity, pos) => pos.x > 100);
expect(result).toBeUndefined();
});
});
describe('any', () => {
it('有匹配实体时应该返回 true', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent());
const query = scene.querySystem.compile(PositionComponent);
expect(query.any()).toBe(true);
});
it('没有匹配实体时应该返回 false', () => {
const query = scene.querySystem.compile(PositionComponent);
expect(query.any()).toBe(false);
});
});
describe('empty', () => {
it('没有匹配实体时应该返回 true', () => {
const query = scene.querySystem.compile(PositionComponent);
expect(query.empty()).toBe(true);
});
it('有匹配实体时应该返回 false', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent());
const query = scene.querySystem.compile(PositionComponent);
expect(query.empty()).toBe(false);
});
});
describe('缓存机制', () => {
it('应该缓存查询结果', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent());
const query = scene.querySystem.compile(PositionComponent);
// 第一次访问
const entities1 = query.entities;
// 第二次访问
const entities2 = query.entities;
// 应该返回相同的缓存数组
expect(entities1).toBe(entities2);
});
it('当实体变化时应该刷新缓存', () => {
const query = scene.querySystem.compile(PositionComponent);
// 初始为空
expect(query.count).toBe(0);
// 添加实体
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent());
// 应该检测到变化
expect(query.count).toBe(1);
});
});
describe('多组件查询', () => {
it('应该支持多组件查询', () => {
const entity = scene.createEntity('test');
entity.addComponent(new PositionComponent(10, 20));
entity.addComponent(new VelocityComponent(1, 2));
entity.addComponent(new HealthComponent(80, 100));
const query = scene.querySystem.compile(
PositionComponent,
VelocityComponent,
HealthComponent
);
query.forEach((entity, pos, vel, health) => {
expect(pos.x).toBe(10);
expect(vel.vx).toBe(1);
expect(health.current).toBe(80);
});
});
});
});

View File

@@ -0,0 +1,380 @@
import {
makeHandle,
indexOf,
genOf,
isValidHandle,
handleEquals,
handleToString,
NULL_HANDLE,
INDEX_BITS,
GEN_BITS,
INDEX_MASK,
GEN_MASK,
MAX_ENTITIES,
MAX_GENERATION,
EntityHandle
} from '../../../src/ECS/Core/EntityHandle';
import { EntityHandleManager } from '../../../src/ECS/Core/EntityHandleManager';
describe('EntityHandle', () => {
describe('常量定义', () => {
it('INDEX_BITS 应该是 28', () => {
expect(INDEX_BITS).toBe(28);
});
it('GEN_BITS 应该是 20', () => {
expect(GEN_BITS).toBe(20);
});
it('INDEX_MASK 应该正确', () => {
expect(INDEX_MASK).toBe((1 << INDEX_BITS) - 1);
});
it('GEN_MASK 应该正确', () => {
expect(GEN_MASK).toBe((1 << GEN_BITS) - 1);
});
it('MAX_ENTITIES 应该是 2^28', () => {
expect(MAX_ENTITIES).toBe(1 << INDEX_BITS);
});
it('MAX_GENERATION 应该是 2^20', () => {
expect(MAX_GENERATION).toBe(1 << GEN_BITS);
});
it('NULL_HANDLE 应该是 0', () => {
expect(NULL_HANDLE).toBe(0);
});
});
describe('makeHandle', () => {
it('应该正确组合 index 和 generation', () => {
const handle = makeHandle(100, 5);
expect(indexOf(handle)).toBe(100);
expect(genOf(handle)).toBe(5);
});
it('应该处理边界值', () => {
// 最小值
const minHandle = makeHandle(0, 0);
expect(indexOf(minHandle)).toBe(0);
expect(genOf(minHandle)).toBe(0);
// 最大 index
const maxIndexHandle = makeHandle(INDEX_MASK, 0);
expect(indexOf(maxIndexHandle)).toBe(INDEX_MASK);
// 最大 generation
const maxGenHandle = makeHandle(0, GEN_MASK);
expect(genOf(maxGenHandle)).toBe(GEN_MASK);
});
it('应该正确处理大数值', () => {
const largeIndex = 1000000;
const largeGen = 500;
const handle = makeHandle(largeIndex, largeGen);
expect(indexOf(handle)).toBe(largeIndex);
expect(genOf(handle)).toBe(largeGen);
});
});
describe('indexOf', () => {
it('应该正确提取 index', () => {
const handle = makeHandle(12345, 67);
expect(indexOf(handle)).toBe(12345);
});
it('应该对 NULL_HANDLE 返回 0', () => {
expect(indexOf(NULL_HANDLE)).toBe(0);
});
});
describe('genOf', () => {
it('应该正确提取 generation', () => {
const handle = makeHandle(12345, 67);
expect(genOf(handle)).toBe(67);
});
it('应该对 NULL_HANDLE 返回 0', () => {
expect(genOf(NULL_HANDLE)).toBe(0);
});
});
describe('isValidHandle', () => {
it('应该判断 NULL_HANDLE 为无效', () => {
expect(isValidHandle(NULL_HANDLE)).toBe(false);
});
it('应该判断非零句柄为有效', () => {
const handle = makeHandle(1, 0);
expect(isValidHandle(handle)).toBe(true);
});
it('应该判断带 generation 的句柄为有效', () => {
const handle = makeHandle(0, 1);
expect(isValidHandle(handle)).toBe(true);
});
});
describe('handleEquals', () => {
it('应该正确比较相同句柄', () => {
const handle1 = makeHandle(100, 5);
const handle2 = makeHandle(100, 5);
expect(handleEquals(handle1, handle2)).toBe(true);
});
it('应该正确比较不同句柄', () => {
const handle1 = makeHandle(100, 5);
const handle2 = makeHandle(100, 6);
expect(handleEquals(handle1, handle2)).toBe(false);
});
it('应该区分 index 不同的句柄', () => {
const handle1 = makeHandle(100, 5);
const handle2 = makeHandle(101, 5);
expect(handleEquals(handle1, handle2)).toBe(false);
});
});
describe('handleToString', () => {
it('应该返回可读的字符串格式', () => {
const handle = makeHandle(100, 5);
const str = handleToString(handle);
expect(str).toContain('100');
expect(str).toContain('5');
});
it('应该对 NULL_HANDLE 返回特殊标记', () => {
const str = handleToString(NULL_HANDLE);
expect(str.toLowerCase()).toContain('null');
});
});
});
describe('EntityHandleManager', () => {
let manager: EntityHandleManager;
beforeEach(() => {
manager = new EntityHandleManager();
});
describe('create', () => {
it('应该创建有效的句柄', () => {
const handle = manager.create();
expect(isValidHandle(handle)).toBe(true);
});
it('应该创建不同的句柄', () => {
const handle1 = manager.create();
const handle2 = manager.create();
expect(handleEquals(handle1, handle2)).toBe(false);
});
it('应该递增 index', () => {
const handle1 = manager.create();
const handle2 = manager.create();
expect(indexOf(handle2)).toBe(indexOf(handle1) + 1);
});
it('创建的句柄应该是存活的', () => {
const handle = manager.create();
expect(manager.isAlive(handle)).toBe(true);
});
it('创建的句柄默认应该是启用的', () => {
const handle = manager.create();
expect(manager.isEnabled(handle)).toBe(true);
});
});
describe('destroy', () => {
it('应该销毁句柄', () => {
const handle = manager.create();
expect(manager.isAlive(handle)).toBe(true);
manager.destroy(handle);
expect(manager.isAlive(handle)).toBe(false);
});
it('销毁后使用相同 index 应该增加 generation', () => {
const handle1 = manager.create();
const index1 = indexOf(handle1);
manager.destroy(handle1);
const handle2 = manager.create();
const index2 = indexOf(handle2);
// 应该复用同一个 index
expect(index2).toBe(index1);
// 但 generation 应该不同
expect(genOf(handle2)).toBe(genOf(handle1) + 1);
});
it('销毁已销毁的句柄不应该报错', () => {
const handle = manager.create();
manager.destroy(handle);
expect(() => {
manager.destroy(handle);
}).not.toThrow();
});
it('销毁 NULL_HANDLE 不应该报错', () => {
expect(() => {
manager.destroy(NULL_HANDLE);
}).not.toThrow();
});
});
describe('isAlive', () => {
it('应该对存活句柄返回 true', () => {
const handle = manager.create();
expect(manager.isAlive(handle)).toBe(true);
});
it('应该对已销毁句柄返回 false', () => {
const handle = manager.create();
manager.destroy(handle);
expect(manager.isAlive(handle)).toBe(false);
});
it('应该对 NULL_HANDLE 返回 false', () => {
expect(manager.isAlive(NULL_HANDLE)).toBe(false);
});
it('应该对过期 generation 返回 false', () => {
const handle1 = manager.create();
manager.destroy(handle1);
const handle2 = manager.create();
// handle1 的 generation 已过期
expect(manager.isAlive(handle1)).toBe(false);
expect(manager.isAlive(handle2)).toBe(true);
});
});
describe('isEnabled/setEnabled', () => {
it('新创建的句柄默认启用', () => {
const handle = manager.create();
expect(manager.isEnabled(handle)).toBe(true);
});
it('应该能够禁用句柄', () => {
const handle = manager.create();
manager.setEnabled(handle, false);
expect(manager.isEnabled(handle)).toBe(false);
});
it('应该能够重新启用句柄', () => {
const handle = manager.create();
manager.setEnabled(handle, false);
manager.setEnabled(handle, true);
expect(manager.isEnabled(handle)).toBe(true);
});
it('对已销毁句柄设置启用状态不应该报错', () => {
const handle = manager.create();
manager.destroy(handle);
expect(() => {
manager.setEnabled(handle, true);
}).not.toThrow();
});
it('已销毁句柄应该返回未启用', () => {
const handle = manager.create();
manager.destroy(handle);
expect(manager.isEnabled(handle)).toBe(false);
});
});
describe('aliveCount', () => {
it('初始时应该为 0', () => {
expect(manager.aliveCount).toBe(0);
});
it('创建句柄后应该增加', () => {
manager.create();
expect(manager.aliveCount).toBe(1);
manager.create();
expect(manager.aliveCount).toBe(2);
});
it('销毁句柄后应该减少', () => {
const handle1 = manager.create();
const handle2 = manager.create();
expect(manager.aliveCount).toBe(2);
manager.destroy(handle1);
expect(manager.aliveCount).toBe(1);
manager.destroy(handle2);
expect(manager.aliveCount).toBe(0);
});
});
describe('reset', () => {
it('应该重置所有状态', () => {
const handle1 = manager.create();
const handle2 = manager.create();
manager.destroy(handle1);
manager.reset();
expect(manager.aliveCount).toBe(0);
expect(manager.isAlive(handle2)).toBe(false);
});
it('重置后应该能重新创建句柄', () => {
manager.create();
manager.create();
manager.reset();
const newHandle = manager.create();
expect(isValidHandle(newHandle)).toBe(true);
expect(manager.isAlive(newHandle)).toBe(true);
});
});
describe('大规模测试', () => {
it('应该能处理大量句柄', () => {
const handles: EntityHandle[] = [];
const count = 10000;
// 创建大量句柄
for (let i = 0; i < count; i++) {
handles.push(manager.create());
}
expect(manager.aliveCount).toBe(count);
// 验证所有句柄都是存活的
for (const handle of handles) {
expect(manager.isAlive(handle)).toBe(true);
}
// 销毁一半
for (let i = 0; i < count / 2; i++) {
manager.destroy(handles[i]!);
}
expect(manager.aliveCount).toBe(count / 2);
});
it('应该正确复用已销毁的 index', () => {
// 创建并销毁
const handle1 = manager.create();
manager.destroy(handle1);
// 再次创建
const handle2 = manager.create();
// 应该复用 index
expect(indexOf(handle2)).toBe(indexOf(handle1));
// 但 generation 增加
expect(genOf(handle2)).toBe(genOf(handle1) + 1);
});
});
});

View File

@@ -0,0 +1,72 @@
import { EpochManager } from '../../../src/ECS/Core/EpochManager';
describe('EpochManager', () => {
let epochManager: EpochManager;
beforeEach(() => {
epochManager = new EpochManager();
});
describe('初始状态', () => {
it('初始 epoch 应该是 1', () => {
expect(epochManager.current).toBe(1);
});
});
describe('increment', () => {
it('应该递增 epoch', () => {
const initial = epochManager.current;
epochManager.increment();
expect(epochManager.current).toBe(initial + 1);
});
it('应该正确递增多次', () => {
const initial = epochManager.current;
epochManager.increment();
epochManager.increment();
epochManager.increment();
expect(epochManager.current).toBe(initial + 3);
});
});
describe('reset', () => {
it('应该重置 epoch 到 1', () => {
epochManager.increment();
epochManager.increment();
expect(epochManager.current).toBeGreaterThan(1);
epochManager.reset();
expect(epochManager.current).toBe(1);
});
});
describe('current getter', () => {
it('应该返回当前 epoch', () => {
expect(epochManager.current).toBe(1);
epochManager.increment();
expect(epochManager.current).toBe(2);
});
});
describe('使用场景', () => {
it('可以用于追踪帧数', () => {
// 模拟 10 帧
for (let i = 0; i < 10; i++) {
epochManager.increment();
}
expect(epochManager.current).toBe(11); // 初始 1 + 10 帧
});
it('可以用于变更检测', () => {
// 保存检查点
const checkpoint = epochManager.current;
// 模拟几帧
epochManager.increment();
epochManager.increment();
// 当前 epoch 应该大于检查点
expect(epochManager.current).toBeGreaterThan(checkpoint);
});
});
});

View File

@@ -0,0 +1,555 @@
import {
SystemScheduler,
CycleDependencyError,
DEFAULT_STAGE_ORDER,
SystemStage
} from '../../../src/ECS/Core/SystemScheduler';
import { SystemDependencyGraph } from '../../../src/ECS/Core/SystemDependencyGraph';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Scene } from '../../../src/ECS/Scene';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { Entity } from '../../../src/ECS/Entity';
import { ECSSystem, Stage, Before, After, InSet } from '../../../src/ECS/Decorators';
// 测试系统
@ECSSystem('TestSystemA')
class TestSystemA extends EntitySystem {
public executionOrder: number = 0;
public static executionCounter = 0;
constructor() {
super(Matcher.nothing());
}
protected override process(entities: readonly Entity[]): void {
this.executionOrder = ++TestSystemA.executionCounter;
}
}
@ECSSystem('TestSystemB')
class TestSystemB extends EntitySystem {
public executionOrder: number = 0;
constructor() {
super(Matcher.nothing());
}
protected override process(entities: readonly Entity[]): void {
this.executionOrder = ++TestSystemA.executionCounter;
}
}
@ECSSystem('TestSystemC')
class TestSystemC extends EntitySystem {
public executionOrder: number = 0;
constructor() {
super(Matcher.nothing());
}
protected override process(entities: readonly Entity[]): void {
this.executionOrder = ++TestSystemA.executionCounter;
}
}
describe('SystemDependencyGraph', () => {
let graph: SystemDependencyGraph;
beforeEach(() => {
graph = new SystemDependencyGraph();
});
describe('addSystemNode', () => {
it('应该添加节点', () => {
graph.addSystemNode('SystemA');
expect(graph.size).toBe(1);
});
it('应该支持添加多个节点', () => {
graph.addSystemNode('SystemA');
graph.addSystemNode('SystemB');
expect(graph.size).toBe(2);
});
});
describe('addSetNode', () => {
it('应该添加虚拟集合节点', () => {
graph.addSetNode('CoreSystems');
expect(graph.size).toBe(1);
});
});
describe('addEdge', () => {
it('应该添加边', () => {
graph.addSystemNode('SystemA');
graph.addSystemNode('SystemB');
graph.addEdge('SystemA', 'SystemB');
// 拓扑排序后 A 应该在 B 之前
const sorted = graph.topologicalSort();
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
});
it('应该忽略自引用边', () => {
graph.addSystemNode('SystemA');
graph.addEdge('SystemA', 'SystemA');
// 不应该产生循环
const sorted = graph.topologicalSort();
expect(sorted).toContain('SystemA');
});
});
describe('topologicalSort', () => {
it('应该返回正确的拓扑顺序', () => {
graph.addSystemNode('SystemA');
graph.addSystemNode('SystemB');
graph.addSystemNode('SystemC');
// A -> B -> C
graph.addEdge('SystemA', 'SystemB');
graph.addEdge('SystemB', 'SystemC');
const sorted = graph.topologicalSort();
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
expect(sorted.indexOf('SystemB')).toBeLessThan(sorted.indexOf('SystemC'));
});
it('应该检测循环依赖', () => {
graph.addSystemNode('SystemA');
graph.addSystemNode('SystemB');
// A -> B -> A (循环)
graph.addEdge('SystemA', 'SystemB');
graph.addEdge('SystemB', 'SystemA');
expect(() => {
graph.topologicalSort();
}).toThrow(CycleDependencyError);
});
it('应该检测多节点循环依赖', () => {
graph.addSystemNode('SystemA');
graph.addSystemNode('SystemB');
graph.addSystemNode('SystemC');
// A -> B -> C -> A (循环)
graph.addEdge('SystemA', 'SystemB');
graph.addEdge('SystemB', 'SystemC');
graph.addEdge('SystemC', 'SystemA');
expect(() => {
graph.topologicalSort();
}).toThrow(CycleDependencyError);
});
it('应该处理没有依赖的系统', () => {
graph.addSystemNode('SystemA');
graph.addSystemNode('SystemB');
// 没有边,两个独立系统
const sorted = graph.topologicalSort();
expect(sorted).toHaveLength(2);
expect(sorted).toContain('SystemA');
expect(sorted).toContain('SystemB');
});
it('不应该包含虚拟节点在结果中', () => {
graph.addSystemNode('SystemA');
graph.addSetNode('CoreSystems');
const sorted = graph.topologicalSort();
expect(sorted).toContain('SystemA');
expect(sorted).not.toContain('set:CoreSystems');
});
});
describe('buildFromSystems', () => {
it('应该从系统依赖信息构建图', () => {
graph.buildFromSystems([
{ name: 'SystemA', before: ['SystemB'], after: [], sets: [] },
{ name: 'SystemB', before: [], after: [], sets: [] }
]);
const sorted = graph.topologicalSort();
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
});
it('应该处理 after 依赖', () => {
graph.buildFromSystems([
{ name: 'SystemA', before: [], after: [], sets: [] },
{ name: 'SystemB', before: [], after: ['SystemA'], sets: [] }
]);
const sorted = graph.topologicalSort();
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
});
it('应该处理 set 依赖', () => {
graph.buildFromSystems([
{ name: 'SystemA', before: [], after: [], sets: ['CoreSystems'] },
{ name: 'SystemB', before: [], after: [], sets: ['CoreSystems'] }
]);
const sorted = graph.topologicalSort();
expect(sorted).toHaveLength(2);
expect(sorted).toContain('SystemA');
expect(sorted).toContain('SystemB');
});
});
describe('clear', () => {
it('应该清除所有节点和边', () => {
graph.addSystemNode('SystemA');
graph.addSystemNode('SystemB');
graph.addEdge('SystemA', 'SystemB');
graph.clear();
expect(graph.size).toBe(0);
});
});
});
describe('SystemScheduler', () => {
let scheduler: SystemScheduler;
beforeEach(() => {
scheduler = new SystemScheduler();
TestSystemA.executionCounter = 0;
});
describe('DEFAULT_STAGE_ORDER', () => {
it('应该包含所有阶段', () => {
expect(DEFAULT_STAGE_ORDER).toContain('startup');
expect(DEFAULT_STAGE_ORDER).toContain('preUpdate');
expect(DEFAULT_STAGE_ORDER).toContain('update');
expect(DEFAULT_STAGE_ORDER).toContain('postUpdate');
expect(DEFAULT_STAGE_ORDER).toContain('cleanup');
});
it('阶段顺序应该正确', () => {
expect(DEFAULT_STAGE_ORDER.indexOf('startup')).toBeLessThan(
DEFAULT_STAGE_ORDER.indexOf('preUpdate')
);
expect(DEFAULT_STAGE_ORDER.indexOf('preUpdate')).toBeLessThan(
DEFAULT_STAGE_ORDER.indexOf('update')
);
expect(DEFAULT_STAGE_ORDER.indexOf('update')).toBeLessThan(
DEFAULT_STAGE_ORDER.indexOf('postUpdate')
);
expect(DEFAULT_STAGE_ORDER.indexOf('postUpdate')).toBeLessThan(
DEFAULT_STAGE_ORDER.indexOf('cleanup')
);
});
});
describe('getSortedSystems', () => {
it('应该返回排序后的系统', () => {
const systemA = new TestSystemA();
const systemB = new TestSystemB();
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
expect(systems.length).toBeGreaterThanOrEqual(2);
});
it('应该按 updateOrder 排序', () => {
const systemA = new TestSystemA();
const systemB = new TestSystemB();
systemA.updateOrder = 10;
systemB.updateOrder = 5;
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
// B 的 updateOrder 更小,应该在 A 之前
expect(systems.indexOf(systemB)).toBeLessThan(systems.indexOf(systemA));
});
it('应该按依赖关系排序', () => {
// 创建带有依赖关系的系统
@ECSSystem('DepSystemA')
@Stage('update')
class DepSystemA extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
@ECSSystem('DepSystemB')
@Stage('update')
@After('DepSystemA')
class DepSystemB extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const systemA = new DepSystemA();
const systemB = new DepSystemB();
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
const indexA = systems.indexOf(systemA);
const indexB = systems.indexOf(systemB);
expect(indexA).toBeLessThan(indexB);
});
it('应该返回指定阶段的系统', () => {
@ECSSystem('PreUpdateSystem')
@Stage('preUpdate')
class PreUpdateSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
@ECSSystem('UpdateSystem')
@Stage('update')
class UpdateSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const preSystem = new PreUpdateSystem();
const updateSystem = new UpdateSystem();
const preSystems = scheduler.getSortedSystems([preSystem, updateSystem], 'preUpdate');
const updateSystems = scheduler.getSortedSystems([preSystem, updateSystem], 'update');
expect(preSystems).toContain(preSystem);
expect(preSystems).not.toContain(updateSystem);
expect(updateSystems).toContain(updateSystem);
expect(updateSystems).not.toContain(preSystem);
});
});
describe('getAllSortedSystems', () => {
it('应该返回所有阶段的系统', () => {
@ECSSystem('AllPreSystem')
@Stage('preUpdate')
class AllPreSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
@ECSSystem('AllUpdateSystem')
@Stage('update')
class AllUpdateSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const preSystem = new AllPreSystem();
const updateSystem = new AllUpdateSystem();
const allSystems = scheduler.getAllSortedSystems([preSystem, updateSystem]);
expect(allSystems).toContain(preSystem);
expect(allSystems).toContain(updateSystem);
// preUpdate 阶段在 update 之前
expect(allSystems.indexOf(preSystem)).toBeLessThan(allSystems.indexOf(updateSystem));
});
});
describe('markDirty', () => {
it('调用 markDirty 后应该重新排序', () => {
const systemA = new TestSystemA();
const systemB = new TestSystemB();
// 第一次排序
scheduler.getSortedSystems([systemA, systemB], 'update');
// 标记脏
scheduler.markDirty();
// 应该重新排序而不出错
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
expect(systems).toHaveLength(2);
});
});
describe('setUseDependencySort', () => {
it('禁用依赖排序后应该只使用 updateOrder', () => {
@ECSSystem('DisabledDepA')
@Stage('update')
class DisabledDepA extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
@ECSSystem('DisabledDepB')
@Stage('update')
@Before('DisabledDepA')
class DisabledDepB extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const systemA = new DisabledDepA();
const systemB = new DisabledDepB();
systemA.updateOrder = 1;
systemB.updateOrder = 2;
scheduler.setUseDependencySort(false);
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
// 禁用依赖排序后,按 updateOrder 排序
expect(systems.indexOf(systemA)).toBeLessThan(systems.indexOf(systemB));
});
});
});
describe('调度装饰器', () => {
describe('@Stage', () => {
it('应该设置系统的执行阶段', () => {
@ECSSystem('StageTestSystem')
@Stage('postUpdate')
class StageTestSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const system = new StageTestSystem();
expect(system.getStage()).toBe('postUpdate');
});
});
describe('@Before', () => {
it('应该设置系统的前置依赖', () => {
@ECSSystem('BeforeTestSystem')
@Before('OtherSystem')
class BeforeTestSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const system = new BeforeTestSystem();
expect(system.getBefore()).toContain('OtherSystem');
});
it('应该支持多个前置依赖', () => {
@ECSSystem('MultiBeforeSystem')
@Before('SystemA', 'SystemB')
class MultiBeforeSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const system = new MultiBeforeSystem();
expect(system.getBefore()).toContain('SystemA');
expect(system.getBefore()).toContain('SystemB');
});
});
describe('@After', () => {
it('应该设置系统的后置依赖', () => {
@ECSSystem('AfterTestSystem')
@After('OtherSystem')
class AfterTestSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const system = new AfterTestSystem();
expect(system.getAfter()).toContain('OtherSystem');
});
});
describe('@InSet', () => {
it('应该设置系统所属的集合', () => {
@ECSSystem('InSetTestSystem')
@InSet('CoreSystems')
class InSetTestSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const system = new InSetTestSystem();
expect(system.getSets()).toContain('CoreSystems');
});
});
describe('组合使用', () => {
it('应该支持组合多个装饰器', () => {
@ECSSystem('CombinedSystem')
@Stage('update')
@After('InputSystem')
@Before('RenderSystem')
@InSet('CoreSystems')
class CombinedSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
}
}
const system = new CombinedSystem();
expect(system.getStage()).toBe('update');
expect(system.getAfter()).toContain('InputSystem');
expect(system.getBefore()).toContain('RenderSystem');
expect(system.getSets()).toContain('CoreSystems');
});
});
});
describe('Fluent API 调度配置', () => {
it('应该支持 stage() 方法', () => {
@ECSSystem('FluentStageSystem')
class FluentStageSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
this.stage('postUpdate');
}
}
const system = new FluentStageSystem();
expect(system.getStage()).toBe('postUpdate');
});
it('应该支持链式调用', () => {
@ECSSystem('FluentChainSystem')
class FluentChainSystem extends EntitySystem {
constructor() {
super(Matcher.nothing());
this.stage('update')
.after('SystemA')
.before('SystemB')
.inSet('CoreSystems');
}
}
const system = new FluentChainSystem();
expect(system.getStage()).toBe('update');
expect(system.getAfter()).toContain('SystemA');
expect(system.getBefore()).toContain('SystemB');
expect(system.getSets()).toContain('CoreSystems');
});
});
describe('CycleDependencyError', () => {
it('应该包含循环节点信息', () => {
const error = new CycleDependencyError(['SystemA', 'SystemB', 'SystemA']);
expect(error.message).toContain('SystemA');
expect(error.message).toContain('SystemB');
expect(error.involvedNodes).toEqual(['SystemA', 'SystemB', 'SystemA']);
});
it('应该是 Error 的实例', () => {
const error = new CycleDependencyError(['SystemA']);
expect(error).toBeInstanceOf(Error);
expect(error.name).toBe('CycleDependencyError');
});
});

View File

@@ -0,0 +1,463 @@
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ECSComponent, ECSSystem } from '../../../src/ECS/Decorators';
// 测试组件
@ECSComponent('ChangeDetect_Position')
class PositionComponent extends Component {
private _x: number;
private _y: number;
constructor(...args: unknown[]) {
super();
const [x = 0, y = 0] = args as [number?, number?];
this._x = x;
this._y = y;
}
public get x(): number {
return this._x;
}
public set x(value: number) {
this._x = value;
// 实际使用中需要通过 entity.scene.epochManager.current 获取
// 这里简化测试,手动调用 markDirty
}
public get y(): number {
return this._y;
}
public set y(value: number) {
this._y = value;
}
public setPosition(x: number, y: number, epoch: number): void {
this._x = x;
this._y = y;
this.markDirty(epoch);
}
}
@ECSComponent('ChangeDetect_Velocity')
class VelocityComponent extends Component {
private _vx: number;
private _vy: number;
constructor(...args: unknown[]) {
super();
const [vx = 0, vy = 0] = args as [number?, number?];
this._vx = vx;
this._vy = vy;
}
public get vx(): number {
return this._vx;
}
public get vy(): number {
return this._vy;
}
public setVelocity(vx: number, vy: number, epoch: number): void {
this._vx = vx;
this._vy = vy;
this.markDirty(epoch);
}
}
@ECSComponent('ChangeDetect_Health')
class HealthComponent extends Component {
public current: number;
public max: number;
constructor(...args: unknown[]) {
super();
const [current = 100, max = 100] = args as [number?, number?];
this.current = current;
this.max = max;
}
}
// 测试系统 - 暴露 protected 方法供测试
@ECSSystem('ChangeDetectionTestSystem')
class ChangeDetectionTestSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public changedEntities: Entity[] = [];
constructor() {
super(Matcher.all(PositionComponent, VelocityComponent));
}
protected override process(entities: readonly Entity[]): void {
this.processedEntities = [...entities];
}
// 暴露 protected 方法供测试
public testForEachChanged(
entities: readonly Entity[],
componentTypes: any[],
processor: (entity: Entity, index: number) => void,
sinceEpoch?: number
): void {
this.forEachChanged(entities, componentTypes, processor, sinceEpoch);
}
public testFilterChanged(
entities: readonly Entity[],
componentTypes: any[],
sinceEpoch?: number
): Entity[] {
return this.filterChanged(entities, componentTypes, sinceEpoch);
}
public testHasChanged(
entity: Entity,
componentTypes: any[],
sinceEpoch?: number
): boolean {
return this.hasChanged(entity, componentTypes, sinceEpoch);
}
public testSaveEpoch(): void {
this.saveEpoch();
}
public testGetLastProcessEpoch(): number {
return this.lastProcessEpoch;
}
public testGetCurrentEpoch(): number {
return this.currentEpoch;
}
}
describe('EntitySystem 变更检测', () => {
let scene: Scene;
let system: ChangeDetectionTestSystem;
let entity1: Entity;
let entity2: Entity;
let entity3: Entity;
beforeEach(() => {
scene = new Scene();
system = new ChangeDetectionTestSystem();
scene.addSystem(system);
// 创建测试实体
entity1 = scene.createEntity('entity1');
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent(1, 2));
entity2 = scene.createEntity('entity2');
entity2.addComponent(new PositionComponent(30, 40));
entity2.addComponent(new VelocityComponent(3, 4));
entity3 = scene.createEntity('entity3');
entity3.addComponent(new PositionComponent(50, 60));
entity3.addComponent(new VelocityComponent(5, 6));
});
afterEach(() => {
scene.removeSystem(system);
scene.end();
});
describe('lastProcessEpoch', () => {
it('初始值应该是 0', () => {
expect(system.testGetLastProcessEpoch()).toBe(0);
});
});
describe('currentEpoch', () => {
it('应该返回场景的当前 epoch', () => {
expect(system.testGetCurrentEpoch()).toBe(scene.epochManager.current);
});
});
describe('saveEpoch', () => {
it('应该保存当前 epoch', () => {
const currentEpoch = scene.epochManager.current;
system.testSaveEpoch();
expect(system.testGetLastProcessEpoch()).toBe(currentEpoch);
});
it('递增 epoch 后 saveEpoch 应该保存新值', () => {
scene.epochManager.increment();
scene.epochManager.increment();
const currentEpoch = scene.epochManager.current;
system.testSaveEpoch();
expect(system.testGetLastProcessEpoch()).toBe(currentEpoch);
});
});
describe('hasChanged', () => {
it('新组件应该被检测为已变更', () => {
// 组件的 lastWriteEpoch 默认是 0如果检查点也是 0则不算变更
// 需要先保存 epoch然后修改组件
system.testSaveEpoch();
scene.epochManager.increment();
const pos = entity1.getComponent(PositionComponent)!;
pos.setPosition(100, 200, scene.epochManager.current);
expect(system.testHasChanged(entity1, [PositionComponent])).toBe(true);
});
it('未修改的组件应该不被检测为变更', () => {
system.testSaveEpoch();
scene.epochManager.increment();
// entity2 的组件未被修改
expect(system.testHasChanged(entity2, [PositionComponent])).toBe(false);
});
it('应该检查多个组件类型', () => {
system.testSaveEpoch();
scene.epochManager.increment();
// 只修改 Velocity
const vel = entity1.getComponent(VelocityComponent)!;
vel.setVelocity(10, 20, scene.epochManager.current);
// 检查 Position 和 Velocity应该检测到变更
expect(system.testHasChanged(entity1, [PositionComponent, VelocityComponent])).toBe(true);
});
it('指定 sinceEpoch 参数应该使用该值作为检查点', () => {
const pos = entity1.getComponent(PositionComponent)!;
// 在 epoch 5 修改组件
scene.epochManager.increment(); // 2
scene.epochManager.increment(); // 3
scene.epochManager.increment(); // 4
scene.epochManager.increment(); // 5
pos.setPosition(100, 200, 5);
// 检查 epoch 4 之后的变更 - 应该检测到
expect(system.testHasChanged(entity1, [PositionComponent], 4)).toBe(true);
// 检查 epoch 5 之后的变更 - 不应该检测到
expect(system.testHasChanged(entity1, [PositionComponent], 5)).toBe(false);
});
});
describe('filterChanged', () => {
it('应该返回有变更的实体', () => {
system.testSaveEpoch();
scene.epochManager.increment();
// 只修改 entity1
const pos1 = entity1.getComponent(PositionComponent)!;
pos1.setPosition(100, 200, scene.epochManager.current);
const entities = [entity1, entity2, entity3];
const changed = system.testFilterChanged(entities, [PositionComponent]);
expect(changed).toHaveLength(1);
expect(changed[0]).toBe(entity1);
});
it('应该返回空数组当没有变更时', () => {
system.testSaveEpoch();
scene.epochManager.increment();
// 没有修改任何组件
const entities = [entity1, entity2, entity3];
const changed = system.testFilterChanged(entities, [PositionComponent]);
expect(changed).toHaveLength(0);
});
it('应该返回所有变更的实体', () => {
system.testSaveEpoch();
scene.epochManager.increment();
// 修改 entity1 和 entity3
const pos1 = entity1.getComponent(PositionComponent)!;
pos1.setPosition(100, 200, scene.epochManager.current);
const pos3 = entity3.getComponent(PositionComponent)!;
pos3.setPosition(500, 600, scene.epochManager.current);
const entities = [entity1, entity2, entity3];
const changed = system.testFilterChanged(entities, [PositionComponent]);
expect(changed).toHaveLength(2);
expect(changed).toContain(entity1);
expect(changed).toContain(entity3);
});
});
describe('forEachChanged', () => {
it('应该只遍历有变更的实体', () => {
system.testSaveEpoch();
scene.epochManager.increment();
// 只修改 entity2
const pos2 = entity2.getComponent(PositionComponent)!;
pos2.setPosition(300, 400, scene.epochManager.current);
const entities = [entity1, entity2, entity3];
const processed: Entity[] = [];
system.testForEachChanged(entities, [PositionComponent], (entity) => {
processed.push(entity);
});
expect(processed).toHaveLength(1);
expect(processed[0]).toBe(entity2);
});
it('应该自动更新 lastProcessEpoch', () => {
system.testSaveEpoch();
const savedEpoch = system.testGetLastProcessEpoch();
scene.epochManager.increment();
const currentEpoch = scene.epochManager.current;
const entities = [entity1, entity2];
system.testForEachChanged(entities, [PositionComponent], () => {});
// forEachChanged 应该更新 lastProcessEpoch
expect(system.testGetLastProcessEpoch()).toBe(currentEpoch);
expect(system.testGetLastProcessEpoch()).toBeGreaterThan(savedEpoch);
});
it('指定 sinceEpoch 时不应该影响自动更新', () => {
scene.epochManager.increment();
scene.epochManager.increment();
const entities = [entity1];
system.testForEachChanged(entities, [PositionComponent], () => {}, 0);
// 应该更新到当前 epoch
expect(system.testGetLastProcessEpoch()).toBe(scene.epochManager.current);
});
});
describe('实际使用场景', () => {
it('应该支持增量更新模式', () => {
// 模拟第一帧
scene.update();
// 保存检查点
system.testSaveEpoch();
const checkpoint = system.testGetLastProcessEpoch();
// 模拟第二帧 - 修改一个实体
scene.epochManager.increment();
const pos1 = entity1.getComponent(PositionComponent)!;
pos1.setPosition(100, 200, scene.epochManager.current);
// 只处理变更的实体
const changed = system.testFilterChanged(system.entities, [PositionComponent]);
expect(changed).toHaveLength(1);
expect(changed[0]).toBe(entity1);
// 更新检查点
system.testSaveEpoch();
// 模拟第三帧 - 没有修改
scene.epochManager.increment();
// 不应该有变更
const noChanges = system.testFilterChanged(system.entities, [PositionComponent]);
expect(noChanges).toHaveLength(0);
});
it('应该正确处理多次变更', () => {
system.testSaveEpoch();
// 第一次变更
scene.epochManager.increment();
const pos1 = entity1.getComponent(PositionComponent)!;
pos1.setPosition(100, 200, scene.epochManager.current);
let changed = system.testFilterChanged(system.entities, [PositionComponent]);
expect(changed).toContain(entity1);
// 更新检查点
system.testSaveEpoch();
// 第二次变更 - 不同实体
scene.epochManager.increment();
const pos2 = entity2.getComponent(PositionComponent)!;
pos2.setPosition(300, 400, scene.epochManager.current);
changed = system.testFilterChanged(system.entities, [PositionComponent]);
expect(changed).not.toContain(entity1);
expect(changed).toContain(entity2);
});
});
describe('边界情况', () => {
it('空实体列表应该正常处理', () => {
const processed: Entity[] = [];
system.testForEachChanged([], [PositionComponent], (entity) => {
processed.push(entity);
});
expect(processed).toHaveLength(0);
});
it('空组件类型列表应该不检测到任何变更', () => {
system.testSaveEpoch();
scene.epochManager.increment();
const pos1 = entity1.getComponent(PositionComponent)!;
pos1.setPosition(100, 200, scene.epochManager.current);
// 空组件类型列表
const changed = system.testFilterChanged(system.entities, []);
expect(changed).toHaveLength(0);
});
it('实体缺少指定组件时应该跳过', () => {
// 创建一个只有 Position 的实体
const entityWithoutVelocity = scene.createEntity('noVel');
entityWithoutVelocity.addComponent(new PositionComponent(70, 80));
system.testSaveEpoch();
scene.epochManager.increment();
// 修改该实体的 Position
const pos = entityWithoutVelocity.getComponent(PositionComponent)!;
pos.setPosition(100, 200, scene.epochManager.current);
// 检查 Velocity 变更 - 该实体没有 Velocity应该不被检测
const entities = [entityWithoutVelocity];
const changed = system.testFilterChanged(entities, [VelocityComponent]);
expect(changed).toHaveLength(0);
});
});
});
describe('Component.markDirty', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
afterEach(() => {
scene.end();
});
it('应该更新 lastWriteEpoch', () => {
const entity = scene.createEntity('test');
const pos = entity.addComponent(new PositionComponent(10, 20));
expect(pos.lastWriteEpoch).toBe(0);
pos.markDirty(5);
expect(pos.lastWriteEpoch).toBe(5);
pos.markDirty(10);
expect(pos.lastWriteEpoch).toBe(10);
});
});