refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions
@@ -0,0 +1,156 @@
import { Component } from '../../src/ECS/Component';
import { Entity } from '../../src/ECS/Entity';
import { Scene } from '../../src/ECS/Scene';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件
@ECSComponent('ComponentTest_TestComponent')
class TestComponent extends Component {
public value: number = 100;
public onAddedCalled = false;
public onRemovedCalled = false;
public override onAddedToEntity(): void {
this.onAddedCalled = true;
}
public override onRemovedFromEntity(): void {
this.onRemovedCalled = true;
}
}
@ECSComponent('ComponentTest_AnotherTestComponent')
class AnotherTestComponent extends Component {
public name: string = 'test';
}
describe('Component - 组件基类测试', () => {
let component: TestComponent;
let entity: Entity;
let scene: Scene;
beforeEach(() => {
component = new TestComponent();
scene = new Scene();
entity = scene.createEntity('TestEntity');
});
describe('基本功能', () => {
test('应该能够创建组件实例', () => {
expect(component).toBeInstanceOf(Component);
expect(component).toBeInstanceOf(TestComponent);
expect(component.id).toBeGreaterThanOrEqual(0);
});
test('每个组件应该有唯一的ID', () => {
const component1 = new TestComponent();
const component2 = new TestComponent();
const component3 = new AnotherTestComponent();
expect(component1.id).not.toBe(component2.id);
expect(component2.id).not.toBe(component3.id);
expect(component1.id).not.toBe(component3.id);
});
test('组件ID应该递增分配', () => {
const component1 = new TestComponent();
const component2 = new TestComponent();
expect(component2.id).toBe(component1.id + 1);
expect(component1.id).toBeGreaterThanOrEqual(0);
});
});
describe('生命周期回调', () => {
test('添加到实体时应该调用onAddedToEntity', () => {
expect(component.onAddedCalled).toBe(false);
entity.addComponent(component);
expect(component.onAddedCalled).toBe(true);
});
test('从实体移除时应该调用onRemovedFromEntity', () => {
entity.addComponent(component);
expect(component.onRemovedCalled).toBe(false);
entity.removeComponent(component);
expect(component.onRemovedCalled).toBe(true);
});
test('基类的默认生命周期方法应该安全调用', () => {
const baseComponent = new (class extends Component {})();
// 这些方法不应该抛出异常
expect(() => {
baseComponent.onAddedToEntity();
baseComponent.onRemovedFromEntity();
}).not.toThrow();
});
});
describe('实体-组件关系', () => {
test('组件可以被添加到实体', () => {
expect(() => {
entity.addComponent(component);
}).not.toThrow();
expect(entity.hasComponent(TestComponent)).toBe(true);
expect(entity.getComponent(TestComponent)).toBe(component);
});
test('组件可以从实体移除', () => {
entity.addComponent(component);
expect(() => {
entity.removeComponent(component);
}).not.toThrow();
expect(entity.hasComponent(TestComponent)).toBe(false);
});
});
describe('边界情况', () => {
test('大量组件创建应该有不同的ID', () => {
const components: Component[] = [];
const count = 1000;
for (let i = 0; i < count; i++) {
components.push(new TestComponent());
}
// 检查所有ID都不同
const ids = new Set(components.map(c => c.id));
expect(ids.size).toBe(count);
});
test('组件应该是纯数据容器', () => {
// 验证组件只有数据字段
const comp = new TestComponent();
expect(comp.value).toBe(100);
// 可以修改数据
comp.value = 200;
expect(comp.value).toBe(200);
});
});
describe('继承和多态', () => {
test('不同类型的组件应该都继承自Component', () => {
const test1 = new TestComponent();
const test2 = new AnotherTestComponent();
expect(test1).toBeInstanceOf(Component);
expect(test2).toBeInstanceOf(Component);
expect(test1).toBeInstanceOf(TestComponent);
expect(test2).toBeInstanceOf(AnotherTestComponent);
});
test('组件应该能够重写基类方法', () => {
const test = new TestComponent();
// 重写生命周期方法应该工作
entity.addComponent(test);
expect(test.onAddedCalled).toBe(true);
});
});
});
@@ -0,0 +1,425 @@
import { CommandBuffer } from '../../../src/ECS/Core/CommandBuffer';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件
@ECSComponent('CmdBuf_HealthComponent')
class HealthComponent extends Component {
public value: number = 100;
constructor(...args: unknown[]) {
super();
const [value = 100] = args as [number?];
this.value = value;
}
}
@ECSComponent('CmdBuf_MarkerComponent')
class MarkerComponent extends Component {
public marked: boolean = true;
}
@ECSComponent('CmdBuf_VelocityComponent')
class VelocityComponent extends Component {
public vx: number = 0;
public vy: number = 0;
}
// 测试系统 - 使用 CommandBuffer 延迟执行
class DamageSystem extends EntitySystem {
public processedEntities: Entity[] = [];
constructor() {
super(Matcher.all(HealthComponent));
}
protected override process(entities: readonly Entity[]): void {
this.processedEntities = [];
for (const entity of entities) {
this.processedEntities.push(entity);
const health = entity.getComponent(HealthComponent);
if (health && health.value <= 0) {
// 使用延迟命令添加标记组件
this.commands.addComponent(entity, new MarkerComponent());
}
}
}
}
describe('CommandBuffer', () => {
let commandBuffer: CommandBuffer;
let scene: Scene;
beforeEach(() => {
scene = new Scene();
commandBuffer = new CommandBuffer(scene);
});
afterEach(() => {
scene.destroyAllEntities();
});
describe('基础功能 | Basic functionality', () => {
test('创建空的 CommandBuffer | should create empty CommandBuffer', () => {
expect(commandBuffer.pendingCount).toBe(0);
expect(commandBuffer.hasPending).toBe(false);
});
test('设置和获取场景 | should set and get scene', () => {
const newScene = new Scene();
commandBuffer.setScene(newScene);
expect(commandBuffer.scene).toBe(newScene);
newScene.destroyAllEntities();
});
test('构造函数参数 | should accept constructor parameters', () => {
const cb = new CommandBuffer(scene, true);
expect(cb.scene).toBe(scene);
});
});
describe('添加组件命令 | Add component command', () => {
test('延迟添加组件 | should defer adding component', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
const component = new MarkerComponent();
commandBuffer.addComponent(entity, component);
// 命令已入队但未执行
expect(commandBuffer.pendingCount).toBe(1);
expect(commandBuffer.hasPending).toBe(true);
expect(entity.hasComponent(MarkerComponent)).toBe(false);
// 执行命令
const executedCount = commandBuffer.flush();
expect(executedCount).toBe(1);
expect(commandBuffer.pendingCount).toBe(0);
expect(entity.hasComponent(MarkerComponent)).toBe(true);
});
test('多个添加组件命令 | should handle multiple add component commands', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
commandBuffer.addComponent(entity, new HealthComponent(50));
commandBuffer.addComponent(entity, new MarkerComponent());
commandBuffer.addComponent(entity, new VelocityComponent());
expect(commandBuffer.pendingCount).toBe(3);
commandBuffer.flush();
expect(entity.hasComponent(HealthComponent)).toBe(true);
expect(entity.hasComponent(MarkerComponent)).toBe(true);
expect(entity.hasComponent(VelocityComponent)).toBe(true);
});
});
describe('移除组件命令 | Remove component command', () => {
test('延迟移除组件 | should defer removing component', () => {
const entity = scene.createEntity('test');
entity.addComponent(new HealthComponent(100));
scene.addEntity(entity);
expect(entity.hasComponent(HealthComponent)).toBe(true);
commandBuffer.removeComponent(entity, HealthComponent);
// 命令已入队但未执行
expect(commandBuffer.pendingCount).toBe(1);
expect(entity.hasComponent(HealthComponent)).toBe(true);
// 执行命令
commandBuffer.flush();
expect(entity.hasComponent(HealthComponent)).toBe(false);
});
test('移除不存在的组件不报错 | should not throw when removing non-existent component', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
commandBuffer.removeComponent(entity, HealthComponent);
expect(() => commandBuffer.flush()).not.toThrow();
});
});
describe('销毁实体命令 | Destroy entity command', () => {
test('延迟销毁实体 | should defer destroying entity', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
expect(entity.isDestroyed).toBe(false);
commandBuffer.destroyEntity(entity);
// 命令已入队但未执行
expect(commandBuffer.pendingCount).toBe(1);
expect(entity.isDestroyed).toBe(false);
// 执行命令
commandBuffer.flush();
expect(entity.isDestroyed).toBe(true);
});
test('多个实体销毁命令 | should handle multiple destroy commands', () => {
const entity1 = scene.createEntity('test1');
const entity2 = scene.createEntity('test2');
const entity3 = scene.createEntity('test3');
scene.addEntity(entity1);
scene.addEntity(entity2);
scene.addEntity(entity3);
commandBuffer.destroyEntity(entity1);
commandBuffer.destroyEntity(entity2);
commandBuffer.destroyEntity(entity3);
expect(commandBuffer.pendingCount).toBe(3);
commandBuffer.flush();
expect(entity1.isDestroyed).toBe(true);
expect(entity2.isDestroyed).toBe(true);
expect(entity3.isDestroyed).toBe(true);
});
});
describe('设置实体激活状态命令 | Set entity active command', () => {
test('延迟设置实体激活状态 | should defer setting entity active state', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
entity.active = true;
commandBuffer.setEntityActive(entity, false);
// 命令已入队但未执行
expect(entity.active).toBe(true);
// 执行命令
commandBuffer.flush();
expect(entity.active).toBe(false);
});
test('切换激活状态 | should toggle active state', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
entity.active = true;
commandBuffer.setEntityActive(entity, false);
commandBuffer.flush();
expect(entity.active).toBe(false);
commandBuffer.setEntityActive(entity, true);
commandBuffer.flush();
expect(entity.active).toBe(true);
});
});
describe('命令执行安全性 | Command execution safety', () => {
test('跳过已销毁的实体 | should skip destroyed entities', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
commandBuffer.addComponent(entity, new MarkerComponent());
// 在执行前销毁实体
entity.destroy();
// 执行命令不应报错
expect(() => commandBuffer.flush()).not.toThrow();
expect(commandBuffer.pendingCount).toBe(0);
});
test('flush 过程中添加新命令 | should handle commands added during flush', () => {
const entity1 = scene.createEntity('test1');
const entity2 = scene.createEntity('test2');
scene.addEntity(entity1);
scene.addEntity(entity2);
// 创建一个特殊的命令缓冲区,在 flush 时会添加新命令
// 但由于 flush 复制了命令列表,新命令不会在本次 flush 中执行
commandBuffer.addComponent(entity1, new MarkerComponent());
const executedCount = commandBuffer.flush();
expect(executedCount).toBe(1);
expect(entity1.hasComponent(MarkerComponent)).toBe(true);
});
test('单个命令失败不影响其他命令 | single command failure should not affect others', () => {
const entity1 = scene.createEntity('test1');
const entity2 = scene.createEntity('test2');
scene.addEntity(entity1);
scene.addEntity(entity2);
commandBuffer.addComponent(entity1, new MarkerComponent());
commandBuffer.addComponent(entity2, new HealthComponent());
// 销毁 entity1,使第一个命令失效
entity1.destroy();
// 执行命令,第一个失败,第二个应该成功
commandBuffer.flush();
expect(entity2.hasComponent(HealthComponent)).toBe(true);
});
});
describe('clear 和 dispose | clear and dispose', () => {
test('clear 清空命令但不执行 | should clear commands without executing', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
commandBuffer.addComponent(entity, new MarkerComponent());
commandBuffer.destroyEntity(entity);
// 由于去重逻辑,destroyEntity 会清除同一实体的其他操作
// Due to deduplication, destroyEntity clears other operations for the same entity
expect(commandBuffer.pendingCount).toBe(1);
commandBuffer.clear();
expect(commandBuffer.pendingCount).toBe(0);
expect(entity.hasComponent(MarkerComponent)).toBe(false);
expect(entity.scene).toBe(scene);
});
test('dispose 清空命令和场景引用 | should dispose buffer', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
commandBuffer.addComponent(entity, new MarkerComponent());
expect(commandBuffer.scene).toBe(scene);
commandBuffer.dispose();
expect(commandBuffer.pendingCount).toBe(0);
expect(commandBuffer.scene).toBeNull();
});
});
describe('混合命令 | Mixed commands', () => {
test('复杂命令序列 | should handle complex command sequence', () => {
const entity = scene.createEntity('test');
entity.addComponent(new HealthComponent(100));
scene.addEntity(entity);
// 添加一个组件
commandBuffer.addComponent(entity, new MarkerComponent());
// 移除原有组件
commandBuffer.removeComponent(entity, HealthComponent);
// 再添加一个组件
commandBuffer.addComponent(entity, new VelocityComponent());
expect(commandBuffer.pendingCount).toBe(3);
commandBuffer.flush();
expect(entity.hasComponent(MarkerComponent)).toBe(true);
expect(entity.hasComponent(HealthComponent)).toBe(false);
expect(entity.hasComponent(VelocityComponent)).toBe(true);
});
test('先添加后销毁 | should handle add then destroy sequence', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
commandBuffer.addComponent(entity, new MarkerComponent());
commandBuffer.destroyEntity(entity);
commandBuffer.flush();
// 实体已销毁,组件添加应该已执行(在销毁前)
expect(entity.isDestroyed).toBe(true);
});
});
describe('与 EntitySystem 集成 | Integration with EntitySystem', () => {
test('系统可以使用 commands 属性 | system should have commands property', () => {
const system = new DamageSystem();
scene.addSystem(system);
expect(system['commands']).toBeInstanceOf(CommandBuffer);
});
test('系统中使用延迟命令 | should use deferred commands in system', () => {
const entity = scene.createEntity('damaged');
entity.addComponent(new HealthComponent(0)); // 生命值为0
scene.addEntity(entity);
const system = new DamageSystem();
scene.addSystem(system);
// 第一次 updatesystem.process 会添加延迟命令
scene.update();
// 检查系统处理了实体
expect(system.processedEntities.length).toBe(1);
// 命令应该已经被 flush 执行了(在 Scene.update 的末尾)
expect(entity.hasComponent(MarkerComponent)).toBe(true);
});
test('延迟命令不影响当前帧迭代 | deferred commands should not affect current iteration', () => {
// 创建多个实体
const entities: Entity[] = [];
for (let i = 0; i < 5; i++) {
const entity = scene.createEntity(`entity${i}`);
entity.addComponent(new HealthComponent(i === 2 ? 0 : 100)); // entity2 的生命值为0
scene.addEntity(entity);
entities.push(entity);
}
const system = new DamageSystem();
scene.addSystem(system);
// update 执行
scene.update();
// 所有5个实体都应该被处理(延迟命令不影响迭代)
expect(system.processedEntities.length).toBe(5);
// entity2 应该有 MarkerComponent
expect(entities[2].hasComponent(MarkerComponent)).toBe(true);
});
});
describe('边界情况 | Edge cases', () => {
test('空的 flush | should handle empty flush', () => {
const count = commandBuffer.flush();
expect(count).toBe(0);
});
test('多次 flush | should handle multiple flushes', () => {
const entity = scene.createEntity('test');
scene.addEntity(entity);
commandBuffer.addComponent(entity, new MarkerComponent());
expect(commandBuffer.flush()).toBe(1);
expect(commandBuffer.flush()).toBe(0); // 第二次应该是空的
});
test('无场景的 CommandBuffer | should work without scene', () => {
const cb = new CommandBuffer();
expect(cb.scene).toBeNull();
// 仍然可以入队命令
const entity = scene.createEntity('test');
scene.addEntity(entity);
cb.addComponent(entity, new MarkerComponent());
expect(cb.pendingCount).toBe(1);
cb.flush();
expect(entity.hasComponent(MarkerComponent)).toBe(true);
});
});
});
@@ -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);
});
});
});
});
@@ -0,0 +1,473 @@
import { ComponentPool, ComponentPoolManager } from '../../../src/ECS/Core/ComponentPool';
import { Component } from '../../../src/ECS/Component';
// 测试用组件类
class TestComponent extends Component {
public value: number = 0;
public name: string = '';
reset(): void {
this.value = 0;
this.name = '';
}
}
class AnotherTestComponent extends Component {
public data: string = '';
reset(): void {
this.data = '';
}
}
describe('ComponentPool - 组件对象池测试', () => {
let pool: ComponentPool<TestComponent>;
let createFn: () => TestComponent;
let resetFn: (component: TestComponent) => void;
beforeEach(() => {
createFn = () => new TestComponent();
resetFn = (component: TestComponent) => component.reset();
pool = new ComponentPool(createFn, resetFn, 10);
});
describe('基本功能测试', () => {
it('应该能够创建组件池', () => {
expect(pool).toBeDefined();
expect(pool.getAvailableCount()).toBe(0);
expect(pool.getMaxSize()).toBe(10);
});
it('应该能够获取组件实例', () => {
const component = pool.acquire();
expect(component).toBeInstanceOf(TestComponent);
expect(component.value).toBe(0);
});
it('第一次获取应该创建新实例', () => {
const component = pool.acquire();
expect(component).toBeInstanceOf(TestComponent);
expect(pool.getAvailableCount()).toBe(0);
});
it('应该能够释放组件回池中', () => {
const component = pool.acquire();
component.value = 42;
component.name = 'test';
pool.release(component);
expect(pool.getAvailableCount()).toBe(1);
expect(component.value).toBe(0); // 应该被重置
expect(component.name).toBe(''); // 应该被重置
});
it('从池中获取的组件应该是之前释放的', () => {
const component1 = pool.acquire();
pool.release(component1);
const component2 = pool.acquire();
expect(component2).toBe(component1); // 应该是同一个实例
});
});
describe('池容量管理', () => {
it('应该能够设置最大容量', () => {
const smallPool = new ComponentPool(createFn, resetFn, 2);
expect(smallPool.getMaxSize()).toBe(2);
});
it('超过最大容量的组件不应该被存储', () => {
const smallPool = new ComponentPool(createFn, resetFn, 2);
const components = [];
for (let i = 0; i < 5; i++) {
components.push(smallPool.acquire());
}
// 释放所有组件
components.forEach(comp => smallPool.release(comp));
// 只有2个组件被存储在池中
expect(smallPool.getAvailableCount()).toBe(2);
});
it('应该正确处理默认最大容量', () => {
const defaultPool = new ComponentPool(createFn);
expect(defaultPool.getMaxSize()).toBe(1000); // 默认值
});
});
describe('重置功能测试', () => {
it('没有重置函数时应该正常工作', () => {
const poolWithoutReset = new ComponentPool<TestComponent>(createFn);
const component = poolWithoutReset.acquire();
component.value = 42;
poolWithoutReset.release(component);
// 没有重置函数,值应该保持不变
expect(component.value).toBe(42);
expect(poolWithoutReset.getAvailableCount()).toBe(1);
});
it('重置函数应该在释放时被调用', () => {
const mockReset = jest.fn();
const poolWithMockReset = new ComponentPool(createFn, mockReset);
const component = poolWithMockReset.acquire();
poolWithMockReset.release(component);
expect(mockReset).toHaveBeenCalledWith(component);
});
});
describe('预热功能', () => {
it('应该能够预填充对象池', () => {
pool.prewarm(5);
expect(pool.getAvailableCount()).toBe(5);
});
it('预热不应该超过最大容量', () => {
pool.prewarm(15); // 超过最大容量10
expect(pool.getAvailableCount()).toBe(10);
});
it('预热0个对象应该安全', () => {
pool.prewarm(0);
expect(pool.getAvailableCount()).toBe(0);
});
it('多次预热应该填充到最大值', () => {
pool.prewarm(3);
expect(pool.getAvailableCount()).toBe(3);
pool.prewarm(5);
expect(pool.getAvailableCount()).toBe(5);
pool.prewarm(2);
expect(pool.getAvailableCount()).toBe(5);
});
});
describe('清空功能', () => {
it('应该能够清空对象池', () => {
pool.prewarm(5);
expect(pool.getAvailableCount()).toBe(5);
pool.clear();
expect(pool.getAvailableCount()).toBe(0);
});
it('空池清空应该安全', () => {
pool.clear();
expect(pool.getAvailableCount()).toBe(0);
});
});
describe('边界情况测试', () => {
it('应该处理连续的获取和释放', () => {
const components: TestComponent[] = [];
// 获取多个组件
for (let i = 0; i < 5; i++) {
components.push(pool.acquire());
}
// 释放所有组件
components.forEach(comp => pool.release(comp));
expect(pool.getAvailableCount()).toBe(5);
// 再次获取应该复用之前的实例
const reusedComponents: TestComponent[] = [];
for (let i = 0; i < 5; i++) {
reusedComponents.push(pool.acquire());
}
expect(pool.getAvailableCount()).toBe(0);
// 验证复用的组件确实是之前的实例
components.forEach(originalComp => {
expect(reusedComponents).toContain(originalComp);
});
});
it('应该处理空池的多次获取', () => {
const component1 = pool.acquire();
const component2 = pool.acquire();
const component3 = pool.acquire();
expect(component1).not.toBe(component2);
expect(component2).not.toBe(component3);
expect(component1).not.toBe(component3);
});
});
});
describe('ComponentPoolManager - 组件池管理器测试', () => {
let manager: ComponentPoolManager;
beforeEach(() => {
manager = ComponentPoolManager.getInstance();
// 重置管理器以确保测试隔离
manager.reset();
});
describe('单例模式测试', () => {
it('应该返回同一个实例', () => {
const instance1 = ComponentPoolManager.getInstance();
const instance2 = ComponentPoolManager.getInstance();
expect(instance1).toBe(instance2);
});
});
describe('池注册和管理', () => {
it('应该能够注册组件池', () => {
const createFn = () => new TestComponent();
const resetFn = (comp: TestComponent) => comp.reset();
manager.registerPool('TestComponent', createFn, resetFn, 20);
const stats = manager.getPoolStats();
expect(stats.has('TestComponent')).toBe(true);
expect(stats.get('TestComponent')?.maxSize).toBe(20);
});
it('应该能够注册多个不同类型的池', () => {
manager.registerPool('TestComponent', () => new TestComponent());
manager.registerPool('AnotherTestComponent', () => new AnotherTestComponent());
const stats = manager.getPoolStats();
expect(stats.size).toBe(2);
expect(stats.has('TestComponent')).toBe(true);
expect(stats.has('AnotherTestComponent')).toBe(true);
});
it('注册池时应该使用默认参数', () => {
manager.registerPool('TestComponent', () => new TestComponent());
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.maxSize).toBe(1000); // 默认值
});
});
describe('组件获取和释放', () => {
beforeEach(() => {
manager.registerPool('TestComponent', () => new TestComponent(), (comp) => comp.reset());
});
it('应该能够获取组件实例', () => {
const component = manager.acquireComponent<TestComponent>('TestComponent');
expect(component).toBeInstanceOf(TestComponent);
});
it('获取未注册池的组件应该返回null', () => {
const component = manager.acquireComponent<TestComponent>('UnknownComponent');
expect(component).toBeNull();
});
it('应该能够释放组件实例', () => {
const component = manager.acquireComponent<TestComponent>('TestComponent')!;
component.value = 42;
manager.releaseComponent('TestComponent', component);
// 验证组件被重置并返回池中
const reusedComponent = manager.acquireComponent<TestComponent>('TestComponent');
expect(reusedComponent).toBe(component);
expect(reusedComponent!.value).toBe(0); // 应该被重置
});
it('释放到未注册池应该安全处理', () => {
const component = new TestComponent();
expect(() => {
manager.releaseComponent('UnknownComponent', component);
}).not.toThrow();
});
});
describe('批量操作', () => {
beforeEach(() => {
manager.registerPool('TestComponent', () => new TestComponent());
manager.registerPool('AnotherTestComponent', () => new AnotherTestComponent());
});
it('应该能够预热所有池', () => {
manager.prewarmAll(5);
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.available).toBe(5);
expect(stats.get('AnotherTestComponent')?.available).toBe(5);
});
it('应该能够清空所有池', () => {
manager.prewarmAll(5);
manager.clearAll();
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.available).toBe(0);
expect(stats.get('AnotherTestComponent')?.available).toBe(0);
});
it('prewarmAll应该使用默认值', () => {
manager.prewarmAll(); // 默认100
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.available).toBe(100);
expect(stats.get('AnotherTestComponent')?.available).toBe(100);
});
});
describe('统计信息', () => {
beforeEach(() => {
manager.registerPool('TestComponent', () => new TestComponent(), undefined, 50);
});
it('应该能够获取池统计信息', () => {
const stats = manager.getPoolStats();
expect(stats.has('TestComponent')).toBe(true);
const poolStat = stats.get('TestComponent')!;
expect(poolStat.available).toBe(0);
expect(poolStat.maxSize).toBe(50);
});
it('应该能够获取池利用率信息', () => {
manager.prewarmAll(30); // 预热30个
const utilization = manager.getPoolUtilization();
const testComponentUtil = utilization.get('TestComponent')!;
expect(testComponentUtil.used).toBe(20); // 50 - 30 = 20
expect(testComponentUtil.total).toBe(50);
expect(testComponentUtil.utilization).toBe(40); // 20/50 * 100
});
it('应该能够获取指定组件的池利用率', () => {
manager.prewarmAll(30);
const utilization = manager.getComponentUtilization('TestComponent');
expect(utilization).toBe(40); // (50-30)/50 * 100
});
it('获取未注册组件的利用率应该返回0', () => {
const utilization = manager.getComponentUtilization('UnknownComponent');
expect(utilization).toBe(0);
});
it('空池的利用率应该为0', () => {
// 完全重置管理器,移除所有池
manager.reset();
const utilization = manager.getComponentUtilization('TestComponent');
expect(utilization).toBe(0);
});
});
describe('动态使用场景测试', () => {
beforeEach(() => {
manager.registerPool('TestComponent', () => new TestComponent(), (comp) => comp.reset(), 10);
});
it('应该正确跟踪组件使用情况', () => {
// 获取5个组件
const components = [];
for (let i = 0; i < 5; i++) {
const comp = manager.acquireComponent<TestComponent>('TestComponent')!;
comp.value = i;
components.push(comp);
}
// 利用率 = (maxSize - available) / maxSize * 100
// 获取了5个,池中应该没有可用的(因为是从空池开始),所以利用率是 10/10 * 100 = 100%
let utilization = manager.getComponentUtilization('TestComponent');
expect(utilization).toBe(100); // 10/10 * 100
// 释放3个组件
for (let i = 0; i < 3; i++) {
manager.releaseComponent('TestComponent', components[i]);
}
// 现在池中有3个可用,7个在使用
utilization = manager.getComponentUtilization('TestComponent');
expect(utilization).toBe(70); // 7/10 * 100
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.available).toBe(3); // 池中有3个可用
});
it('应该处理池满的情况', () => {
// 预热到满容量
manager.prewarmAll(10);
// 获取所有组件
const components = [];
for (let i = 0; i < 10; i++) {
components.push(manager.acquireComponent<TestComponent>('TestComponent')!);
}
expect(manager.getComponentUtilization('TestComponent')).toBe(100);
// 尝试释放更多组件(超过容量)
for (let i = 0; i < 15; i++) {
const extraComp = new TestComponent();
manager.releaseComponent('TestComponent', extraComp);
}
// 池应该仍然是满的,不会超过容量
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.available).toBe(10);
});
});
describe('性能测试', () => {
beforeEach(() => {
manager.registerPool('TestComponent', () => new TestComponent(), (comp) => comp.reset());
});
it('大量组件获取和释放应该高效', () => {
const startTime = performance.now();
const components = [];
// 获取1000个组件
for (let i = 0; i < 1000; i++) {
components.push(manager.acquireComponent<TestComponent>('TestComponent')!);
}
// 释放所有组件
components.forEach(comp => manager.releaseComponent('TestComponent', comp));
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(100); // 应该在100ms内完成
});
});
describe('边界情况和错误处理', () => {
it('空管理器的统计信息应该正确', () => {
// 完全重置管理器以确保清洁状态
const emptyManager = ComponentPoolManager.getInstance();
emptyManager.reset();
const stats = emptyManager.getPoolStats();
const utilization = emptyManager.getPoolUtilization();
expect(stats.size).toBe(0);
expect(utilization.size).toBe(0);
});
it('重复注册同一组件类型应该覆盖之前的池', () => {
manager.registerPool('TestComponent', () => new TestComponent(), undefined, 10);
manager.registerPool('TestComponent', () => new TestComponent(), undefined, 20);
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.maxSize).toBe(20);
});
it('处理极端的预热数量', () => {
manager.registerPool('TestComponent', () => new TestComponent(), undefined, 5);
// 预热超过最大容量
manager.prewarmAll(100);
const stats = manager.getPoolStats();
expect(stats.get('TestComponent')?.available).toBe(5); // 不应该超过最大容量
});
});
});
@@ -0,0 +1,337 @@
import { Component } from '../../../src/ECS/Component';
import { GlobalComponentRegistry } from '../../../src/ECS/Core/ComponentStorage/ComponentRegistry';
import { Entity } from '../../../src/ECS/Entity';
import { Scene } from '../../../src/ECS/Scene';
describe('ComponentRegistry Extended - 64+ 组件支持', () => {
// 组件类缓存 | Component class cache
const componentClassCache = new Map<number, any>();
beforeEach(() => {
GlobalComponentRegistry.reset();
componentClassCache.clear();
});
afterEach(() => {
GlobalComponentRegistry.reset();
componentClassCache.clear();
});
// 动态创建或获取缓存的组件类
function createTestComponent(index: number) {
if (componentClassCache.has(index)) {
return componentClassCache.get(index);
}
class TestComponent extends Component {
static readonly typeName = `TestComponent${index}`;
public value: number = index;
}
componentClassCache.set(index, TestComponent);
return TestComponent;
}
describe('扩展组件注册', () => {
it('应该能够注册超过 64 个组件类型', () => {
const componentTypes: any[] = [];
// 注册 100 个组件类型
for (let i = 0; i < 100; i++) {
const ComponentClass = createTestComponent(i);
const bitIndex = GlobalComponentRegistry.register(ComponentClass);
componentTypes.push(ComponentClass);
expect(bitIndex).toBe(i);
expect(GlobalComponentRegistry.isRegistered(ComponentClass)).toBe(true);
}
expect(componentTypes.length).toBe(100);
});
it('应该能够获取超过 64 索引的组件位掩码', () => {
// 注册 80 个组件
for (let i = 0; i < 80; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
}
// 验证第 70 个组件的位掩码
const Component70 = createTestComponent(70);
GlobalComponentRegistry.register(Component70);
const bitMask = GlobalComponentRegistry.getBitMask(Component70);
expect(bitMask).toBeDefined();
expect(bitMask.segments).toBeDefined(); // 应该有扩展段
expect(bitMask.segments!.length).toBeGreaterThan(0);
});
it('应该支持超过 1000 个组件类型(无限制)', () => {
// 注册 1500 个组件验证无限制
for (let i = 0; i < 1500; i++) {
const ComponentClass = createTestComponent(i);
const bitIndex = GlobalComponentRegistry.register(ComponentClass);
expect(bitIndex).toBe(i);
}
expect(GlobalComponentRegistry.getRegisteredCount()).toBe(1500);
});
});
describe('Entity 扩展组件支持', () => {
let scene: Scene;
let entity: Entity;
beforeEach(() => {
scene = new Scene();
entity = scene.createEntity('TestEntity');
});
it('应该能够添加和获取超过 64 个组件', () => {
const componentTypes: any[] = [];
const components: any[] = [];
// 添加 80 个组件 | Add 80 components
// 需要同时注册到 GlobalComponentRegistryArchetypeSystem 使用)和 Scene registry
// Need to register to both GlobalComponentRegistry (used by ArchetypeSystem) and Scene registry
for (let i = 0; i < 80; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
scene.componentRegistry.register(ComponentClass);
componentTypes.push(ComponentClass);
const component = new ComponentClass();
entity.addComponent(component);
components.push(component);
}
// 验证所有组件都能获取
for (let i = 0; i < 80; i++) {
const ComponentClass = componentTypes[i];
const retrieved = entity.getComponent(ComponentClass);
expect(retrieved).toBeDefined();
expect(retrieved).toBe(components[i]);
expect((retrieved as any).value).toBe(i);
}
});
it('应该能够正确检查超过 64 个组件的存在性', () => {
// 添加组件 0-79 | Add components 0-79
for (let i = 0; i < 80; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
scene.componentRegistry.register(ComponentClass);
entity.addComponent(new ComponentClass());
}
// 验证 hasComponent 对所有组件都工作 | Verify hasComponent works for all
for (let i = 0; i < 80; i++) {
const ComponentClass = createTestComponent(i);
expect(entity.hasComponent(ComponentClass)).toBe(true);
}
// 验证不存在的组件 | Verify non-existent component
const NonExistentComponent = createTestComponent(999);
GlobalComponentRegistry.register(NonExistentComponent);
scene.componentRegistry.register(NonExistentComponent);
expect(entity.hasComponent(NonExistentComponent)).toBe(false);
});
it('应该能够移除超过 64 索引的组件', () => {
const componentTypes: any[] = [];
// 添加 80 个组件 | Add 80 components
for (let i = 0; i < 80; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
scene.componentRegistry.register(ComponentClass);
componentTypes.push(ComponentClass);
entity.addComponent(new ComponentClass());
}
// 移除第 70 个组件
const Component70 = componentTypes[70];
const component70 = entity.getComponent(Component70);
expect(component70).toBeDefined();
entity.removeComponent(component70!);
// 验证已移除
expect(entity.hasComponent(Component70)).toBe(false);
expect(entity.getComponent(Component70)).toBeNull();
// 验证其他组件仍然存在
expect(entity.hasComponent(componentTypes[69])).toBe(true);
expect(entity.hasComponent(componentTypes[71])).toBe(true);
});
it('应该能够正确遍历超过 64 个组件', () => {
// 添加 80 个组件 | Add 80 components
for (let i = 0; i < 80; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
scene.componentRegistry.register(ComponentClass);
entity.addComponent(new ComponentClass());
}
const components = entity.components;
expect(components.length).toBe(80);
// 验证组件值
const values = components.map((c: any) => c.value).sort((a, b) => a - b);
for (let i = 0; i < 80; i++) {
expect(values[i]).toBe(i);
}
});
});
describe('热更新模式', () => {
it('默认应该禁用热更新模式', () => {
expect(GlobalComponentRegistry.isHotReloadEnabled()).toBe(false);
});
it('应该能够启用和禁用热更新模式', () => {
expect(GlobalComponentRegistry.isHotReloadEnabled()).toBe(false);
GlobalComponentRegistry.enableHotReload();
expect(GlobalComponentRegistry.isHotReloadEnabled()).toBe(true);
GlobalComponentRegistry.disableHotReload();
expect(GlobalComponentRegistry.isHotReloadEnabled()).toBe(false);
});
it('reset 应该重置热更新模式为禁用', () => {
GlobalComponentRegistry.enableHotReload();
expect(GlobalComponentRegistry.isHotReloadEnabled()).toBe(true);
GlobalComponentRegistry.reset();
expect(GlobalComponentRegistry.isHotReloadEnabled()).toBe(false);
});
it('启用热更新时应该替换同名组件类', () => {
GlobalComponentRegistry.enableHotReload();
// 模拟热更新场景:两个不同的类但有相同的 constructor.name
// Simulate hot reload: two different classes with same constructor.name
const createHotReloadComponent = (version: number) => {
// 使用 eval 创建具有相同名称的类
// Use function constructor to create classes with same name
const cls = (new Function('Component', `
return class HotReloadTestComponent extends Component {
constructor() {
super();
this.version = ${version};
}
}
`))(Component);
return cls;
};
const TestComponentV1 = createHotReloadComponent(1);
const TestComponentV2 = createHotReloadComponent(2);
// 确保两个类名相同但不是同一个类
expect(TestComponentV1.name).toBe(TestComponentV2.name);
expect(TestComponentV1).not.toBe(TestComponentV2);
const index1 = GlobalComponentRegistry.register(TestComponentV1);
const index2 = GlobalComponentRegistry.register(TestComponentV2);
// 应该复用相同的 bitIndex
expect(index1).toBe(index2);
// 新类应该替换旧类
expect(GlobalComponentRegistry.isRegistered(TestComponentV2)).toBe(true);
expect(GlobalComponentRegistry.isRegistered(TestComponentV1)).toBe(false);
});
it('禁用热更新时不应该替换同名组件类', () => {
// 确保热更新被禁用
GlobalComponentRegistry.disableHotReload();
// 创建两个同名组件
// Create two classes with same constructor.name
const createNoHotReloadComponent = (id: number) => {
const cls = (new Function('Component', `
return class NoHotReloadComponent extends Component {
constructor() {
super();
this.id = ${id};
}
}
`))(Component);
return cls;
};
const TestCompA = createNoHotReloadComponent(1);
const TestCompB = createNoHotReloadComponent(2);
// 确保两个类名相同但不是同一个类
expect(TestCompA.name).toBe(TestCompB.name);
expect(TestCompA).not.toBe(TestCompB);
const index1 = GlobalComponentRegistry.register(TestCompA);
const index2 = GlobalComponentRegistry.register(TestCompB);
// 应该分配不同的 bitIndex(因为热更新被禁用)
expect(index2).toBe(index1 + 1);
// 两个类都应该被注册
expect(GlobalComponentRegistry.isRegistered(TestCompA)).toBe(true);
expect(GlobalComponentRegistry.isRegistered(TestCompB)).toBe(true);
});
});
describe('边界情况', () => {
it('应该正确处理第 64 个组件(边界)', () => {
const scene = new Scene();
const entity = scene.createEntity('TestEntity');
// 注册 65 个组件(跨越 64 位边界)| Register 65 components (crossing 64-bit boundary)
for (let i = 0; i < 65; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
scene.componentRegistry.register(ComponentClass);
entity.addComponent(new ComponentClass());
}
// 验证第 63, 64, 65 个组件 | Verify components 63, 64
const Component63 = createTestComponent(63);
const Component64 = createTestComponent(64);
expect(entity.hasComponent(Component63)).toBe(true);
expect(entity.hasComponent(Component64)).toBe(true);
});
it('应该在组件缓存重建时正确处理扩展位', () => {
const scene = new Scene();
const entity = scene.createEntity('TestEntity');
// 添加 80 个组件 | Add 80 components
for (let i = 0; i < 80; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
scene.componentRegistry.register(ComponentClass);
entity.addComponent(new ComponentClass());
}
// 强制重建缓存(通过访问 components| Force cache rebuild
const components1 = entity.components;
expect(components1.length).toBe(80);
// 添加更多组件 | Add more components
for (let i = 80; i < 90; i++) {
const ComponentClass = createTestComponent(i);
GlobalComponentRegistry.register(ComponentClass);
scene.componentRegistry.register(ComponentClass);
entity.addComponent(new ComponentClass());
}
// 重新获取组件数组(应该重建缓存)| Re-get component array (should rebuild cache)
const components2 = entity.components;
expect(components2.length).toBe(90);
});
});
});
@@ -0,0 +1,89 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { EnableSoA } from '../../../src/ECS/Core/SoAStorage';
// 默认原始存储组件
class PositionComponent extends Component {
public x: number = 0;
public y: number = 0;
public z: number = 0;
}
// 启用SoA优化的组件(用于大规模批量操作)
@EnableSoA
class LargeScaleComponent extends Component {
public x: number = 0;
public y: number = 0;
public z: number = 0;
public vx: number = 0;
public vy: number = 0;
public vz: number = 0;
}
describe('SoA优化选择测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
test('默认使用原始存储', () => {
const storage = manager.getStorage(PositionComponent);
// 添加组件
manager.addComponent(1, new PositionComponent());
// 验证能正常工作
const component = manager.getComponent(1, PositionComponent);
expect(component).toBeTruthy();
expect(component?.x).toBe(0);
// 验证使用原始存储
expect(storage.constructor.name).toBe('ComponentStorage');
});
test('@EnableSoA装饰器启用优化', () => {
const storage = manager.getStorage(LargeScaleComponent);
// 添加组件
const component = new LargeScaleComponent();
component.x = 100;
component.vx = 10;
manager.addComponent(1, component);
// 验证能正常工作
const retrieved = manager.getComponent(1, LargeScaleComponent);
expect(retrieved).toBeTruthy();
expect(retrieved?.x).toBe(100);
expect(retrieved?.vx).toBe(10);
// 验证使用SoA存储
expect(storage.constructor.name).toBe('SoAStorage');
});
test('SoA存储功能验证', () => {
const entityCount = 1000;
// 创建实体(使用SoA优化)
for (let i = 0; i < entityCount; i++) {
const component = new LargeScaleComponent();
component.x = i;
component.y = i * 2;
component.vx = 1;
component.vy = 2;
manager.addComponent(i, component);
}
// 验证数据正确性
const testComponent = manager.getComponent(100, LargeScaleComponent);
expect(testComponent?.x).toBe(100);
expect(testComponent?.y).toBe(200);
expect(testComponent?.vx).toBe(1);
expect(testComponent?.vy).toBe(2);
// 验证存储类型
const storage = manager.getStorage(LargeScaleComponent);
expect(storage.constructor.name).toBe('SoAStorage');
console.log(`成功创建 ${entityCount} 个SoA实体,数据验证通过`);
});
});
@@ -0,0 +1,597 @@
import { ComponentRegistry, GlobalComponentRegistry, ComponentStorage, ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { Component } from '../../../src/ECS/Component';
import { BitMask64Utils } from '../../../src/ECS/Utils/BigIntCompatibility';
// 为测试创建独立的注册表实例 | Create isolated registry instance for tests
let testRegistry: ComponentRegistry;
// 测试组件类(默认使用原始存储)
class TestComponent extends Component {
public value: number;
constructor(...args: unknown[]) {
super();
const [value = 0] = args as [number?];
this.value = value;
}
}
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;
}
}
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;
}
}
class HealthComponent extends Component {
public health: number;
public maxHealth: number;
constructor(...args: unknown[]) {
super();
const [health = 100, maxHealth = 100] = args as [number?, number?];
this.health = health;
this.maxHealth = maxHealth;
}
}
describe('ComponentRegistry - 组件注册表测试', () => {
beforeEach(() => {
// 每个测试创建新的注册表实例 | Create new registry instance for each test
testRegistry = new ComponentRegistry();
});
describe('组件注册功能', () => {
test('应该能够注册组件类型', () => {
const bitIndex = testRegistry.register(TestComponent);
expect(bitIndex).toBe(0);
expect(testRegistry.isRegistered(TestComponent)).toBe(true);
});
test('重复注册相同组件应该返回相同的位索引', () => {
const bitIndex1 = testRegistry.register(TestComponent);
const bitIndex2 = testRegistry.register(TestComponent);
expect(bitIndex1).toBe(bitIndex2);
expect(bitIndex1).toBe(0);
});
test('应该能够注册多个组件类型', () => {
const bitIndex1 = testRegistry.register(TestComponent);
const bitIndex2 = testRegistry.register(PositionComponent);
const bitIndex3 = testRegistry.register(VelocityComponent);
expect(bitIndex1).toBe(0);
expect(bitIndex2).toBe(1);
expect(bitIndex3).toBe(2);
});
test('应该能够检查组件是否已注册', () => {
expect(testRegistry.isRegistered(TestComponent)).toBe(false);
testRegistry.register(TestComponent);
expect(testRegistry.isRegistered(TestComponent)).toBe(true);
});
});
describe('位掩码功能', () => {
test('应该能够获取组件的位掩码', () => {
testRegistry.register(TestComponent);
testRegistry.register(PositionComponent);
const mask1 = testRegistry.getBitMask(TestComponent);
const mask2 = testRegistry.getBitMask(PositionComponent);
expect(BitMask64Utils.getBit(mask1,0)).toBe(true); // 2^0
expect(BitMask64Utils.getBit(mask2,1)).toBe(true); // 2^1
});
test('应该能够获取组件的位索引', () => {
testRegistry.register(TestComponent);
testRegistry.register(PositionComponent);
const index1 = testRegistry.getBitIndex(TestComponent);
const index2 = testRegistry.getBitIndex(PositionComponent);
expect(index1).toBe(0);
expect(index2).toBe(1);
});
test('获取未注册组件的位掩码应该抛出错误', () => {
expect(() => {
testRegistry.getBitMask(TestComponent);
}).toThrow('Component type TestComponent is not registered');
});
test('获取未注册组件的位索引应该抛出错误', () => {
expect(() => {
testRegistry.getBitIndex(TestComponent);
}).toThrow('Component type TestComponent is not registered');
});
});
describe('注册表管理', () => {
test('应该能够获取所有已注册的组件类型', () => {
testRegistry.register(TestComponent);
testRegistry.register(PositionComponent);
const allTypes = testRegistry.getAllRegisteredTypes();
expect(allTypes.size).toBe(2);
expect(allTypes.has(TestComponent)).toBe(true);
expect(allTypes.has(PositionComponent)).toBe(true);
expect(allTypes.get(TestComponent)).toBe(0);
expect(allTypes.get(PositionComponent)).toBe(1);
});
test('返回的注册表副本不应该影响原始数据', () => {
testRegistry.register(TestComponent);
const allTypes = testRegistry.getAllRegisteredTypes();
allTypes.set(PositionComponent, 999);
expect(testRegistry.isRegistered(PositionComponent)).toBe(false);
});
});
});
describe('ComponentStorage - 组件存储器测试', () => {
let storage: ComponentStorage<TestComponent>;
beforeEach(() => {
// 每个测试创建新的注册表实例 | Create new registry instance for each test
testRegistry = new ComponentRegistry();
storage = new ComponentStorage(TestComponent);
});
describe('基本存储功能', () => {
test('应该能够创建组件存储器', () => {
expect(storage).toBeInstanceOf(ComponentStorage);
expect(storage.size).toBe(0);
expect(storage.type).toBe(TestComponent);
});
test('应该能够添加组件', () => {
const component = new TestComponent(100);
storage.addComponent(1, component);
expect(storage.size).toBe(1);
expect(storage.hasComponent(1)).toBe(true);
expect(storage.getComponent(1)).toBe(component);
});
test('重复添加组件到同一实体应该抛出错误', () => {
const component1 = new TestComponent(100);
const component2 = new TestComponent(200);
storage.addComponent(1, component1);
expect(() => {
storage.addComponent(1, component2);
}).toThrow('Entity 1 already has component TestComponent');
});
test('应该能够获取组件', () => {
const component = new TestComponent(100);
storage.addComponent(1, component);
const retrieved = storage.getComponent(1);
expect(retrieved).toBe(component);
expect(retrieved!.value).toBe(100);
});
test('获取不存在的组件应该返回null', () => {
const retrieved = storage.getComponent(999);
expect(retrieved).toBeNull();
});
test('应该能够检查实体是否有组件', () => {
expect(storage.hasComponent(1)).toBe(false);
storage.addComponent(1, new TestComponent(100));
expect(storage.hasComponent(1)).toBe(true);
});
test('应该能够移除组件', () => {
const component = new TestComponent(100);
storage.addComponent(1, component);
const removed = storage.removeComponent(1);
expect(removed).toBe(component);
expect(storage.size).toBe(0);
expect(storage.hasComponent(1)).toBe(false);
expect(storage.getComponent(1)).toBeNull();
});
test('移除不存在的组件应该返回null', () => {
const removed = storage.removeComponent(999);
expect(removed).toBeNull();
});
});
describe('遍历和批量操作', () => {
test('应该能够遍历所有组件', () => {
const component1 = new TestComponent(100);
const component2 = new TestComponent(200);
const component3 = new TestComponent(300);
storage.addComponent(1, component1);
storage.addComponent(2, component2);
storage.addComponent(3, component3);
const results: Array<{component: TestComponent, entityId: number, index: number}> = [];
storage.forEach((component, entityId, index) => {
results.push({ component, entityId, index });
});
expect(results.length).toBe(3);
expect(results.find(r => r.entityId === 1)?.component).toBe(component1);
expect(results.find(r => r.entityId === 2)?.component).toBe(component2);
expect(results.find(r => r.entityId === 3)?.component).toBe(component3);
});
test('应该能够获取密集数组', () => {
const component1 = new TestComponent(100);
const component2 = new TestComponent(200);
storage.addComponent(1, component1);
storage.addComponent(2, component2);
const { components, entityIds } = storage.getDenseArray();
expect(components.length).toBe(2);
expect(entityIds.length).toBe(2);
expect(components).toContain(component1);
expect(components).toContain(component2);
expect(entityIds).toContain(1);
expect(entityIds).toContain(2);
});
test('应该能够清空所有组件', () => {
storage.addComponent(1, new TestComponent(100));
storage.addComponent(2, new TestComponent(200));
expect(storage.size).toBe(2);
storage.clear();
expect(storage.size).toBe(0);
expect(storage.hasComponent(1)).toBe(false);
expect(storage.hasComponent(2)).toBe(false);
});
});
describe('内存管理和优化', () => {
test('应该维持紧凑存储', () => {
const component1 = new TestComponent(100);
const component2 = new TestComponent(200);
const component3 = new TestComponent(300);
// 添加三个组件
storage.addComponent(1, component1);
storage.addComponent(2, component2);
storage.addComponent(3, component3);
// 移除中间的组件,稀疏集合会自动保持紧凑
storage.removeComponent(2);
// 添加新组件
const component4 = new TestComponent(400);
storage.addComponent(4, component4);
expect(storage.size).toBe(3);
expect(storage.getComponent(4)).toBe(component4);
// 验证存储保持紧凑
const stats = storage.getStats();
expect(stats.freeSlots).toBe(0);
expect(stats.fragmentation).toBe(0);
});
test('应该能够获取存储统计信息', () => {
storage.addComponent(1, new TestComponent(100));
storage.addComponent(2, new TestComponent(200));
storage.addComponent(3, new TestComponent(300));
// 移除一个组件,稀疏集合会自动紧凑
storage.removeComponent(2);
const stats = storage.getStats();
expect(stats.totalSlots).toBe(2); // 稀疏集合自动紧凑
expect(stats.usedSlots).toBe(2);
expect(stats.freeSlots).toBe(0); // 无空洞
expect(stats.fragmentation).toBe(0); // 无碎片
});
});
describe('边界情况', () => {
test('空存储器的统计信息应该正确', () => {
const stats = storage.getStats();
expect(stats.totalSlots).toBe(0);
expect(stats.usedSlots).toBe(0);
expect(stats.freeSlots).toBe(0);
expect(stats.fragmentation).toBe(0);
});
test('遍历空存储器应该安全', () => {
let callCount = 0;
storage.forEach(() => { callCount++; });
expect(callCount).toBe(0);
});
test('获取空存储器的密集数组应该返回空数组', () => {
const { components, entityIds } = storage.getDenseArray();
expect(components.length).toBe(0);
expect(entityIds.length).toBe(0);
});
});
});
describe('ComponentStorageManager - 组件存储管理器测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
// 重置全局注册表 | Reset global registry
GlobalComponentRegistry.reset();
manager = new ComponentStorageManager();
});
describe('存储器管理', () => {
test('应该能够创建组件存储管理器', () => {
expect(manager).toBeInstanceOf(ComponentStorageManager);
});
test('应该能够获取或创建组件存储器', () => {
const storage1 = manager.getStorage(TestComponent);
const storage2 = manager.getStorage(TestComponent);
expect(storage1).toBeInstanceOf(ComponentStorage);
expect(storage1).toBe(storage2); // 应该是同一个实例
});
test('不同组件类型应该有不同的存储器', () => {
const storage1 = manager.getStorage(TestComponent);
const storage2 = manager.getStorage(PositionComponent);
expect(storage1).not.toBe(storage2);
expect(storage1.type).toBe(TestComponent);
expect(storage2.type).toBe(PositionComponent);
});
});
describe('组件操作', () => {
test('应该能够添加组件', () => {
const component = new TestComponent(100);
manager.addComponent(1, component);
expect(manager.hasComponent(1, TestComponent)).toBe(true);
expect(manager.getComponent(1, TestComponent)).toBe(component);
});
test('应该能够获取组件', () => {
const testComponent = new TestComponent(100);
const positionComponent = new PositionComponent(10, 20);
manager.addComponent(1, testComponent);
manager.addComponent(1, positionComponent);
expect(manager.getComponent(1, TestComponent)).toBe(testComponent);
expect(manager.getComponent(1, PositionComponent)).toBe(positionComponent);
});
test('获取不存在的组件应该返回null', () => {
const result = manager.getComponent(999, TestComponent);
expect(result).toBeNull();
});
test('应该能够检查实体是否有组件', () => {
expect(manager.hasComponent(1, TestComponent)).toBe(false);
manager.addComponent(1, new TestComponent(100));
expect(manager.hasComponent(1, TestComponent)).toBe(true);
});
test('应该能够移除组件', () => {
const component = new TestComponent(100);
manager.addComponent(1, component);
const removed = manager.removeComponent(1, TestComponent);
expect(removed).toBe(component);
expect(manager.hasComponent(1, TestComponent)).toBe(false);
});
test('移除不存在的组件应该返回null', () => {
const removed = manager.removeComponent(999, TestComponent);
expect(removed).toBeNull();
});
test('应该能够移除实体的所有组件', () => {
manager.addComponent(1, new TestComponent(100));
manager.addComponent(1, new PositionComponent(10, 20));
manager.addComponent(1, new VelocityComponent(1, 2));
expect(manager.hasComponent(1, TestComponent)).toBe(true);
expect(manager.hasComponent(1, PositionComponent)).toBe(true);
expect(manager.hasComponent(1, VelocityComponent)).toBe(true);
manager.removeAllComponents(1);
expect(manager.hasComponent(1, TestComponent)).toBe(false);
expect(manager.hasComponent(1, PositionComponent)).toBe(false);
expect(manager.hasComponent(1, VelocityComponent)).toBe(false);
});
});
describe('位掩码功能', () => {
test('应该能够获取实体的组件位掩码', () => {
// 确保组件已注册 | Ensure components are registered
GlobalComponentRegistry.register(TestComponent);
GlobalComponentRegistry.register(PositionComponent);
GlobalComponentRegistry.register(VelocityComponent);
manager.addComponent(1, new TestComponent(100));
manager.addComponent(1, new PositionComponent(10, 20));
const mask = manager.getComponentMask(1);
// 应该包含TestComponent(位0)和PositionComponent(位1)的掩码
expect(BitMask64Utils.getBit(mask,0) && BitMask64Utils.getBit(mask,1)).toBe(true);
});
test('没有组件的实体应该有零掩码', () => {
const mask = manager.getComponentMask(999);
expect(BitMask64Utils.equals(mask, BitMask64Utils.ZERO)).toBe(true);
});
test('添加和移除组件应该更新掩码', () => {
GlobalComponentRegistry.register(TestComponent);
GlobalComponentRegistry.register(PositionComponent);
manager.addComponent(1, new TestComponent(100));
let mask = manager.getComponentMask(1);
expect(BitMask64Utils.getBit(mask,0)).toBe(true);
manager.addComponent(1, new PositionComponent(10, 20));
mask = manager.getComponentMask(1);
expect(BitMask64Utils.getBit(mask,1)).toBe(true); // 0b11
manager.removeComponent(1, TestComponent);
mask = manager.getComponentMask(1);
expect(BitMask64Utils.getBit(mask,0)).toBe(false); // 0b10
});
});
describe('管理器级别操作', () => {
test('应该能够获取所有存储器的统计信息', () => {
manager.addComponent(1, new TestComponent(100));
manager.addComponent(2, new TestComponent(200));
manager.addComponent(1, new PositionComponent(10, 20));
const allStats = manager.getAllStats();
expect(allStats).toBeInstanceOf(Map);
expect(allStats.size).toBe(2);
expect(allStats.has('TestComponent')).toBe(true);
expect(allStats.has('PositionComponent')).toBe(true);
const testStats = allStats.get('TestComponent');
expect(testStats?.usedSlots).toBe(2);
const positionStats = allStats.get('PositionComponent');
expect(positionStats?.usedSlots).toBe(1);
});
test('应该能够清空所有存储器', () => {
manager.addComponent(1, new TestComponent(100));
manager.addComponent(2, new PositionComponent(10, 20));
manager.addComponent(3, new VelocityComponent(1, 2));
expect(manager.hasComponent(1, TestComponent)).toBe(true);
expect(manager.hasComponent(2, PositionComponent)).toBe(true);
expect(manager.hasComponent(3, VelocityComponent)).toBe(true);
manager.clear();
expect(manager.hasComponent(1, TestComponent)).toBe(false);
expect(manager.hasComponent(2, PositionComponent)).toBe(false);
expect(manager.hasComponent(3, VelocityComponent)).toBe(false);
});
});
describe('边界情况和错误处理', () => {
test('对不存在存储器的操作应该安全处理', () => {
expect(manager.getComponent(1, TestComponent)).toBeNull();
expect(manager.hasComponent(1, TestComponent)).toBe(false);
expect(manager.removeComponent(1, TestComponent)).toBeNull();
});
test('移除所有组件对空实体应该安全', () => {
expect(() => {
manager.removeAllComponents(999);
}).not.toThrow();
});
test('统计信息应该处理未知组件类型', () => {
// 创建一个匿名组件类来测试未知类型处理
const AnonymousComponent = class extends Component {};
manager.addComponent(1, new AnonymousComponent());
const stats = manager.getAllStats();
// 检查是否有任何统计条目(匿名类可能显示为空字符串或其他名称)
expect(stats.size).toBeGreaterThan(0);
});
test('多次清空应该安全', () => {
manager.addComponent(1, new TestComponent(100));
manager.clear();
manager.clear(); // 第二次清空应该安全
expect(manager.hasComponent(1, TestComponent)).toBe(false);
});
});
describe('性能和内存测试', () => {
test('大量组件操作应该高效', () => {
const entityCount = 1000;
// 添加大量组件
for (let i = 1; i <= entityCount; i++) {
manager.addComponent(i, new TestComponent(i));
if (i % 2 === 0) {
manager.addComponent(i, new PositionComponent(i, i));
}
}
// 验证添加成功
expect(manager.hasComponent(1, TestComponent)).toBe(true);
expect(manager.hasComponent(500, TestComponent)).toBe(true);
expect(manager.hasComponent(2, PositionComponent)).toBe(true);
expect(manager.hasComponent(1, PositionComponent)).toBe(false);
// 移除部分组件
for (let i = 1; i <= entityCount; i += 3) {
manager.removeComponent(i, TestComponent);
}
// 验证移除成功
expect(manager.hasComponent(1, TestComponent)).toBe(false);
expect(manager.hasComponent(2, TestComponent)).toBe(true);
const stats = manager.getAllStats();
expect(stats.get('TestComponent')?.usedSlots).toBeLessThan(entityCount);
});
});
});
@@ -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);
});
});
});
@@ -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);
});
});
});
@@ -0,0 +1,479 @@
import { EventBus, GlobalEventBus } from '../../../src/ECS/Core/EventBus';
import { IEventListenerConfig, IEventStats } from '../../../src/Types';
import { ECSEventType, EventPriority } from '../../../src/ECS/CoreEvents';
// 测试数据接口
interface TestEventData {
message: string;
value: number;
}
interface MockEntityData {
entityId: number;
timestamp: number;
eventId?: string;
source?: string;
}
interface MockComponentData {
entityId: number;
componentType: string;
timestamp: number;
eventId?: string;
source?: string;
}
describe('EventBus - 事件总线测试', () => {
let eventBus: EventBus;
beforeEach(() => {
eventBus = new EventBus(false);
});
afterEach(() => {
eventBus.clear();
});
describe('基本事件功能', () => {
test('应该能够创建事件总线', () => {
expect(eventBus).toBeInstanceOf(EventBus);
});
test('应该能够发射和监听同步事件', () => {
let receivedData: TestEventData | null = null;
const listenerId = eventBus.on<TestEventData>('test:event', (data) => {
receivedData = data;
});
const testData: TestEventData = { message: 'hello', value: 42 };
eventBus.emit('test:event', testData);
expect(receivedData).not.toBeNull();
expect(receivedData!.message).toBe('hello');
expect(receivedData!.value).toBe(42);
expect(typeof listenerId).toBe('string');
});
test('应该能够发射和监听异步事件', async () => {
let receivedData: TestEventData | null = null;
eventBus.onAsync<TestEventData>('async:event', async (data) => {
receivedData = data;
});
const testData: TestEventData = { message: 'async hello', value: 100 };
await eventBus.emitAsync('async:event', testData);
expect(receivedData).not.toBeNull();
expect(receivedData!.message).toBe('async hello');
expect(receivedData!.value).toBe(100);
});
test('应该能够一次性监听事件', () => {
let callCount = 0;
eventBus.once<TestEventData>('once:event', () => {
callCount++;
});
eventBus.emit('once:event', { message: 'first', value: 1 });
eventBus.emit('once:event', { message: 'second', value: 2 });
expect(callCount).toBe(1);
});
test('应该能够移除事件监听器', () => {
let callCount = 0;
const listenerId = eventBus.on<TestEventData>('removable:event', () => {
callCount++;
});
eventBus.emit('removable:event', { message: 'test', value: 1 });
expect(callCount).toBe(1);
const removed = eventBus.off('removable:event', listenerId);
expect(removed).toBe(true);
eventBus.emit('removable:event', { message: 'test', value: 2 });
expect(callCount).toBe(1); // 应该没有增加
});
test('应该能够移除所有事件监听器', () => {
let callCount1 = 0;
let callCount2 = 0;
eventBus.on<TestEventData>('multi:event', () => { callCount1++; });
eventBus.on<TestEventData>('multi:event', () => { callCount2++; });
eventBus.emit('multi:event', { message: 'test', value: 1 });
expect(callCount1).toBe(1);
expect(callCount2).toBe(1);
eventBus.offAll('multi:event');
eventBus.emit('multi:event', { message: 'test', value: 2 });
expect(callCount1).toBe(1);
expect(callCount2).toBe(1);
});
});
describe('事件配置和优先级', () => {
test('应该能够使用事件监听器配置', () => {
let receivedData: TestEventData | null = null;
const config: IEventListenerConfig = {
once: false,
priority: EventPriority.HIGH,
async: false
};
eventBus.on<TestEventData>('config:event', (data) => {
receivedData = data;
}, config);
eventBus.emit('config:event', { message: 'configured', value: 99 });
expect(receivedData).not.toBeNull();
expect(receivedData!.message).toBe('configured');
});
test('应该能够检查事件是否有监听器', () => {
expect(eventBus.hasListeners('nonexistent:event')).toBe(false);
eventBus.on('existing:event', () => {});
expect(eventBus.hasListeners('existing:event')).toBe(true);
});
test('应该能够获取监听器数量', () => {
expect(eventBus.getListenerCount('count:event')).toBe(0);
eventBus.on('count:event', () => {});
eventBus.on('count:event', () => {});
expect(eventBus.getListenerCount('count:event')).toBe(2);
});
});
describe('系统配置和管理', () => {
test('应该能够启用和禁用事件系统', () => {
let callCount = 0;
eventBus.on('disable:event', () => { callCount++; });
eventBus.emit('disable:event', { message: 'enabled', value: 1 });
expect(callCount).toBe(1);
eventBus.setEnabled(false);
eventBus.emit('disable:event', { message: 'disabled', value: 2 });
expect(callCount).toBe(1); // 应该没有增加
eventBus.setEnabled(true);
eventBus.emit('disable:event', { message: 'enabled again', value: 3 });
expect(callCount).toBe(2);
});
test('应该能够设置调试模式', () => {
const consoleSpy = jest.spyOn(console, 'info').mockImplementation(() => {});
eventBus.setDebugMode(true);
eventBus.on('debug:event', () => {});
eventBus.emit('debug:event', { message: 'debug', value: 1 });
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
test('应该能够设置最大监听器数量', () => {
expect(() => {
eventBus.setMaxListeners(5);
}).not.toThrow();
});
test('应该能够清空所有监听器', () => {
eventBus.on('clear:event1', () => {});
eventBus.on('clear:event2', () => {});
expect(eventBus.hasListeners('clear:event1')).toBe(true);
expect(eventBus.hasListeners('clear:event2')).toBe(true);
eventBus.clear();
expect(eventBus.hasListeners('clear:event1')).toBe(false);
expect(eventBus.hasListeners('clear:event2')).toBe(false);
});
});
describe('批处理功能', () => {
test('应该能够设置批处理配置', () => {
expect(() => {
eventBus.setBatchConfig('batch:event', 5, 100);
}).not.toThrow();
});
test('应该能够刷新批处理队列', () => {
eventBus.setBatchConfig('flush:event', 10, 200);
expect(() => {
eventBus.flushBatch('flush:event');
}).not.toThrow();
});
});
describe('事件统计', () => {
test('应该能够获取事件统计信息', () => {
eventBus.on('stats:event', () => {});
eventBus.emit('stats:event', { message: 'stat test', value: 1 });
eventBus.emit('stats:event', { message: 'stat test', value: 2 });
const stats = eventBus.getStats('stats:event') as IEventStats;
expect(stats).toBeDefined();
expect(stats.eventType).toBe('stats:event');
expect(stats.triggerCount).toBe(2);
expect(stats.listenerCount).toBe(1);
});
test('应该能够获取所有事件的统计信息', () => {
eventBus.on('all-stats:event1', () => {});
eventBus.on('all-stats:event2', () => {});
eventBus.emit('all-stats:event1', { message: 'test1', value: 1 });
eventBus.emit('all-stats:event2', { message: 'test2', value: 2 });
const allStats = eventBus.getStats() as Map<string, IEventStats>;
expect(allStats).toBeInstanceOf(Map);
expect(allStats.size).toBeGreaterThan(0);
});
test('应该能够重置事件统计', () => {
eventBus.on('reset:event', () => {});
eventBus.emit('reset:event', { message: 'before reset', value: 1 });
let stats = eventBus.getStats('reset:event') as IEventStats;
expect(stats.triggerCount).toBe(1);
eventBus.resetStats('reset:event');
stats = eventBus.getStats('reset:event') as IEventStats;
expect(stats.triggerCount).toBe(0);
});
});
describe('预定义ECS事件', () => {
test('应该能够发射和监听实体创建事件', () => {
let receivedData: MockEntityData | null = null;
eventBus.onEntityCreated((data) => {
receivedData = data;
});
const entityData: MockEntityData = {
entityId: 1,
timestamp: Date.now()
};
eventBus.emit(ECSEventType.ENTITY_CREATED, entityData, true);
expect(receivedData).not.toBeNull();
expect(receivedData!.entityId).toBe(1);
expect(receivedData!.timestamp).toBeDefined();
expect(receivedData!.eventId).toBeDefined();
expect(receivedData!.source).toBeDefined();
});
test('应该能够发射和监听组件添加事件', () => {
let receivedData: MockComponentData | null = null;
eventBus.onComponentAdded((data) => {
receivedData = data;
});
const componentData: MockComponentData = {
entityId: 1,
componentType: 'PositionComponent',
timestamp: Date.now()
};
eventBus.emitComponentAdded(componentData);
expect(receivedData).not.toBeNull();
expect(receivedData!.entityId).toBe(1);
expect(receivedData!.componentType).toBe('PositionComponent');
});
test('应该能够监听系统错误事件', () => {
let errorReceived = false;
eventBus.onSystemError(() => {
errorReceived = true;
});
eventBus.emit(ECSEventType.SYSTEM_ERROR, {
systemName: 'TestSystem',
error: 'Test error'
});
expect(errorReceived).toBe(true);
});
test('应该能够监听性能警告事件', () => {
let warningReceived = false;
eventBus.onPerformanceWarning(() => {
warningReceived = true;
});
eventBus.emitPerformanceWarning({
operation: 'frame_render',
executionTime: 16.67,
metadata: { fps: 30, threshold: 60, message: 'FPS dropped below threshold' },
timestamp: Date.now()
});
expect(warningReceived).toBe(true);
});
test('应该能够发射其他预定义事件', () => {
let entityDestroyedReceived = false;
let componentRemovedReceived = false;
let systemAddedReceived = false;
let systemRemovedReceived = false;
let sceneChangedReceived = false;
eventBus.on(ECSEventType.ENTITY_DESTROYED, () => { entityDestroyedReceived = true; });
eventBus.on(ECSEventType.COMPONENT_REMOVED, () => { componentRemovedReceived = true; });
eventBus.on(ECSEventType.SYSTEM_ADDED, () => { systemAddedReceived = true; });
eventBus.on(ECSEventType.SYSTEM_REMOVED, () => { systemRemovedReceived = true; });
eventBus.on(ECSEventType.SCENE_ACTIVATED, () => { sceneChangedReceived = true; });
eventBus.emitEntityDestroyed({ entityId: 1, timestamp: Date.now() });
eventBus.emitComponentRemoved({ entityId: 1, componentType: 'Test', timestamp: Date.now() });
eventBus.emitSystemAdded({ systemName: 'TestSystem', systemType: 'EntitySystem', timestamp: Date.now() });
eventBus.emitSystemRemoved({ systemName: 'TestSystem', systemType: 'EntitySystem', timestamp: Date.now() });
eventBus.emitSceneChanged({ sceneName: 'TestScene', timestamp: Date.now() });
expect(entityDestroyedReceived).toBe(true);
expect(componentRemovedReceived).toBe(true);
expect(systemAddedReceived).toBe(true);
expect(systemRemovedReceived).toBe(true);
expect(sceneChangedReceived).toBe(true);
});
});
describe('数据增强功能', () => {
test('应该能够自动增强事件数据', () => {
let receivedData: any = null;
eventBus.on('enhanced:event', (data) => {
receivedData = data;
});
const originalData = { message: 'test' };
eventBus.emit('enhanced:event', originalData, true);
expect(receivedData.message).toBe('test');
expect(receivedData.timestamp).toBeDefined();
expect(receivedData.eventId).toBeDefined();
expect(receivedData.source).toBeDefined();
expect(typeof receivedData.timestamp).toBe('number');
expect(typeof receivedData.eventId).toBe('string');
expect(receivedData.source).toBe('EventBus');
});
test('增强数据时不应该覆盖现有属性', () => {
let receivedData: any = null;
eventBus.on('no-override:event', (data) => {
receivedData = data;
});
const originalData = {
message: 'test',
timestamp: 12345,
eventId: 'custom-id',
source: 'CustomSource'
};
eventBus.emit('no-override:event', originalData);
expect(receivedData.timestamp).toBe(12345);
expect(receivedData.eventId).toBe('custom-id');
expect(receivedData.source).toBe('CustomSource');
});
});
describe('边界情况和错误处理', () => {
test('移除不存在的监听器应该返回false', () => {
const removed = eventBus.off('nonexistent:event', 'invalid-id');
expect(removed).toBe(false);
});
test('获取不存在事件的监听器数量应该返回0', () => {
const count = eventBus.getListenerCount('nonexistent:event');
expect(count).toBe(0);
});
test('检查不存在事件的监听器应该返回false', () => {
const hasListeners = eventBus.hasListeners('nonexistent:event');
expect(hasListeners).toBe(false);
});
test('对不存在的事件类型执行操作应该安全', () => {
expect(() => {
eventBus.offAll('nonexistent:event');
eventBus.resetStats('nonexistent:event');
eventBus.flushBatch('nonexistent:event');
}).not.toThrow();
});
test('传入空数据应该安全处理', () => {
let receivedData: any = null;
eventBus.on('null-data:event', (data) => {
receivedData = data;
});
expect(() => {
eventBus.emit('null-data:event', null);
eventBus.emit('null-data:event', undefined);
eventBus.emit('null-data:event', {});
}).not.toThrow();
expect(receivedData).toBeDefined();
});
});
});
describe('GlobalEventBus - 全局事件总线测试', () => {
afterEach(() => {
// 重置全局实例以避免测试间干扰
GlobalEventBus.reset();
});
test('应该能够获取全局事件总线实例', () => {
const instance1 = GlobalEventBus.getInstance();
const instance2 = GlobalEventBus.getInstance();
expect(instance1).toBeInstanceOf(EventBus);
expect(instance1).toBe(instance2); // 应该是同一个实例
});
test('应该能够重置全局事件总线实例', () => {
const instance1 = GlobalEventBus.getInstance();
instance1.on('test:event', () => {});
expect(instance1.hasListeners('test:event')).toBe(true);
const instance2 = GlobalEventBus.reset();
expect(instance2).toBeInstanceOf(EventBus);
expect(instance2).not.toBe(instance1);
expect(instance2.hasListeners('test:event')).toBe(false);
});
test('应该能够使用调试模式创建全局实例', () => {
const instance = GlobalEventBus.getInstance(true);
expect(instance).toBeInstanceOf(EventBus);
});
});
@@ -0,0 +1,624 @@
import { TypeSafeEventSystem, GlobalEventSystem } from '../../../src/ECS/Core/EventSystem';
import { ECSEventType } from '../../../src/ECS/CoreEvents';
// 测试事件数据类型
interface TestCustomEvent {
playerId: number;
message: string;
timestamp: number;
}
interface PlayerLevelUpEvent {
playerId: number;
oldLevel: number;
newLevel: number;
experience: number;
}
interface EntityCreatedEvent {
entityId: number;
entityName: string;
componentCount: number;
}
describe('EventSystem - 事件系统测试', () => {
let eventSystem: TypeSafeEventSystem;
beforeEach(() => {
eventSystem = new TypeSafeEventSystem();
});
describe('基本事件功能', () => {
test('应该能够注册事件监听器', () => {
let eventReceived = false;
eventSystem.on('test:event', () => {
eventReceived = true;
});
eventSystem.emit('test:event', {});
expect(eventReceived).toBe(true);
});
test('应该能够传递事件数据', () => {
let receivedData: TestCustomEvent | null = null;
eventSystem.on('custom:test', (data: TestCustomEvent) => {
receivedData = data;
});
const testData: TestCustomEvent = {
playerId: 123,
message: 'Hello World',
timestamp: Date.now()
};
eventSystem.emit('custom:test', testData);
expect(receivedData).toEqual(testData);
});
test('应该能够移除事件监听器', () => {
let callCount = 0;
const handler = () => {
callCount++;
};
const listenerId = eventSystem.on('removable:event', handler);
eventSystem.emit('removable:event', {});
expect(callCount).toBe(1);
eventSystem.off('removable:event', listenerId);
eventSystem.emit('removable:event', {});
expect(callCount).toBe(1); // 应该保持不变
});
test('应该能够一次性监听事件', async () => {
let callCount = 0;
eventSystem.once('once:event', () => {
callCount++;
});
await eventSystem.emit('once:event', {});
await eventSystem.emit('once:event', {});
await eventSystem.emit('once:event', {});
expect(callCount).toBe(1); // 只应该被调用一次
});
test('应该能够处理多个监听器', () => {
const results: string[] = [];
eventSystem.on('multi:event', () => {
results.push('handler1');
});
eventSystem.on('multi:event', () => {
results.push('handler2');
});
eventSystem.on('multi:event', () => {
results.push('handler3');
});
eventSystem.emit('multi:event', {});
expect(results).toEqual(['handler1', 'handler2', 'handler3']);
});
});
describe('预定义ECS事件', () => {
test('应该能够监听实体创建事件', () => {
let entityCreatedData: any = null;
eventSystem.on(ECSEventType.ENTITY_CREATED, (data: any) => {
entityCreatedData = data;
});
const testData = {
entityId: 123,
entityName: 'TestEntity',
componentCount: 3
};
eventSystem.emit(ECSEventType.ENTITY_CREATED, testData);
expect(entityCreatedData).toEqual(testData);
});
test('应该能够监听实体销毁事件', () => {
let entityDestroyedData: any = null;
eventSystem.on(ECSEventType.ENTITY_DESTROYED, (data: any) => {
entityDestroyedData = data;
});
const testData = {
entityId: 456,
entityName: 'DestroyedEntity',
componentCount: 2
};
eventSystem.emit(ECSEventType.ENTITY_DESTROYED, testData);
expect(entityDestroyedData).toEqual(testData);
});
test('应该能够监听组件添加事件', () => {
let componentAddedData: any = null;
eventSystem.on(ECSEventType.COMPONENT_ADDED, (data: any) => {
componentAddedData = data;
});
const testData = {
entityId: 789,
componentType: 'PositionComponent',
componentData: { x: 10, y: 20 }
};
eventSystem.emit(ECSEventType.COMPONENT_ADDED, testData);
expect(componentAddedData).toEqual(testData);
});
test('应该能够监听组件移除事件', () => {
let componentRemovedData: any = null;
eventSystem.on(ECSEventType.COMPONENT_REMOVED, (data: any) => {
componentRemovedData = data;
});
const testData = {
entityId: 101112,
componentType: 'VelocityComponent',
componentData: { vx: 5, vy: -3 }
};
eventSystem.emit(ECSEventType.COMPONENT_REMOVED, testData);
expect(componentRemovedData).toEqual(testData);
});
test('应该能够监听系统添加事件', () => {
let systemAddedData: any = null;
eventSystem.on(ECSEventType.SYSTEM_ADDED, (data: any) => {
systemAddedData = data;
});
const testData = {
systemName: 'MovementSystem',
systemType: 'EntitySystem',
updateOrder: 1
};
eventSystem.emit(ECSEventType.SYSTEM_ADDED, testData);
expect(systemAddedData).toEqual(testData);
});
test('应该能够监听系统移除事件', () => {
let systemRemovedData: any = null;
eventSystem.on(ECSEventType.SYSTEM_REMOVED, (data: any) => {
systemRemovedData = data;
});
const testData = {
systemName: 'RenderSystem',
systemType: 'EntitySystem',
updateOrder: 2
};
eventSystem.emit(ECSEventType.SYSTEM_REMOVED, testData);
expect(systemRemovedData).toEqual(testData);
});
});
describe('事件优先级和执行顺序', () => {
test('应该按优先级顺序执行监听器', () => {
const executionOrder: string[] = [];
// 添加不同优先级的监听器
eventSystem.on('priority:event', () => {
executionOrder.push('normal');
});
eventSystem.on('priority:event', () => {
executionOrder.push('high');
}, { priority: 10 });
eventSystem.on('priority:event', () => {
executionOrder.push('low');
}, { priority: -10 });
eventSystem.emit('priority:event', {});
// 应该按照 high -> normal -> low 的顺序执行
expect(executionOrder).toEqual(['high', 'normal', 'low']);
});
test('相同优先级的监听器应该按注册顺序执行', () => {
const executionOrder: string[] = [];
eventSystem.on('order:event', () => {
executionOrder.push('first');
});
eventSystem.on('order:event', () => {
executionOrder.push('second');
});
eventSystem.on('order:event', () => {
executionOrder.push('third');
});
eventSystem.emit('order:event', {});
expect(executionOrder).toEqual(['first', 'second', 'third']);
});
});
describe('异步事件处理', () => {
test('应该能够处理异步事件监听器', async () => {
let asyncResult = '';
eventSystem.on('async:event', async (data: { message: string }) => {
await new Promise(resolve => setTimeout(resolve, 10));
asyncResult = data.message;
}, { async: true });
await eventSystem.emit('async:event', { message: 'async test' });
expect(asyncResult).toBe('async test');
});
test('应该能够等待所有异步监听器完成', async () => {
const results: string[] = [];
eventSystem.on('multi-async:event', async () => {
await new Promise(resolve => setTimeout(resolve, 20));
results.push('handler1');
}, { async: true });
eventSystem.on('multi-async:event', async () => {
await new Promise(resolve => setTimeout(resolve, 10));
results.push('handler2');
}, { async: true });
eventSystem.on('multi-async:event', async () => {
await new Promise(resolve => setTimeout(resolve, 5));
results.push('handler3');
}, { async: true });
await eventSystem.emit('multi-async:event', {});
// 所有异步处理器都应该完成
expect(results.length).toBe(3);
expect(results).toContain('handler1');
expect(results).toContain('handler2');
expect(results).toContain('handler3');
});
test('异步事件处理中的错误应该被正确处理', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
let successHandlerCalled = false;
eventSystem.on('error:event', async () => {
throw new Error('Test async error');
}, { async: true });
eventSystem.on('error:event', async () => {
successHandlerCalled = true;
}, { async: true });
// emit方法应该内部处理异步错误,不向外抛出
await expect(eventSystem.emit('error:event', {})).resolves.toBeUndefined();
// 成功的处理器应该被调用
expect(successHandlerCalled).toBe(true);
consoleSpy.mockRestore();
});
});
describe('事件验证和类型安全', () => {
test('应该能够验证事件数据类型', () => {
let validationPassed = false;
eventSystem.on('typed:event', (data: PlayerLevelUpEvent) => {
// TypeScript应该确保类型安全
expect(typeof data.playerId).toBe('number');
expect(typeof data.oldLevel).toBe('number');
expect(typeof data.newLevel).toBe('number');
expect(typeof data.experience).toBe('number');
validationPassed = true;
});
const levelUpData: PlayerLevelUpEvent = {
playerId: 123,
oldLevel: 5,
newLevel: 6,
experience: 1500
};
eventSystem.emit('typed:event', levelUpData);
expect(validationPassed).toBe(true);
});
test('应该能够处理复杂的事件数据结构', () => {
interface ComplexEvent {
metadata: {
timestamp: number;
source: string;
};
payload: {
entities: Array<{
id: number;
components: string[];
}>;
systems: string[];
};
}
let receivedEvent: ComplexEvent | null = null;
eventSystem.on('complex:event', (data: ComplexEvent) => {
receivedEvent = data;
});
const complexData: ComplexEvent = {
metadata: {
timestamp: Date.now(),
source: 'test'
},
payload: {
entities: [
{ id: 1, components: ['Position', 'Velocity'] },
{ id: 2, components: ['Health', 'Render'] }
],
systems: ['Movement', 'Render', 'Combat']
}
};
eventSystem.emit('complex:event', complexData);
expect(receivedEvent).toEqual(complexData);
});
});
describe('性能和内存管理', () => {
test('大量事件监听器应该有良好的性能', () => {
const listenerCount = 50; // 减少数量以避免超过限制
let callCount = 0;
// 注册大量监听器
for (let i = 0; i < listenerCount; i++) {
eventSystem.on('perf:event', () => {
callCount++;
});
}
const startTime = performance.now();
eventSystem.emit('perf:event', {});
const endTime = performance.now();
expect(callCount).toBe(listenerCount);
const duration = endTime - startTime;
// 性能记录:多监听器性能数据,不设硬阈值避免CI不稳定
console.log(`${listenerCount}个监听器的事件触发耗时: ${duration.toFixed(2)}ms`);
});
test('频繁的事件触发应该有良好的性能', () => {
let eventCount = 0;
eventSystem.on('frequent:event', () => {
eventCount++;
});
const emitCount = 10000;
const startTime = performance.now();
for (let i = 0; i < emitCount; i++) {
eventSystem.emit('frequent:event', { index: i });
}
const endTime = performance.now();
expect(eventCount).toBe(emitCount);
const duration = endTime - startTime;
// 性能记录:事件系统性能数据,不设硬阈值避免CI不稳定
console.log(`${emitCount}次事件触发耗时: ${duration.toFixed(2)}ms`);
});
test('移除监听器应该释放内存', () => {
const listenerIds: string[] = [];
// 添加大量监听器
for (let i = 0; i < 100; i++) {
const handler = () => {};
const id = eventSystem.on('memory:event', handler);
listenerIds.push(id);
}
// 触发事件以确保监听器正常工作
eventSystem.emit('memory:event', {});
// 移除所有监听器
listenerIds.forEach(id => {
eventSystem.off('memory:event', id);
});
// 再次触发事件,应该没有监听器被调用
let callCount = 0;
eventSystem.on('memory:event', () => {
callCount++;
});
eventSystem.emit('memory:event', {});
expect(callCount).toBe(1); // 只有新添加的监听器被调用
});
test('应该能够清理所有事件监听器', () => {
let callCount = 0;
eventSystem.on('cleanup:event1', () => callCount++);
eventSystem.on('cleanup:event2', () => callCount++);
eventSystem.on('cleanup:event3', () => callCount++);
// 清理所有监听器
eventSystem.clear();
// 触发事件,应该没有监听器被调用
eventSystem.emit('cleanup:event1', {});
eventSystem.emit('cleanup:event2', {});
eventSystem.emit('cleanup:event3', {});
expect(callCount).toBe(0);
});
});
describe('错误处理', () => {
test('监听器中的错误不应该影响其他监听器', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
let successHandlerCalled = false;
eventSystem.on('error:event', () => {
throw new Error('Test error in handler');
});
eventSystem.on('error:event', () => {
successHandlerCalled = true;
});
// 触发事件不应该抛出异常
expect(() => {
eventSystem.emit('error:event', {});
}).not.toThrow();
// 成功的处理器应该被调用
expect(successHandlerCalled).toBe(true);
consoleSpy.mockRestore();
});
test('应该能够处理监听器注册和移除中的边界情况', () => {
const handler = () => {};
// 移除不存在的监听器应该安全
expect(() => {
const result = eventSystem.off('nonexistent:event', 'non-existent-id');
expect(result).toBe(false);
}).not.toThrow();
// 重复添加相同的监听器应该安全
const id1 = eventSystem.on('duplicate:event', handler);
const id2 = eventSystem.on('duplicate:event', handler);
let callCount = 0;
eventSystem.on('duplicate:event', () => {
callCount++;
});
eventSystem.emit('duplicate:event', {});
// 所有监听器都应该被调用
expect(callCount).toBe(1); // 新添加的监听器被调用
});
test('触发不存在的事件应该安全', () => {
expect(() => {
eventSystem.emit('nonexistent:event', {});
}).not.toThrow();
});
});
describe('全局事件系统', () => {
test('全局事件系统应该能够跨实例通信', () => {
let receivedData: any = null;
GlobalEventSystem.on('global:test', (data) => {
receivedData = data;
});
const testData = { message: 'global event test' };
GlobalEventSystem.emit('global:test', testData);
expect(receivedData).toEqual(testData);
});
test('全局事件系统应该是全局实例', () => {
// GlobalEventSystem 是全局实例,不需要getInstance
expect(GlobalEventSystem).toBeDefined();
expect(GlobalEventSystem).toBeInstanceOf(TypeSafeEventSystem);
});
test('全局事件系统应该能够与局部事件系统独立工作', () => {
let localCallCount = 0;
let globalCallCount = 0;
eventSystem.on('isolated:event', () => {
localCallCount++;
});
GlobalEventSystem.on('isolated:event', () => {
globalCallCount++;
});
// 触发局部事件
eventSystem.emit('isolated:event', {});
expect(localCallCount).toBe(1);
expect(globalCallCount).toBe(0);
// 触发全局事件
GlobalEventSystem.emit('isolated:event', {});
expect(localCallCount).toBe(1);
expect(globalCallCount).toBe(1);
});
});
describe('事件统计和调试', () => {
test('应该能够获取事件系统统计信息', () => {
// 添加一些监听器
eventSystem.on('stats:event1', () => {});
eventSystem.on('stats:event1', () => {});
eventSystem.on('stats:event2', () => {});
// 触发一些事件
eventSystem.emit('stats:event1', {});
eventSystem.emit('stats:event2', {});
eventSystem.emit('stats:event1', {});
const stats = eventSystem.getStats() as Map<string, any>;
expect(stats).toBeInstanceOf(Map);
expect(stats.size).toBe(2);
});
test('应该能够获取特定事件的统计信息', async () => {
eventSystem.on('specific:event', () => {});
eventSystem.on('specific:event', () => {});
await eventSystem.emit('specific:event', {});
await eventSystem.emit('specific:event', {});
await eventSystem.emit('specific:event', {});
const eventStats = eventSystem.getStats('specific:event') as any;
expect(eventStats.listenerCount).toBe(2);
expect(eventStats.triggerCount).toBe(3);
});
});
});
@@ -0,0 +1,321 @@
import { EntityBuilder } from '../../../../src/ECS/Core/FluentAPI/EntityBuilder';
import { Scene } from '../../../../src/ECS/Scene';
import { Component } from '../../../../src/ECS/Component';
import { HierarchySystem } from '../../../../src/ECS/Systems/HierarchySystem';
import { ECSComponent } from '../../../../src/ECS/Decorators';
@ECSComponent('BuilderTestPosition')
class PositionComponent extends Component {
public x: number = 0;
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('BuilderTestVelocity')
class VelocityComponent extends Component {
public vx: number = 0;
public vy: number = 0;
}
@ECSComponent('BuilderTestHealth')
class HealthComponent extends Component {
public current: number = 100;
public max: number = 100;
}
// Helper function to create EntityBuilder
function createBuilder(scene: Scene): EntityBuilder {
return new EntityBuilder(scene, scene.componentStorageManager);
}
describe('EntityBuilder', () => {
let scene: Scene;
let hierarchySystem: HierarchySystem;
beforeEach(() => {
scene = new Scene({ name: 'BuilderTestScene' });
hierarchySystem = new HierarchySystem();
scene.addSystem(hierarchySystem);
});
afterEach(() => {
scene.end();
});
describe('basic building', () => {
test('should create entity with name', () => {
const builder = createBuilder(scene);
const entity = builder.named('TestEntity').build();
expect(entity.name).toBe('TestEntity');
});
test('should create entity with tag', () => {
const builder = createBuilder(scene);
const entity = builder.tagged(0x100).build();
expect(entity.tag).toBe(0x100);
});
test('should support chaining name and tag', () => {
const entity = createBuilder(scene)
.named('ChainedEntity')
.tagged(0x200)
.build();
expect(entity.name).toBe('ChainedEntity');
expect(entity.tag).toBe(0x200);
});
});
describe('component management', () => {
test('should add single component with .with()', () => {
const entity = createBuilder(scene)
.with(new PositionComponent(10, 20))
.build();
const pos = entity.getComponent(PositionComponent);
expect(pos).not.toBeNull();
expect(pos!.x).toBe(10);
expect(pos!.y).toBe(20);
});
test('should add multiple components with .withComponents()', () => {
const entity = createBuilder(scene)
.withComponents(
new PositionComponent(5, 10),
new VelocityComponent(),
new HealthComponent()
)
.build();
expect(entity.hasComponent(PositionComponent)).toBe(true);
expect(entity.hasComponent(VelocityComponent)).toBe(true);
expect(entity.hasComponent(HealthComponent)).toBe(true);
});
test('should conditionally add component with .withIf()', () => {
const shouldAdd = true;
const shouldNotAdd = false;
const entity = createBuilder(scene)
.withIf(shouldAdd, new PositionComponent())
.withIf(shouldNotAdd, new VelocityComponent())
.build();
expect(entity.hasComponent(PositionComponent)).toBe(true);
expect(entity.hasComponent(VelocityComponent)).toBe(false);
});
test('should add component using factory with .withFactory()', () => {
const entity = createBuilder(scene)
.withFactory(() => new PositionComponent(100, 200))
.build();
const pos = entity.getComponent(PositionComponent);
expect(pos).not.toBeNull();
expect(pos!.x).toBe(100);
expect(pos!.y).toBe(200);
});
test('should configure existing component with .configure()', () => {
const entity = createBuilder(scene)
.with(new PositionComponent(0, 0))
.configure(PositionComponent, (pos: PositionComponent) => {
pos.x = 999;
pos.y = 888;
})
.build();
const pos = entity.getComponent(PositionComponent);
expect(pos!.x).toBe(999);
expect(pos!.y).toBe(888);
});
test('.configure() should do nothing if component does not exist', () => {
const entity = createBuilder(scene)
.configure(PositionComponent, (pos: PositionComponent) => {
pos.x = 100;
})
.build();
expect(entity.hasComponent(PositionComponent)).toBe(false);
});
});
describe('entity state', () => {
test('should set enabled state', () => {
const disabledEntity = createBuilder(scene)
.enabled(false)
.build();
const enabledEntity = createBuilder(scene)
.enabled(true)
.build();
expect(disabledEntity.enabled).toBe(false);
expect(enabledEntity.enabled).toBe(true);
});
test('should set active state', () => {
const inactiveEntity = createBuilder(scene)
.active(false)
.build();
const activeEntity = createBuilder(scene)
.active(true)
.build();
expect(inactiveEntity.active).toBe(false);
expect(activeEntity.active).toBe(true);
});
});
describe('hierarchy building', () => {
test('should call withChild method', () => {
const childBuilder = createBuilder(scene).named('Child');
const builder = createBuilder(scene)
.named('Parent')
.withChild(childBuilder);
// withChild returns the builder for chaining
expect(builder).toBeInstanceOf(EntityBuilder);
});
test('should call withChildren method', () => {
const child1Builder = createBuilder(scene).named('Child1');
const child2Builder = createBuilder(scene).named('Child2');
const builder = createBuilder(scene)
.named('Parent')
.withChildren(child1Builder, child2Builder);
// withChildren returns the builder for chaining
expect(builder).toBeInstanceOf(EntityBuilder);
});
test('should call withChildFactory method', () => {
const builder = createBuilder(scene)
.named('Parent')
.with(new PositionComponent(100, 100))
.withChildFactory((parentEntity) => {
return createBuilder(scene)
.named('ChildFromFactory')
.with(new PositionComponent(10, 20));
});
// withChildFactory returns the builder for chaining
expect(builder).toBeInstanceOf(EntityBuilder);
});
test('should call withChildIf method', () => {
const shouldAdd = true;
const shouldNotAdd = false;
const child1Builder = createBuilder(scene).named('Child1');
const builder1 = createBuilder(scene)
.named('Parent')
.withChildIf(shouldAdd, child1Builder);
expect(builder1).toBeInstanceOf(EntityBuilder);
const child2Builder = createBuilder(scene).named('Child2');
const builder2 = createBuilder(scene)
.named('Parent2')
.withChildIf(shouldNotAdd, child2Builder);
expect(builder2).toBeInstanceOf(EntityBuilder);
});
});
describe('spawning and cloning', () => {
test('should spawn entity to scene with .spawn()', () => {
const initialCount = scene.entities.count;
const entity = createBuilder(scene)
.named('SpawnedEntity')
.with(new PositionComponent())
.spawn();
expect(scene.entities.count).toBe(initialCount + 1);
expect(scene.findEntityById(entity.id)).toBe(entity);
});
test('.build() should not add to scene automatically', () => {
const initialCount = scene.entities.count;
createBuilder(scene)
.named('BuiltEntity')
.build();
expect(scene.entities.count).toBe(initialCount);
});
test('should clone builder', () => {
const builder = createBuilder(scene)
.named('OriginalEntity')
.tagged(0x50);
const clonedBuilder = builder.clone();
expect(clonedBuilder).not.toBe(builder);
expect(clonedBuilder).toBeInstanceOf(EntityBuilder);
});
});
describe('complex building scenarios', () => {
test('should build complete entity with all options', () => {
const entity = createBuilder(scene)
.named('CompleteEntity')
.tagged(0x100)
.with(new PositionComponent(50, 75))
.with(new VelocityComponent())
.withFactory(() => new HealthComponent())
.configure(HealthComponent, (h: HealthComponent) => {
h.current = 80;
h.max = 100;
})
.enabled(true)
.active(true)
.build();
expect(entity.name).toBe('CompleteEntity');
expect(entity.tag).toBe(0x100);
expect(entity.enabled).toBe(true);
expect(entity.active).toBe(true);
expect(entity.hasComponent(PositionComponent)).toBe(true);
expect(entity.hasComponent(VelocityComponent)).toBe(true);
expect(entity.hasComponent(HealthComponent)).toBe(true);
const health = entity.getComponent(HealthComponent);
expect(health!.current).toBe(80);
expect(health!.max).toBe(100);
});
test('should support complex chaining', () => {
const builder = createBuilder(scene)
.named('Root')
.with(new PositionComponent(1, 1));
// Add child builder chain
const childBuilder = createBuilder(scene)
.named('Child')
.with(new PositionComponent(2, 2));
// Chain withChild
builder.withChild(childBuilder);
// Build and spawn
const root = builder.spawn();
expect(root.name).toBe('Root');
expect(root.hasComponent(PositionComponent)).toBe(true);
});
});
});
@@ -0,0 +1,93 @@
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 { ECSComponent } from '../../../src/ECS/Decorators';
// 简单的测试组件
@ECSComponent('MinSysInit_HealthComponent')
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
// 简单的测试系统
class HealthSystem extends EntitySystem {
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(HealthComponent));
}
protected override onAdded(entity: Entity): void {
console.log('[HealthSystem] onAdded called:', { id: entity.id, name: entity.name });
this.onAddedEntities.push(entity);
}
}
describe('MinimalSystemInit - 最小化系统初始化测试', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
afterEach(() => {
if (scene) {
scene.end();
}
});
test('先创建实体和组件,再添加系统 - 应该触发onAdded', () => {
console.log('\\n=== Test 1: 先创建实体再添加系统 ===');
// 1. 创建实体并添加组件
const entity = scene.createEntity('TestEntity');
entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with HealthComponent');
// 2. 验证QuerySystem能查询到实体
const queryResult = scene.querySystem.queryAll(HealthComponent);
console.log('[Test] QuerySystem result:', { count: queryResult.count });
// 3. 添加系统
const system = new HealthSystem();
console.log('[Test] Adding system to scene...');
scene.addEntityProcessor(system);
console.log('[Test] System added, onAddedEntities.length =', system.onAddedEntities.length);
// 4. 验证
expect(system.onAddedEntities).toHaveLength(1);
});
test('先添加系统,再创建实体和组件 - 应该在update时触发onAdded', () => {
console.log('\\n=== Test 2: 先添加系统再创建实体 ===');
// 1. 先添加系统
const system = new HealthSystem();
scene.addEntityProcessor(system);
console.log('[Test] System added, onAddedEntities.length =', system.onAddedEntities.length);
// 2. 创建实体并添加组件
const entity = scene.createEntity('TestEntity');
entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with HealthComponent');
// 3. 调用update触发系统查询
console.log('[Test] Calling scene.update()...');
scene.update();
console.log('[Test] After update, onAddedEntities.length =', system.onAddedEntities.length);
// 4. 验证
expect(system.onAddedEntities).toHaveLength(1);
});
});
@@ -0,0 +1,161 @@
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 { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件
@ECSComponent('MultiSysInit_PositionComponent')
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('MultiSysInit_VelocityComponent')
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('MultiSysInit_HealthComponent')
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
// 测试系统
class MovementSystem extends EntitySystem {
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
protected override onAdded(entity: Entity): void {
console.log('[MovementSystem] onAdded:', { id: entity.id, name: entity.name });
this.onAddedEntities.push(entity);
}
}
class HealthSystem extends EntitySystem {
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(HealthComponent));
}
protected override onAdded(entity: Entity): void {
console.log('[HealthSystem] onAdded:', { id: entity.id, name: entity.name });
this.onAddedEntities.push(entity);
}
}
describe('MultiSystemInit - 多系统初始化测试', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
afterEach(() => {
if (scene) {
scene.end();
}
});
test('多个系统同时响应同一实体 - 复现失败场景', () => {
console.log('\\n=== Test: 多个系统同时响应同一实体 ===');
// 1. 创建实体并添加所有组件
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with Position, Velocity, Health');
// 2. 验证QuerySystem能查询到实体
const movementQuery = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
const healthQuery = scene.querySystem.queryAll(HealthComponent);
console.log('[Test] MovementQuery result:', { count: movementQuery.count });
console.log('[Test] HealthQuery result:', { count: healthQuery.count });
// 3. 添加两个系统
console.log('[Test] Adding MovementSystem...');
const movementSystem = new MovementSystem();
scene.addEntityProcessor(movementSystem);
console.log('[Test] MovementSystem added, onAddedEntities.length =', movementSystem.onAddedEntities.length);
console.log('[Test] Adding HealthSystem...');
const healthSystem = new HealthSystem();
scene.addEntityProcessor(healthSystem);
console.log('[Test] HealthSystem added, onAddedEntities.length =', healthSystem.onAddedEntities.length);
// 4. 验证
console.log('[Test] Final check:');
console.log(' MovementSystem.onAddedEntities.length =', movementSystem.onAddedEntities.length);
console.log(' HealthSystem.onAddedEntities.length =', healthSystem.onAddedEntities.length);
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities).toHaveLength(1);
});
test('不同系统匹配不同实体 - 复现失败场景', () => {
console.log('\\n=== Test: 不同系统匹配不同实体 ===');
// 1. 创建两个实体
const movingEntity = scene.createEntity('Moving');
movingEntity.addComponent(new PositionComponent(0, 0));
movingEntity.addComponent(new VelocityComponent(1, 1));
const healthEntity = scene.createEntity('Health');
healthEntity.addComponent(new HealthComponent(100));
console.log('[Test] Two entities created');
// 2. 验证QuerySystem
const movementQuery = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
const healthQuery = scene.querySystem.queryAll(HealthComponent);
console.log('[Test] MovementQuery result:', { count: movementQuery.count });
console.log('[Test] HealthQuery result:', { count: healthQuery.count });
// 3. 添加系统
console.log('[Test] Adding systems...');
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
console.log('[Test] Systems added');
console.log(' MovementSystem.onAddedEntities.length =', movementSystem.onAddedEntities.length);
console.log(' HealthSystem.onAddedEntities.length =', healthSystem.onAddedEntities.length);
// 4. 验证
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(movementSystem.onAddedEntities[0]).toBe(movingEntity);
expect(healthSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities[0]).toBe(healthEntity);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,256 @@
import { describe, test, expect, beforeEach } from '@jest/globals';
import { ReferenceTracker } from '../../../src/ECS/Core/ReferenceTracker';
import { Component } from '../../../src/ECS/Component';
import { Entity } from '../../../src/ECS/Entity';
import { Scene } from '../../../src/ECS/Scene';
import { ECSComponent } from '../../../src/ECS/Decorators';
@ECSComponent('RefTrackerTestComponent')
class TestComponent extends Component {
public target: Entity | null = null;
}
describe('ReferenceTracker', () => {
let tracker: ReferenceTracker;
let scene: Scene;
let entity1: Entity;
let entity2: Entity;
let component: TestComponent;
beforeEach(() => {
tracker = new ReferenceTracker();
scene = new Scene();
entity1 = scene.createEntity('Entity1');
entity2 = scene.createEntity('Entity2');
component = new TestComponent();
entity1.addComponent(component);
});
describe('registerReference', () => {
test('应该成功注册Entity引用', () => {
tracker.registerReference(entity2, component, 'target');
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(1);
expect(refs[0].component.deref()).toBe(component);
expect(refs[0].propertyKey).toBe('target');
});
test('应该避免重复注册相同引用', () => {
tracker.registerReference(entity2, component, 'target');
tracker.registerReference(entity2, component, 'target');
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(1);
});
test('应该支持多个Component引用同一Entity', () => {
const component2 = new TestComponent();
const entity3 = scene.createEntity('Entity3');
entity3.addComponent(component2);
tracker.registerReference(entity2, component, 'target');
tracker.registerReference(entity2, component2, 'target');
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(2);
});
test('应该支持同一Component引用多个属性', () => {
tracker.registerReference(entity2, component, 'target');
tracker.registerReference(entity2, component, 'parent');
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(2);
});
});
describe('unregisterReference', () => {
test('应该成功注销Entity引用', () => {
tracker.registerReference(entity2, component, 'target');
tracker.unregisterReference(entity2, component, 'target');
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(0);
});
test('注销不存在的引用不应报错', () => {
expect(() => {
tracker.unregisterReference(entity2, component, 'target');
}).not.toThrow();
});
test('应该只注销指定的引用', () => {
const component2 = new TestComponent();
const entity3 = scene.createEntity('Entity3');
entity3.addComponent(component2);
tracker.registerReference(entity2, component, 'target');
tracker.registerReference(entity2, component2, 'target');
tracker.unregisterReference(entity2, component, 'target');
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(1);
expect(refs[0].component.deref()).toBe(component2);
});
});
describe('clearReferencesTo', () => {
test('应该将所有引用设为null', () => {
component.target = entity2;
tracker.registerReference(entity2, component, 'target');
tracker.clearReferencesTo(entity2.id);
expect(component.target).toBeNull();
});
test('应该清理多个Component的引用', () => {
const component2 = new TestComponent();
const entity3 = scene.createEntity('Entity3');
entity3.addComponent(component2);
component.target = entity2;
component2.target = entity2;
tracker.registerReference(entity2, component, 'target');
tracker.registerReference(entity2, component2, 'target');
tracker.clearReferencesTo(entity2.id);
expect(component.target).toBeNull();
expect(component2.target).toBeNull();
});
test('清理不存在的Entity引用不应报错', () => {
expect(() => {
tracker.clearReferencesTo(999);
}).not.toThrow();
});
test('应该移除引用记录', () => {
tracker.registerReference(entity2, component, 'target');
tracker.clearReferencesTo(entity2.id);
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(0);
});
});
describe('clearComponentReferences', () => {
test('应该清理Component的所有引用注册', () => {
tracker.registerReference(entity2, component, 'target');
tracker.clearComponentReferences(component);
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(0);
});
test('应该只清理指定Component的引用', () => {
const component2 = new TestComponent();
const entity3 = scene.createEntity('Entity3');
entity3.addComponent(component2);
tracker.registerReference(entity2, component, 'target');
tracker.registerReference(entity2, component2, 'target');
tracker.clearComponentReferences(component);
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(1);
expect(refs[0].component.deref()).toBe(component2);
});
});
describe('getReferencesTo', () => {
test('应该返回空数组当Entity没有引用时', () => {
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toEqual([]);
});
test('应该只返回有效的引用记录', () => {
tracker.registerReference(entity2, component, 'target');
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(1);
});
});
describe('cleanup', () => {
test('应该清理失效的WeakRef引用', () => {
let tempComponent: TestComponent | null = new TestComponent();
const entity3 = scene.createEntity('Entity3');
entity3.addComponent(tempComponent);
tracker.registerReference(entity2, tempComponent, 'target');
expect(tracker.getReferencesTo(entity2.id)).toHaveLength(1);
tempComponent = null;
if (global.gc) {
global.gc();
}
tracker.cleanup();
const refs = tracker.getReferencesTo(entity2.id);
expect(refs.length).toBeLessThanOrEqual(1);
});
});
describe('getDebugInfo', () => {
test('应该返回调试信息', () => {
tracker.registerReference(entity2, component, 'target');
const debugInfo = tracker.getDebugInfo();
expect(debugInfo).toHaveProperty(`entity_${entity2.id}`);
const entityRefs = (debugInfo as any)[`entity_${entity2.id}`];
expect(entityRefs).toHaveLength(1);
expect(entityRefs[0]).toMatchObject({
componentId: component.id,
propertyKey: 'target'
});
});
test('应该只包含有效的引用', () => {
tracker.registerReference(entity2, component, 'target');
const debugInfo = tracker.getDebugInfo();
expect(Object.keys(debugInfo)).toHaveLength(1);
});
});
describe('边界情况', () => {
test('应该处理Component被GC回收的情况', () => {
tracker.registerReference(entity2, component, 'target');
tracker.cleanup();
const refs = tracker.getReferencesTo(entity2.id);
expect(refs.length).toBeGreaterThanOrEqual(0);
});
test('应该支持大量引用', () => {
const components: TestComponent[] = [];
for (let i = 0; i < 1000; i++) {
const comp = new TestComponent();
const ent = scene.createEntity(`Entity${i}`);
ent.addComponent(comp);
components.push(comp);
tracker.registerReference(entity2, comp, 'target');
}
const refs = tracker.getReferencesTo(entity2.id);
expect(refs).toHaveLength(1000);
tracker.clearReferencesTo(entity2.id);
const refsAfter = tracker.getReferencesTo(entity2.id);
expect(refsAfter).toHaveLength(0);
});
});
});
@@ -0,0 +1,183 @@
import { SoASerializer } from '../../../src/ECS/Core/SoASerializer';
describe('SoASerializer', () => {
describe('serialize', () => {
test('should serialize Map to JSON string', () => {
const map = new Map([['key1', 'value1'], ['key2', 'value2']]);
const result = SoASerializer.serialize(map, 'testMap', { isMap: true });
expect(result).toBe('[["key1","value1"],["key2","value2"]]');
});
test('should serialize Set to JSON string', () => {
const set = new Set([1, 2, 3]);
const result = SoASerializer.serialize(set, 'testSet', { isSet: true });
expect(result).toBe('[1,2,3]');
});
test('should serialize Array to JSON string', () => {
const arr = [1, 2, 3];
const result = SoASerializer.serialize(arr, 'testArray', { isArray: true });
expect(result).toBe('[1,2,3]');
});
test('should serialize plain object to JSON string', () => {
const obj = { a: 1, b: 'test' };
const result = SoASerializer.serialize(obj, 'testObj');
expect(result).toBe('{"a":1,"b":"test"}');
});
test('should serialize primitive values', () => {
expect(SoASerializer.serialize(42, 'num')).toBe('42');
expect(SoASerializer.serialize('hello', 'str')).toBe('"hello"');
expect(SoASerializer.serialize(true, 'bool')).toBe('true');
expect(SoASerializer.serialize(null, 'null')).toBe('null');
});
test('should return empty object on serialization error', () => {
const circular: Record<string, unknown> = {};
circular.self = circular;
const result = SoASerializer.serialize(circular, 'circular');
expect(result).toBe('{}');
});
});
describe('deserialize', () => {
test('should deserialize JSON string to Map', () => {
const json = '[["key1","value1"],["key2","value2"]]';
const result = SoASerializer.deserialize(json, 'testMap', { isMap: true });
expect(result).toBeInstanceOf(Map);
expect((result as Map<string, string>).get('key1')).toBe('value1');
expect((result as Map<string, string>).get('key2')).toBe('value2');
});
test('should deserialize JSON string to Set', () => {
const json = '[1,2,3]';
const result = SoASerializer.deserialize(json, 'testSet', { isSet: true });
expect(result).toBeInstanceOf(Set);
expect((result as Set<number>).has(1)).toBe(true);
expect((result as Set<number>).has(2)).toBe(true);
expect((result as Set<number>).has(3)).toBe(true);
});
test('should deserialize JSON string to Array', () => {
const json = '[1,2,3]';
const result = SoASerializer.deserialize(json, 'testArray', { isArray: true });
expect(result).toEqual([1, 2, 3]);
});
test('should deserialize JSON string to object', () => {
const json = '{"a":1,"b":"test"}';
const result = SoASerializer.deserialize(json, 'testObj');
expect(result).toEqual({ a: 1, b: 'test' });
});
test('should deserialize primitive values', () => {
expect(SoASerializer.deserialize('42', 'num')).toBe(42);
expect(SoASerializer.deserialize('"hello"', 'str')).toBe('hello');
expect(SoASerializer.deserialize('true', 'bool')).toBe(true);
expect(SoASerializer.deserialize('null', 'null')).toBe(null);
});
test('should return null on deserialization error', () => {
const result = SoASerializer.deserialize('invalid json', 'field');
expect(result).toBe(null);
});
});
describe('deepClone', () => {
test('should return primitive values as-is', () => {
expect(SoASerializer.deepClone(42)).toBe(42);
expect(SoASerializer.deepClone('hello')).toBe('hello');
expect(SoASerializer.deepClone(true)).toBe(true);
expect(SoASerializer.deepClone(null)).toBe(null);
expect(SoASerializer.deepClone(undefined)).toBe(undefined);
});
test('should clone Date objects', () => {
const date = new Date('2023-01-01');
const cloned = SoASerializer.deepClone(date);
expect(cloned).toBeInstanceOf(Date);
expect(cloned.getTime()).toBe(date.getTime());
expect(cloned).not.toBe(date);
});
test('should clone arrays deeply', () => {
const arr = [1, [2, 3], { a: 4 }];
const cloned = SoASerializer.deepClone(arr);
expect(cloned).toEqual(arr);
expect(cloned).not.toBe(arr);
expect(cloned[1]).not.toBe(arr[1]);
expect(cloned[2]).not.toBe(arr[2]);
});
test('should clone Map objects deeply', () => {
const map = new Map([
['key1', { value: 1 }],
['key2', { value: 2 }]
]);
const cloned = SoASerializer.deepClone(map);
expect(cloned).toBeInstanceOf(Map);
expect(cloned.size).toBe(2);
expect(cloned.get('key1')).toEqual({ value: 1 });
expect(cloned.get('key1')).not.toBe(map.get('key1'));
});
test('should clone Set objects deeply', () => {
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const set = new Set([obj1, obj2]);
const cloned = SoASerializer.deepClone(set);
expect(cloned).toBeInstanceOf(Set);
expect(cloned.size).toBe(2);
const clonedArray = Array.from(cloned);
expect(clonedArray[0]).toEqual(obj1);
expect(clonedArray[0]).not.toBe(obj1);
});
test('should clone nested objects deeply', () => {
const obj = {
a: 1,
b: {
c: 2,
d: {
e: 3
}
}
};
const cloned = SoASerializer.deepClone(obj);
expect(cloned).toEqual(obj);
expect(cloned).not.toBe(obj);
expect(cloned.b).not.toBe(obj.b);
expect(cloned.b.d).not.toBe(obj.b.d);
});
test('should clone complex nested structures', () => {
const complex = {
array: [1, 2, 3],
map: new Map([['a', 1]]),
set: new Set([1, 2]),
date: new Date('2023-01-01'),
nested: {
value: 'test'
}
};
const cloned = SoASerializer.deepClone(complex);
expect(cloned.array).toEqual(complex.array);
expect(cloned.array).not.toBe(complex.array);
expect(cloned.map).toBeInstanceOf(Map);
expect(cloned.map.get('a')).toBe(1);
expect(cloned.set).toBeInstanceOf(Set);
expect(cloned.set.has(1)).toBe(true);
expect(cloned.date).toBeInstanceOf(Date);
expect(cloned.date.getTime()).toBe(complex.date.getTime());
expect(cloned.nested).toEqual(complex.nested);
expect(cloned.nested).not.toBe(complex.nested);
});
});
});
@@ -0,0 +1,630 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import {
EnableSoA,
Float64,
Int32,
SerializeMap,
SerializeSet,
SerializeArray,
DeepCopy,
SoAStorage
} from '../../../src/ECS/Core/SoAStorage';
/**
* SoA存储完整测试套件
*/
// 测试组件定义
@EnableSoA
class BasicTypesComponent extends Component {
public intNumber: number;
public floatNumber: number;
public boolValue: boolean;
public stringValue: string;
public nullValue: null;
public undefinedValue: undefined;
constructor(...args: unknown[]) {
super();
const [
intNumber = 42,
floatNumber = 3.14,
boolValue = true,
stringValue = 'test',
nullValue = null,
undefinedValue = undefined
] = args as [number?, number?, boolean?, string?, null?, undefined?];
this.intNumber = intNumber;
this.floatNumber = floatNumber;
this.boolValue = boolValue;
this.stringValue = stringValue;
this.nullValue = nullValue;
this.undefinedValue = undefinedValue;
}
}
@EnableSoA
class DecoratedNumberComponent extends Component {
public normalFloat: number;
@Float64
public highPrecisionNumber: number;
@Float64
public preciseFloat: number;
@Int32
public integerValue: number;
constructor(...args: unknown[]) {
super();
const [
normalFloat = 3.14,
highPrecisionNumber = Number.MAX_SAFE_INTEGER,
preciseFloat = Math.PI,
integerValue = 42
] = args as [number?, number?, number?, number?];
this.normalFloat = normalFloat;
this.highPrecisionNumber = highPrecisionNumber;
this.preciseFloat = preciseFloat;
this.integerValue = integerValue;
}
}
@EnableSoA
class CollectionComponent extends Component {
@SerializeMap
public mapData: Map<string, any>;
@SerializeSet
public setData: Set<any>;
@SerializeArray
public arrayData: any[];
@DeepCopy
public deepCopyData: any;
constructor(...args: unknown[]) {
super();
const [
mapData = new Map(),
setData = new Set(),
arrayData = [],
deepCopyData = null
] = args as [Map<string, any>?, Set<any>?, any[]?, any?];
this.mapData = mapData;
this.setData = setData;
this.arrayData = arrayData;
this.deepCopyData = deepCopyData;
}
}
class MockNode {
public name: string;
public active: boolean;
constructor(name: string) {
this.name = name;
this.active = true;
}
}
@EnableSoA
class ComplexObjectComponent extends Component {
public x: number;
public y: number;
public node: MockNode | null;
public callback: Function | null;
public data: any;
constructor(...args: unknown[]) {
super();
const [
x = 0,
y = 0,
node = null as MockNode | null,
callback = null as Function | null,
data = null as any
] = args as [number?, number?, (MockNode | null)?, (Function | null)?, any?];
this.x = x;
this.y = y;
this.node = node;
this.callback = callback;
this.data = data;
}
}
@EnableSoA
class MixedComponent extends Component {
@Float64
public bigIntId: number;
@Float64
public preciseValue: number;
@Int32
public intValue: number;
@SerializeMap
public gameMap: Map<string, any>;
@SerializeSet
public flags: Set<number>;
@SerializeArray
public items: any[];
@DeepCopy
public config: any;
public normalFloat: number;
public boolFlag: boolean;
public text: string;
constructor(...args: unknown[]) {
super();
const [
bigIntId = 0,
preciseValue = 0,
intValue = 0,
normalFloat = 0,
boolFlag = false,
text = ''
] = args as [number?, number?, number?, number?, boolean?, string?];
this.bigIntId = bigIntId;
this.preciseValue = preciseValue;
this.intValue = intValue;
this.gameMap = new Map();
this.flags = new Set();
this.items = [];
this.config = null;
this.normalFloat = normalFloat;
this.boolFlag = boolFlag;
this.text = text;
}
}
describe('SoAStorage - SoA存储测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
describe('基础数据类型', () => {
test('应该正确存储和检索number类型', () => {
const component = new BasicTypesComponent(999, 2.718);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.intNumber).toBe(999);
expect(retrieved?.floatNumber).toBeCloseTo(2.718);
});
test('应该正确存储和检索boolean类型', () => {
const component = new BasicTypesComponent(0, 0, false);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.boolValue).toBe(false);
});
test('应该正确存储和检索string类型', () => {
const testString = '测试中文字符串 with emoji 🎉';
const component = new BasicTypesComponent(0, 0, true, testString);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe(testString);
});
test('应该正确处理null和undefined', () => {
const component = new BasicTypesComponent();
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.nullValue).toBe(null);
// undefined在SoA存储中保持为undefined,不会序列化
expect(retrieved?.undefinedValue).toBeUndefined();
});
test('应该正确处理空字符串', () => {
const component = new BasicTypesComponent(0, 0, true, '');
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe('');
});
test('应该正确处理数值边界值', () => {
// Float32可精确表示的最大整数约为2^24 (16777216)
// Float32最小正值约为1.4e-45Number.MIN_VALUE (5e-324)会被截断为0
const maxFloat32Int = 16777216;
const minFloat32 = 1.401298464324817e-45;
const component = new BasicTypesComponent(
maxFloat32Int,
minFloat32
);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.intNumber).toBe(maxFloat32Int);
expect(retrieved?.floatNumber).toBeCloseTo(minFloat32, 45);
});
test('应该正确处理特殊字符串', () => {
const specialString = '\n\t\r"\'\\\\';
const component = new BasicTypesComponent(0, 0, true, specialString);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe(specialString);
});
test('应该正确处理长字符串', () => {
const longString = 'a'.repeat(1000);
const component = new BasicTypesComponent(0, 0, true, longString);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe(longString);
});
});
describe('数值类型装饰器', () => {
test('@Float64应该保持高精度数值', () => {
const component = new DecoratedNumberComponent(
0,
Number.MAX_SAFE_INTEGER
);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
});
test('@Float64应该使用双精度浮点存储', () => {
const component = new DecoratedNumberComponent(0, 0, Math.PI);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.preciseFloat).toBeCloseTo(Math.PI, 15);
});
test('@Int32应该使用32位整数存储', () => {
const component = new DecoratedNumberComponent(0, 0, 0, -2147483648);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.integerValue).toBe(-2147483648);
});
test('默认应该使用Float32存储', () => {
const component = new DecoratedNumberComponent(3.14159);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.normalFloat).toBeCloseTo(3.14159, 5);
});
test('应该使用正确的TypedArray类型', () => {
const component = new DecoratedNumberComponent();
manager.addComponent(1, component);
const storage = manager.getStorage(DecoratedNumberComponent) as SoAStorage<DecoratedNumberComponent>;
expect(storage.getFieldArray('normalFloat')).toBeInstanceOf(Float32Array);
expect(storage.getFieldArray('preciseFloat')).toBeInstanceOf(Float64Array);
expect(storage.getFieldArray('integerValue')).toBeInstanceOf(Int32Array);
expect(storage.getFieldArray('highPrecisionNumber')).toBeInstanceOf(Float64Array);
});
});
describe('集合类型序列化', () => {
test('@SerializeMap应该正确序列化Map', () => {
const component = new CollectionComponent();
component.mapData.set('key1', 'value1');
component.mapData.set('key2', 123);
component.mapData.set('key3', { nested: 'object' });
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.mapData).toBeInstanceOf(Map);
expect(retrieved?.mapData.size).toBe(3);
expect(retrieved?.mapData.get('key1')).toBe('value1');
expect(retrieved?.mapData.get('key2')).toBe(123);
expect(retrieved?.mapData.get('key3')).toEqual({ nested: 'object' });
});
test('@SerializeSet应该正确序列化Set', () => {
const component = new CollectionComponent();
component.setData.add('item1');
component.setData.add('item2');
component.setData.add(123);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.setData).toBeInstanceOf(Set);
expect(retrieved?.setData.size).toBe(3);
expect(retrieved?.setData.has('item1')).toBe(true);
expect(retrieved?.setData.has('item2')).toBe(true);
expect(retrieved?.setData.has(123)).toBe(true);
});
test('@SerializeArray应该正确序列化Array', () => {
const component = new CollectionComponent();
component.arrayData = ['item1', 'item2', 123, { nested: 'object' }];
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(Array.isArray(retrieved?.arrayData)).toBe(true);
expect(retrieved?.arrayData).toEqual(['item1', 'item2', 123, { nested: 'object' }]);
});
test('@DeepCopy应该创建深拷贝', () => {
const component = new CollectionComponent();
component.deepCopyData = { level1: { level2: { value: 42 } } };
const originalRef = component.deepCopyData;
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.deepCopyData).toEqual(component.deepCopyData);
expect(retrieved?.deepCopyData).not.toBe(originalRef);
component.deepCopyData.level1.level2.value = 100;
expect(retrieved?.deepCopyData.level1.level2.value).toBe(42);
});
test('Map应该正确处理边界值', () => {
const component = new CollectionComponent();
component.mapData.set('null', null);
component.mapData.set('undefined', undefined);
component.mapData.set('empty', '');
component.mapData.set('zero', 0);
component.mapData.set('false', false);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.mapData.get('null')).toBe(null);
expect(retrieved?.mapData.get('undefined')).toBe(null);
expect(retrieved?.mapData.get('empty')).toBe('');
expect(retrieved?.mapData.get('zero')).toBe(0);
expect(retrieved?.mapData.get('false')).toBe(false);
});
test('Set应该支持数值0', () => {
const component = new CollectionComponent();
component.setData.add(0);
component.setData.add(1);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.setData.has(0)).toBe(true);
expect(retrieved?.setData.has(1)).toBe(true);
});
test('Array应该正确处理null和undefined', () => {
const component = new CollectionComponent();
component.arrayData = [null, undefined, '', 0, false];
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.arrayData).toEqual([null, null, '', 0, false]);
});
});
describe('复杂对象处理', () => {
test('应该正确保存复杂对象引用', () => {
const node = new MockNode('testNode');
const callback = () => console.log('test');
const data = { complex: 'object' };
const component = new ComplexObjectComponent(100, 200, node, callback, data);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, ComplexObjectComponent);
expect(retrieved?.x).toBe(100);
expect(retrieved?.y).toBe(200);
expect(retrieved?.node?.name).toBe('testNode');
expect(retrieved?.node?.active).toBe(true);
expect(retrieved?.callback).toBe(callback);
expect(retrieved?.data).toEqual(data);
});
test('应该正确处理null对象', () => {
const component = new ComplexObjectComponent(0, 0, null, null, null);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, ComplexObjectComponent);
expect(retrieved?.node).toBe(null);
expect(retrieved?.callback).toBe(null);
expect(retrieved?.data).toBe(null);
});
});
describe('混合装饰器使用', () => {
test('应该支持多种装饰器混合使用', () => {
const component = new MixedComponent(
Number.MAX_SAFE_INTEGER,
Math.PI,
-2147483648,
1.23,
true,
'test'
);
component.gameMap.set('player1', { level: 10 });
component.flags.add(1);
component.flags.add(2);
component.items.push('item1');
component.config = { settings: { volume: 0.8 } };
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, MixedComponent);
expect(retrieved?.bigIntId).toBe(Number.MAX_SAFE_INTEGER);
expect(retrieved?.preciseValue).toBeCloseTo(Math.PI, 15);
expect(retrieved?.intValue).toBe(-2147483648);
expect(retrieved?.normalFloat).toBeCloseTo(1.23, 5);
expect(retrieved?.boolFlag).toBe(true);
expect(retrieved?.text).toBe('test');
expect(retrieved?.gameMap.get('player1')).toEqual({ level: 10 });
expect(retrieved?.flags.has(1)).toBe(true);
expect(retrieved?.flags.has(2)).toBe(true);
expect(retrieved?.items).toContain('item1');
expect(retrieved?.config.settings.volume).toBe(0.8);
});
});
describe('存储管理', () => {
test('应该正确统计存储信息', () => {
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
for (let i = 1; i <= 5; i++) {
const component = new MixedComponent(i, i * Math.PI, i * 10);
manager.addComponent(i, component);
}
const stats = storage.getStats();
expect(stats.size).toBe(5);
expect(stats.capacity).toBeGreaterThanOrEqual(5);
expect(stats.memoryUsage).toBeGreaterThan(0);
});
test('应该支持压缩操作', () => {
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
for (let i = 1; i <= 5; i++) {
const component = new MixedComponent();
manager.addComponent(i, component);
}
storage.removeComponent(2);
storage.removeComponent(4);
const statsBefore = storage.getStats();
storage.compact();
const statsAfter = storage.getStats();
expect(statsAfter.size).toBe(3);
expect(statsAfter.size).toBeLessThan(statsBefore.capacity);
});
test('应该正确处理循环引用', () => {
const component = new MixedComponent();
const cyclicObject: any = { name: 'test' };
cyclicObject.self = cyclicObject;
component.items.push(cyclicObject);
expect(() => {
manager.addComponent(1, component);
}).not.toThrow();
const retrieved = manager.getComponent(1, MixedComponent);
expect(retrieved).toBeDefined();
});
});
describe('性能测试', () => {
test('大容量创建性能应该可接受', () => {
const entityCount = 2000;
const startTime = performance.now();
for (let i = 1; i <= entityCount; i++) {
const component = new MixedComponent(i, i * 0.1, i * 10);
component.gameMap.set(`key${i}`, i);
manager.addComponent(i, component);
}
const createTime = performance.now() - startTime;
expect(createTime).toBeLessThan(1000);
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
const stats = storage.getStats();
expect(stats.size).toBe(entityCount);
});
test('随机访问性能应该可接受', () => {
const entityCount = 2000;
for (let i = 1; i <= entityCount; i++) {
const component = new MixedComponent(i);
manager.addComponent(i, component);
}
const startTime = performance.now();
for (let i = 0; i < 100; i++) {
const randomId = Math.floor(Math.random() * entityCount) + 1;
const component = manager.getComponent(randomId, MixedComponent);
expect(component?.bigIntId).toBe(randomId);
}
const readTime = performance.now() - startTime;
expect(readTime).toBeLessThan(100);
});
test('向量化批量操作应该正确执行', () => {
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
for (let i = 1; i <= 10; i++) {
const component = new MixedComponent(0, 0, i * 10, i);
manager.addComponent(i, component);
}
let operationExecuted = false;
storage.performVectorizedOperation((fieldArrays, activeIndices) => {
operationExecuted = true;
const normalFloatArray = fieldArrays.get('normalFloat') as Float32Array;
const intArray = fieldArrays.get('intValue') as Int32Array;
expect(normalFloatArray).toBeInstanceOf(Float32Array);
expect(intArray).toBeInstanceOf(Int32Array);
expect(activeIndices.length).toBe(10);
for (let i = 0; i < activeIndices.length; i++) {
const idx = activeIndices[i];
normalFloatArray[idx] *= 2;
intArray[idx] += 5;
}
});
expect(operationExecuted).toBe(true);
const component = manager.getComponent(5, MixedComponent);
expect(component?.normalFloat).toBe(10);
expect(component?.intValue).toBe(55);
});
});
});
@@ -0,0 +1,233 @@
import { Component } from '../../../src/ECS/Component';
import {
SoATypeRegistry,
TypedArrayTypeName
} from '../../../src/ECS/Core/SoATypeRegistry';
// Test components
class SimpleComponent extends Component {
public value: number = 0;
public flag: boolean = false;
public name: string = '';
}
describe('SoATypeRegistry', () => {
describe('getConstructor', () => {
test('should return Float32Array constructor for float32', () => {
expect(SoATypeRegistry.getConstructor('float32')).toBe(Float32Array);
});
test('should return Float64Array constructor for float64', () => {
expect(SoATypeRegistry.getConstructor('float64')).toBe(Float64Array);
});
test('should return Int32Array constructor for int32', () => {
expect(SoATypeRegistry.getConstructor('int32')).toBe(Int32Array);
});
test('should return Uint32Array constructor for uint32', () => {
expect(SoATypeRegistry.getConstructor('uint32')).toBe(Uint32Array);
});
test('should return Int16Array constructor for int16', () => {
expect(SoATypeRegistry.getConstructor('int16')).toBe(Int16Array);
});
test('should return Uint16Array constructor for uint16', () => {
expect(SoATypeRegistry.getConstructor('uint16')).toBe(Uint16Array);
});
test('should return Int8Array constructor for int8', () => {
expect(SoATypeRegistry.getConstructor('int8')).toBe(Int8Array);
});
test('should return Uint8Array constructor for uint8', () => {
expect(SoATypeRegistry.getConstructor('uint8')).toBe(Uint8Array);
});
test('should return Uint8ClampedArray constructor for uint8clamped', () => {
expect(SoATypeRegistry.getConstructor('uint8clamped')).toBe(Uint8ClampedArray);
});
test('should return Float32Array as default for unknown type', () => {
expect(SoATypeRegistry.getConstructor('unknown' as TypedArrayTypeName)).toBe(Float32Array);
});
});
describe('getBytesPerElement', () => {
test('should return 4 for float32', () => {
expect(SoATypeRegistry.getBytesPerElement('float32')).toBe(4);
});
test('should return 8 for float64', () => {
expect(SoATypeRegistry.getBytesPerElement('float64')).toBe(8);
});
test('should return 4 for int32', () => {
expect(SoATypeRegistry.getBytesPerElement('int32')).toBe(4);
});
test('should return 4 for uint32', () => {
expect(SoATypeRegistry.getBytesPerElement('uint32')).toBe(4);
});
test('should return 2 for int16', () => {
expect(SoATypeRegistry.getBytesPerElement('int16')).toBe(2);
});
test('should return 2 for uint16', () => {
expect(SoATypeRegistry.getBytesPerElement('uint16')).toBe(2);
});
test('should return 1 for int8', () => {
expect(SoATypeRegistry.getBytesPerElement('int8')).toBe(1);
});
test('should return 1 for uint8', () => {
expect(SoATypeRegistry.getBytesPerElement('uint8')).toBe(1);
});
test('should return 1 for uint8clamped', () => {
expect(SoATypeRegistry.getBytesPerElement('uint8clamped')).toBe(1);
});
test('should return 4 as default for unknown type', () => {
expect(SoATypeRegistry.getBytesPerElement('unknown' as TypedArrayTypeName)).toBe(4);
});
});
describe('getTypeName', () => {
test('should return float32 for Float32Array', () => {
expect(SoATypeRegistry.getTypeName(new Float32Array(1))).toBe('float32');
});
test('should return float64 for Float64Array', () => {
expect(SoATypeRegistry.getTypeName(new Float64Array(1))).toBe('float64');
});
test('should return int32 for Int32Array', () => {
expect(SoATypeRegistry.getTypeName(new Int32Array(1))).toBe('int32');
});
test('should return uint32 for Uint32Array', () => {
expect(SoATypeRegistry.getTypeName(new Uint32Array(1))).toBe('uint32');
});
test('should return int16 for Int16Array', () => {
expect(SoATypeRegistry.getTypeName(new Int16Array(1))).toBe('int16');
});
test('should return uint16 for Uint16Array', () => {
expect(SoATypeRegistry.getTypeName(new Uint16Array(1))).toBe('uint16');
});
test('should return int8 for Int8Array', () => {
expect(SoATypeRegistry.getTypeName(new Int8Array(1))).toBe('int8');
});
test('should return uint8 for Uint8Array', () => {
expect(SoATypeRegistry.getTypeName(new Uint8Array(1))).toBe('uint8');
});
test('should return uint8clamped for Uint8ClampedArray', () => {
expect(SoATypeRegistry.getTypeName(new Uint8ClampedArray(1))).toBe('uint8clamped');
});
});
describe('createSameType', () => {
test('should create Float32Array from Float32Array source', () => {
const source = new Float32Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Float32Array);
expect(result.length).toBe(20);
});
test('should create Float64Array from Float64Array source', () => {
const source = new Float64Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Float64Array);
expect(result.length).toBe(20);
});
test('should create Int32Array from Int32Array source', () => {
const source = new Int32Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Int32Array);
expect(result.length).toBe(20);
});
test('should create Uint32Array from Uint32Array source', () => {
const source = new Uint32Array(10);
const result = SoATypeRegistry.createSameType(source, 20);
expect(result).toBeInstanceOf(Uint32Array);
expect(result.length).toBe(20);
});
test('should create Int16Array from Int16Array source', () => {
const source = new Int16Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Int16Array);
expect(result.length).toBe(15);
});
test('should create Uint16Array from Uint16Array source', () => {
const source = new Uint16Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Uint16Array);
expect(result.length).toBe(15);
});
test('should create Int8Array from Int8Array source', () => {
const source = new Int8Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Int8Array);
expect(result.length).toBe(15);
});
test('should create Uint8Array from Uint8Array source', () => {
const source = new Uint8Array(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(15);
});
test('should create Uint8ClampedArray from Uint8ClampedArray source', () => {
const source = new Uint8ClampedArray(10);
const result = SoATypeRegistry.createSameType(source, 15);
expect(result).toBeInstanceOf(Uint8ClampedArray);
expect(result.length).toBe(15);
});
});
describe('extractFieldMetadata', () => {
test('should extract metadata for simple component', () => {
const metadata = SoATypeRegistry.extractFieldMetadata(SimpleComponent);
expect(metadata.has('value')).toBe(true);
expect(metadata.get('value')?.type).toBe('number');
expect(metadata.get('value')?.arrayType).toBe('float32');
expect(metadata.has('flag')).toBe(true);
expect(metadata.get('flag')?.type).toBe('boolean');
expect(metadata.get('flag')?.arrayType).toBe('uint8');
expect(metadata.has('name')).toBe(true);
expect(metadata.get('name')?.type).toBe('string');
});
test('should not include id field in metadata', () => {
const metadata = SoATypeRegistry.extractFieldMetadata(SimpleComponent);
expect(metadata.has('id')).toBe(false);
});
test('should handle component with object fields', () => {
class ObjectComponent extends Component {
public data: object = {};
}
const metadata = SoATypeRegistry.extractFieldMetadata(ObjectComponent);
expect(metadata.has('data')).toBe(true);
expect(metadata.get('data')?.type).toBe('object');
});
});
});
@@ -0,0 +1,522 @@
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 { ECSComponent } from '../../../src/ECS/Decorators';
/**
* System初始化测试套件
*
* 测试覆盖:
* - 系统初始化时序问题(先添加实体 vs 先添加系统)
* - 系统重复初始化防护
* - 动态组件修改响应
* - 系统生命周期管理
*/
// 测试组件
@ECSComponent('SysInit_PositionComponent')
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('SysInit_VelocityComponent')
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('SysInit_HealthComponent')
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
@ECSComponent('SysInit_TagComponent')
class TagComponent extends Component {
public tag: string;
constructor(...args: unknown[]) {
super();
const [tag = ''] = args as [string?];
this.tag = tag;
}
}
@ECSComponent('SysInit_TestComponent')
class TestComponent extends Component {
public value: number;
constructor(...args: unknown[]) {
super();
const [value = 0] = args as [number?];
this.value = value;
}
}
// 测试系统
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;
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, HealthComponent, TagComponent));
}
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
protected override onAdded(entity: Entity): void {
this.onAddedEntities.push(entity);
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
}
}
class TrackingSystem extends EntitySystem {
public initializeCallCount = 0;
public onChangedCallCount = 0;
public trackedEntities: Entity[] = [];
public override initialize(): void {
const wasInitialized = (this as any)._initialized;
super.initialize();
if (!wasInitialized) {
this.initializeCallCount++;
if (this.scene) {
for (const entity of this.scene.entities.buffer) {
this.onChanged(entity);
}
}
}
}
public 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('SystemInitialization - 系统初始化测试', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
scene.name = 'InitializationTestScene';
});
afterEach(() => {
if (scene) {
scene.end();
}
});
describe('初始化时序', () => {
test('先添加实体再添加系统 - 系统应该正确初始化', () => {
const player = scene.createEntity('Player');
player.addComponent(new PositionComponent(10, 20));
player.addComponent(new VelocityComponent(1, 1));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.initializeCalled).toBe(true);
expect(system.onAddedEntities).toHaveLength(1);
expect(system.onAddedEntities[0]).toBe(player);
});
test('先添加系统再添加实体 - 系统应该正确响应', () => {
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.initializeCalled).toBe(true);
expect(system.onAddedEntities).toHaveLength(0);
const player = scene.createEntity('Player');
player.addComponent(new PositionComponent(10, 20));
player.addComponent(new VelocityComponent(1, 1));
scene.update(); // 触发系统查询
expect(system.onAddedEntities).toHaveLength(1);
expect(system.onAddedEntities[0]).toBe(player);
});
test('先添加部分实体,再添加系统,再添加更多实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(0, 0));
entity1.addComponent(new VelocityComponent(1, 0));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(1);
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new PositionComponent(0, 0));
entity2.addComponent(new VelocityComponent(0, 1));
scene.update(); // 触发系统查询
expect(system.onAddedEntities).toHaveLength(2);
expect(system.onAddedEntities[1]).toBe(entity2);
});
test('批量实体创建后系统初始化应该正确', () => {
const entities: Entity[] = [];
for (let i = 0; i < 5; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i, i));
entity.addComponent(new VelocityComponent(1, 1));
entities.push(entity);
}
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(5);
for (let i = 0; i < 5; i++) {
expect(system.onAddedEntities).toContain(entities[i]);
}
});
});
describe('重复初始化防护', () => {
test('系统被多次添加到场景 - 应该防止重复初始化', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
const system = new TrackingSystem();
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
expect(system.onChangedCallCount).toBe(1);
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
expect(system.onChangedCallCount).toBe(1);
});
test('手动多次调用initialize - 应该防止重复处理', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
const system = new TrackingSystem();
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
expect(system.onChangedCallCount).toBe(1);
system.initialize();
expect(system.initializeCallCount).toBe(1);
expect(system.onChangedCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
});
test('系统被移除后重新添加 - 应该重新初始化', () => {
const system = new TrackingSystem();
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
scene.removeEntityProcessor(system);
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(2);
expect(system.trackedEntities).toHaveLength(1);
});
});
describe('动态组件修改响应', () => {
test('运行时添加组件 - 系统应该自动响应', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(0);
entity.addComponent(new VelocityComponent(1, 1));
scene.update(); // 触发系统查询
expect(system.onAddedEntities).toHaveLength(1);
expect(system.onAddedEntities[0]).toBe(entity);
});
test('运行时移除组件 - 系统应该自动响应', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
const velocity = entity.addComponent(new VelocityComponent(1, 1));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(1);
entity.removeComponent(velocity);
scene.update(); // 触发系统查询
expect(system.onRemovedEntities).toHaveLength(1);
expect(system.onRemovedEntities[0]).toBe(entity);
});
test('复杂的组件添加移除序列', () => {
const entity = scene.createEntity('Entity');
const system = new MovementSystem();
scene.addEntityProcessor(system);
entity.addComponent(new PositionComponent(0, 0));
scene.update();
expect(system.onAddedEntities).toHaveLength(0);
const velocity1 = entity.addComponent(new VelocityComponent(1, 1));
scene.update();
expect(system.onAddedEntities).toHaveLength(1);
entity.removeComponent(velocity1);
scene.update();
expect(system.onRemovedEntities).toHaveLength(1);
entity.addComponent(new VelocityComponent(2, 2));
scene.update();
expect(system.onAddedEntities).toHaveLength(2);
});
test('多个组件同时满足条件', () => {
const entity = scene.createEntity('Entity');
const system = new MultiComponentSystem();
scene.addEntityProcessor(system);
entity.addComponent(new PositionComponent(0, 0));
scene.update();
expect(system.onAddedEntities).toHaveLength(0);
entity.addComponent(new HealthComponent(100));
scene.update();
expect(system.onAddedEntities).toHaveLength(0);
entity.addComponent(new TagComponent('player'));
scene.update();
expect(system.onAddedEntities).toHaveLength(1);
});
});
describe('多系统协同', () => {
test('多个系统同时响应同一实体', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.addComponent(new HealthComponent(100));
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities).toHaveLength(1);
expect(movementSystem.onAddedEntities[0]).toBe(entity);
expect(healthSystem.onAddedEntities[0]).toBe(entity);
});
test('不同系统匹配不同实体', () => {
const movingEntity = scene.createEntity('Moving');
movingEntity.addComponent(new PositionComponent(0, 0));
movingEntity.addComponent(new VelocityComponent(1, 1));
const healthEntity = scene.createEntity('Health');
healthEntity.addComponent(new HealthComponent(100));
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(movementSystem.onAddedEntities[0]).toBe(movingEntity);
expect(healthSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities[0]).toBe(healthEntity);
});
test('组件变化影响多个系统', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.addComponent(new HealthComponent(100));
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
const multiSystem = new MultiComponentSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
scene.addEntityProcessor(multiSystem);
entity.addComponent(new TagComponent('player'));
scene.update(); // 触发系统查询
expect(multiSystem.onAddedEntities).toHaveLength(1);
expect(multiSystem.onAddedEntities[0]).toBe(entity);
});
});
describe('边界情况', () => {
test('空场景添加系统', () => {
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.initializeCalled).toBe(true);
expect(system.onAddedEntities).toHaveLength(0);
});
test('实体禁用状态不影响系统初始化', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.enabled = false;
const system = new MovementSystem();
scene.addEntityProcessor(system);
// 禁用的实体仍然被系统跟踪,但在process时会被过滤
expect(system.onAddedEntities).toHaveLength(1);
});
test('系统初始化时实体被销毁', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
const system = new MovementSystem();
scene.addEntityProcessor(system);
entity.destroy();
scene.update(); // 触发系统查询检测移除
expect(system.onRemovedEntities).toHaveLength(1);
});
});
});
@@ -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');
});
});
@@ -0,0 +1,197 @@
import { Core } from '../../../src/Core';
import { Scene } from '../../../src/ECS/Scene';
import { World, IGlobalSystem } from '../../../src/ECS/World';
import { WorldManager } from '../../../src/ECS/WorldManager';
import { SceneManager } from '../../../src/ECS/SceneManager';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Component } from '../../../src/ECS/Component';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { Entity } from '../../../src/ECS/Entity';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试用组件
@ECSComponent('WorldCore_TestComponent')
class TestComponent extends Component {
public value: number = 0;
constructor(value: number = 0) {
super();
this.value = value;
}
public reset(): void {
this.value = 0;
}
}
// 全局系统实现
class NetworkGlobalSystem implements IGlobalSystem {
public readonly name = 'NetworkGlobalSystem';
public syncCount: number = 0;
public initialize(): void {
// 初始化网络连接
}
public update(): void {
this.syncCount++;
// 全局网络同步逻辑
}
public reset(): void {
this.syncCount = 0;
}
public destroy(): void {
// 清理网络连接
}
}
/**
* World与Core集成测试
*
* 注意:v3.0重构后,Core不再直接管理Scene/World
* - 场景管理由 SceneManager 负责
* - 多世界管理由 WorldManager 负责
* - Core 仅负责全局服务(Timer、Performance等)
*
* 大部分旧的集成测试已移至 SceneManager.test.ts 和 WorldManager.test.ts
*/
describe('World与Core集成测试', () => {
let worldManager: WorldManager;
let sceneManager: SceneManager;
beforeEach(() => {
// 重置Core
if (Core.Instance) {
Core.destroy();
}
Core.create({ debug: false });
// WorldManager和SceneManager不再是单例
worldManager = new WorldManager();
sceneManager = new SceneManager();
});
afterEach(() => {
// 清理资源
if (sceneManager) {
sceneManager.destroy();
}
if (worldManager) {
worldManager.destroy();
}
if (Core.Instance) {
Core.destroy();
}
});
describe('基础功能', () => {
test('Core应该能够独立运行', () => {
expect(Core.Instance).toBeDefined();
// Core.update 仅更新全局服务
Core.update(0.016);
});
test('SceneManager应该能够独立管理场景', () => {
const scene = new Scene();
scene.name = 'TestScene';
sceneManager.setScene(scene);
expect(sceneManager.currentScene).toBe(scene);
expect(sceneManager.hasScene).toBe(true);
// 场景更新独立于Core
sceneManager.update();
});
test('WorldManager应该能够独立管理多个World', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
expect(worldManager.worldCount).toBe(2);
expect(worldManager.getWorld('world1')).toBe(world1);
expect(worldManager.getWorld('world2')).toBe(world2);
});
});
describe('组合使用', () => {
test('Core + SceneManager 应该正确协作', () => {
const scene = new Scene();
sceneManager.setScene(scene);
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(42));
// 游戏循环
Core.update(0.016); // 更新全局服务
sceneManager.update(); // 更新场景
expect(scene.entities.count).toBe(1);
});
test('Core + WorldManager 应该正确协作', () => {
const world = worldManager.createWorld('test-world');
const scene = world.createScene('main', new Scene());
world.start();
// 游戏循环
Core.update(0.016); // 更新全局服务
worldManager.updateAll(); // 更新所有World
expect(world.isActive).toBe(true);
});
test('World的全局系统应该能够正常工作', () => {
const world = worldManager.createWorld('test-world');
const globalSystem = new NetworkGlobalSystem();
world.addGlobalSystem(globalSystem);
worldManager.setWorldActive('test-world', true);
// 更新World
worldManager.updateAll();
expect(globalSystem.syncCount).toBeGreaterThan(0);
});
});
describe('隔离性测试', () => {
test('多个WorldManager实例应该完全隔离', () => {
const manager1 = new WorldManager();
const manager2 = new WorldManager();
manager1.createWorld('world1');
manager2.createWorld('world2');
expect(manager1.getWorld('world1')).toBeDefined();
expect(manager1.getWorld('world2')).toBeNull();
expect(manager2.getWorld('world2')).toBeDefined();
expect(manager2.getWorld('world1')).toBeNull();
// 清理
manager1.destroy();
manager2.destroy();
});
test('多个SceneManager实例应该完全隔离', () => {
const sm1 = new SceneManager();
const sm2 = new SceneManager();
const scene1 = new Scene();
const scene2 = new Scene();
sm1.setScene(scene1);
sm2.setScene(scene2);
expect(sm1.currentScene).toBe(scene1);
expect(sm2.currentScene).toBe(scene2);
// 清理
sm1.destroy();
sm2.destroy();
});
});
});
@@ -0,0 +1,230 @@
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import {
ECSComponent,
ECSSystem,
getComponentTypeName,
getSystemTypeName,
getComponentInstanceTypeName,
getSystemInstanceTypeName,
getComponentDependencies,
Property,
getPropertyMetadata,
hasPropertyMetadata
} from '../../../src/ECS/Decorators';
describe('TypeDecorators', () => {
describe('@ECSComponent', () => {
test('应该为组件类设置类型名称', () => {
@ECSComponent('TestComponent')
class TestComponent extends Component {
public value: number = 10;
}
const typeName = getComponentTypeName(TestComponent);
expect(typeName).toBe('TestComponent');
});
test('应该从组件实例获取类型名称', () => {
@ECSComponent('PlayerComponent')
class PlayerComponent extends Component {
public name: string = 'Player';
}
const instance = new PlayerComponent();
const typeName = getComponentInstanceTypeName(instance);
expect(typeName).toBe('PlayerComponent');
});
test('未装饰的组件应该使用constructor.name作为后备', () => {
class UndecoredComponent extends Component {
public data: string = 'test';
}
const typeName = getComponentTypeName(UndecoredComponent);
expect(typeName).toBe('UndecoredComponent');
});
});
describe('@ECSSystem', () => {
test('应该为系统类设置类型名称', () => {
@ECSSystem('TestSystem')
class TestSystem extends EntitySystem {
protected override process(_entities: any[]): void {
// 测试系统
}
}
const typeName = getSystemTypeName(TestSystem);
expect(typeName).toBe('TestSystem');
});
test('应该从系统实例获取类型名称', () => {
@ECSSystem('MovementSystem')
class MovementSystem extends EntitySystem {
protected override process(_entities: any[]): void {
// 移动系统
}
}
const instance = new MovementSystem();
const typeName = getSystemInstanceTypeName(instance);
expect(typeName).toBe('MovementSystem');
});
test('未装饰的系统应该使用constructor.name作为后备', () => {
class UndecoredSystem extends EntitySystem {
protected override process(_entities: any[]): void {
// 未装饰的系统
}
}
const typeName = getSystemTypeName(UndecoredSystem);
expect(typeName).toBe('UndecoredSystem');
});
});
describe('混淆场景模拟', () => {
test('装饰器名称在混淆后仍然有效', () => {
// 模拟混淆后的类名
@ECSComponent('OriginalComponent')
class a extends Component {
public prop: boolean = true;
}
@ECSSystem('OriginalSystem')
class b extends EntitySystem {
protected override process(_entities: any[]): void {
// 原始系统
}
}
// 即使类名被混淆为a和b,装饰器名称依然正确
expect(getComponentTypeName(a)).toBe('OriginalComponent');
expect(getSystemTypeName(b)).toBe('OriginalSystem');
const componentInstance = new a();
const systemInstance = new b();
expect(getComponentInstanceTypeName(componentInstance)).toBe('OriginalComponent');
expect(getSystemInstanceTypeName(systemInstance)).toBe('OriginalSystem');
});
});
describe('错误处理', () => {
test('装饰器应该验证类型名称参数', () => {
expect(() => {
@ECSComponent('')
class EmptyNameComponent extends Component {}
}).toThrow('ECSComponent装饰器必须提供有效的类型名称');
expect(() => {
@ECSSystem('')
class EmptyNameSystem extends EntitySystem {
protected override process(_entities: any[]): void {}
}
}).toThrow('ECSSystem装饰器必须提供有效的类型名称');
});
});
describe('组件依赖', () => {
test('应该存储和获取组件依赖关系', () => {
@ECSComponent('BaseComponent')
class BaseComponent extends Component {}
@ECSComponent('DependentComponent', { requires: ['BaseComponent'] })
class DependentComponent extends Component {}
const dependencies = getComponentDependencies(DependentComponent);
expect(dependencies).toEqual(['BaseComponent']);
});
test('没有依赖的组件应该返回undefined', () => {
@ECSComponent('IndependentComponent')
class IndependentComponent extends Component {}
const dependencies = getComponentDependencies(IndependentComponent);
expect(dependencies).toBeUndefined();
});
test('应该支持多个依赖', () => {
@ECSComponent('MultiDependentComponent', { requires: ['ComponentA', 'ComponentB', 'ComponentC'] })
class MultiDependentComponent extends Component {}
const dependencies = getComponentDependencies(MultiDependentComponent);
expect(dependencies).toEqual(['ComponentA', 'ComponentB', 'ComponentC']);
});
});
describe('@Property 装饰器', () => {
test('应该为属性设置元数据', () => {
@ECSComponent('PropertyTestComponent')
class PropertyTestComponent extends Component {
@Property({ type: 'number', label: 'Speed' })
public speed: number = 10;
}
const metadata = getPropertyMetadata(PropertyTestComponent);
expect(metadata).toBeDefined();
expect(metadata!['speed']).toEqual({ type: 'number', label: 'Speed' });
});
test('应该支持多个属性装饰器', () => {
@ECSComponent('MultiPropertyComponent')
class MultiPropertyComponent extends Component {
@Property({ type: 'number', label: 'X Position' })
public x: number = 0;
@Property({ type: 'number', label: 'Y Position' })
public y: number = 0;
@Property({ type: 'string', label: 'Name' })
public name: string = '';
}
const metadata = getPropertyMetadata(MultiPropertyComponent);
expect(metadata).toBeDefined();
expect(metadata!['x']).toEqual({ type: 'number', label: 'X Position' });
expect(metadata!['y']).toEqual({ type: 'number', label: 'Y Position' });
expect(metadata!['name']).toEqual({ type: 'string', label: 'Name' });
});
test('hasPropertyMetadata 应该正确检测属性元数据', () => {
@ECSComponent('HasMetadataComponent')
class HasMetadataComponent extends Component {
@Property({ type: 'boolean' })
public active: boolean = true;
}
@ECSComponent('NoMetadataComponent')
class NoMetadataComponent extends Component {
public value: number = 0;
}
expect(hasPropertyMetadata(HasMetadataComponent)).toBe(true);
expect(hasPropertyMetadata(NoMetadataComponent)).toBe(false);
});
test('应该支持完整的属性选项', () => {
@ECSComponent('FullOptionsComponent')
class FullOptionsComponent extends Component {
@Property({
type: 'number',
label: 'Health',
min: 0,
max: 100,
step: 1
})
public health: number = 100;
}
const metadata = getPropertyMetadata(FullOptionsComponent);
expect(metadata!['health']).toEqual({
type: 'number',
label: 'Health',
min: 0,
max: 100,
step: 1
});
});
});
});
@@ -0,0 +1,280 @@
import { Entity } from '../../src/ECS/Entity';
import { Component } from '../../src/ECS/Component';
import { Scene } from '../../src/ECS/Scene';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件类
@ECSComponent('EntityTest_PositionComponent')
class TestPositionComponent extends Component {
public x: number = 0;
public y: number = 0;
constructor(...args: unknown[]) {
super();
const [x = 0, y = 0] = args as [number?, number?];
this.x = x;
this.y = y;
}
}
@ECSComponent('EntityTest_HealthComponent')
class TestHealthComponent extends Component {
public health: number = 100;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
@ECSComponent('EntityTest_VelocityComponent')
class TestVelocityComponent extends Component {
public vx: number = 0;
public vy: number = 0;
constructor(...args: unknown[]) {
super();
const [vx = 0, vy = 0] = args as [number?, number?];
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('EntityTest_RenderComponent')
class TestRenderComponent extends Component {
public visible: boolean = true;
constructor(...args: unknown[]) {
super();
const [visible = true] = args as [boolean?];
this.visible = visible;
}
}
describe('Entity - 组件缓存优化测试', () => {
let entity: Entity;
let scene: Scene;
beforeEach(() => {
scene = new Scene();
entity = scene.createEntity('TestEntity');
});
describe('基本功能测试', () => {
test('应该能够创建实体', () => {
expect(entity.name).toBe('TestEntity');
expect(entity.id).toBeGreaterThanOrEqual(0);
expect(entity.components.length).toBe(0);
expect(entity.scene).toBe(scene);
});
test('应该能够添加组件', () => {
const position = new TestPositionComponent(10, 20);
const addedComponent = entity.addComponent(position);
expect(addedComponent).toBe(position);
expect(entity.components.length).toBe(1);
expect(entity.components[0]).toBe(position);
expect(entity.hasComponent(TestPositionComponent)).toBe(true);
});
test('应该能够获取组件', () => {
const position = new TestPositionComponent(10, 20);
entity.addComponent(position);
const retrieved = entity.getComponent(TestPositionComponent);
expect(retrieved).toBe(position);
expect(retrieved?.x).toBe(10);
expect(retrieved?.y).toBe(20);
});
test('应该能够检查组件存在性', () => {
const position = new TestPositionComponent(10, 20);
entity.addComponent(position);
expect(entity.hasComponent(TestPositionComponent)).toBe(true);
expect(entity.hasComponent(TestHealthComponent)).toBe(false);
});
test('应该能够移除组件', () => {
const position = new TestPositionComponent(10, 20);
entity.addComponent(position);
entity.removeComponent(position);
expect(entity.components.length).toBe(0);
expect(entity.hasComponent(TestPositionComponent)).toBe(false);
expect(entity.getComponent(TestPositionComponent)).toBeNull();
});
});
describe('多组件管理测试', () => {
test('应该能够管理多个不同类型的组件', () => {
const position = new TestPositionComponent(10, 20);
const health = new TestHealthComponent(150);
const velocity = new TestVelocityComponent(5, -3);
entity.addComponent(position);
entity.addComponent(health);
entity.addComponent(velocity);
expect(entity.components.length).toBe(3);
expect(entity.hasComponent(TestPositionComponent)).toBe(true);
expect(entity.hasComponent(TestHealthComponent)).toBe(true);
expect(entity.hasComponent(TestVelocityComponent)).toBe(true);
});
test('应该能够正确获取多个组件', () => {
const position = new TestPositionComponent(10, 20);
const health = new TestHealthComponent(150);
const velocity = new TestVelocityComponent(5, -3);
entity.addComponent(position);
entity.addComponent(health);
entity.addComponent(velocity);
const retrievedPosition = entity.getComponent(TestPositionComponent);
const retrievedHealth = entity.getComponent(TestHealthComponent);
const retrievedVelocity = entity.getComponent(TestVelocityComponent);
expect(retrievedPosition).toBe(position);
expect(retrievedHealth).toBe(health);
expect(retrievedVelocity).toBe(velocity);
});
test('应该能够批量添加组件', () => {
const components = [
new TestPositionComponent(10, 20),
new TestHealthComponent(150),
new TestVelocityComponent(5, -3)
];
const addedComponents = entity.addComponents(components);
expect(addedComponents.length).toBe(3);
expect(entity.components.length).toBe(3);
expect(addedComponents[0]).toBe(components[0]);
expect(addedComponents[1]).toBe(components[1]);
expect(addedComponents[2]).toBe(components[2]);
});
test('应该能够移除所有组件', () => {
entity.addComponent(new TestPositionComponent(10, 20));
entity.addComponent(new TestHealthComponent(150));
entity.addComponent(new TestVelocityComponent(5, -3));
entity.removeAllComponents();
expect(entity.components.length).toBe(0);
expect(entity.hasComponent(TestPositionComponent)).toBe(false);
expect(entity.hasComponent(TestHealthComponent)).toBe(false);
expect(entity.hasComponent(TestVelocityComponent)).toBe(false);
});
});
describe('性能优化验证', () => {
test('位掩码应该正确工作', () => {
const position = new TestPositionComponent(10, 20);
const health = new TestHealthComponent(150);
entity.addComponent(position);
entity.addComponent(health);
// 位掩码应该反映组件的存在
expect(entity.hasComponent(TestPositionComponent)).toBe(true);
expect(entity.hasComponent(TestHealthComponent)).toBe(true);
expect(entity.hasComponent(TestVelocityComponent)).toBe(false);
});
test('索引映射应该正确维护', () => {
const position = new TestPositionComponent(10, 20);
const health = new TestHealthComponent(150);
const velocity = new TestVelocityComponent(5, -3);
entity.addComponent(position);
entity.addComponent(health);
entity.addComponent(velocity);
// 获取组件应该通过索引映射快速完成
const retrievedPosition = entity.getComponent(TestPositionComponent);
const retrievedHealth = entity.getComponent(TestHealthComponent);
const retrievedVelocity = entity.getComponent(TestVelocityComponent);
expect(retrievedPosition).toBe(position);
expect(retrievedHealth).toBe(health);
expect(retrievedVelocity).toBe(velocity);
});
test('组件获取性能应该良好', () => {
const position = new TestPositionComponent(10, 20);
const health = new TestHealthComponent(150);
const velocity = new TestVelocityComponent(5, -3);
const render = new TestRenderComponent(true);
entity.addComponent(position);
entity.addComponent(health);
entity.addComponent(velocity);
entity.addComponent(render);
// 测试大量获取操作的性能
const iterations = 1000;
const startTime = performance.now();
for (let i = 0; i < iterations; i++) {
entity.getComponent(TestPositionComponent);
entity.getComponent(TestHealthComponent);
entity.getComponent(TestVelocityComponent);
entity.getComponent(TestRenderComponent);
}
const endTime = performance.now();
const duration = endTime - startTime;
// 1000次 * 4个组件 = 4000次获取操作应该在合理时间内完成
// 性能记录:实体操作性能数据,不设硬阈值避免CI不稳定
});
});
describe('边界情况测试', () => {
test('获取不存在的组件应该返回null', () => {
const result = entity.getComponent(TestPositionComponent);
expect(result).toBeNull();
});
test('不应该允许添加重复类型的组件', () => {
const position1 = new TestPositionComponent(10, 20);
const position2 = new TestPositionComponent(30, 40);
entity.addComponent(position1);
expect(() => {
entity.addComponent(position2);
}).toThrow();
});
test('移除不存在的组件应该安全处理', () => {
const position = new TestPositionComponent(10, 20);
expect(() => {
entity.removeComponent(position);
}).not.toThrow();
});
test('调试信息应该正确反映实体状态', () => {
const position = new TestPositionComponent(10, 20);
const health = new TestHealthComponent(150);
entity.addComponent(position);
entity.addComponent(health);
const debugInfo = entity.getDebugInfo();
expect(debugInfo.name).toBe('TestEntity');
expect(debugInfo.id).toBeGreaterThanOrEqual(0);
expect(debugInfo.componentCount).toBe(2);
expect(debugInfo.componentTypes).toContain('EntityTest_PositionComponent');
expect(debugInfo.componentTypes).toContain('EntityTest_HealthComponent');
expect(debugInfo.cacheBuilt).toBe(true);
});
});
});
@@ -0,0 +1,567 @@
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, InjectProperty } from '../../src/Core/DI';
import { Core } from '../../src/Core';
import type { IService } from '../../src/Core/ServiceContainer';
import { ECSSystem, ECSComponent } from '../../src/ECS/Decorators';
@ECSComponent('DI_Transform')
class Transform extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
@ECSComponent('DI_Velocity')
class Velocity extends Component {
constructor(public vx: number = 0, public vy: number = 0) {
super();
}
}
@ECSComponent('DI_Health')
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 {
@InjectProperty(CollisionSystem)
public collision!: CollisionSystem;
constructor() {
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 {
@InjectProperty(SystemA)
public systemA!: SystemA;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('C')
class SystemC extends EntitySystem implements IService {
@InjectProperty(SystemA)
public systemA!: SystemA;
@InjectProperty(SystemB)
public systemB!: SystemB;
constructor() {
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 {
@InjectProperty(CollisionSystem)
public collision!: CollisionSystem;
constructor() {
super(Matcher.empty().all(Transform, Velocity));
}
override dispose() {}
}
@Injectable()
@ECSSystem('Render', { updateOrder: 20 })
class RenderSystem extends EntitySystem implements IService {
@InjectProperty(PhysicsSystem)
public physics!: PhysicsSystem;
constructor() {
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 {
@InjectProperty(TimeService)
private time!: TimeService;
@InjectProperty(CollisionService)
private collision!: CollisionService;
constructor() {
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);
});
});
describe('属性注入 @InjectProperty', () => {
test('应该支持单个属性注入', () => {
@Injectable()
@ECSSystem('Config')
class GameConfig extends EntitySystem implements IService {
public bulletDamage = 10;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Combat')
class CombatSystem extends EntitySystem implements IService {
@InjectProperty(GameConfig)
gameConfig!: GameConfig;
constructor() {
super(Matcher.empty().all(Health));
}
protected override onInitialize(): void {
expect(this.gameConfig).toBeInstanceOf(GameConfig);
expect(this.gameConfig.bulletDamage).toBe(10);
}
override dispose() {}
}
scene.addEntityProcessor(GameConfig);
scene.addEntityProcessor(CombatSystem);
});
test('应该支持多个属性注入', () => {
@Injectable()
@ECSSystem('Time')
class TimeService extends EntitySystem implements IService {
public deltaTime = 0.016;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Collision')
class CollisionSystem extends EntitySystem implements IService {
public checkCount = 0;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem implements IService {
@InjectProperty(TimeService)
time!: TimeService;
@InjectProperty(CollisionSystem)
collision!: CollisionSystem;
constructor() {
super(Matcher.empty());
}
protected override onInitialize(): void {
expect(this.time).toBeInstanceOf(TimeService);
expect(this.collision).toBeInstanceOf(CollisionSystem);
expect(this.time.deltaTime).toBe(0.016);
}
override dispose() {}
}
scene.registerSystems([TimeService, CollisionSystem, PhysicsSystem]);
});
test('属性注入应该在onInitialize之前完成', () => {
@Injectable()
@ECSSystem('Service')
class TestService extends EntitySystem implements IService {
public value = 42;
constructor() {
super(Matcher.empty());
}
override dispose() {}
}
@Injectable()
@ECSSystem('Consumer')
class ConsumerSystem extends EntitySystem implements IService {
@InjectProperty(TestService)
service!: TestService;
private initializeValue = 0;
constructor() {
super(Matcher.empty());
}
protected override onInitialize(): void {
this.initializeValue = this.service.value;
}
public getInitializeValue(): number {
return this.initializeValue;
}
override dispose() {}
}
scene.addEntityProcessor(TestService);
const consumer = scene.addEntityProcessor(ConsumerSystem);
expect(consumer.getInitializeValue()).toBe(42);
});
});
});
@@ -0,0 +1,237 @@
import {
EntityTags,
hasEntityTag,
addEntityTag,
removeEntityTag,
isFolder,
isHidden,
isLocked
} from '../../src/ECS/EntityTags';
describe('EntityTags', () => {
describe('tag constants', () => {
test('should have correct NONE value', () => {
expect(EntityTags.NONE).toBe(0x0000);
});
test('should have correct FOLDER value', () => {
expect(EntityTags.FOLDER).toBe(0x1000);
});
test('should have correct HIDDEN value', () => {
expect(EntityTags.HIDDEN).toBe(0x2000);
});
test('should have correct LOCKED value', () => {
expect(EntityTags.LOCKED).toBe(0x4000);
});
test('should have correct EDITOR_ONLY value', () => {
expect(EntityTags.EDITOR_ONLY).toBe(0x8000);
});
test('should have correct PREFAB_INSTANCE value', () => {
expect(EntityTags.PREFAB_INSTANCE).toBe(0x0100);
});
test('should have correct PREFAB_ROOT value', () => {
expect(EntityTags.PREFAB_ROOT).toBe(0x0200);
});
test('all tags should have unique values', () => {
const values = Object.values(EntityTags).filter((v) => typeof v === 'number');
const uniqueValues = new Set(values);
expect(uniqueValues.size).toBe(values.length);
});
});
describe('hasEntityTag', () => {
test('should return true when tag is present', () => {
const tag = EntityTags.FOLDER;
expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(true);
});
test('should return false when tag is not present', () => {
const tag = EntityTags.FOLDER;
expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(false);
});
test('should work with combined tags', () => {
const combined = EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.LOCKED;
expect(hasEntityTag(combined, EntityTags.FOLDER)).toBe(true);
expect(hasEntityTag(combined, EntityTags.HIDDEN)).toBe(true);
expect(hasEntityTag(combined, EntityTags.LOCKED)).toBe(true);
expect(hasEntityTag(combined, EntityTags.EDITOR_ONLY)).toBe(false);
});
test('should return false for NONE tag', () => {
const tag = EntityTags.NONE;
expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(false);
});
});
describe('addEntityTag', () => {
test('should add tag to empty tags', () => {
const result = addEntityTag(EntityTags.NONE, EntityTags.FOLDER);
expect(result).toBe(EntityTags.FOLDER);
});
test('should add tag to existing tags', () => {
const existing = EntityTags.FOLDER as number;
const result = addEntityTag(existing, EntityTags.HIDDEN);
expect(hasEntityTag(result, EntityTags.FOLDER)).toBe(true);
expect(hasEntityTag(result, EntityTags.HIDDEN)).toBe(true);
});
test('should not change value when adding same tag', () => {
const existing = EntityTags.FOLDER as number;
const result = addEntityTag(existing, EntityTags.FOLDER);
expect(result).toBe(EntityTags.FOLDER);
});
test('should handle multiple tag additions', () => {
let tag: number = EntityTags.NONE;
tag = addEntityTag(tag, EntityTags.FOLDER);
tag = addEntityTag(tag, EntityTags.HIDDEN);
tag = addEntityTag(tag, EntityTags.LOCKED);
expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(true);
expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(true);
expect(hasEntityTag(tag, EntityTags.LOCKED)).toBe(true);
});
});
describe('removeEntityTag', () => {
test('should remove tag from combined tags', () => {
const combined = (EntityTags.FOLDER | EntityTags.HIDDEN) as number;
const result = removeEntityTag(combined, EntityTags.HIDDEN);
expect(hasEntityTag(result, EntityTags.FOLDER)).toBe(true);
expect(hasEntityTag(result, EntityTags.HIDDEN)).toBe(false);
});
test('should return same value when removing non-existent tag', () => {
const existing = EntityTags.FOLDER as number;
const result = removeEntityTag(existing, EntityTags.HIDDEN);
expect(result).toBe(EntityTags.FOLDER);
});
test('should return NONE when removing last tag', () => {
const result = removeEntityTag(EntityTags.FOLDER, EntityTags.FOLDER);
expect(result).toBe(EntityTags.NONE);
});
test('should handle multiple tag removals', () => {
let tag: number = EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.LOCKED;
tag = removeEntityTag(tag, EntityTags.FOLDER);
tag = removeEntityTag(tag, EntityTags.LOCKED);
expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(false);
expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(true);
expect(hasEntityTag(tag, EntityTags.LOCKED)).toBe(false);
});
});
describe('isFolder', () => {
test('should return true for folder tag', () => {
expect(isFolder(EntityTags.FOLDER)).toBe(true);
});
test('should return true for combined tags including folder', () => {
const combined = EntityTags.FOLDER | EntityTags.HIDDEN;
expect(isFolder(combined)).toBe(true);
});
test('should return false for non-folder tag', () => {
expect(isFolder(EntityTags.HIDDEN)).toBe(false);
expect(isFolder(EntityTags.NONE)).toBe(false);
});
});
describe('isHidden', () => {
test('should return true for hidden tag', () => {
expect(isHidden(EntityTags.HIDDEN)).toBe(true);
});
test('should return true for combined tags including hidden', () => {
const combined = EntityTags.FOLDER | EntityTags.HIDDEN;
expect(isHidden(combined)).toBe(true);
});
test('should return false for non-hidden tag', () => {
expect(isHidden(EntityTags.FOLDER)).toBe(false);
expect(isHidden(EntityTags.NONE)).toBe(false);
});
});
describe('isLocked', () => {
test('should return true for locked tag', () => {
expect(isLocked(EntityTags.LOCKED)).toBe(true);
});
test('should return true for combined tags including locked', () => {
const combined = EntityTags.FOLDER | EntityTags.LOCKED;
expect(isLocked(combined)).toBe(true);
});
test('should return false for non-locked tag', () => {
expect(isLocked(EntityTags.FOLDER)).toBe(false);
expect(isLocked(EntityTags.NONE)).toBe(false);
});
});
describe('tag combinations', () => {
test('should correctly combine and identify multiple tags', () => {
const tag = (EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.PREFAB_ROOT) as number;
expect(isFolder(tag)).toBe(true);
expect(isHidden(tag)).toBe(true);
expect(hasEntityTag(tag, EntityTags.PREFAB_ROOT)).toBe(true);
expect(isLocked(tag)).toBe(false);
});
test('should support complex add/remove operations', () => {
let tag: number = EntityTags.NONE;
// Add tags
tag = addEntityTag(tag, EntityTags.FOLDER);
tag = addEntityTag(tag, EntityTags.HIDDEN);
tag = addEntityTag(tag, EntityTags.LOCKED);
tag = addEntityTag(tag, EntityTags.EDITOR_ONLY);
expect(isFolder(tag)).toBe(true);
expect(isHidden(tag)).toBe(true);
expect(isLocked(tag)).toBe(true);
expect(hasEntityTag(tag, EntityTags.EDITOR_ONLY)).toBe(true);
// Remove some tags
tag = removeEntityTag(tag, EntityTags.HIDDEN);
tag = removeEntityTag(tag, EntityTags.LOCKED);
expect(isFolder(tag)).toBe(true);
expect(isHidden(tag)).toBe(false);
expect(isLocked(tag)).toBe(false);
expect(hasEntityTag(tag, EntityTags.EDITOR_ONLY)).toBe(true);
});
test('should work correctly with prefab tags', () => {
const prefabInstanceTag = EntityTags.PREFAB_INSTANCE as number;
const prefabRootTag = EntityTags.PREFAB_ROOT as number;
expect(hasEntityTag(prefabInstanceTag, EntityTags.PREFAB_INSTANCE)).toBe(true);
expect(hasEntityTag(prefabInstanceTag, EntityTags.PREFAB_ROOT)).toBe(false);
expect(hasEntityTag(prefabRootTag, EntityTags.PREFAB_ROOT)).toBe(true);
expect(hasEntityTag(prefabRootTag, EntityTags.PREFAB_INSTANCE)).toBe(false);
// Combine both
const combined = (prefabInstanceTag | prefabRootTag) as number;
expect(hasEntityTag(combined, EntityTags.PREFAB_INSTANCE)).toBe(true);
expect(hasEntityTag(combined, EntityTags.PREFAB_ROOT)).toBe(true);
});
});
});
@@ -0,0 +1,648 @@
import { Scene, Entity, HierarchyComponent, HierarchySystem } from '../../src';
describe('HierarchySystem', () => {
let scene: Scene;
let hierarchySystem: HierarchySystem;
beforeEach(() => {
scene = new Scene();
scene.initialize();
hierarchySystem = new HierarchySystem();
scene.addSystem(hierarchySystem);
});
afterEach(() => {
scene.end();
});
describe('setParent', () => {
it('should set parent-child relationship', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.getParent(child)).toBe(parent);
expect(hierarchySystem.getChildren(parent)).toContain(child);
expect(hierarchySystem.getChildCount(parent)).toBe(1);
});
it('should auto-add HierarchyComponent if not present', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
expect(parent.getComponent(HierarchyComponent)).toBeNull();
expect(child.getComponent(HierarchyComponent)).toBeNull();
hierarchySystem.setParent(child, parent);
expect(parent.getComponent(HierarchyComponent)).not.toBeNull();
expect(child.getComponent(HierarchyComponent)).not.toBeNull();
});
it('should move child to root when parent is null', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.getParent(child)).toBe(parent);
hierarchySystem.setParent(child, null);
expect(hierarchySystem.getParent(child)).toBeNull();
expect(hierarchySystem.getChildren(parent)).not.toContain(child);
});
it('should transfer child to new parent', () => {
const parent1 = scene.createEntity('Parent1');
const parent2 = scene.createEntity('Parent2');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent1);
expect(hierarchySystem.getParent(child)).toBe(parent1);
expect(hierarchySystem.getChildCount(parent1)).toBe(1);
hierarchySystem.setParent(child, parent2);
expect(hierarchySystem.getParent(child)).toBe(parent2);
expect(hierarchySystem.getChildCount(parent1)).toBe(0);
expect(hierarchySystem.getChildCount(parent2)).toBe(1);
});
it('should throw error on circular reference', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, parent);
hierarchySystem.setParent(grandchild, child);
expect(() => {
hierarchySystem.setParent(parent, grandchild);
}).toThrow('Cannot set parent: would create circular reference');
});
it('should not change if setting same parent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
const hierarchy = child.getComponent(HierarchyComponent)!;
hierarchy.bCacheDirty = false;
hierarchySystem.setParent(child, parent);
// Should not mark dirty since parent didn't change
expect(hierarchy.bCacheDirty).toBe(false);
});
});
describe('insertChildAt', () => {
it('should insert child at specific position', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child3, parent);
hierarchySystem.insertChildAt(parent, child2, 1);
const children = hierarchySystem.getChildren(parent);
expect(children[0]).toBe(child1);
expect(children[1]).toBe(child2);
expect(children[2]).toBe(child3);
});
it('should append child when index is -1', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
hierarchySystem.setParent(child1, parent);
hierarchySystem.insertChildAt(parent, child2, -1);
const children = hierarchySystem.getChildren(parent);
expect(children[children.length - 1]).toBe(child2);
});
});
describe('removeChild', () => {
it('should remove child from parent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.getChildCount(parent)).toBe(1);
const result = hierarchySystem.removeChild(parent, child);
expect(result).toBe(true);
expect(hierarchySystem.getChildCount(parent)).toBe(0);
expect(hierarchySystem.getParent(child)).toBeNull();
});
it('should return false if child is not a child of parent', () => {
const parent1 = scene.createEntity('Parent1');
const parent2 = scene.createEntity('Parent2');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent1);
const result = hierarchySystem.removeChild(parent2, child);
expect(result).toBe(false);
});
});
describe('removeAllChildren', () => {
it('should remove all children from parent', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
hierarchySystem.setParent(child3, parent);
expect(hierarchySystem.getChildCount(parent)).toBe(3);
hierarchySystem.removeAllChildren(parent);
expect(hierarchySystem.getChildCount(parent)).toBe(0);
expect(hierarchySystem.getParent(child1)).toBeNull();
expect(hierarchySystem.getParent(child2)).toBeNull();
expect(hierarchySystem.getParent(child3)).toBeNull();
});
});
describe('hierarchy queries', () => {
it('should check if entity has children', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
expect(hierarchySystem.hasChildren(parent)).toBe(false);
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.hasChildren(parent)).toBe(true);
});
it('should check isAncestorOf', () => {
const grandparent = scene.createEntity('Grandparent');
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(parent, grandparent);
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.isAncestorOf(grandparent, child)).toBe(true);
expect(hierarchySystem.isAncestorOf(parent, child)).toBe(true);
expect(hierarchySystem.isAncestorOf(child, grandparent)).toBe(false);
});
it('should check isDescendantOf', () => {
const grandparent = scene.createEntity('Grandparent');
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(parent, grandparent);
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.isDescendantOf(child, grandparent)).toBe(true);
expect(hierarchySystem.isDescendantOf(child, parent)).toBe(true);
expect(hierarchySystem.isDescendantOf(grandparent, child)).toBe(false);
});
it('should get root entity', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
expect(hierarchySystem.getRoot(grandchild)).toBe(root);
expect(hierarchySystem.getRoot(child)).toBe(root);
expect(hierarchySystem.getRoot(root)).toBe(root);
});
it('should get depth correctly', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
root.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
expect(hierarchySystem.getDepth(root)).toBe(0);
expect(hierarchySystem.getDepth(child)).toBe(1);
expect(hierarchySystem.getDepth(grandchild)).toBe(2);
});
});
describe('findChild', () => {
it('should find child by name', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Target');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
const found = hierarchySystem.findChild(parent, 'Target');
expect(found).toBe(child2);
});
it('should find child recursively', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Target');
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
const found = hierarchySystem.findChild(root, 'Target', true);
expect(found).toBe(grandchild);
const notFound = hierarchySystem.findChild(root, 'Target', false);
expect(notFound).toBeNull();
});
});
describe('forEachChild', () => {
it('should iterate over children', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
const visited: Entity[] = [];
hierarchySystem.forEachChild(parent, (child) => {
visited.push(child);
});
expect(visited).toContain(child1);
expect(visited).toContain(child2);
expect(visited.length).toBe(2);
});
it('should iterate recursively', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
const visited: Entity[] = [];
hierarchySystem.forEachChild(root, (entity) => {
visited.push(entity);
}, true);
expect(visited).toContain(child);
expect(visited).toContain(grandchild);
expect(visited.length).toBe(2);
});
});
describe('getRootEntities', () => {
it('should return all root entities', () => {
const root1 = scene.createEntity('Root1');
const root2 = scene.createEntity('Root2');
const child = scene.createEntity('Child');
root1.addComponent(new HierarchyComponent());
root2.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, root1);
const roots = hierarchySystem.getRootEntities();
expect(roots).toContain(root1);
expect(roots).toContain(root2);
expect(roots).not.toContain(child);
});
});
describe('activeInHierarchy', () => {
it('should be inactive if parent is inactive', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
expect(hierarchySystem.isActiveInHierarchy(child)).toBe(true);
parent.active = false;
// Mark cache dirty to recalculate
const childHierarchy = child.getComponent(HierarchyComponent)!;
childHierarchy.bCacheDirty = true;
expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false);
});
it('should be inactive if self is inactive', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
child.active = false;
expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false);
});
});
});
describe('HierarchyComponent', () => {
it('should have correct default values', () => {
const component = new HierarchyComponent();
expect(component.parentId).toBeNull();
expect(component.childIds).toEqual([]);
expect(component.depth).toBe(0);
expect(component.bActiveInHierarchy).toBe(true);
expect(component.bCacheDirty).toBe(true);
});
});
describe('HierarchySystem - Extended Tests', () => {
let scene: Scene;
let hierarchySystem: HierarchySystem;
beforeEach(() => {
scene = new Scene();
scene.initialize();
hierarchySystem = new HierarchySystem();
scene.addSystem(hierarchySystem);
});
afterEach(() => {
scene.end();
});
describe('findChildrenByTag', () => {
it('should find children by tag', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
child1.tag = 0x01;
child2.tag = 0x02;
child3.tag = 0x01;
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
hierarchySystem.setParent(child3, parent);
const found = hierarchySystem.findChildrenByTag(parent, 0x01);
expect(found.length).toBe(2);
expect(found).toContain(child1);
expect(found).toContain(child3);
});
it('should find children by tag recursively', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
child.tag = 0x01;
grandchild.tag = 0x01;
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
const foundNonRecursive = hierarchySystem.findChildrenByTag(root, 0x01, false);
expect(foundNonRecursive.length).toBe(1);
expect(foundNonRecursive[0]).toBe(child);
const foundRecursive = hierarchySystem.findChildrenByTag(root, 0x01, true);
expect(foundRecursive.length).toBe(2);
expect(foundRecursive).toContain(child);
expect(foundRecursive).toContain(grandchild);
});
it('should return empty array when no children match tag', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
child.tag = 0x01;
hierarchySystem.setParent(child, parent);
const found = hierarchySystem.findChildrenByTag(parent, 0x02);
expect(found).toEqual([]);
});
});
describe('flattenHierarchy', () => {
it('should flatten hierarchy with expanded nodes', () => {
const root = scene.createEntity('Root');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const grandchild = scene.createEntity('Grandchild');
root.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child1, root);
hierarchySystem.setParent(child2, root);
hierarchySystem.setParent(grandchild, child1);
const expandedIds = new Set([root.id, child1.id]);
const flattened = hierarchySystem.flattenHierarchy(expandedIds);
expect(flattened.length).toBe(4);
expect(flattened[0].entity).toBe(root);
expect(flattened[0].depth).toBe(0);
expect(flattened[0].bHasChildren).toBe(true);
expect(flattened[0].bIsExpanded).toBe(true);
});
it('should not include children of collapsed nodes', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
root.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
// Root is expanded, but child is collapsed
const expandedIds = new Set([root.id]);
const flattened = hierarchySystem.flattenHierarchy(expandedIds);
expect(flattened.length).toBe(2);
expect(flattened[0].entity).toBe(root);
expect(flattened[1].entity).toBe(child);
expect(flattened[1].bHasChildren).toBe(true);
expect(flattened[1].bIsExpanded).toBe(false);
});
it('should return empty array when no root entities', () => {
const flattened = hierarchySystem.flattenHierarchy(new Set());
expect(flattened).toEqual([]);
});
});
describe('updateOrder', () => {
it('should have negative update order for early processing', () => {
expect(hierarchySystem.updateOrder).toBe(-1000);
});
});
describe('process - cache update', () => {
it('should update dirty caches during process', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
// Cache should be dirty after setParent
const childHierarchy = child.getComponent(HierarchyComponent)!;
expect(childHierarchy.bCacheDirty).toBe(true);
// Update scene to process
scene.update();
// Cache should be clean after process
expect(childHierarchy.bCacheDirty).toBe(false);
});
});
describe('insertChildAt edge cases', () => {
it('should handle circular reference prevention', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, parent);
hierarchySystem.setParent(grandchild, child);
expect(() => {
hierarchySystem.insertChildAt(grandchild, parent, 0);
}).toThrow('Cannot set parent: would create circular reference');
});
it('should move child within same parent to different position', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
const child3 = scene.createEntity('Child3');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
hierarchySystem.setParent(child3, parent);
// Move child3 to position 0
hierarchySystem.insertChildAt(parent, child3, 0);
const children = hierarchySystem.getChildren(parent);
expect(children[0]).toBe(child3);
expect(children[1]).toBe(child1);
expect(children[2]).toBe(child2);
});
});
describe('removeChild edge cases', () => {
it('should return false when parent has no HierarchyComponent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
const result = hierarchySystem.removeChild(parent, child);
expect(result).toBe(false);
});
it('should return false when child has no HierarchyComponent', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
parent.addComponent(new HierarchyComponent());
const result = hierarchySystem.removeChild(parent, child);
expect(result).toBe(false);
});
});
describe('removeAllChildren edge cases', () => {
it('should handle entity with no HierarchyComponent', () => {
const parent = scene.createEntity('Parent');
expect(() => {
hierarchySystem.removeAllChildren(parent);
}).not.toThrow();
});
});
describe('getChildren edge cases', () => {
it('should return empty array when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
const children = hierarchySystem.getChildren(entity);
expect(children).toEqual([]);
});
});
describe('getChildCount edge cases', () => {
it('should return 0 when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
expect(hierarchySystem.getChildCount(entity)).toBe(0);
});
});
describe('getDepth edge cases', () => {
it('should return 0 when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
expect(hierarchySystem.getDepth(entity)).toBe(0);
});
it('should use cached depth when cache is valid', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
parent.addComponent(new HierarchyComponent());
hierarchySystem.setParent(child, parent);
// First call computes depth
const depth1 = hierarchySystem.getDepth(child);
expect(depth1).toBe(1);
// Mark cache as valid
const childHierarchy = child.getComponent(HierarchyComponent)!;
childHierarchy.bCacheDirty = false;
// Second call should use cache
const depth2 = hierarchySystem.getDepth(child);
expect(depth2).toBe(1);
});
});
describe('isActiveInHierarchy edge cases', () => {
it('should return entity.active when entity has no HierarchyComponent', () => {
const entity = scene.createEntity('Entity');
entity.active = true;
expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(true);
entity.active = false;
expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(false);
});
it('should use cached value when cache is valid', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
// First call computes activeInHierarchy
const active1 = hierarchySystem.isActiveInHierarchy(child);
expect(active1).toBe(true);
// Mark cache as valid
const childHierarchy = child.getComponent(HierarchyComponent)!;
childHierarchy.bCacheDirty = false;
// Second call should use cache
const active2 = hierarchySystem.isActiveInHierarchy(child);
expect(active2).toBe(true);
});
});
describe('dispose', () => {
it('should not throw when disposing', () => {
expect(() => {
hierarchySystem.dispose();
}).not.toThrow();
});
});
});
@@ -0,0 +1,428 @@
import { Entity } from '../../src/ECS/Entity';
import { Component } from '../../src/ECS/Component';
import { Scene } from '../../src/ECS/Scene';
import { SceneManager } from '../../src/ECS/SceneManager';
import { EEntityLifecyclePolicy } from '../../src/ECS/Core/EntityLifecyclePolicy';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件
@ECSComponent('Persistent_PositionComponent')
class PositionComponent extends Component {
public x: number;
public y: number;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('Persistent_PlayerComponent')
class PlayerComponent extends Component {
public name: string;
public score: number;
constructor(name: string = 'Player', score: number = 0) {
super();
this.name = name;
this.score = score;
}
}
@ECSComponent('Persistent_EnemyComponent')
class EnemyComponent extends Component {
public type: string;
constructor(type: string = 'normal') {
super();
this.type = type;
}
}
// 测试场景
class TestScene extends Scene {
public initializeCalled = false;
override initialize(): void {
this.initializeCalled = true;
}
}
describe('PersistentEntity - 持久化实体测试', () => {
describe('Entity.setPersistent', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
test('默认实体应为 SceneLocal 策略', () => {
const entity = scene.createEntity('NormalEntity');
expect(entity.lifecyclePolicy).toBe(EEntityLifecyclePolicy.SceneLocal);
expect(entity.isPersistent).toBe(false);
});
test('setPersistent() 应标记实体为持久化', () => {
const entity = scene.createEntity('Player');
entity.setPersistent();
expect(entity.lifecyclePolicy).toBe(EEntityLifecyclePolicy.Persistent);
expect(entity.isPersistent).toBe(true);
});
test('setPersistent() 应支持链式调用', () => {
const entity = scene.createEntity('Player').setPersistent();
entity.addComponent(new PositionComponent(100, 200));
expect(entity.isPersistent).toBe(true);
expect(entity.hasComponent(PositionComponent)).toBe(true);
});
test('setSceneLocal() 应恢复为默认策略', () => {
const entity = scene.createEntity('Player');
entity.setPersistent();
expect(entity.isPersistent).toBe(true);
entity.setSceneLocal();
expect(entity.isPersistent).toBe(false);
expect(entity.lifecyclePolicy).toBe(EEntityLifecyclePolicy.SceneLocal);
});
});
describe('Scene.findPersistentEntities', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
test('应返回所有持久化实体', () => {
// 创建混合实体
const player = scene.createEntity('Player').setPersistent();
const enemy1 = scene.createEntity('Enemy1');
const gameManager = scene.createEntity('GameManager').setPersistent();
const enemy2 = scene.createEntity('Enemy2');
const persistentEntities = scene.findPersistentEntities();
expect(persistentEntities.length).toBe(2);
expect(persistentEntities).toContain(player);
expect(persistentEntities).toContain(gameManager);
expect(persistentEntities).not.toContain(enemy1);
expect(persistentEntities).not.toContain(enemy2);
});
test('没有持久化实体时应返回空数组', () => {
scene.createEntity('Enemy1');
scene.createEntity('Enemy2');
const persistentEntities = scene.findPersistentEntities();
expect(persistentEntities).toEqual([]);
});
});
describe('Scene.extractPersistentEntities', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
test('应提取并从场景中移除持久化实体', () => {
const player = scene.createEntity('Player').setPersistent();
player.addComponent(new PositionComponent(100, 200));
const enemy = scene.createEntity('Enemy');
expect(scene.entities.count).toBe(2);
const extracted = scene.extractPersistentEntities();
expect(extracted.length).toBe(1);
expect(extracted[0]).toBe(player);
expect(scene.entities.count).toBe(1);
expect(scene.findEntity('Player')).toBeNull();
expect(scene.findEntity('Enemy')).toBe(enemy);
});
test('提取后实体的 scene 引用应为 null', () => {
const player = scene.createEntity('Player').setPersistent();
const extracted = scene.extractPersistentEntities();
expect(extracted[0].scene).toBeNull();
});
test('提取后实体的组件数据应保留', () => {
const player = scene.createEntity('Player').setPersistent();
player.addComponent(new PositionComponent(100, 200));
player.addComponent(new PlayerComponent('Hero', 999));
const extracted = scene.extractPersistentEntities();
// 组件数据应保留(虽然 scene 为 null,组件缓存仍有效)
expect(extracted[0].components.length).toBe(2);
});
});
describe('Scene.receiveMigratedEntities', () => {
test('应将迁移的实体添加到新场景', () => {
const sourceScene = new Scene();
const targetScene = new Scene();
// 在源场景创建持久化实体
const player = sourceScene.createEntity('Player').setPersistent();
player.addComponent(new PositionComponent(100, 200));
player.addComponent(new PlayerComponent('Hero', 500));
// 提取并迁移
const extracted = sourceScene.extractPersistentEntities();
targetScene.receiveMigratedEntities(extracted);
// 验证实体已迁移
expect(targetScene.entities.count).toBe(1);
expect(targetScene.findEntity('Player')).toBe(player);
expect(player.scene).toBe(targetScene);
});
test('迁移后组件数据应完整保留', () => {
const sourceScene = new Scene();
const targetScene = new Scene();
const player = sourceScene.createEntity('Player').setPersistent();
player.addComponent(new PositionComponent(100, 200));
player.addComponent(new PlayerComponent('Hero', 999));
const extracted = sourceScene.extractPersistentEntities();
targetScene.receiveMigratedEntities(extracted);
// 验证组件数据
const migratedPlayer = targetScene.findEntity('Player')!;
const position = migratedPlayer.getComponent(PositionComponent);
const playerComp = migratedPlayer.getComponent(PlayerComponent);
expect(position).not.toBeNull();
expect(position!.x).toBe(100);
expect(position!.y).toBe(200);
expect(playerComp).not.toBeNull();
expect(playerComp!.name).toBe('Hero');
expect(playerComp!.score).toBe(999);
});
test('迁移后实体应能被查询系统找到', () => {
const sourceScene = new Scene();
const targetScene = new Scene();
const player = sourceScene.createEntity('Player').setPersistent();
player.addComponent(new PositionComponent(100, 200));
const extracted = sourceScene.extractPersistentEntities();
targetScene.receiveMigratedEntities(extracted);
// 通过查询系统查找
const result = targetScene.queryAll(PositionComponent);
expect(result.entities.length).toBe(1);
expect(result.entities[0]).toBe(player);
});
});
describe('SceneManager 场景切换迁移', () => {
let sceneManager: SceneManager;
beforeEach(() => {
sceneManager = new SceneManager();
});
afterEach(() => {
sceneManager.destroy();
});
test('场景切换时应自动迁移持久化实体', () => {
// 设置初始场景
const scene1 = new TestScene();
sceneManager.setScene(scene1);
// 创建持久化实体和普通实体
const player = scene1.createEntity('Player').setPersistent();
player.addComponent(new PositionComponent(100, 200));
player.addComponent(new PlayerComponent('Hero', 500));
const enemy = scene1.createEntity('Enemy');
enemy.addComponent(new EnemyComponent('boss'));
expect(scene1.entities.count).toBe(2);
// 切换到新场景
const scene2 = new TestScene();
sceneManager.setScene(scene2);
// 验证:player 应迁移到新场景,enemy 应被销毁
expect(scene2.entities.count).toBe(1);
expect(scene2.findEntity('Player')).toBe(player);
expect(scene2.findEntity('Enemy')).toBeNull();
expect(player.scene).toBe(scene2);
});
test('迁移后组件状态应保持不变', () => {
const scene1 = new TestScene();
sceneManager.setScene(scene1);
const player = scene1.createEntity('Player').setPersistent();
player.addComponent(new PositionComponent(100, 200));
const playerComp = player.addComponent(new PlayerComponent('Hero', 500));
// 修改组件状态
playerComp.score = 999;
// 切换场景
const scene2 = new TestScene();
sceneManager.setScene(scene2);
// 验证组件状态
const migratedPlayer = scene2.findEntity('Player')!;
const position = migratedPlayer.getComponent(PositionComponent);
const migratedPlayerComp = migratedPlayer.getComponent(PlayerComponent);
expect(position!.x).toBe(100);
expect(position!.y).toBe(200);
expect(migratedPlayerComp!.score).toBe(999);
});
test('多个持久化实体应全部迁移', () => {
const scene1 = new TestScene();
sceneManager.setScene(scene1);
const player = scene1.createEntity('Player').setPersistent();
const audioManager = scene1.createEntity('AudioManager').setPersistent();
const gameState = scene1.createEntity('GameState').setPersistent();
const enemy = scene1.createEntity('Enemy'); // 普通实体
expect(scene1.entities.count).toBe(4);
const scene2 = new TestScene();
sceneManager.setScene(scene2);
expect(scene2.entities.count).toBe(3);
expect(scene2.findEntity('Player')).toBe(player);
expect(scene2.findEntity('AudioManager')).toBe(audioManager);
expect(scene2.findEntity('GameState')).toBe(gameState);
expect(scene2.findEntity('Enemy')).toBeNull();
});
test('没有持久化实体时场景切换应正常工作', () => {
const scene1 = new TestScene();
sceneManager.setScene(scene1);
scene1.createEntity('Enemy1');
scene1.createEntity('Enemy2');
const scene2 = new TestScene();
sceneManager.setScene(scene2);
expect(scene2.entities.count).toBe(0);
});
test('延迟场景切换应正确迁移持久化实体', () => {
const scene1 = new TestScene();
sceneManager.setScene(scene1);
const player = scene1.createEntity('Player').setPersistent();
player.addComponent(new PlayerComponent('Hero', 100));
// 延迟加载
const scene2 = new TestScene();
sceneManager.loadScene(scene2);
// 此时还未切换
expect(sceneManager.currentScene).toBe(scene1);
expect(scene1.findEntity('Player')).toBe(player);
// 触发更新,执行延迟切换
sceneManager.update();
// 验证迁移
expect(sceneManager.currentScene).toBe(scene2);
expect(scene2.findEntity('Player')).toBe(player);
expect(player.scene).toBe(scene2);
});
test('连续场景切换应正确迁移持久化实体', () => {
const scene1 = new TestScene();
sceneManager.setScene(scene1);
const player = scene1.createEntity('Player').setPersistent();
// 第一次切换
const scene2 = new TestScene();
sceneManager.setScene(scene2);
expect(scene2.findEntity('Player')).toBe(player);
// 第二次切换
const scene3 = new TestScene();
sceneManager.setScene(scene3);
expect(scene3.findEntity('Player')).toBe(player);
// 第三次切换
const scene4 = new TestScene();
sceneManager.setScene(scene4);
expect(scene4.findEntity('Player')).toBe(player);
expect(player.scene).toBe(scene4);
});
});
describe('边界情况', () => {
test('实体销毁后不应被迁移', () => {
const sceneManager = new SceneManager();
const scene1 = new TestScene();
sceneManager.setScene(scene1);
const player = scene1.createEntity('Player').setPersistent();
player.destroy();
const scene2 = new TestScene();
sceneManager.setScene(scene2);
expect(scene2.entities.count).toBe(0);
sceneManager.destroy();
});
test('动态切换持久化状态应生效', () => {
const sceneManager = new SceneManager();
const scene1 = new TestScene();
sceneManager.setScene(scene1);
const entity = scene1.createEntity('DynamicEntity');
expect(entity.isPersistent).toBe(false);
// 动态设为持久化
entity.setPersistent();
expect(entity.isPersistent).toBe(true);
const scene2 = new TestScene();
sceneManager.setScene(scene2);
expect(scene2.findEntity('DynamicEntity')).toBe(entity);
sceneManager.destroy();
});
test('动态取消持久化状态应生效', () => {
const sceneManager = new SceneManager();
const scene1 = new TestScene();
sceneManager.setScene(scene1);
const entity = scene1.createEntity('DynamicEntity').setPersistent();
// 动态取消持久化
entity.setSceneLocal();
const scene2 = new TestScene();
sceneManager.setScene(scene2);
expect(scene2.findEntity('DynamicEntity')).toBeNull();
sceneManager.destroy();
});
});
});
@@ -0,0 +1,935 @@
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 { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件
@ECSComponent('SceneTest_PositionComponent')
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('SceneTest_VelocityComponent')
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('SceneTest_HealthComponent')
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
@ECSComponent('SceneTest_RenderComponent')
class RenderComponent extends Component {
public visible: boolean;
constructor(...args: unknown[]) {
super();
const [visible = true] = args as [boolean?];
this.visible = visible;
}
}
// 测试系统
class MovementSystem extends EntitySystem {
public processCallCount = 0;
public lastProcessedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
this.lastProcessedEntities = [...entities];
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
const velocity = entity.getComponent(VelocityComponent);
if (position && velocity) {
position.x += velocity.vx;
position.y += velocity.vy;
}
}
}
}
class RenderSystem extends EntitySystem {
public processCallCount = 0;
public lastProcessedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, RenderComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
this.lastProcessedEntities = [...entities];
}
}
// 测试场景
class TestScene extends Scene {
public initializeCalled = false;
public beginCalled = false;
public endCalled = false;
public updateCallCount = 0;
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
public override begin(): void {
this.beginCalled = true;
super.begin();
}
public override end(): void {
this.endCalled = true;
super.end();
}
public override update(): void {
this.updateCallCount++;
super.update();
}
}
describe('Scene - 场景管理系统测试', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
});
describe('基本功能测试', () => {
test('应该能够创建场景', () => {
expect(scene).toBeDefined();
expect(scene).toBeInstanceOf(Scene);
expect(scene.name).toBe("");
expect(scene.entities).toBeDefined();
expect(scene.systems).toBeDefined();
expect(scene.identifierPool).toBeDefined();
});
test('应该能够设置场景名称', () => {
scene.name = "TestScene";
expect(scene.name).toBe("TestScene");
});
test('场景应该有正确的初始状态', () => {
expect(scene.entities.count).toBe(0);
expect(scene.systems.length).toBe(0);
});
test('应该能够使用配置创建场景', () => {
const configScene = new Scene({ name: "ConfigScene" });
expect(configScene.name).toBe("ConfigScene");
});
test('应该能够获取调试信息', () => {
scene.name = "DebugScene";
scene.createEntity("TestEntity");
scene.addEntityProcessor(new MovementSystem());
const debugInfo = scene.getDebugInfo();
expect(debugInfo.name).toBe("DebugScene");
expect(debugInfo.entityCount).toBe(1);
expect(debugInfo.processorCount).toBe(1);
expect(debugInfo.isRunning).toBe(false);
expect(debugInfo.entities).toBeDefined();
expect(debugInfo.processors).toBeDefined();
expect(debugInfo.componentStats).toBeDefined();
});
});
describe('实体管理', () => {
test('应该能够创建实体', () => {
const entity = scene.createEntity("TestEntity");
expect(entity).toBeDefined();
expect(entity.name).toBe("TestEntity");
expect(entity.id).toBeGreaterThan(0);
expect(scene.entities.count).toBe(1);
});
test('应该能够批量创建实体', () => {
const entities = scene.createEntities(5, "Entity");
expect(entities.length).toBe(5);
expect(scene.entities.count).toBe(5);
for (let i = 0; i < entities.length; i++) {
expect(entities[i].name).toBe(`Entity_${i}`);
expect(entities[i].id).toBeGreaterThan(0);
}
});
test('应该能够通过ID查找实体', () => {
const entity = scene.createEntity("TestEntity");
const found = scene.findEntityById(entity.id);
expect(found).toBe(entity);
});
test('查找不存在的实体应该返回null', () => {
const found = scene.findEntityById(999999);
expect(found).toBeNull();
});
test('应该能够销毁实体', () => {
const entity = scene.createEntity("TestEntity");
const entityId = entity.id;
scene.entities.remove(entity);
expect(scene.entities.count).toBe(0);
expect(scene.findEntityById(entityId)).toBeNull();
});
test('应该能够通过ID销毁实体', () => {
const entity = scene.createEntity("TestEntity");
const entityId = entity.id;
const entityToRemove = scene.findEntityById(entityId)!;
scene.entities.remove(entityToRemove);
expect(scene.entities.count).toBe(0);
expect(scene.findEntityById(entityId)).toBeNull();
});
test('销毁不存在的实体应该安全处理', () => {
expect(() => {
// Scene doesn't have destroyEntity method
expect(scene.findEntityById(999999)).toBeNull();
}).not.toThrow();
});
test('应该能够销毁所有实体', () => {
scene.createEntities(10, "Entity");
expect(scene.entities.count).toBe(10);
scene.destroyAllEntities();
expect(scene.entities.count).toBe(0);
});
});
describe('实体系统管理', () => {
let movementSystem: MovementSystem;
let renderSystem: RenderSystem;
beforeEach(() => {
movementSystem = new MovementSystem();
renderSystem = new RenderSystem();
});
test('应该能够添加实体系统', () => {
scene.addEntityProcessor(movementSystem);
expect(scene.systems.length).toBe(1);
expect(movementSystem.scene).toBe(scene);
});
test('应该能够移除实体系统', () => {
scene.addEntityProcessor(movementSystem);
scene.removeEntityProcessor(movementSystem);
expect(scene.systems.length).toBe(0);
expect(movementSystem.scene).toBeNull();
});
test('移除不存在的系统应该安全处理', () => {
expect(() => {
scene.removeEntityProcessor(movementSystem);
}).not.toThrow();
});
test('应该能够管理多个实体系统', () => {
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(renderSystem);
expect(scene.systems.length).toBe(2);
});
test('系统应该按更新顺序执行', () => {
movementSystem.updateOrder = 1;
renderSystem.updateOrder = 0;
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(renderSystem);
// 创建测试实体
const entity = scene.createEntity("TestEntity");
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.addComponent(new RenderComponent(true));
scene.update();
// RenderSystem应该先执行(updateOrder = 0
// MovementSystem应该后执行(updateOrder = 1
expect(renderSystem.processCallCount).toBe(1);
expect(movementSystem.processCallCount).toBe(1);
});
});
describe('组件查询系统', () => {
beforeEach(() => {
// 创建测试实体
const entity1 = scene.createEntity("Entity1");
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent(1, 0));
const entity2 = scene.createEntity("Entity2");
entity2.addComponent(new PositionComponent(30, 40));
entity2.addComponent(new HealthComponent(80));
const entity3 = scene.createEntity("Entity3");
entity3.addComponent(new VelocityComponent(0, 1));
entity3.addComponent(new HealthComponent(120));
});
test('应该能够查询具有特定组件的实体', () => {
const result = scene.querySystem.queryAll(PositionComponent);
expect(result.entities.length).toBe(2);
expect(result.entities[0].name).toBe("Entity1");
expect(result.entities[1].name).toBe("Entity2");
});
test('应该能够查询具有多个组件的实体', () => {
const result = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
expect(result.entities.length).toBe(1);
expect(result.entities[0].name).toBe("Entity1");
});
test('查询不存在的组件应该返回空结果', () => {
const result = scene.querySystem.queryAll(RenderComponent);
expect(result.entities.length).toBe(0);
});
test('查询系统应该支持缓存', () => {
// 第一次查询
const result1 = scene.querySystem.queryAll(PositionComponent);
// 第二次查询(应该使用缓存)
const result2 = scene.querySystem.queryAll(PositionComponent);
// 实体数组应该相同,并且第二次查询应该来自缓存
expect(result1.entities).toEqual(result2.entities);
expect(result2.fromCache).toBe(true);
});
test('组件变化应该更新查询缓存', () => {
const result1 = scene.querySystem.queryAll(PositionComponent);
expect(result1.entities.length).toBe(2);
// 添加新实体
const entity4 = scene.createEntity("Entity4");
entity4.addComponent(new PositionComponent(50, 60));
const result2 = scene.querySystem.queryAll(PositionComponent);
expect(result2.entities.length).toBe(3);
});
});
describe('事件系统', () => {
test('场景应该有事件系统', () => {
expect(scene.eventSystem).toBeDefined();
});
test('应该能够监听实体事件', () => {
let entityCreatedEvent: any = null;
scene.eventSystem.on('entity:created', (data: any) => {
entityCreatedEvent = data;
});
const entity = scene.createEntity("TestEntity");
expect(entityCreatedEvent).toBeDefined();
expect(entityCreatedEvent.entityName).toBe("TestEntity");
});
test('应该能够监听组件事件', () => {
let componentAddedEvent: any = null;
scene.eventSystem.on('component:added', (data: any) => {
componentAddedEvent = data;
});
const entity = scene.createEntity("TestEntity");
entity.addComponent(new PositionComponent(10, 20));
expect(componentAddedEvent).toBeDefined();
expect(componentAddedEvent.componentType).toBe('SceneTest_PositionComponent');
});
});
describe('场景生命周期管理', () => {
let testScene: TestScene;
beforeEach(() => {
testScene = new TestScene();
});
test('应该能够初始化场景', () => {
testScene.initialize();
expect(testScene.initializeCalled).toBe(true);
});
test('应该能够开始场景', () => {
testScene.begin();
expect(testScene.beginCalled).toBe(true);
});
test('应该能够结束场景', () => {
testScene.end();
expect(testScene.endCalled).toBe(true);
});
test('应该能够更新场景', () => {
const movementSystem = new MovementSystem();
testScene.addEntityProcessor(movementSystem);
// 创建测试实体
const entity = testScene.createEntity("TestEntity");
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
testScene.update();
expect(testScene.updateCallCount).toBe(1);
expect(movementSystem.processCallCount).toBe(1);
// 验证移动系统是否正确处理了实体
const position = entity.getComponent(PositionComponent);
expect(position?.x).toBe(1);
expect(position?.y).toBe(1);
});
});
describe('统计和调试信息', () => {
test('应该能够获取场景统计信息', () => {
// 创建一些实体和系统
scene.createEntities(5, "Entity");
scene.addEntityProcessor(new MovementSystem());
scene.addEntityProcessor(new RenderSystem());
const stats = scene.getStats();
expect(stats.entityCount).toBe(5);
expect(stats.processorCount).toBe(2);
});
test('空场景的统计信息应该正确', () => {
const stats = scene.getStats();
expect(stats.entityCount).toBe(0);
expect(stats.processorCount).toBe(0);
});
test('查询系统应该有统计信息', () => {
// 执行一些查询以产生统计数据
scene.querySystem.queryAll(PositionComponent);
scene.querySystem.queryAll(VelocityComponent);
const stats = scene.querySystem.getStats();
expect(stats.queryStats.totalQueries).toBeGreaterThan(0);
expect(parseFloat(stats.cacheStats.hitRate)).toBeGreaterThanOrEqual(0);
});
});
describe('内存管理和性能', () => {
test('销毁大量实体应该正确清理内存', () => {
const entityCount = 1000;
const entities = scene.createEntities(entityCount, "Entity");
// 为每个实体添加组件
entities.forEach(entity => {
entity.addComponent(new PositionComponent(Math.random() * 100, Math.random() * 100));
entity.addComponent(new VelocityComponent(Math.random() * 5, Math.random() * 5));
});
expect(scene.entities.count).toBe(entityCount);
// 销毁所有实体
scene.destroyAllEntities();
expect(scene.entities.count).toBe(0);
// 查询应该返回空结果
const positionResult = scene.querySystem.queryAll(PositionComponent);
const velocityResult = scene.querySystem.queryAll(VelocityComponent);
expect(positionResult.entities.length).toBe(0);
expect(velocityResult.entities.length).toBe(0);
});
test('大量实体的创建和查询性能应该可接受', () => {
const entityCount = 5000;
const startTime = performance.now();
// 创建大量实体
const entities = scene.createEntities(entityCount, "Entity");
// 为每个实体添加组件
entities.forEach((entity, index) => {
entity.addComponent(new PositionComponent(index, index));
if (index % 2 === 0) {
entity.addComponent(new VelocityComponent(1, 1));
}
if (index % 3 === 0) {
entity.addComponent(new HealthComponent(100));
}
});
const creationTime = performance.now() - startTime;
// 测试查询性能
const queryStartTime = performance.now();
const positionResult = scene.querySystem.queryAll(PositionComponent);
const velocityResult = scene.querySystem.queryAll(VelocityComponent);
const healthResult = scene.querySystem.queryAll(HealthComponent);
const queryTime = performance.now() - queryStartTime;
expect(positionResult.entities.length).toBe(entityCount);
expect(velocityResult.entities.length).toBe(entityCount / 2);
expect(healthResult.entities.length).toBe(Math.floor(entityCount / 3) + 1);
console.log(`创建${entityCount}个实体耗时: ${creationTime.toFixed(2)}ms`);
console.log(`查询操作耗时: ${queryTime.toFixed(2)}ms`);
});
});
describe('错误处理和边界情况', () => {
test('重复添加同一个系统应该安全处理', () => {
const system = new MovementSystem();
scene.addEntityProcessor(system);
scene.addEntityProcessor(system);
expect(scene.systems.length).toBe(1);
});
test('系统处理过程中的异常应该被正确处理', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
class ErrorSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected override process(entities: Entity[]): void {
throw new Error("Test system error");
}
}
const errorSystem = new ErrorSystem();
scene.addEntityProcessor(errorSystem);
const entity = scene.createEntity("TestEntity");
entity.addComponent(new PositionComponent(0, 0));
// 更新不应该抛出异常
expect(() => {
scene.update();
}).not.toThrow();
consoleSpy.mockRestore();
});
test('空场景的更新应该安全', () => {
expect(() => {
scene.update();
}).not.toThrow();
});
test('对已销毁实体的操作应该安全处理', () => {
const entity = scene.createEntity("TestEntity");
scene.entities.remove(entity);
// 对已销毁实体的操作应该安全
expect(() => {
entity.addComponent(new PositionComponent(0, 0));
}).not.toThrow();
});
});
describe('性能监控', () => {
test('Scene应该自动创建PerformanceMonitor', () => {
const customScene = new Scene({
name: 'CustomScene'
});
class TestSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
}
const system = new TestSystem();
customScene.addEntityProcessor(system);
expect(customScene).toBeDefined();
customScene.end();
});
test('每个Scene应该有独立的PerformanceMonitor', () => {
const scene1 = new Scene({ name: 'Scene1' });
const scene2 = new Scene({ name: 'Scene2' });
class TestSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
}
scene1.addEntityProcessor(new TestSystem());
scene2.addEntityProcessor(new TestSystem());
expect(scene1).toBeDefined();
expect(scene2).toBeDefined();
scene1.end();
scene2.end();
});
});
describe('扩展测试 - 补齐覆盖率', () => {
describe('实体标签查找', () => {
test('findEntitiesByTag 应该返回具有指定标签的实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.tag = 0x01;
const entity2 = scene.createEntity('Entity2');
entity2.tag = 0x02;
const entity3 = scene.createEntity('Entity3');
entity3.tag = 0x01;
const found = scene.findEntitiesByTag(0x01);
expect(found.length).toBe(2);
expect(found).toContain(entity1);
expect(found).toContain(entity3);
});
test('findEntitiesByTag 应该在没有匹配时返回空数组', () => {
scene.createEntity('Entity1');
const found = scene.findEntitiesByTag(0xFF);
expect(found).toEqual([]);
});
});
describe('批量实体操作', () => {
test('destroyEntities 应该批量销毁实体', () => {
const entities = scene.createEntities(5, 'Entity');
expect(scene.entities.count).toBe(5);
const toDestroy = entities.slice(0, 3);
scene.destroyEntities(toDestroy);
expect(scene.entities.count).toBe(2);
});
test('destroyEntities 应该处理空数组', () => {
scene.createEntities(3, 'Entity');
expect(() => {
scene.destroyEntities([]);
}).not.toThrow();
expect(scene.entities.count).toBe(3);
});
});
describe('查询方法', () => {
test('queryAny 应该返回具有任意一个组件的实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent());
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
const entity3 = scene.createEntity('Entity3');
entity3.addComponent(new HealthComponent());
const result = scene.queryAny(PositionComponent, VelocityComponent);
expect(result.entities.length).toBe(2);
});
test('queryNone 应该返回不包含指定组件的实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent());
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
const entity3 = scene.createEntity('Entity3');
const result = scene.queryNone(PositionComponent);
expect(result.entities.length).toBe(2);
expect(result.entities).toContain(entity2);
expect(result.entities).toContain(entity3);
});
test('query 应该创建类型安全的查询构建器', () => {
const builder = scene.query();
expect(builder).toBeDefined();
const matcher = builder.withAll(PositionComponent).buildMatcher();
expect(matcher).toBeDefined();
});
});
describe('服务容器', () => {
test('scene.services 应该返回服务容器', () => {
expect(scene.services).toBeDefined();
});
});
describe('系统错误处理', () => {
test('频繁出错的系统应该被自动禁用', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
class ErrorProneSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected override process(): void {
throw new Error('Intentional error');
}
}
const system = new ErrorProneSystem();
scene.addEntityProcessor(system);
const entity = scene.createEntity('TestEntity');
entity.addComponent(new PositionComponent(0, 0));
// 多次更新以触发错误阈值
for (let i = 0; i < 15; i++) {
scene.update();
}
// 系统应该被禁用
expect(system.enabled).toBe(false);
consoleSpy.mockRestore();
});
});
describe('已废弃方法', () => {
test('getEntityByName 应该作为 findEntity 的别名工作', () => {
const entity = scene.createEntity('TestEntity');
const found = scene.getEntityByName('TestEntity');
expect(found).toBe(entity);
});
test('getEntitiesByTag 应该作为 findEntitiesByTag 的别名工作', () => {
const entity = scene.createEntity('Entity');
entity.tag = 0x10;
const found = scene.getEntitiesByTag(0x10);
expect(found.length).toBe(1);
expect(found[0]).toBe(entity);
});
});
describe('系统管理扩展', () => {
test('getSystem 应该返回指定类型的系统', () => {
const movementSystem = new MovementSystem();
scene.addEntityProcessor(movementSystem);
const found = scene.getSystem(MovementSystem);
expect(found).toBe(movementSystem);
});
test('getSystem 应该在系统不存在时返回 null', () => {
const found = scene.getSystem(MovementSystem);
expect(found).toBeNull();
});
test('markSystemsOrderDirty 应该标记系统顺序为脏', () => {
const system1 = new MovementSystem();
const system2 = new RenderSystem();
scene.addEntityProcessor(system1);
scene.addEntityProcessor(system2);
// 访问 systems 以清除脏标记
const _ = scene.systems;
// 标记为脏
scene.markSystemsOrderDirty();
// 再次访问应该重新构建缓存
const systems = scene.systems;
expect(systems).toBeDefined();
});
});
describe('延迟缓存清理', () => {
test('addEntity 应该支持延迟缓存清理', () => {
scene.createEntity('Entity1');
const entity2 = new Entity('Entity2', scene.identifierPool.checkOut());
// 延迟缓存清理
scene.addEntity(entity2, true);
expect(scene.entities.count).toBe(2);
});
});
describe('系统排序稳定性', () => {
test('相同 updateOrder 的系统应按添加顺序稳定排序', () => {
// 创建多个 updateOrder 都为 0 的系统
// Create multiple systems with updateOrder = 0
class SystemA extends EntitySystem {
name = 'SystemA';
constructor() { super(); }
}
class SystemB extends EntitySystem {
name = 'SystemB';
constructor() { super(); }
}
class SystemC extends EntitySystem {
name = 'SystemC';
constructor() { super(); }
}
const systemA = new SystemA();
const systemB = new SystemB();
const systemC = new SystemC();
// 按 A, B, C 顺序添加
scene.addEntityProcessor(systemA);
scene.addEntityProcessor(systemB);
scene.addEntityProcessor(systemC);
// 验证 addOrder 按添加顺序递增
expect(systemA.addOrder).toBe(0);
expect(systemB.addOrder).toBe(1);
expect(systemC.addOrder).toBe(2);
// 验证系统列表按添加顺序排列
const systems = scene.systems;
expect(systems[0]).toBe(systemA);
expect(systems[1]).toBe(systemB);
expect(systems[2]).toBe(systemC);
});
test('updateOrder 优先于 addOrder 排序', () => {
class SystemA extends EntitySystem {
name = 'SystemA';
constructor() { super(); }
}
class SystemB extends EntitySystem {
name = 'SystemB';
constructor() { super(); }
}
class SystemC extends EntitySystem {
name = 'SystemC';
constructor() { super(); }
}
const systemA = new SystemA();
const systemB = new SystemB();
const systemC = new SystemC();
// 按 A, B, C 顺序添加,但设置不同的 updateOrder
scene.addEntityProcessor(systemA);
systemA.updateOrder = 10;
scene.addEntityProcessor(systemB);
systemB.updateOrder = 5;
scene.addEntityProcessor(systemC);
systemC.updateOrder = 5; // 与 B 相同
// 验证排序:B(5,1), C(5,2), A(10,0)
const systems = scene.systems;
expect(systems[0]).toBe(systemB); // updateOrder=5, addOrder=1
expect(systems[1]).toBe(systemC); // updateOrder=5, addOrder=2
expect(systems[2]).toBe(systemA); // updateOrder=10, addOrder=0
});
test('多次重新排序后仍保持稳定性', () => {
class SystemA extends EntitySystem {
name = 'SystemA';
constructor() { super(); }
}
class SystemB extends EntitySystem {
name = 'SystemB';
constructor() { super(); }
}
const systemA = new SystemA();
const systemB = new SystemB();
scene.addEntityProcessor(systemA);
scene.addEntityProcessor(systemB);
// 多次触发重新排序
for (let i = 0; i < 10; i++) {
scene.markSystemsOrderDirty();
const systems = scene.systems;
// 每次排序后顺序应该相同
expect(systems[0]).toBe(systemA);
expect(systems[1]).toBe(systemB);
}
});
});
});
});
@@ -0,0 +1,497 @@
import { EntitySerializer, SerializedEntity } from '../../../src/ECS/Serialization/EntitySerializer';
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem';
import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent';
import { ECSComponent } from '../../../src/ECS/Decorators';
import { GlobalComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage';
import { Serializable, Serialize } from '../../../src/ECS/Serialization';
@ECSComponent('EntitySerTest_Position')
@Serializable({ version: 1 })
class PositionComponent extends Component {
@Serialize()
public x: number = 0;
@Serialize()
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('EntitySerTest_Velocity')
@Serializable({ version: 1 })
class VelocityComponent extends Component {
@Serialize()
public vx: number = 0;
@Serialize()
public vy: number = 0;
}
describe('EntitySerializer', () => {
let scene: Scene;
let hierarchySystem: HierarchySystem;
let componentRegistry: Map<string, ComponentType>;
beforeEach(() => {
// 重置全局注册表 | Reset global registry
GlobalComponentRegistry.reset();
GlobalComponentRegistry.register(PositionComponent);
GlobalComponentRegistry.register(VelocityComponent);
GlobalComponentRegistry.register(HierarchyComponent);
scene = new Scene({ name: 'EntitySerializerTestScene' });
hierarchySystem = new HierarchySystem();
scene.addSystem(hierarchySystem);
componentRegistry = GlobalComponentRegistry.getAllComponentNames() as Map<string, ComponentType>;
});
afterEach(() => {
scene.end();
});
describe('serialize', () => {
test('should serialize basic entity properties', () => {
const entity = scene.createEntity('TestEntity');
entity.tag = 42;
entity.active = false;
entity.enabled = false;
entity.updateOrder = 10;
const serialized = EntitySerializer.serialize(entity, false);
expect(serialized.id).toBe(entity.id);
expect(serialized.name).toBe('TestEntity');
expect(serialized.tag).toBe(42);
expect(serialized.active).toBe(false);
expect(serialized.enabled).toBe(false);
expect(serialized.updateOrder).toBe(10);
});
test('should serialize entity with components', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(100, 200));
entity.addComponent(new VelocityComponent());
const serialized = EntitySerializer.serialize(entity, false);
expect(serialized.components.length).toBe(2);
});
test('should serialize entity without children when includeChildren is false', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
const serialized = EntitySerializer.serialize(parent, false, hierarchySystem);
expect(serialized.children).toEqual([]);
});
test('should serialize entity with children when includeChildren is true', () => {
const parent = scene.createEntity('Parent');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
const serialized = EntitySerializer.serialize(parent, true, hierarchySystem);
expect(serialized.children.length).toBe(2);
expect(serialized.children.some(c => c.name === 'Child1')).toBe(true);
expect(serialized.children.some(c => c.name === 'Child2')).toBe(true);
});
test('should serialize nested hierarchy', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
const grandchild = scene.createEntity('Grandchild');
hierarchySystem.setParent(child, root);
hierarchySystem.setParent(grandchild, child);
const serialized = EntitySerializer.serialize(root, true, hierarchySystem);
expect(serialized.children.length).toBe(1);
expect(serialized.children[0].name).toBe('Child');
expect(serialized.children[0].children.length).toBe(1);
expect(serialized.children[0].children[0].name).toBe('Grandchild');
});
test('should include parentId in serialized data', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
const serializedChild = EntitySerializer.serialize(child, false, hierarchySystem);
expect(serializedChild.parentId).toBe(parent.id);
});
});
describe('deserialize', () => {
test('should deserialize basic entity properties', () => {
const serialized: SerializedEntity = {
id: 999,
name: 'DeserializedEntity',
tag: 77,
active: false,
enabled: false,
updateOrder: 5,
components: [],
children: []
};
let nextId = 1;
const entity = EntitySerializer.deserialize(
serialized,
componentRegistry,
() => nextId++,
false
);
expect(entity.name).toBe('DeserializedEntity');
expect(entity.tag).toBe(77);
expect(entity.active).toBe(false);
expect(entity.enabled).toBe(false);
expect(entity.updateOrder).toBe(5);
});
test('should preserve IDs when preserveIds is true', () => {
const serialized: SerializedEntity = {
id: 999,
name: 'Entity',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: []
};
const entity = EntitySerializer.deserialize(
serialized,
componentRegistry,
() => 1,
true
);
expect(entity.id).toBe(999);
});
test('should generate new IDs when preserveIds is false', () => {
const serialized: SerializedEntity = {
id: 999,
name: 'Entity',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: []
};
let nextId = 100;
const entity = EntitySerializer.deserialize(
serialized,
componentRegistry,
() => nextId++,
false
);
expect(entity.id).toBe(100);
});
test('should deserialize components', () => {
const serialized: SerializedEntity = {
id: 1,
name: 'Entity',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [
{ type: 'EntitySerTest_Position', version: 1, data: { x: 100, y: 200 } }
],
children: []
};
const entity = EntitySerializer.deserialize(
serialized,
componentRegistry,
() => 1,
true,
scene
);
expect(entity.hasComponent(PositionComponent)).toBe(true);
const pos = entity.getComponent(PositionComponent)!;
expect(pos.x).toBe(100);
expect(pos.y).toBe(200);
});
test('should deserialize children with hierarchy relationships', () => {
const serialized: SerializedEntity = {
id: 1,
name: 'Parent',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: [
{
id: 2,
name: 'Child',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: []
}
]
};
let nextId = 10;
const allEntities = new Map<number, Entity>();
const entity = EntitySerializer.deserialize(
serialized,
componentRegistry,
() => nextId++,
false,
scene,
hierarchySystem,
allEntities
);
expect(allEntities.size).toBe(2);
// Add deserialized entities to scene so hierarchySystem can find them
for (const [, e] of allEntities) {
scene.addEntity(e);
}
const children = hierarchySystem.getChildren(entity);
expect(children.length).toBe(1);
expect(children[0].name).toBe('Child');
});
});
describe('serializeEntities', () => {
test('should serialize multiple entities', () => {
const entity1 = scene.createEntity('Entity1');
const entity2 = scene.createEntity('Entity2');
const serialized = EntitySerializer.serializeEntities([entity1, entity2], false);
expect(serialized.length).toBe(2);
});
test('should only serialize root entities when includeChildren is true', () => {
const root = scene.createEntity('Root');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, root);
const serialized = EntitySerializer.serializeEntities(
[root, child],
true,
hierarchySystem
);
// Should only have root (child is serialized inside root)
expect(serialized.length).toBe(1);
expect(serialized[0].name).toBe('Root');
expect(serialized[0].children.length).toBe(1);
});
});
describe('deserializeEntities', () => {
test('should deserialize multiple entities', () => {
const serializedEntities: SerializedEntity[] = [
{
id: 1,
name: 'Entity1',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: []
},
{
id: 2,
name: 'Entity2',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: []
}
];
let nextId = 100;
const { rootEntities, allEntities } = EntitySerializer.deserializeEntities(
serializedEntities,
componentRegistry,
() => nextId++,
false,
scene
);
expect(rootEntities.length).toBe(2);
expect(allEntities.size).toBe(2);
});
test('should deserialize entities with nested hierarchy', () => {
const serializedEntities: SerializedEntity[] = [
{
id: 1,
name: 'Root',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: [
{
id: 2,
name: 'Child',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: [
{
id: 3,
name: 'Grandchild',
tag: 0,
active: true,
enabled: true,
updateOrder: 0,
components: [],
children: []
}
]
}
]
}
];
let nextId = 10;
const { rootEntities, allEntities } = EntitySerializer.deserializeEntities(
serializedEntities,
componentRegistry,
() => nextId++,
false,
scene,
hierarchySystem
);
expect(rootEntities.length).toBe(1);
expect(allEntities.size).toBe(3);
});
});
describe('clone', () => {
test('should clone entity with new ID', () => {
const original = scene.createEntity('Original');
original.tag = 99;
original.addComponent(new PositionComponent(50, 100));
// Use serialize + deserialize with scene for proper cloning
const serialized = EntitySerializer.serialize(original, false);
let nextId = 1000;
const cloned = EntitySerializer.deserialize(
serialized,
componentRegistry,
() => nextId++,
false,
scene // Pass scene so components can be added
);
expect(cloned.id).not.toBe(original.id);
expect(cloned.name).toBe('Original');
expect(cloned.tag).toBe(99);
expect(cloned.hasComponent(PositionComponent)).toBe(true);
const clonedPos = cloned.getComponent(PositionComponent)!;
expect(clonedPos.x).toBe(50);
expect(clonedPos.y).toBe(100);
});
test('should clone entity basic properties without components', () => {
// Test the clone method directly with entity that has no components
const original = scene.createEntity('Original');
original.tag = 99;
original.active = false;
original.enabled = false;
original.updateOrder = 5;
let nextId = 1000;
const cloned = EntitySerializer.clone(
original,
componentRegistry,
() => nextId++
);
expect(cloned.id).not.toBe(original.id);
expect(cloned.name).toBe('Original');
expect(cloned.tag).toBe(99);
expect(cloned.active).toBe(false);
expect(cloned.enabled).toBe(false);
expect(cloned.updateOrder).toBe(5);
});
test('should clone entity with children hierarchy data', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
hierarchySystem.setParent(child, parent);
// Serialize with children
const serialized = EntitySerializer.serialize(parent, true, hierarchySystem);
// Verify the serialized data contains children
expect(serialized.children.length).toBe(1);
expect(serialized.children[0].name).toBe('Child');
});
});
describe('edge cases', () => {
test('should handle entity with no components', () => {
const entity = scene.createEntity('Empty');
const serialized = EntitySerializer.serialize(entity, false);
expect(serialized.components).toEqual([]);
});
test('should handle entity with no hierarchy component', () => {
const entity = new Entity('Standalone', 999);
const serialized = EntitySerializer.serialize(entity, true);
expect(serialized.children).toEqual([]);
expect(serialized.parentId).toBeUndefined();
});
test('should handle default values in serialization', () => {
const entity = scene.createEntity('Default');
const serialized = EntitySerializer.serialize(entity, false);
expect(serialized.active).toBe(true);
expect(serialized.enabled).toBe(true);
expect(serialized.updateOrder).toBe(0);
});
});
});
@@ -0,0 +1,750 @@
/**
* 增量序列化系统测试
*/
import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import {
Serializable,
Serialize,
IncrementalSerializer,
ChangeOperation
} from '../../../src/ECS/Serialization';
import { ECSComponent } from '../../../src/ECS/Decorators';
import { GlobalComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
// 测试组件定义
@ECSComponent('IncTest_Position')
@Serializable({ version: 1 })
class PositionComponent extends Component {
@Serialize()
public x: number = 0;
@Serialize()
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('IncTest_Velocity')
@Serializable({ version: 1 })
class VelocityComponent extends Component {
@Serialize()
public dx: number = 0;
@Serialize()
public dy: number = 0;
}
@ECSComponent('IncTest_Health')
@Serializable({ version: 1 })
class HealthComponent extends Component {
@Serialize()
public current: number = 100;
@Serialize()
public max: number = 100;
}
describe('Incremental Serialization System', () => {
let scene: Scene;
beforeEach(() => {
IncrementalSerializer.resetVersion();
// 重置全局注册表 | Reset global registry
GlobalComponentRegistry.reset();
// 重新注册测试组件 | Re-register test components
GlobalComponentRegistry.register(PositionComponent);
GlobalComponentRegistry.register(VelocityComponent);
GlobalComponentRegistry.register(HealthComponent);
scene = new Scene({ name: 'IncrementalTestScene' });
});
afterEach(() => {
scene.end();
});
describe('Scene Snapshot', () => {
it('应该创建场景快照', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
scene.createIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(true);
});
it('应该在快照中包含所有实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
const snapshot = IncrementalSerializer.createSnapshot(scene);
expect(snapshot.entityIds.size).toBe(2);
expect(snapshot.entityIds.has(entity1.id)).toBe(true);
expect(snapshot.entityIds.has(entity2.id)).toBe(true);
});
it('应该在快照中包含实体基本信息', () => {
const entity = scene.createEntity('TestEntity');
entity.tag = 42;
entity.active = false;
entity.enabled = false;
entity.updateOrder = 5;
const snapshot = IncrementalSerializer.createSnapshot(scene);
const entityData = snapshot.entities.get(entity.id);
expect(entityData).toBeDefined();
expect(entityData!.name).toBe('TestEntity');
expect(entityData!.tag).toBe(42);
expect(entityData!.active).toBe(false);
expect(entityData!.enabled).toBe(false);
expect(entityData!.updateOrder).toBe(5);
});
it('应该在快照中包含组件数据', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(100, 200));
const snapshot = IncrementalSerializer.createSnapshot(scene, {
deepComponentComparison: true
});
const components = snapshot.components.get(entity.id);
expect(components).toBeDefined();
expect(components!.has('IncTest_Position')).toBe(true);
});
});
describe('Entity Changes Detection', () => {
it('应该检测新增的实体', () => {
scene.createIncrementalSnapshot();
const newEntity = scene.createEntity('NewEntity');
newEntity.addComponent(new PositionComponent(50, 100));
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityAdded);
expect(incremental.entityChanges[0].entityId).toBe(newEntity.id);
expect(incremental.entityChanges[0].entityName).toBe('NewEntity');
});
it('应该检测删除的实体', () => {
const entity = scene.createEntity('ToDelete');
scene.createIncrementalSnapshot();
entity.destroy();
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityRemoved);
expect(incremental.entityChanges[0].entityId).toBe(entity.id);
});
it('应该检测实体属性变更', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.name = 'UpdatedName';
entity.tag = 99;
entity.active = false;
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityUpdated);
expect(incremental.entityChanges[0].entityData!.name).toBe('UpdatedName');
expect(incremental.entityChanges[0].entityData!.tag).toBe(99);
expect(incremental.entityChanges[0].entityData!.active).toBe(false);
});
});
describe('Component Changes Detection', () => {
it('应该检测新增的组件', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.addComponent(new PositionComponent(10, 20));
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentAdded);
expect(incremental.componentChanges[0].entityId).toBe(entity.id);
expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position');
});
it('应该检测删除的组件', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
entity.removeComponentByType(PositionComponent);
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentRemoved);
expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position');
});
it('应该检测组件数据变更', () => {
const entity = scene.createEntity('Entity');
const pos = new PositionComponent(10, 20);
entity.addComponent(pos);
scene.createIncrementalSnapshot();
pos.x = 100;
pos.y = 200;
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated);
expect(incremental.componentChanges[0].componentData!.data.x).toBe(100);
expect(incremental.componentChanges[0].componentData!.data.y).toBe(200);
});
it('应该检测多个组件变更', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
entity.addComponent(new VelocityComponent());
entity.addComponent(new HealthComponent());
entity.removeComponentByType(PositionComponent);
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(3);
});
});
describe('Scene Data Changes Detection', () => {
it('应该检测新增的场景数据', () => {
scene.createIncrementalSnapshot();
scene.sceneData.set('weather', 'sunny');
scene.sceneData.set('time', 12.5);
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(2);
});
it('应该检测更新的场景数据', () => {
scene.sceneData.set('weather', 'sunny');
scene.createIncrementalSnapshot();
scene.sceneData.set('weather', 'rainy');
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(1);
expect(incremental.sceneDataChanges[0].key).toBe('weather');
expect(incremental.sceneDataChanges[0].value).toBe('rainy');
});
it('应该检测删除的场景数据', () => {
scene.sceneData.set('temp', 'value');
scene.createIncrementalSnapshot();
scene.sceneData.delete('temp');
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(1);
expect(incremental.sceneDataChanges[0].deleted).toBe(true);
});
});
describe('Apply Incremental Changes', () => {
it('应该应用实体添加变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const newEntity = scene1.createEntity('NewEntity');
newEntity.addComponent(new PositionComponent(50, 100));
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
expect(scene2.entities.count).toBe(1);
const entity = scene2.findEntity('NewEntity');
expect(entity).not.toBeNull();
expect(entity!.hasComponent(PositionComponent)).toBe(true);
scene1.end();
scene2.end();
});
it('应该应用实体删除变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity = scene1.createEntity('ToDelete');
scene1.createIncrementalSnapshot();
entity.destroy();
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('ToDelete');
Object.defineProperty(entity2, 'id', { value: entity.id, writable: true });
scene2.applyIncremental(incremental);
expect(scene2.entities.count).toBe(0);
scene1.end();
scene2.end();
});
it('应该应用实体属性更新', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
scene1.createIncrementalSnapshot();
entity1.name = 'UpdatedName';
entity1.tag = 42;
entity1.active = false;
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
scene2.applyIncremental(incremental);
expect(entity2.name).toBe('UpdatedName');
expect(entity2.tag).toBe(42);
expect(entity2.active).toBe(false);
scene1.end();
scene2.end();
});
it('应该应用组件添加变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
scene1.createIncrementalSnapshot();
entity1.addComponent(new PositionComponent(100, 200));
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
scene2.applyIncremental(incremental);
expect(entity2.hasComponent(PositionComponent)).toBe(true);
const pos = entity2.getComponent(PositionComponent);
expect(pos!.x).toBe(100);
expect(pos!.y).toBe(200);
scene1.end();
scene2.end();
});
it('应该应用组件删除变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
entity1.addComponent(new PositionComponent(10, 20));
scene1.createIncrementalSnapshot();
entity1.removeComponentByType(PositionComponent);
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
entity2.addComponent(new PositionComponent(10, 20));
scene2.applyIncremental(incremental);
expect(entity2.hasComponent(PositionComponent)).toBe(false);
scene1.end();
scene2.end();
});
it('应该应用组件数据更新', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
const pos1 = new PositionComponent(10, 20);
entity1.addComponent(pos1);
scene1.createIncrementalSnapshot();
pos1.x = 100;
pos1.y = 200;
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
entity2.addComponent(new PositionComponent(10, 20));
scene2.applyIncremental(incremental);
const pos2 = entity2.getComponent(PositionComponent);
expect(pos2!.x).toBe(100);
expect(pos2!.y).toBe(200);
scene1.end();
scene2.end();
});
it('应该应用场景数据变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
scene1.sceneData.set('weather', 'sunny');
scene1.sceneData.set('time', 12.5);
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
expect(scene2.sceneData.get('weather')).toBe('sunny');
expect(scene2.sceneData.get('time')).toBe(12.5);
scene1.end();
scene2.end();
});
});
describe('Incremental Serialization', () => {
it('应该序列化和反序列化增量快照(JSON格式)', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(50, 100));
const incremental = scene.serializeIncremental();
const json = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
expect(typeof json).toBe('string');
const deserialized = IncrementalSerializer.deserializeIncremental(json);
expect(deserialized.version).toBe(incremental.version);
expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length);
expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length);
});
it('应该序列化和反序列化增量快照(二进制格式)', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(50, 100));
entity.tag = 42;
scene.sceneData.set('weather', 'sunny');
const incremental = scene.serializeIncremental();
const binary = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
expect(binary instanceof Uint8Array).toBe(true);
const deserialized = IncrementalSerializer.deserializeIncremental(binary);
expect(deserialized.version).toBe(incremental.version);
expect(deserialized.sceneName).toBe(incremental.sceneName);
expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length);
expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length);
expect(deserialized.sceneDataChanges.length).toBe(incremental.sceneDataChanges.length);
});
it('应该美化JSON输出', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
const incremental = scene.serializeIncremental();
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, { format: 'json', pretty: true });
expect(typeof prettyJson).toBe('string');
expect(prettyJson).toContain('\n');
expect(prettyJson).toContain(' ');
});
it('二进制格式应该可以正常序列化', () => {
const entities = [];
for (let i = 0; i < 50; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i * 10, i * 20));
entity.addComponent(new VelocityComponent());
entities.push(entity);
}
scene.createIncrementalSnapshot();
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent)!;
pos.x += 100;
pos.y += 200;
}
const incremental = scene.serializeIncremental();
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
expect(binaryData).toBeInstanceOf(Uint8Array);
expect(binaryData.length).toBeGreaterThan(0);
});
it('二进制和JSON格式应该包含相同的数据', () => {
scene.createIncrementalSnapshot();
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent());
entity1.tag = 99;
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
scene.sceneData.set('level', 5);
scene.sceneData.set('score', 1000);
const incremental = scene.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const fromJson = IncrementalSerializer.deserializeIncremental(jsonData);
const fromBinary = IncrementalSerializer.deserializeIncremental(binaryData);
expect(fromJson.version).toBe(fromBinary.version);
expect(fromJson.timestamp).toBe(fromBinary.timestamp);
expect(fromJson.sceneName).toBe(fromBinary.sceneName);
expect(fromJson.entityChanges.length).toBe(fromBinary.entityChanges.length);
expect(fromJson.componentChanges.length).toBe(fromBinary.componentChanges.length);
expect(fromJson.sceneDataChanges.length).toBe(fromBinary.sceneDataChanges.length);
expect(fromJson.entityChanges[0].entityName).toBe(fromBinary.entityChanges[0].entityName);
expect(fromJson.entityChanges[0].entityData?.tag).toBe(fromBinary.entityChanges[0].entityData?.tag);
});
it('应该正确应用二进制格式反序列化的增量快照', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity = scene1.createEntity('TestEntity');
entity.addComponent(new PositionComponent(100, 200));
entity.tag = 77;
const incremental = scene1.serializeIncremental();
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const deserializedIncremental = IncrementalSerializer.deserializeIncremental(binaryData);
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(deserializedIncremental);
expect(scene2.entities.count).toBe(1);
const restoredEntity = scene2.findEntity('TestEntity');
expect(restoredEntity).not.toBeNull();
expect(restoredEntity!.tag).toBe(77);
expect(restoredEntity!.hasComponent(PositionComponent)).toBe(true);
const pos = restoredEntity!.getComponent(PositionComponent)!;
expect(pos.x).toBe(100);
expect(pos.y).toBe(200);
scene1.end();
scene2.end();
});
it('Scene.applyIncremental应该直接支持二进制Buffer', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity1 = scene1.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent());
const entity2 = scene1.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
const incremental = scene1.serializeIncremental();
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(binaryData);
expect(scene2.entities.count).toBe(2);
const e1 = scene2.findEntity('Entity1');
expect(e1).not.toBeNull();
expect(e1!.hasComponent(PositionComponent)).toBe(true);
expect(e1!.hasComponent(VelocityComponent)).toBe(true);
const e2 = scene2.findEntity('Entity2');
expect(e2).not.toBeNull();
expect(e2!.hasComponent(HealthComponent)).toBe(true);
scene1.end();
scene2.end();
});
it('Scene.applyIncremental应该直接支持JSON字符串', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity = scene1.createEntity('TestEntity');
entity.addComponent(new PositionComponent(50, 100));
entity.tag = 99;
const incremental = scene1.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(jsonData);
expect(scene2.entities.count).toBe(1);
const restoredEntity = scene2.findEntity('TestEntity');
expect(restoredEntity).not.toBeNull();
expect(restoredEntity!.tag).toBe(99);
scene1.end();
scene2.end();
});
});
describe('Snapshot Management', () => {
it('应该更新增量快照基准', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.addComponent(new PositionComponent(10, 20));
const incremental1 = scene.serializeIncremental();
scene.updateIncrementalSnapshot();
const pos = entity.getComponent(PositionComponent)!;
pos.x = 100;
const incremental2 = scene.serializeIncremental();
// incremental2应该只包含Position的更新,不包含添加
expect(incremental1.componentChanges.length).toBe(1);
expect(incremental2.componentChanges.length).toBe(1);
expect(incremental2.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated);
});
it('应该清除增量快照', () => {
scene.createIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(true);
scene.clearIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(false);
});
it('应该在没有快照时抛出错误', () => {
expect(() => {
scene.serializeIncremental();
}).toThrow('必须先调用 createIncrementalSnapshot() 创建基础快照');
});
});
describe('Statistics and Utilities', () => {
it('应该提供增量快照统计信息', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
entity1.destroy();
const incremental = scene.serializeIncremental();
const stats = IncrementalSerializer.getIncrementalStats(incremental);
expect(stats.addedEntities).toBe(1);
expect(stats.removedEntities).toBe(1);
expect(stats.addedComponents).toBe(1);
expect(stats.totalChanges).toBeGreaterThan(0);
});
});
describe('Performance and Edge Cases', () => {
it('应该处理大量实体变更', () => {
scene.createIncrementalSnapshot();
for (let i = 0; i < 100; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i, i * 2));
}
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(100);
expect(incremental.componentChanges.length).toBe(100);
});
it('应该处理空变更', () => {
scene.createIncrementalSnapshot();
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(0);
expect(incremental.componentChanges.length).toBe(0);
expect(incremental.sceneDataChanges.length).toBe(0);
});
it('应该处理复杂嵌套场景数据', () => {
scene.createIncrementalSnapshot();
scene.sceneData.set('config', {
nested: {
deep: {
value: 42
}
},
array: [1, 2, 3]
});
const incremental = scene.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
const config = scene2.sceneData.get('config');
expect(config.nested.deep.value).toBe(42);
expect(config.array).toEqual([1, 2, 3]);
scene2.end();
});
it('应该正确处理快照版本号', () => {
IncrementalSerializer.resetVersion();
const snapshot1 = IncrementalSerializer.createSnapshot(scene);
expect(snapshot1.version).toBe(1);
const snapshot2 = IncrementalSerializer.createSnapshot(scene);
expect(snapshot2.version).toBe(2);
});
});
});
@@ -0,0 +1,436 @@
import { SceneSerializer } from '../../../src/ECS/Serialization/SceneSerializer';
import { Scene } from '../../../src/ECS/Scene';
import { Component } from '../../../src/ECS/Component';
import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem';
import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent';
import { ECSComponent } from '../../../src/ECS/Decorators';
import { GlobalComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage';
import { Serializable, Serialize } from '../../../src/ECS/Serialization';
@ECSComponent('SceneSerTest_Position')
@Serializable({ version: 1 })
class PositionComponent extends Component {
@Serialize()
public x: number = 0;
@Serialize()
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('SceneSerTest_Velocity')
@Serializable({ version: 1 })
class VelocityComponent extends Component {
@Serialize()
public vx: number = 0;
@Serialize()
public vy: number = 0;
}
describe('SceneSerializer', () => {
let scene: Scene;
let componentRegistry: Map<string, ComponentType>;
beforeEach(() => {
scene = new Scene({ name: 'SceneSerializerTestScene' });
componentRegistry = GlobalComponentRegistry.getAllComponentNames() as Map<string, ComponentType>;
});
afterEach(() => {
scene.end();
});
describe('serialize', () => {
test('should serialize scene to JSON string', () => {
scene.createEntity('Entity1').addComponent(new PositionComponent(10, 20));
scene.createEntity('Entity2').addComponent(new VelocityComponent());
const result = SceneSerializer.serialize(scene);
expect(typeof result).toBe('string');
const parsed = JSON.parse(result as string);
expect(parsed.name).toBe('SceneSerializerTestScene');
expect(parsed.entities.length).toBe(2);
});
test('should serialize scene to binary format', () => {
scene.createEntity('Entity');
const result = SceneSerializer.serialize(scene, { format: 'binary' });
expect(result).toBeInstanceOf(Uint8Array);
});
test('should include metadata when requested', () => {
scene.createEntity('Entity');
const result = SceneSerializer.serialize(scene, { includeMetadata: true });
const parsed = JSON.parse(result as string);
expect(parsed.metadata).toBeDefined();
expect(parsed.metadata.entityCount).toBe(1);
expect(parsed.timestamp).toBeDefined();
});
test('should pretty print JSON when requested', () => {
scene.createEntity('Entity');
const result = SceneSerializer.serialize(scene, { pretty: true });
expect(typeof result).toBe('string');
expect((result as string).includes('\n')).toBe(true);
expect((result as string).includes(' ')).toBe(true);
});
test('should serialize scene data', () => {
scene.sceneData.set('level', 5);
scene.sceneData.set('config', { difficulty: 'hard' });
const result = SceneSerializer.serialize(scene);
const parsed = JSON.parse(result as string);
expect(parsed.sceneData).toBeDefined();
expect(parsed.sceneData.level).toBe(5);
expect(parsed.sceneData.config.difficulty).toBe('hard');
});
test('should serialize with component filter', () => {
scene.createEntity('Entity1').addComponent(new PositionComponent());
scene.createEntity('Entity2').addComponent(new VelocityComponent());
const result = SceneSerializer.serialize(scene, {
components: [PositionComponent]
});
const parsed = JSON.parse(result as string);
// Only entities with PositionComponent should be included
expect(parsed.entities.length).toBe(1);
});
});
describe('deserialize', () => {
test('should deserialize scene from JSON string', () => {
scene.createEntity('Entity1').addComponent(new PositionComponent(100, 200));
const serialized = SceneSerializer.serialize(scene);
const newScene = new Scene({ name: 'NewScene' });
SceneSerializer.deserialize(newScene, serialized, { componentRegistry });
expect(newScene.entities.count).toBe(1);
const entity = newScene.findEntity('Entity1');
expect(entity).not.toBeNull();
expect(entity!.hasComponent(PositionComponent)).toBe(true);
const pos = entity!.getComponent(PositionComponent)!;
expect(pos.x).toBe(100);
expect(pos.y).toBe(200);
newScene.end();
});
test('should deserialize scene from binary format', () => {
scene.createEntity('BinaryEntity').addComponent(new PositionComponent(50, 75));
const serialized = SceneSerializer.serialize(scene, { format: 'binary' });
const newScene = new Scene({ name: 'NewScene' });
SceneSerializer.deserialize(newScene, serialized, { componentRegistry });
expect(newScene.entities.count).toBe(1);
const entity = newScene.findEntity('BinaryEntity');
expect(entity).not.toBeNull();
newScene.end();
});
test('should replace existing entities with strategy replace', () => {
scene.createEntity('Original');
const serialized = SceneSerializer.serialize(scene);
const targetScene = new Scene({ name: 'Target' });
targetScene.createEntity('Existing1');
targetScene.createEntity('Existing2');
expect(targetScene.entities.count).toBe(2);
SceneSerializer.deserialize(targetScene, serialized, {
strategy: 'replace',
componentRegistry
});
expect(targetScene.entities.count).toBe(1);
expect(targetScene.findEntity('Original')).not.toBeNull();
expect(targetScene.findEntity('Existing1')).toBeNull();
targetScene.end();
});
test('should merge with existing entities with strategy merge', () => {
scene.createEntity('FromSave');
const serialized = SceneSerializer.serialize(scene);
const targetScene = new Scene({ name: 'Target' });
targetScene.createEntity('Existing');
expect(targetScene.entities.count).toBe(1);
SceneSerializer.deserialize(targetScene, serialized, {
strategy: 'merge',
componentRegistry
});
expect(targetScene.entities.count).toBe(2);
expect(targetScene.findEntity('Existing')).not.toBeNull();
expect(targetScene.findEntity('FromSave')).not.toBeNull();
targetScene.end();
});
test('should restore scene data', () => {
scene.sceneData.set('weather', 'sunny');
scene.sceneData.set('time', 12.5);
const serialized = SceneSerializer.serialize(scene);
const newScene = new Scene({ name: 'NewScene' });
SceneSerializer.deserialize(newScene, serialized, { componentRegistry });
expect(newScene.sceneData.get('weather')).toBe('sunny');
expect(newScene.sceneData.get('time')).toBe(12.5);
newScene.end();
});
test('should call migration function when versions differ', () => {
scene.createEntity('Entity');
const serialized = SceneSerializer.serialize(scene);
// Manually modify version
const parsed = JSON.parse(serialized as string);
parsed.version = 0;
const modifiedSerialized = JSON.stringify(parsed);
const migrationFn = jest.fn((oldVersion, newVersion, data) => {
expect(oldVersion).toBe(0);
return data;
});
const newScene = new Scene({ name: 'NewScene' });
SceneSerializer.deserialize(newScene, modifiedSerialized, {
componentRegistry,
migration: migrationFn
});
expect(migrationFn).toHaveBeenCalled();
newScene.end();
});
test('should throw on invalid JSON', () => {
const newScene = new Scene({ name: 'NewScene' });
expect(() => {
SceneSerializer.deserialize(newScene, 'invalid json{{{', { componentRegistry });
}).toThrow();
newScene.end();
});
});
describe('validate', () => {
test('should validate correct save data', () => {
scene.createEntity('Entity');
const serialized = SceneSerializer.serialize(scene);
const result = SceneSerializer.validate(serialized as string);
expect(result.valid).toBe(true);
expect(result.version).toBe(1);
});
test('should return errors for missing version', () => {
const invalid = JSON.stringify({ entities: [], componentTypeRegistry: [] });
const result = SceneSerializer.validate(invalid);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing version field');
});
test('should return errors for missing entities', () => {
const invalid = JSON.stringify({ version: 1, componentTypeRegistry: [] });
const result = SceneSerializer.validate(invalid);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing or invalid entities field');
});
test('should return errors for missing componentTypeRegistry', () => {
const invalid = JSON.stringify({ version: 1, entities: [] });
const result = SceneSerializer.validate(invalid);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing or invalid componentTypeRegistry field');
});
test('should handle JSON parse errors', () => {
const result = SceneSerializer.validate('not valid json');
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0]).toContain('JSON parse error');
});
});
describe('getInfo', () => {
test('should return info from save data', () => {
scene.name = 'InfoTestScene';
scene.createEntity('Entity1');
scene.createEntity('Entity2');
scene.createEntity('Entity3');
const serialized = SceneSerializer.serialize(scene);
const info = SceneSerializer.getInfo(serialized as string);
expect(info).not.toBeNull();
expect(info!.name).toBe('InfoTestScene');
expect(info!.entityCount).toBe(3);
expect(info!.version).toBe(1);
});
test('should return null for invalid data', () => {
const info = SceneSerializer.getInfo('invalid');
expect(info).toBeNull();
});
test('should include timestamp when present', () => {
scene.createEntity('Entity');
const serialized = SceneSerializer.serialize(scene, { includeMetadata: true });
const info = SceneSerializer.getInfo(serialized as string);
expect(info!.timestamp).toBeDefined();
});
});
describe('scene data serialization', () => {
test('should serialize Date objects', () => {
const date = new Date('2024-01-01T00:00:00Z');
scene.sceneData.set('createdAt', date);
const serialized = SceneSerializer.serialize(scene);
const parsed = JSON.parse(serialized as string);
expect(parsed.sceneData.createdAt.__type).toBe('Date');
});
test('should deserialize Date objects', () => {
const date = new Date('2024-01-01T00:00:00Z');
scene.sceneData.set('createdAt', date);
const serialized = SceneSerializer.serialize(scene);
const newScene = new Scene({ name: 'NewScene' });
SceneSerializer.deserialize(newScene, serialized, { componentRegistry });
const restoredDate = newScene.sceneData.get('createdAt');
expect(restoredDate).toBeInstanceOf(Date);
expect(restoredDate.getTime()).toBe(date.getTime());
newScene.end();
});
test('should serialize Map objects', () => {
const map = new Map([['key1', 'value1'], ['key2', 'value2']]);
scene.sceneData.set('mapping', map);
const serialized = SceneSerializer.serialize(scene);
const parsed = JSON.parse(serialized as string);
expect(parsed.sceneData.mapping.__type).toBe('Map');
});
test('should deserialize Map objects', () => {
const map = new Map([['key1', 'value1'], ['key2', 'value2']]);
scene.sceneData.set('mapping', map);
const serialized = SceneSerializer.serialize(scene);
const newScene = new Scene({ name: 'NewScene' });
SceneSerializer.deserialize(newScene, serialized, { componentRegistry });
const restoredMap = newScene.sceneData.get('mapping');
expect(restoredMap).toBeInstanceOf(Map);
expect(restoredMap.get('key1')).toBe('value1');
expect(restoredMap.get('key2')).toBe('value2');
newScene.end();
});
test('should serialize Set objects', () => {
const set = new Set([1, 2, 3]);
scene.sceneData.set('numbers', set);
const serialized = SceneSerializer.serialize(scene);
const parsed = JSON.parse(serialized as string);
expect(parsed.sceneData.numbers.__type).toBe('Set');
});
test('should deserialize Set objects', () => {
const set = new Set([1, 2, 3]);
scene.sceneData.set('numbers', set);
const serialized = SceneSerializer.serialize(scene);
const newScene = new Scene({ name: 'NewScene' });
SceneSerializer.deserialize(newScene, serialized, { componentRegistry });
const restoredSet = newScene.sceneData.get('numbers');
expect(restoredSet).toBeInstanceOf(Set);
expect(restoredSet.has(1)).toBe(true);
expect(restoredSet.has(2)).toBe(true);
expect(restoredSet.has(3)).toBe(true);
newScene.end();
});
});
describe('hierarchy serialization', () => {
test('should serialize and deserialize entity hierarchy', () => {
const hierarchySystem = new HierarchySystem();
scene.addSystem(hierarchySystem);
const root = scene.createEntity('Root');
const child1 = scene.createEntity('Child1');
const child2 = scene.createEntity('Child2');
hierarchySystem.setParent(child1, root);
hierarchySystem.setParent(child2, root);
const serialized = SceneSerializer.serialize(scene);
const newScene = new Scene({ name: 'NewScene' });
const newHierarchySystem = new HierarchySystem();
newScene.addSystem(newHierarchySystem);
SceneSerializer.deserialize(newScene, serialized, { componentRegistry });
const newRoot = newScene.findEntity('Root');
expect(newRoot).not.toBeNull();
const children = newHierarchySystem.getChildren(newRoot!);
expect(children.length).toBe(2);
newScene.end();
});
});
});
File diff suppressed because it is too large Load Diff
@@ -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);
});
});
@@ -0,0 +1,511 @@
import { Scene } from '../../../src/ECS/Scene';
import { PassiveSystem } from '../../../src/ECS/Systems/PassiveSystem';
import { IntervalSystem } from '../../../src/ECS/Systems/IntervalSystem';
import { ProcessingSystem } from '../../../src/ECS/Systems/ProcessingSystem';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { GlobalComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
import { Time } from '../../../src/Utils/Time';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { Core } from '../../../src/Core';
// 测试组件
class TestComponent extends Component {
public value: number = 0;
constructor(...args: unknown[]) {
super();
const [value = 0] = args as [number?];
this.value = value;
}
}
class AnotherComponent extends Component {
public name: string = 'test';
constructor(...args: unknown[]) {
super();
const [name = 'test'] = args as [string?];
this.name = name;
}
}
// 具体的被动系统实现
class ConcretePassiveSystem extends PassiveSystem {
public processCallCount = 0;
constructor() {
super(Matcher.all(TestComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
// 被动系统的process方法会被调用,但不做任何处理
super.process(entities);
}
}
// 具体的间隔系统实现
class ConcreteIntervalSystem extends IntervalSystem {
public processCallCount = 0;
public lastDelta = 0;
constructor(interval: number) {
super(interval, Matcher.all(TestComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
this.lastDelta = this.getIntervalDelta();
}
}
// 用于独立测试的间隔系统(避免 Scene 的类型去重)
class IndependentIntervalSystem extends IntervalSystem {
public processCallCount = 0;
constructor(interval: number) {
super(interval, Matcher.all(TestComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
}
}
// 用于长时间运行测试的间隔系统
class LongRunIntervalSystem extends IntervalSystem {
public processCallCount = 0;
constructor(interval: number) {
super(interval, Matcher.all(TestComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
}
}
// 具体的处理系统实现
class ConcreteProcessingSystem extends ProcessingSystem {
public processSystemCallCount = 0;
public processCallCount = 0;
constructor() {
super(Matcher.all(TestComponent));
}
public processSystem(): void {
this.processSystemCallCount++;
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
super.process(entities);
}
}
describe('System Types - 系统类型测试', () => {
let scene: Scene;
let entity: Entity;
beforeEach(() => {
Core.create();
// 注册测试组件类型 | Register test component types
// 必须在创建 Scene 之前注册,因为 Scene 会克隆 GlobalComponentRegistry
// Must register before Scene creation, as Scene clones GlobalComponentRegistry
GlobalComponentRegistry.register(TestComponent);
GlobalComponentRegistry.register(AnotherComponent);
scene = new Scene();
entity = scene.createEntity('TestEntity');
// 重置时间系统
Time.update(0.016);
});
describe('PassiveSystem - 被动系统', () => {
let passiveSystem: ConcretePassiveSystem;
beforeEach(() => {
passiveSystem = new ConcretePassiveSystem();
scene.addEntityProcessor(passiveSystem);
});
test('应该能够创建被动系统', () => {
expect(passiveSystem).toBeInstanceOf(PassiveSystem);
expect(passiveSystem).toBeInstanceOf(ConcretePassiveSystem);
});
test('process方法不应该做任何处理', () => {
const entities = [entity];
const initialProcessCount = passiveSystem.processCallCount;
passiveSystem.update();
// 虽然process被调用了,但被动系统不做任何实际处理
expect(passiveSystem.processCallCount).toBe(initialProcessCount + 1);
});
test('应该能够动态查询匹配的实体', () => {
// 现在使用动态查询,不需要手动add/remove
// 先检查没有匹配的实体
expect(passiveSystem.entities.length).toBe(0);
// 添加匹配的组件后,系统应该能查询到实体
entity.addComponent(new TestComponent(100));
// 需要设置场景和QuerySystem才能进行动态查询
// 这里我们只测试entities getter的存在性
expect(passiveSystem.entities).toBeDefined();
});
});
describe('IntervalSystem - 间隔系统', () => {
let intervalSystem: ConcreteIntervalSystem;
const testInterval = 0.1; // 100ms
beforeEach(() => {
intervalSystem = new ConcreteIntervalSystem(testInterval);
scene.addEntityProcessor(intervalSystem);
});
test('应该能够创建间隔系统', () => {
expect(intervalSystem).toBeInstanceOf(IntervalSystem);
expect(intervalSystem).toBeInstanceOf(ConcreteIntervalSystem);
});
test('在间隔时间内不应该处理', () => {
const initialProcessCount = intervalSystem.processCallCount;
// 模拟时间更新,但不足以触发间隔
Time.update(testInterval / 2);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(initialProcessCount);
});
test('达到间隔时间时应该处理', () => {
const initialProcessCount = intervalSystem.processCallCount;
// 模拟时间更新,刚好达到间隔
Time.update(testInterval);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
});
test('超过间隔时间应该处理并记录余数', () => {
const initialProcessCount = intervalSystem.processCallCount;
const overTime = testInterval + 0.02; // 超过20ms
Time.update(overTime);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
expect(intervalSystem.lastDelta).toBe(testInterval + 0.02);
});
test('多次累积应该正确触发', () => {
let processCount = intervalSystem.processCallCount;
// 多次小的时间增量
for (let i = 0; i < 10; i++) {
Time.update(testInterval / 5);
intervalSystem.update();
}
// 10 * (interval/5) = 2 * interval,应该触发2次
expect(intervalSystem.processCallCount).toBeGreaterThanOrEqual(processCount + 1);
});
test('getIntervalDelta应该返回正确的增量', () => {
const overTime = testInterval + 0.03;
Time.update(overTime);
intervalSystem.update();
expect(intervalSystem.lastDelta).toBe(testInterval + 0.03);
});
test('重置后应该重新开始计时', () => {
// 第一次触发
Time.update(testInterval);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(1);
// 再次触发需要等待完整间隔
Time.update(testInterval / 2);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(1);
Time.update(testInterval / 2);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(2);
});
test('update和lateUpdate同时调用时,onCheckProcessing只应执行一次副作用', () => {
// 这个测试验证修复:onCheckProcessing() 的副作用(时间累加)不应该在 lateUpdate 中重复执行
// 模拟一帧内的完整调用流程
const initialProcessCount = intervalSystem.processCallCount;
// 第一帧:时间不足,不应触发
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount);
// 第二帧:累计时间刚好达到间隔,应该触发一次
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
// 第三帧:需要再等待完整间隔
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
// 第四帧:再次达到间隔
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 2);
});
test('lateUpdate应复用update的检查结果,不重复累加时间', () => {
// 这是用户报告的 bug 场景:5秒间隔,但实际触发不规律
// 原因是 onCheckProcessing() 在 update 和 lateUpdate 中都被调用,导致时间累加了两次
// 核心验证:在 N*interval 时间内,触发次数应该接近 N 次
// 如果 bug 存在(时间累加两次),触发次数会接近 2N 次
const initialCount = intervalSystem.processCallCount;
// 模拟 50 帧,每帧 testInterval/5 (总共 10*testInterval)
for (let frame = 0; frame < 50; frame++) {
Time.update(testInterval / 5);
intervalSystem.update();
intervalSystem.lateUpdate();
}
// 50帧 * (testInterval/5) = 10*testInterval,应该触发约 10 次
// 如果 bug 存在(时间累加两次),会触发约 20 次
const triggers = intervalSystem.processCallCount - initialCount;
// 正常情况:触发 9-11 次
// Bug 情况:触发 18-22 次
expect(triggers).toBeGreaterThanOrEqual(9);
expect(triggers).toBeLessThanOrEqual(12);
});
test('精确间隔测试 - 验证触发间隔的一致性', () => {
// 验证触发间隔是稳定的,而不是忽大忽小
const initialCount = intervalSystem.processCallCount;
const frameTimes: number[] = [];
let framesSinceLastTrigger = 0;
// 模拟 100 帧,每帧 testInterval/5
for (let frame = 0; frame < 100; frame++) {
Time.update(testInterval / 5);
framesSinceLastTrigger++;
const beforeCount = intervalSystem.processCallCount;
intervalSystem.update();
intervalSystem.lateUpdate();
if (intervalSystem.processCallCount > beforeCount) {
frameTimes.push(framesSinceLastTrigger);
framesSinceLastTrigger = 0;
}
}
// 100帧 * (testInterval/5) = 20*testInterval,应该触发约 20 次
const totalTriggers = intervalSystem.processCallCount - initialCount;
expect(totalTriggers).toBeGreaterThanOrEqual(19);
expect(totalTriggers).toBeLessThanOrEqual(21);
// 验证每次触发的帧间隔都接近 5 帧(因为 5 * testInterval/5 = testInterval
// 如果 bug 存在,帧间隔会不稳定(有的 2-3 帧,有的 7-8 帧)
for (const frames of frameTimes) {
expect(frames).toBeGreaterThanOrEqual(4);
expect(frames).toBeLessThanOrEqual(6);
}
});
});
describe('ProcessingSystem - 处理系统', () => {
let processingSystem: ConcreteProcessingSystem;
beforeEach(() => {
processingSystem = new ConcreteProcessingSystem();
scene.addEntityProcessor(processingSystem);
});
test('应该能够创建处理系统', () => {
expect(processingSystem).toBeInstanceOf(ProcessingSystem);
expect(processingSystem).toBeInstanceOf(ConcreteProcessingSystem);
});
test('process方法应该调用processSystem', () => {
const initialProcessSystemCount = processingSystem.processSystemCallCount;
const initialProcessCount = processingSystem.processCallCount;
processingSystem.update();
expect(processingSystem.processCallCount).toBe(initialProcessCount + 1);
expect(processingSystem.processSystemCallCount).toBe(initialProcessSystemCount + 1);
});
test('每次更新都应该调用processSystem', () => {
const initialCount = processingSystem.processSystemCallCount;
processingSystem.update();
processingSystem.update();
processingSystem.update();
expect(processingSystem.processSystemCallCount).toBe(initialCount + 3);
});
test('应该能够动态查询多个实体', () => {
// 现在使用动态查询,不需要手动add
// 测试系统的基本功能
const initialCount = processingSystem.processSystemCallCount;
processingSystem.update();
// processSystem应该被调用,不管有多少实体
expect(processingSystem.processSystemCallCount).toBe(initialCount + 1);
// 测试entities getter的存在性
expect(processingSystem.entities).toBeDefined();
expect(Array.isArray(processingSystem.entities)).toBe(true);
});
});
describe('系统集成测试', () => {
test('不同类型的系统应该都继承自EntitySystem', () => {
const passive = new ConcretePassiveSystem();
const interval = new ConcreteIntervalSystem(0.1);
const processing = new ConcreteProcessingSystem();
expect(passive.matcher).toBeDefined();
expect(interval.matcher).toBeDefined();
expect(processing.matcher).toBeDefined();
expect(passive.entities).toBeDefined();
expect(interval.entities).toBeDefined();
expect(processing.entities).toBeDefined();
expect(passive.systemName).toBeDefined();
expect(interval.systemName).toBeDefined();
expect(processing.systemName).toBeDefined();
});
test('系统应该能够正确匹配实体', () => {
const passive = new ConcretePassiveSystem();
const interval = new ConcreteIntervalSystem(0.1);
const processing = new ConcreteProcessingSystem();
const matchingEntity = scene.createEntity('Matching');
matchingEntity.addComponent(new TestComponent(100));
const nonMatchingEntity = scene.createEntity('NonMatching');
nonMatchingEntity.addComponent(new AnotherComponent('test'));
// 所有系统都应该匹配TestComponent
// 直接检查实体是否有需要的组件
expect(matchingEntity.hasComponent(TestComponent)).toBe(true);
expect(nonMatchingEntity.hasComponent(TestComponent)).toBe(false);
expect(nonMatchingEntity.hasComponent(AnotherComponent)).toBe(true);
});
});
describe('Matcher高级查询功能测试', () => {
test('应该能使用新的静态方法创建匹配器', () => {
// 测试新的静态方法
const byTagMatcher = Matcher.byTag(100);
const byNameMatcher = Matcher.byName('Player');
const byComponentMatcher = Matcher.byComponent(TestComponent);
expect(byTagMatcher.getCondition().tag).toBe(100);
expect(byNameMatcher.getCondition().name).toBe('Player');
expect(byComponentMatcher.getCondition().component).toBe(TestComponent);
});
test('应该支持链式组合查询', () => {
const complexMatcher = Matcher.all(TestComponent)
.withTag(100)
.withName('Player')
.none(AnotherComponent);
const condition = complexMatcher.getCondition();
expect(condition.all).toContain(TestComponent);
expect(condition.tag).toBe(100);
expect(condition.name).toBe('Player');
expect(condition.none).toContain(AnotherComponent);
});
test('应该能够移除特定条件', () => {
const matcher = Matcher.byTag(100)
.withName('Player')
.withComponent(TestComponent);
// 移除条件
matcher.withoutTag().withoutName();
const condition = matcher.getCondition();
expect(condition.tag).toBeUndefined();
expect(condition.name).toBeUndefined();
expect(condition.component).toBe(TestComponent);
});
test('应该能够正确重置所有条件', () => {
const matcher = Matcher.all(TestComponent)
.withTag(100)
.withName('Player')
.any(AnotherComponent);
matcher.reset();
expect(matcher.isEmpty()).toBe(true);
expect(matcher.getCondition().all.length).toBe(0);
expect(matcher.getCondition().any.length).toBe(0);
expect(matcher.getCondition().tag).toBeUndefined();
expect(matcher.getCondition().name).toBeUndefined();
});
test('应该能够正确克隆匹配器', () => {
const original = Matcher.all(TestComponent)
.withTag(100)
.withName('Player');
const cloned = original.clone();
expect(cloned.getCondition().all).toEqual(original.getCondition().all);
expect(cloned.getCondition().tag).toBe(original.getCondition().tag);
expect(cloned.getCondition().name).toBe(original.getCondition().name);
// 修改克隆的不应该影响原始的
cloned.withTag(200);
expect(original.getCondition().tag).toBe(100);
expect(cloned.getCondition().tag).toBe(200);
});
test('应该能够生成正确的字符串表示', () => {
const complexMatcher = Matcher.all(TestComponent)
.withTag(100)
.withName('Player')
.none(AnotherComponent);
const str = complexMatcher.toString();
expect(str).toContain('all(TestComponent)');
expect(str).toContain('tag(100)');
expect(str).toContain('name(Player)');
expect(str).toContain('none(AnotherComponent)');
});
});
});
@@ -0,0 +1,183 @@
import { BitMask64Data, BitMask64Utils } from "../../../src";
describe("BitMask64Utils 位掩码工具测试", () => {
test("create() 应该在指定索引位置设置位", () => {
const mask = BitMask64Utils.create(0);
expect(mask.base[0]).toBe(1);
expect(mask.base[1]).toBe(0);
const mask2 = BitMask64Utils.create(33);
expect(mask2.base[0]).toBe(0);
expect(mask2.base[1]).toBe(0b10);
});
test("fromNumber() 应该把数值放入低32位", () => {
const mask = BitMask64Utils.fromNumber(123456);
expect(mask.base[0]).toBe(123456);
expect(mask.base[1]).toBe(0);
});
test("setBit/getBit/clearBit 应该正确设置、读取和清除位", () => {
const mask: BitMask64Data = { base: [0, 0] };
BitMask64Utils.setBit(mask, 5);
expect(BitMask64Utils.getBit(mask, 5)).toBe(true);
BitMask64Utils.clearBit(mask, 5);
expect(BitMask64Utils.getBit(mask, 5)).toBe(false);
// 测试扩展段
BitMask64Utils.setBit(mask, 70);
expect(mask.segments).toBeDefined();
expect(BitMask64Utils.getBit(mask, 70)).toBe(true);
});
test("hasAny/hasAll/hasNone 判断应正确", () => {
const maskA = BitMask64Utils.create(1);
const maskB = BitMask64Utils.create(1);
const maskC = BitMask64Utils.create(2);
expect(BitMask64Utils.hasAny(maskA, maskB)).toBe(true);
expect(BitMask64Utils.hasAll(maskA, maskB)).toBe(true);
expect(BitMask64Utils.hasNone(maskA, maskC)).toBe(true);
});
test("isZero 应正确判断", () => {
const mask = BitMask64Utils.create(3);
expect(BitMask64Utils.isZero(mask)).toBe(false);
BitMask64Utils.clear(mask);
expect(BitMask64Utils.isZero(mask)).toBe(true);
});
test("equals 应正确判断两个掩码是否相等", () => {
const mask1 = BitMask64Utils.create(10);
const mask2 = BitMask64Utils.create(10);
const mask3 = BitMask64Utils.create(11);
expect(BitMask64Utils.equals(mask1, mask2)).toBe(true);
expect(BitMask64Utils.equals(mask1, mask3)).toBe(false);
});
test("orInPlace/andInPlace/xorInPlace 运算应正确", () => {
const mask1 = BitMask64Utils.create(1);
const mask2 = BitMask64Utils.create(2);
BitMask64Utils.orInPlace(mask1, mask2);
expect(BitMask64Utils.getBit(mask1, 1)).toBe(true);
expect(BitMask64Utils.getBit(mask1, 2)).toBe(true);
BitMask64Utils.andInPlace(mask1, mask2);
expect(BitMask64Utils.getBit(mask1, 1)).toBe(false);
expect(BitMask64Utils.getBit(mask1, 2)).toBe(true);
BitMask64Utils.xorInPlace(mask1, mask2);
expect(BitMask64Utils.getBit(mask1, 2)).toBe(false);
});
test("copy/clone 应正确复制数据", () => {
const source = BitMask64Utils.create(15);
const target: BitMask64Data = { base: [0, 0] };
BitMask64Utils.copy(source, target);
expect(BitMask64Utils.equals(source, target)).toBe(true);
const clone = BitMask64Utils.clone(source);
expect(BitMask64Utils.equals(source, clone)).toBe(true);
expect(clone).not.toBe(source); // 深拷贝
});
test("越界与非法输入处理", () => {
expect(() => BitMask64Utils.create(-1)).toThrow();
expect(BitMask64Utils.getBit({ base: [0,0] }, -5)).toBe(false);
expect(() => BitMask64Utils.clearBit({ base: [0,0] }, -2)).toThrow();
});
test("大于64位的扩展段逻辑 - hasAny/hasAll/hasNone/equals", () => {
// 掩码 A: 只在 bit 150 位置为 1
const maskA = BitMask64Utils.create(150);
// 掩码 B: 只在 bit 200 位置为 1
const maskB = BitMask64Utils.create(200);
// A 与 B 在不同扩展段,不存在重叠位
expect(BitMask64Utils.hasAny(maskA, maskB)).toBe(false);
expect(BitMask64Utils.hasNone(maskA, maskB)).toBe(true);
// C: 在 150 与 200 都置位
const maskC = BitMask64Utils.clone(maskA);
BitMask64Utils.setBit(maskC, 200);
// A 是 C 的子集
expect(BitMask64Utils.hasAll(maskC, maskA)).toBe(true);
// B 是 C 的子集
expect(BitMask64Utils.hasAll(maskC, maskB)).toBe(true);
// A 和 C 不相等
expect(BitMask64Utils.equals(maskA, maskC)).toBe(false);
// C 与自身相等
expect(BitMask64Utils.equals(maskC, maskC)).toBe(true);
//copy
const copyMask = BitMask64Utils.create(0);
BitMask64Utils.copy(maskA,copyMask);
expect(BitMask64Utils.equals(copyMask,maskA)).toBe(true);
// hasAll短路测试,对第一个if的测试覆盖
BitMask64Utils.setBit(copyMask,64);
expect(BitMask64Utils.hasAll(maskA, copyMask)).toBe(false);
BitMask64Utils.clearBit(copyMask, 64);
// 扩展到350位,对最后一个短路if的测试覆盖
BitMask64Utils.setBit(copyMask,350);
expect(BitMask64Utils.hasAll(maskA, copyMask)).toBe(false);
});
test("大于64位的逻辑运算 - or/and/xor 跨段处理", () => {
const maskA = BitMask64Utils.create(128); // 第一扩展段
const maskB = BitMask64Utils.create(190); // 同一扩展段但不同位置
const maskC = BitMask64Utils.create(300); // 不同扩展段
// OR: 合并不同扩展段
const orMask = BitMask64Utils.clone(maskA);
BitMask64Utils.orInPlace(orMask, maskC);
expect(BitMask64Utils.getBit(orMask, 128)).toBe(true);
expect(BitMask64Utils.getBit(orMask, 300)).toBe(true);
// AND: 交集为空
const andMask = BitMask64Utils.clone(maskA);
BitMask64Utils.andInPlace(andMask, maskB);
expect(BitMask64Utils.isZero(andMask)).toBe(true);
// XOR: 不同扩展段应该都保留
const xorMask = BitMask64Utils.clone(maskA);
BitMask64Utils.xorInPlace(xorMask, maskC);
expect(BitMask64Utils.getBit(xorMask, 128)).toBe(true);
expect(BitMask64Utils.getBit(xorMask, 300)).toBe(true);
});
test("toString 与 popCount 应该在扩展段正常工作", () => {
const mask = BitMask64Utils.create(0);
BitMask64Utils.setBit(mask, 130); // 扩展段,此时扩展段长度延长到2
BitMask64Utils.setBit(mask, 260); // 再设置另一个超出当前最高段范围更高位,此时扩展段长度延长到3
// 现在应该有三个置位
expect(BitMask64Utils.popCount(mask)).toBe(3);
const strBin = BitMask64Utils.toString(mask, 2);
const strHex = BitMask64Utils.toString(mask, 16);
// 第三个区段应该以100结尾(130位为1)
expect(strBin.split(' ')[2].endsWith('100')).toBe(true);
// 不存在高位的第三个区段字符串应为0x4
expect(strHex.split(' ')[2]).toBe('0x4');
// 设置第244位为1 这是第四个区段的第(256 - 244 =)12位
BitMask64Utils.setBit(mask, 244);
// 四个区段的在二进制下第12位的字符串应为'1'
expect(BitMask64Utils.toString(mask, 2).split(' ')[3][11]).toBe('1');
// 第四个区段的十六进制下所有字符串应为'0x10000000000000',即二进制的'10000 00000000 00000000 00000000 00000000 00000000 00000000'
expect(BitMask64Utils.toString(mask, 16).split(' ')[3]).toBe('0x10000000000000');
});
});
@@ -0,0 +1,75 @@
// FlatHashMap.test.ts
import { BitMaskHashMap } from "../../../src/ECS/Utils/BitMaskHashMap";
import { BitMask64Data, BitMask64Utils } from "../../../src";
describe("FlatHashMap 基础功能", () => {
test("set/get/has/delete 基本操作", () => {
const map = new BitMaskHashMap<number>();
const keyA = BitMask64Utils.create(5);
const keyB = BitMask64Utils.create(63);
map.set(keyA, 100);
map.set(keyB, 200);
expect(map.size).toBe(2);
expect(map.get(keyA)).toBe(100);
expect(map.get(keyB)).toBe(200);
expect(map.has(keyA)).toBe(true);
map.delete(keyA);
expect(map.has(keyA)).toBe(false);
expect(map.size).toBe(1);
map.clear();
expect(map.size).toBe(0);
});
test("覆盖 set 应该更新 value 而不是新增", () => {
const map = new BitMaskHashMap<string>();
const key = BitMask64Utils.create(10);
map.set(key, "foo");
map.set(key, "bar");
expect(map.size).toBe(1);
expect(map.get(key)).toBe("bar");
});
test("不同 key 产生相同 primaryHash 时应正确区分", () => {
const map = new BitMaskHashMap<number>();
// 伪造两个不同 key,理论上可能 hash 冲突
// 为了测试,我们直接用两个高位 bit(分段不同)
const keyA = BitMask64Utils.create(150);
const keyB = BitMask64Utils.create(300);
map.set(keyA, 111);
map.set(keyB, 222);
expect(map.get(keyA)).toBe(111);
expect(map.get(keyB)).toBe(222);
expect(map.size).toBe(2);
});
test("100000 个掩码连续的 key 不应存在冲突", () => {
const map = new BitMaskHashMap<number>();
const count = 100000;
const mask: BitMask64Data = { base: [0,0] };
for (let i = 0; i < count; i++) {
let temp = i;
// 遍历 i 的二进制表示的每一位
let bitIndex = 0;
while (temp > 0) {
if (temp & 1) {
BitMask64Utils.setBit(mask, bitIndex);
}
temp = temp >>> 1; // 无符号右移一位,检查下一位
bitIndex++;
}
map.set(mask,1);
}
// 预计没有任何冲突,每一个元素都在单独的桶中。
expect(map.innerBuckets.size).toBe(map.size);
});
});
@@ -0,0 +1,559 @@
import { Bits } from '../../../src/ECS/Utils/Bits';
import { BitMask64Utils } from '../../../src/ECS/Utils/BigIntCompatibility';
describe('Bits - 高性能位操作类测试', () => {
let bits: Bits;
beforeEach(() => {
bits = new Bits();
});
describe('基本构造和初始化', () => {
it('应该能够创建空的Bits对象', () => {
expect(bits).toBeDefined();
expect(bits.isEmpty()).toBe(true);
expect(BitMask64Utils.isZero(bits.getValue())).toBe(true);
});
it('应该能够使用初始值创建Bits对象', () => {
const bitsWithValue = new Bits(5); // 二进制: 101
expect(bitsWithValue.toHexString()).toBe('0x5');
expect(bitsWithValue.isEmpty()).toBe(false);
expect(bitsWithValue.get(0)).toBe(true); // 第0位
expect(bitsWithValue.get(1)).toBe(false); // 第1位
expect(bitsWithValue.get(2)).toBe(true); // 第2位
});
it('默认构造函数应该创建值为0的对象', () => {
const defaultBits = new Bits();
expect(BitMask64Utils.isZero(defaultBits.getValue())).toBe(true);
});
});
describe('位设置和清除操作', () => {
it('应该能够设置指定位置的位', () => {
bits.set(0);
expect(bits.get(0)).toBe(true);
expect(bits.toHexString()).toBe('0x1');
bits.set(3);
expect(bits.get(3)).toBe(true);
expect(bits.toHexString()).toBe('0x9'); // 1001 in binary
});
it('应该能够清除指定位置的位', () => {
bits.set(0);
bits.set(1);
bits.set(2);
expect(bits.toHexString()).toBe('0x7'); // 111 in binary
bits.clear(1);
expect(bits.get(1)).toBe(false);
expect(bits.toHexString()).toBe('0x5'); // 101 in binary
});
it('重复设置同一位应该保持不变', () => {
bits.set(0);
const value1 = bits.getValue();
bits.set(0);
const value2 = bits.getValue();
expect(BitMask64Utils.equals(value1, value2)).toBe(true);
});
it('清除未设置的位应该安全', () => {
bits.clear(5);
expect(BitMask64Utils.isZero(bits.getValue())).toBe(true);
});
it('设置负索引应该抛出错误', () => {
expect(() => {
bits.set(-1);
}).toThrow('Bit index cannot be negative');
});
it('清除负索引应该抛出错误', () => {
expect(() => {
bits.clear(-1);
}).toThrow('Bit index cannot be negative');
});
});
describe('位获取操作', () => {
beforeEach(() => {
bits.set(0);
bits.set(2);
bits.set(4);
});
it('应该能够正确获取设置的位', () => {
expect(bits.get(0)).toBe(true);
expect(bits.get(2)).toBe(true);
expect(bits.get(4)).toBe(true);
});
it('应该能够正确获取未设置的位', () => {
expect(bits.get(1)).toBe(false);
expect(bits.get(3)).toBe(false);
expect(bits.get(5)).toBe(false);
});
it('获取负索引应该返回false', () => {
expect(bits.get(-1)).toBe(false);
expect(bits.get(-10)).toBe(false);
});
it('获取超大索引应该正确处理', () => {
expect(bits.get(1000)).toBe(false);
});
});
describe('位运算操作', () => {
let otherBits: Bits;
beforeEach(() => {
bits.set(0);
bits.set(2);
bits.set(4); // 10101 in binary = 21
otherBits = new Bits();
otherBits.set(1);
otherBits.set(2);
otherBits.set(3); // 1110 in binary = 14
});
it('AND运算应该正确', () => {
const result = bits.and(otherBits);
expect(result.toHexString()).toBe('0x4'); // 10101 & 01110 = 00100 = 4
expect(result.get(2)).toBe(true);
expect(result.get(0)).toBe(false);
expect(result.get(1)).toBe(false);
});
it('OR运算应该正确', () => {
const result = bits.or(otherBits);
expect(result.toHexString()).toBe('0x1F'); // 10101 | 01110 = 11111 = 31
expect(result.get(0)).toBe(true);
expect(result.get(1)).toBe(true);
expect(result.get(2)).toBe(true);
expect(result.get(3)).toBe(true);
expect(result.get(4)).toBe(true);
});
it('XOR运算应该正确', () => {
const result = bits.xor(otherBits);
expect(result.toHexString()).toBe('0x1B'); // 10101 ^ 01110 = 11011 = 27
expect(result.get(0)).toBe(true);
expect(result.get(1)).toBe(true);
expect(result.get(2)).toBe(false); // 相同位XOR为0
expect(result.get(3)).toBe(true);
expect(result.get(4)).toBe(true);
});
it('NOT运算应该正确', () => {
const simpleBits = new Bits(5); // 101 in binary
const result = simpleBits.not(8); // 限制为8位
expect(result.toHexString()).toBe('0xFA'); // ~00000101 = 11111010 = 250 (8位)
});
it('NOT运算默认64位应该正确', () => {
const simpleBits = new Bits(1);
const result = simpleBits.not();
// 64位全1减去最低位,但由于JavaScript数字精度问题,我们检查特定位
expect(result.get(0)).toBe(false); // 位0应该是0
expect(result.get(1)).toBe(true); // 位1应该是1
expect(result.get(63)).toBe(true); // 最高位应该是1
});
});
describe('包含性检查', () => {
let otherBits: Bits;
beforeEach(() => {
bits.set(0);
bits.set(2);
bits.set(4); // 10101
otherBits = new Bits();
});
it('containsAll应该正确检查包含所有位', () => {
otherBits.set(0);
otherBits.set(2); // 101
expect(bits.containsAll(otherBits)).toBe(true);
otherBits.set(1); // 111
expect(bits.containsAll(otherBits)).toBe(false);
});
it('intersects应该正确检查交集', () => {
otherBits.set(1);
otherBits.set(3); // 1010
expect(bits.intersects(otherBits)).toBe(false);
otherBits.set(0); // 1011
expect(bits.intersects(otherBits)).toBe(true);
});
it('excludes应该正确检查互斥', () => {
otherBits.set(1);
otherBits.set(3); // 1010
expect(bits.excludes(otherBits)).toBe(true);
otherBits.set(0); // 1011
expect(bits.excludes(otherBits)).toBe(false);
});
it('空Bits对象的包含性检查', () => {
const emptyBits = new Bits();
expect(bits.containsAll(emptyBits)).toBe(true);
expect(bits.intersects(emptyBits)).toBe(false);
expect(bits.excludes(emptyBits)).toBe(true);
});
});
describe('状态检查和计数', () => {
it('isEmpty应该正确检查空状态', () => {
expect(bits.isEmpty()).toBe(true);
bits.set(0);
expect(bits.isEmpty()).toBe(false);
bits.clear(0);
expect(bits.isEmpty()).toBe(true);
});
it('cardinality应该正确计算设置的位数量', () => {
expect(bits.cardinality()).toBe(0);
bits.set(0);
expect(bits.cardinality()).toBe(1);
bits.set(2);
bits.set(4);
expect(bits.cardinality()).toBe(3);
bits.clear(2);
expect(bits.cardinality()).toBe(2);
});
it('最大64位的cardinality应该正确', () => {
// 设置很多位,但不超过64位限制
for (let i = 0; i < 64; i += 2) {
bits.set(i);
}
expect(bits.cardinality()).toBe(32); // 32个偶数位
});
});
describe('清空和重置操作', () => {
beforeEach(() => {
bits.set(0);
bits.set(1);
bits.set(2);
});
it('clearAll应该清空所有位', () => {
expect(bits.isEmpty()).toBe(false);
bits.clearAll();
expect(bits.isEmpty()).toBe(true);
expect(BitMask64Utils.isZero(bits.getValue())).toBe(true);
});
it('clearAll后应该能重新设置位', () => {
bits.clearAll();
bits.set(5);
expect(bits.get(5)).toBe(true);
expect(bits.cardinality()).toBe(1);
});
});
describe('复制和克隆操作', () => {
beforeEach(() => {
bits.set(1);
bits.set(3);
bits.set(5);
});
it('copyFrom应该正确复制另一个Bits对象', () => {
const newBits = new Bits();
newBits.copyFrom(bits);
expect(BitMask64Utils.equals(newBits.getValue(), bits.getValue())).toBe(true);
expect(newBits.equals(bits)).toBe(true);
});
it('clone应该创建相同的副本', () => {
const clonedBits = bits.clone();
expect(BitMask64Utils.equals(clonedBits.getValue(), bits.getValue())).toBe(true);
expect(clonedBits.equals(bits)).toBe(true);
expect(clonedBits).not.toBe(bits); // 应该是不同的对象
});
it('修改克隆对象不应该影响原对象', () => {
const clonedBits = bits.clone();
clonedBits.set(7);
expect(bits.get(7)).toBe(false);
expect(clonedBits.get(7)).toBe(true);
});
});
describe('值操作', () => {
it('getValue和setValue应该正确工作', () => {
bits.setValue(42);
expect(bits.toHexString()).toBe('0x2A');
});
it('setValue应该正确反映在位操作中', () => {
bits.setValue(5); // 101 in binary
expect(bits.get(0)).toBe(true);
expect(bits.get(1)).toBe(false);
expect(bits.get(2)).toBe(true);
});
it('setValue为0应该清空所有位', () => {
bits.set(1);
bits.set(2);
bits.setValue(0);
expect(bits.isEmpty()).toBe(true);
});
});
describe('字符串表示和解析', () => {
beforeEach(() => {
bits.set(0);
bits.set(2);
bits.set(4); // 10101 = 21
});
it('toString应该返回可读的位表示', () => {
const str = bits.toString();
expect(str).toBe('Bits[0, 2, 4]');
});
it('空Bits的toString应该正确', () => {
const emptyBits = new Bits();
expect(emptyBits.toString()).toBe('Bits[]');
});
it('toBinaryString应该返回正确的二进制表示', () => {
const binaryStr = bits.toBinaryString(8);
expect(binaryStr).toBe('00010101');
});
it('toBinaryString应该正确处理空格分隔', () => {
const binaryStr = bits.toBinaryString(16);
expect(binaryStr).toBe('00000000 00010101');
});
it('toHexString应该返回正确的十六进制表示', () => {
const hexStr = bits.toHexString();
expect(hexStr).toBe('0x15'); // 21 in hex
});
it('fromBinaryString应该正确解析', () => {
const parsedBits = Bits.fromBinaryString('10101');
expect(parsedBits.toHexString()).toBe('0x15');
expect(parsedBits.equals(bits)).toBe(true);
});
it('fromBinaryString应该处理带空格的字符串', () => {
const parsedBits = Bits.fromBinaryString('0001 0101');
expect(parsedBits.toHexString()).toBe('0x15');
});
it('fromHexString应该正确解析', () => {
const parsedBits = Bits.fromHexString('0x15');
expect(parsedBits.toHexString()).toBe('0x15');
expect(parsedBits.equals(bits)).toBe(true);
});
it('fromHexString应该处理不带0x前缀的字符串', () => {
const parsedBits = Bits.fromHexString('15');
expect(parsedBits.toHexString()).toBe('0x15');
});
});
describe('比较操作', () => {
let otherBits: Bits;
beforeEach(() => {
bits.set(0);
bits.set(2);
otherBits = new Bits();
});
it('equals应该正确比较相等的Bits', () => {
otherBits.set(0);
otherBits.set(2);
expect(bits.equals(otherBits)).toBe(true);
});
it('equals应该正确比较不相等的Bits', () => {
otherBits.set(0);
otherBits.set(1);
expect(bits.equals(otherBits)).toBe(false);
});
it('空Bits对象应该相等', () => {
const emptyBits1 = new Bits();
const emptyBits2 = new Bits();
expect(emptyBits1.equals(emptyBits2)).toBe(true);
});
});
describe('索引查找操作', () => {
it('getHighestBitIndex应该返回最高设置位的索引', () => {
bits.set(0);
bits.set(5);
bits.set(10);
expect(bits.getHighestBitIndex()).toBe(10);
});
it('getLowestBitIndex应该返回最低设置位的索引', () => {
bits.set(3);
bits.set(7);
bits.set(1);
expect(bits.getLowestBitIndex()).toBe(1);
});
it('空Bits的索引查找应该返回-1', () => {
expect(bits.getHighestBitIndex()).toBe(-1);
expect(bits.getLowestBitIndex()).toBe(-1);
});
it('只有一个位设置时索引查找应该正确', () => {
bits.set(5);
expect(bits.getHighestBitIndex()).toBe(5);
expect(bits.getLowestBitIndex()).toBe(5);
});
});
describe('64位边界处理', () => {
it('应该正确处理64位边界', () => {
bits.set(63); // 最高位
expect(bits.get(63)).toBe(true);
expect(bits.cardinality()).toBe(1);
});
it('超过64位索引应该自动扩展', () => {
// 现在支持自动扩展到 128/256 位
expect(() => bits.set(64)).not.toThrow();
expect(() => bits.set(100)).not.toThrow();
// 验证扩展后的位可以正确读取
expect(bits.get(64)).toBe(true);
expect(bits.get(100)).toBe(true);
expect(bits.get(65)).toBe(false);
});
it('64位范围内的位运算应该正确', () => {
const bits1 = new Bits();
const bits2 = new Bits();
bits1.set(10);
bits1.set(20);
bits2.set(10);
bits2.set(30);
const result = bits1.and(bits2);
expect(result.get(10)).toBe(true);
expect(result.get(20)).toBe(false);
expect(result.get(30)).toBe(false);
});
});
describe('性能测试', () => {
it('64位范围内的位设置操作应该高效', () => {
const startTime = performance.now();
// 只在64位范围内设置
for (let i = 0; i < 64; i++) {
bits.set(i);
}
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(100); // 应该在100ms内完成
expect(bits.cardinality()).toBe(64);
});
it('64位范围内的位查询操作应该高效', () => {
// 先设置一些位
for (let i = 0; i < 64; i += 2) {
bits.set(i);
}
const startTime = performance.now();
let trueCount = 0;
for (let i = 0; i < 64; i++) {
if (bits.get(i)) {
trueCount++;
}
}
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(10); // 应该在10ms内完成
expect(trueCount).toBe(32); // 32个偶数位
});
it('位运算操作应该高效', () => {
const otherBits = new Bits();
// 设置一些位,限制在64位范围内
for (let i = 0; i < 32; i++) {
bits.set(i * 2); // 偶数位
otherBits.set(i * 2 + 1); // 奇数位
}
const startTime = performance.now();
for (let i = 0; i < 1000; i++) {
const result = bits.or(otherBits);
}
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(50); // 应该在50ms内完成
});
});
describe('边界情况和错误处理', () => {
it('应该处理0值的各种操作', () => {
const zeroBits = new Bits(0);
expect(zeroBits.isEmpty()).toBe(true);
expect(zeroBits.cardinality()).toBe(0);
expect(zeroBits.getHighestBitIndex()).toBe(-1);
expect(zeroBits.getLowestBitIndex()).toBe(-1);
});
it('应该处理最大BigInt值', () => {
const maxBits = new Bits(Number.MAX_SAFE_INTEGER);
expect(maxBits.isEmpty()).toBe(false);
expect(maxBits.cardinality()).toBeGreaterThan(0);
});
it('位操作的结果应该是新对象', () => {
bits.set(0);
const otherBits = new Bits();
otherBits.set(1);
const result = bits.or(otherBits);
expect(result).not.toBe(bits);
expect(result).not.toBe(otherBits);
});
it('连续的设置和清除操作应该正确', () => {
for (let i = 0; i < 64; i++) {
bits.set(i);
expect(bits.get(i)).toBe(true);
bits.clear(i);
expect(bits.get(i)).toBe(false);
}
expect(bits.isEmpty()).toBe(true);
});
});
});
@@ -0,0 +1,399 @@
import { ComponentSparseSet } from '../../../src/ECS/Utils/ComponentSparseSet';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件类
@ECSComponent('SparseSet_PositionComponent')
class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
@ECSComponent('SparseSet_VelocityComponent')
class VelocityComponent extends Component {
constructor(public dx: number = 0, public dy: number = 0) {
super();
}
}
@ECSComponent('SparseSet_HealthComponent')
class HealthComponent extends Component {
constructor(public health: number = 100, public maxHealth: number = 100) {
super();
}
}
@ECSComponent('SparseSet_RenderComponent')
class RenderComponent extends Component {
constructor(public visible: boolean = true) {
super();
}
}
describe('ComponentSparseSet', () => {
let componentSparseSet: ComponentSparseSet;
let entity1: Entity;
let entity2: Entity;
let entity3: Entity;
let scene: Scene;
beforeEach(() => {
componentSparseSet = new ComponentSparseSet();
scene = new Scene();
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 HealthComponent(80, 100));
entity3 = scene.createEntity('entity3');
entity3.addComponent(new VelocityComponent(3, 4));
entity3.addComponent(new HealthComponent(50, 100));
entity3.addComponent(new RenderComponent(true));
});
describe('基本实体操作', () => {
it('应该能添加实体', () => {
componentSparseSet.addEntity(entity1);
expect(componentSparseSet.size).toBe(1);
expect(componentSparseSet.getAllEntities()).toContain(entity1);
});
it('应该能移除实体', () => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
componentSparseSet.removeEntity(entity1);
expect(componentSparseSet.size).toBe(1);
expect(componentSparseSet.getAllEntities()).not.toContain(entity1);
expect(componentSparseSet.getAllEntities()).toContain(entity2);
});
it('应该处理重复添加实体', () => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity1);
expect(componentSparseSet.size).toBe(1);
});
it('应该处理移除不存在的实体', () => {
componentSparseSet.removeEntity(entity1);
expect(componentSparseSet.size).toBe(0);
});
});
describe('单组件查询', () => {
beforeEach(() => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
componentSparseSet.addEntity(entity3);
});
it('应该能查询Position组件', () => {
const entities = componentSparseSet.queryByComponent(PositionComponent);
expect(entities.size).toBe(2);
expect(entities.has(entity1)).toBe(true);
expect(entities.has(entity2)).toBe(true);
expect(entities.has(entity3)).toBe(false);
});
it('应该能查询Velocity组件', () => {
const entities = componentSparseSet.queryByComponent(VelocityComponent);
expect(entities.size).toBe(2);
expect(entities.has(entity1)).toBe(true);
expect(entities.has(entity2)).toBe(false);
expect(entities.has(entity3)).toBe(true);
});
it('应该能查询Health组件', () => {
const entities = componentSparseSet.queryByComponent(HealthComponent);
expect(entities.size).toBe(2);
expect(entities.has(entity1)).toBe(false);
expect(entities.has(entity2)).toBe(true);
expect(entities.has(entity3)).toBe(true);
});
it('应该能查询Render组件', () => {
const entities = componentSparseSet.queryByComponent(RenderComponent);
expect(entities.size).toBe(1);
expect(entities.has(entity3)).toBe(true);
});
});
describe('多组件AND查询', () => {
beforeEach(() => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
componentSparseSet.addEntity(entity3);
});
it('应该能查询Position+Velocity组件', () => {
const entities = componentSparseSet.queryMultipleAnd([PositionComponent, VelocityComponent]);
expect(entities.size).toBe(1);
expect(entities.has(entity1)).toBe(true);
});
it('应该能查询Position+Health组件', () => {
const entities = componentSparseSet.queryMultipleAnd([PositionComponent, HealthComponent]);
expect(entities.size).toBe(1);
expect(entities.has(entity2)).toBe(true);
});
it('应该能查询Velocity+Health组件', () => {
const entities = componentSparseSet.queryMultipleAnd([VelocityComponent, HealthComponent]);
expect(entities.size).toBe(1);
expect(entities.has(entity3)).toBe(true);
});
it('应该能查询三个组件', () => {
const entities = componentSparseSet.queryMultipleAnd([
VelocityComponent,
HealthComponent,
RenderComponent
]);
expect(entities.size).toBe(1);
expect(entities.has(entity3)).toBe(true);
});
it('应该处理不存在的组合', () => {
const entities = componentSparseSet.queryMultipleAnd([
PositionComponent,
VelocityComponent,
HealthComponent,
RenderComponent
]);
expect(entities.size).toBe(0);
});
});
describe('多组件OR查询', () => {
beforeEach(() => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
componentSparseSet.addEntity(entity3);
});
it('应该能查询Position或Velocity组件', () => {
const entities = componentSparseSet.queryMultipleOr([PositionComponent, VelocityComponent]);
expect(entities.size).toBe(3);
expect(entities.has(entity1)).toBe(true);
expect(entities.has(entity2)).toBe(true);
expect(entities.has(entity3)).toBe(true);
});
it('应该能查询Health或Render组件', () => {
const entities = componentSparseSet.queryMultipleOr([HealthComponent, RenderComponent]);
expect(entities.size).toBe(2);
expect(entities.has(entity1)).toBe(false);
expect(entities.has(entity2)).toBe(true);
expect(entities.has(entity3)).toBe(true);
});
it('应该处理单个组件的OR查询', () => {
const entities = componentSparseSet.queryMultipleOr([RenderComponent]);
expect(entities.size).toBe(1);
expect(entities.has(entity3)).toBe(true);
});
});
describe('组件检查', () => {
beforeEach(() => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
});
it('应该能检查实体是否有组件', () => {
expect(componentSparseSet.hasComponent(entity1, PositionComponent)).toBe(true);
expect(componentSparseSet.hasComponent(entity1, VelocityComponent)).toBe(true);
expect(componentSparseSet.hasComponent(entity1, HealthComponent)).toBe(false);
expect(componentSparseSet.hasComponent(entity2, PositionComponent)).toBe(true);
expect(componentSparseSet.hasComponent(entity2, HealthComponent)).toBe(true);
expect(componentSparseSet.hasComponent(entity2, VelocityComponent)).toBe(false);
});
it('应该处理不存在的实体', () => {
expect(componentSparseSet.hasComponent(entity3, PositionComponent)).toBe(false);
});
});
describe('位掩码操作', () => {
beforeEach(() => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
});
it('应该能获取实体的组件位掩码', () => {
const mask1 = componentSparseSet.getEntityMask(entity1);
const mask2 = componentSparseSet.getEntityMask(entity2);
expect(mask1).toBeDefined();
expect(mask2).toBeDefined();
expect(mask1).not.toEqual(mask2);
});
it('应该处理不存在的实体', () => {
const mask = componentSparseSet.getEntityMask(entity3);
expect(mask).toBeUndefined();
});
});
describe('遍历操作', () => {
beforeEach(() => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
});
it('应该能遍历所有实体', () => {
const entities: Entity[] = [];
const masks: any[] = [];
const indices: number[] = [];
componentSparseSet.forEach((entity, mask, index) => {
entities.push(entity);
masks.push(mask);
indices.push(index);
});
expect(entities.length).toBe(2);
expect(masks.length).toBe(2);
expect(indices).toEqual([0, 1]);
expect(entities).toContain(entity1);
expect(entities).toContain(entity2);
});
});
describe('工具方法', () => {
it('应该能检查空状态', () => {
expect(componentSparseSet.isEmpty).toBe(true);
componentSparseSet.addEntity(entity1);
expect(componentSparseSet.isEmpty).toBe(false);
});
it('应该能清空数据', () => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
componentSparseSet.clear();
expect(componentSparseSet.size).toBe(0);
expect(componentSparseSet.isEmpty).toBe(true);
});
it('应该提供内存统计', () => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
const stats = componentSparseSet.getMemoryStats();
expect(stats.entitiesMemory).toBeGreaterThan(0);
expect(stats.masksMemory).toBeGreaterThan(0);
expect(stats.mappingsMemory).toBeGreaterThan(0);
expect(stats.totalMemory).toBe(
stats.entitiesMemory + stats.masksMemory + stats.mappingsMemory
);
});
it('应该能验证数据结构完整性', () => {
componentSparseSet.addEntity(entity1);
componentSparseSet.addEntity(entity2);
componentSparseSet.removeEntity(entity1);
expect(componentSparseSet.validate()).toBe(true);
});
});
describe('边界情况', () => {
it('应该处理空查询', () => {
componentSparseSet.addEntity(entity1);
const andResult = componentSparseSet.queryMultipleAnd([]);
const orResult = componentSparseSet.queryMultipleOr([]);
expect(andResult.size).toBe(0);
expect(orResult.size).toBe(0);
});
it('应该处理未注册的组件类型', () => {
class UnknownComponent extends Component {}
componentSparseSet.addEntity(entity1);
const entities = componentSparseSet.queryByComponent(UnknownComponent);
expect(entities.size).toBe(0);
});
it('应该正确处理实体组件变化', () => {
// 添加实体
componentSparseSet.addEntity(entity1);
expect(componentSparseSet.hasComponent(entity1, PositionComponent)).toBe(true);
// 移除组件后重新添加实体
entity1.removeComponentByType(PositionComponent);
componentSparseSet.addEntity(entity1);
expect(componentSparseSet.hasComponent(entity1, PositionComponent)).toBe(false);
expect(componentSparseSet.hasComponent(entity1, VelocityComponent)).toBe(true);
});
});
describe('性能测试', () => {
it('应该处理大量实体操作', () => {
const entities: Entity[] = [];
// 创建大量实体
for (let i = 0; i < 1000; i++) {
const entity = scene.createEntity(`entity${i}`);
entity.addComponent(new PositionComponent(i, i));
if (i % 2 === 0) {
entity.addComponent(new VelocityComponent(1, 1));
}
if (i % 3 === 0) {
entity.addComponent(new HealthComponent(100, 100));
}
entities.push(entity);
componentSparseSet.addEntity(entity);
}
expect(componentSparseSet.size).toBe(1000);
// 查询性能测试
const positionEntities = componentSparseSet.queryByComponent(PositionComponent);
expect(positionEntities.size).toBe(1000);
const velocityEntities = componentSparseSet.queryByComponent(VelocityComponent);
expect(velocityEntities.size).toBe(500);
const healthEntities = componentSparseSet.queryByComponent(HealthComponent);
expect(healthEntities.size).toBeGreaterThan(300);
// AND查询
const posVelEntities = componentSparseSet.queryMultipleAnd([PositionComponent, VelocityComponent]);
expect(posVelEntities.size).toBe(500);
});
});
});
@@ -0,0 +1,330 @@
import { EntityList } from '../../../src/ECS/Utils/EntityList';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
class TestComponent extends Component {
public value: number = 0;
constructor(...args: unknown[]) {
super();
const [value = 0] = args as [number?];
this.value = value;
}
}
// Mock scene with identifier pool
function createMockScene() {
const recycledIds: number[] = [];
return {
identifierPool: {
checkIn: (id: number) => {
recycledIds.push(id);
},
getRecycledIds: () => recycledIds
}
};
}
// Mock entity
function createMockEntity(id: number, name: string = '', tag: number = 0, options?: { enabled?: boolean; isDestroyed?: boolean }): Entity {
const entity = {
id,
name,
tag,
enabled: options?.enabled ?? true,
isDestroyed: options?.isDestroyed ?? false,
destroy: jest.fn(),
hasComponent: jest.fn().mockReturnValue(false)
} as unknown as Entity;
return entity;
}
describe('EntityList', () => {
let entityList: EntityList;
let mockScene: ReturnType<typeof createMockScene>;
beforeEach(() => {
mockScene = createMockScene();
entityList = new EntityList(mockScene);
});
describe('add and remove', () => {
it('should add entity to list', () => {
const entity = createMockEntity(1, 'test');
entityList.add(entity);
expect(entityList.count).toBe(1);
expect(entityList.buffer[0]).toBe(entity);
});
it('should not add duplicate entity', () => {
const entity = createMockEntity(1, 'test');
entityList.add(entity);
entityList.add(entity);
expect(entityList.count).toBe(1);
});
it('should remove entity from list', () => {
const entity = createMockEntity(1, 'test');
entityList.add(entity);
entityList.remove(entity);
expect(entityList.count).toBe(0);
});
it('should recycle entity id on remove', () => {
const entity = createMockEntity(1, 'test');
entityList.add(entity);
entityList.remove(entity);
expect(mockScene.identifierPool.getRecycledIds()).toContain(1);
});
});
describe('findEntity methods', () => {
it('should find entity by name', () => {
const entity = createMockEntity(1, 'player');
entityList.add(entity);
const found = entityList.findEntity('player');
expect(found).toBe(entity);
});
it('should return null for non-existent name', () => {
const found = entityList.findEntity('nonexistent');
expect(found).toBeNull();
});
it('should find entity by id', () => {
const entity = createMockEntity(42, 'test');
entityList.add(entity);
const found = entityList.findEntityById(42);
expect(found).toBe(entity);
});
it('should return null for non-existent id', () => {
const found = entityList.findEntityById(999);
expect(found).toBeNull();
});
it('should find all entities by name', () => {
const entity1 = createMockEntity(1, 'enemy');
const entity2 = createMockEntity(2, 'enemy');
const entity3 = createMockEntity(3, 'player');
entityList.add(entity1);
entityList.add(entity2);
entityList.add(entity3);
const enemies = entityList.findEntitiesByName('enemy');
expect(enemies).toHaveLength(2);
expect(enemies).toContain(entity1);
expect(enemies).toContain(entity2);
});
it('should find entities by tag', () => {
const entity1 = createMockEntity(1, 'e1', 1);
const entity2 = createMockEntity(2, 'e2', 2);
const entity3 = createMockEntity(3, 'e3', 1);
entityList.add(entity1);
entityList.add(entity2);
entityList.add(entity3);
const tagged = entityList.findEntitiesByTag(1);
expect(tagged).toHaveLength(2);
expect(tagged).toContain(entity1);
expect(tagged).toContain(entity3);
});
it('should find entities with component', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
(entity1.hasComponent as jest.Mock).mockReturnValue(true);
(entity2.hasComponent as jest.Mock).mockReturnValue(false);
entityList.add(entity1);
entityList.add(entity2);
const withComponent = entityList.findEntitiesWithComponent(TestComponent);
expect(withComponent).toHaveLength(1);
expect(withComponent[0]).toBe(entity1);
});
});
describe('removeAllEntities', () => {
it('should remove all entities and clear indices', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
entityList.add(entity1);
entityList.add(entity2);
entityList.removeAllEntities();
expect(entityList.count).toBe(0);
expect(entityList.findEntityById(1)).toBeNull();
expect(entityList.findEntityById(2)).toBeNull();
});
it('should call destroy on all entities', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
entityList.add(entity1);
entityList.add(entity2);
entityList.removeAllEntities();
expect(entity1.destroy).toHaveBeenCalled();
expect(entity2.destroy).toHaveBeenCalled();
});
it('should recycle all entity ids', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
entityList.add(entity1);
entityList.add(entity2);
entityList.removeAllEntities();
const recycled = mockScene.identifierPool.getRecycledIds();
expect(recycled).toContain(1);
expect(recycled).toContain(2);
});
});
describe('reorderEntity', () => {
it('should reorder entity to new position', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
const entity3 = createMockEntity(3, 'e3');
entityList.add(entity1);
entityList.add(entity2);
entityList.add(entity3);
entityList.reorderEntity(3, 0);
expect(entityList.buffer[0]).toBe(entity3);
expect(entityList.buffer[1]).toBe(entity1);
expect(entityList.buffer[2]).toBe(entity2);
});
it('should clamp index to valid range', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
entityList.add(entity1);
entityList.add(entity2);
entityList.reorderEntity(1, 100);
expect(entityList.buffer[1]).toBe(entity1);
});
it('should do nothing for non-existent entity', () => {
const entity1 = createMockEntity(1, 'e1');
entityList.add(entity1);
entityList.reorderEntity(999, 0);
expect(entityList.buffer[0]).toBe(entity1);
});
it('should do nothing if already at target position', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
entityList.add(entity1);
entityList.add(entity2);
entityList.reorderEntity(1, 0);
expect(entityList.buffer[0]).toBe(entity1);
expect(entityList.buffer[1]).toBe(entity2);
});
});
describe('getStats', () => {
it('should return correct statistics', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2', 0, { enabled: false });
const entity3 = createMockEntity(3, 'e3', 0, { isDestroyed: true });
entityList.add(entity1);
entityList.add(entity2);
entityList.add(entity3);
const stats = entityList.getStats();
expect(stats.totalEntities).toBe(3);
expect(stats.activeEntities).toBe(1);
expect(stats.pendingAdd).toBe(0);
expect(stats.pendingRemove).toBe(0);
expect(stats.nameIndexSize).toBe(3);
});
});
describe('forEach methods', () => {
it('should iterate all entities with forEach', () => {
const entity1 = createMockEntity(1, 'e1');
const entity2 = createMockEntity(2, 'e2');
entityList.add(entity1);
entityList.add(entity2);
const visited: number[] = [];
entityList.forEach((entity) => {
visited.push(entity.id);
});
expect(visited).toEqual([1, 2]);
});
it('should iterate filtered entities with forEachWhere', () => {
const entity1 = createMockEntity(1, 'e1', 1);
const entity2 = createMockEntity(2, 'e2', 2);
const entity3 = createMockEntity(3, 'e3', 1);
entityList.add(entity1);
entityList.add(entity2);
entityList.add(entity3);
const visited: number[] = [];
entityList.forEachWhere(
(entity) => entity.tag === 1,
(entity) => visited.push(entity.id)
);
expect(visited).toEqual([1, 3]);
});
});
describe('name index management', () => {
it('should update name index when entity is removed', () => {
const entity1 = createMockEntity(1, 'shared');
const entity2 = createMockEntity(2, 'shared');
entityList.add(entity1);
entityList.add(entity2);
entityList.remove(entity1);
const found = entityList.findEntitiesByName('shared');
expect(found).toHaveLength(1);
expect(found[0]).toBe(entity2);
});
it('should handle entities without names', () => {
const entity = createMockEntity(1, '');
entityList.add(entity);
expect(entityList.count).toBe(1);
expect(entityList.findEntity('')).toBeNull();
});
});
describe('updateLists', () => {
it('should process pending operations via update', () => {
const entity = createMockEntity(1, 'test');
entityList.add(entity);
entityList.update();
expect(entityList.count).toBe(1);
});
});
});
@@ -0,0 +1,414 @@
/**
* IdentifierPool ID池测试
*
* ID的分配
*/
import { IdentifierPool } from '../../../src/ECS/Utils/IdentifierPool';
import { TestUtils } from '../../setup';
describe('IdentifierPool 世代式ID池测试', () => {
let pool: IdentifierPool;
beforeEach(() => {
pool = new IdentifierPool();
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
// 测试基本功能
describe('基本功能测试', () => {
test('应该能创建IdentifierPool实例', () => {
expect(pool).toBeDefined();
expect(pool).toBeInstanceOf(IdentifierPool);
});
test('应该能分配连续的ID', () => {
const id1 = pool.checkOut();
const id2 = pool.checkOut();
const id3 = pool.checkOut();
expect(id1).toBe(65536); // 世代1,索引0
expect(id2).toBe(65537); // 世代1,索引1
expect(id3).toBe(65538); // 世代1,索引2
});
test('应该能验证有效的ID', () => {
const id = pool.checkOut();
expect(pool.isValid(id)).toBe(true);
});
test('应该能获取统计信息', () => {
const id1 = pool.checkOut();
const id2 = pool.checkOut();
const stats = pool.getStats();
expect(stats.totalAllocated).toBe(2);
expect(stats.currentActive).toBe(2);
expect(stats.currentlyFree).toBe(0);
expect(stats.pendingRecycle).toBe(0);
expect(stats.maxPossibleEntities).toBe(65536); // 2^16
expect(stats.averageGeneration).toBe(1);
expect(stats.memoryUsage).toBeGreaterThan(0);
});
});
// 测试回收功能
describe('ID回收功能测试', () => {
test('应该能回收有效的ID', () => {
const id = pool.checkOut();
const result = pool.checkIn(id);
expect(result).toBe(true);
const stats = pool.getStats();
expect(stats.pendingRecycle).toBe(1);
expect(stats.currentActive).toBe(0);
});
test('应该拒绝回收无效的ID', () => {
const invalidId = 999999;
const result = pool.checkIn(invalidId);
expect(result).toBe(false);
const stats = pool.getStats();
expect(stats.pendingRecycle).toBe(0);
});
test('应该拒绝重复回收同一个ID', () => {
const id = pool.checkOut();
const firstResult = pool.checkIn(id);
const secondResult = pool.checkIn(id);
expect(firstResult).toBe(true);
expect(secondResult).toBe(false);
});
});
// 测试延迟回收
describe('延迟回收机制测试', () => {
test('应该支持延迟回收', () => {
const pool = new IdentifierPool(100); // 100ms延迟
const id = pool.checkOut();
pool.checkIn(id);
// 立即检查,ID应该还在延迟队列中
let stats = pool.getStats();
expect(stats.pendingRecycle).toBe(1);
expect(stats.currentlyFree).toBe(0);
// 模拟时间前进150ms
jest.advanceTimersByTime(150);
// 触发延迟回收处理(通过分配新ID)
pool.checkOut();
// 现在ID应该被真正回收了
stats = pool.getStats();
expect(stats.pendingRecycle).toBe(0);
expect(stats.currentlyFree).toBe(0); // 因为被重新分配了
});
test('延迟时间内ID应该仍然有效', () => {
const pool = new IdentifierPool(100);
const id = pool.checkOut();
pool.checkIn(id);
// 在延迟时间内,ID应该仍然有效
expect(pool.isValid(id)).toBe(true);
// 模拟时间前进150ms并触发处理
jest.advanceTimersByTime(150);
pool.checkOut(); // 触发延迟回收处理
// 现在ID应该无效了(世代已递增)
expect(pool.isValid(id)).toBe(false);
});
test('应该支持强制延迟回收处理', () => {
const id = pool.checkOut();
pool.checkIn(id);
// 在延迟时间内强制处理
pool.forceProcessDelayedRecycle();
// ID应该立即变为无效
expect(pool.isValid(id)).toBe(false);
const stats = pool.getStats();
expect(stats.pendingRecycle).toBe(0);
expect(stats.currentlyFree).toBe(1);
});
});
// 测试世代版本控制
describe('世代版本控制测试', () => {
test('回收后的ID应该增加世代版本', () => {
const pool = new IdentifierPool(0); // 无延迟,立即回收
const originalId = pool.checkOut();
pool.checkIn(originalId);
// 分配新ID触发回收处理
const newId = pool.checkOut();
// 原ID应该无效
expect(pool.isValid(originalId)).toBe(false);
// 新ID应该有不同的世代版本
expect(newId).not.toBe(originalId);
expect(newId).toBe(131072); // 世代2,索引0
});
test('应该能重用回收的索引', () => {
const pool = new IdentifierPool(0);
const id1 = pool.checkOut(); // 索引0
const id2 = pool.checkOut(); // 索引1
pool.checkIn(id1);
const id3 = pool.checkOut(); // 应该重用索引0,但世代递增
expect(id3 & 0xFFFF).toBe(0); // 索引部分应该是0
expect(id3 >> 16).toBe(2); // 世代应该是2
});
test('世代版本溢出应该重置为1', () => {
const pool = new IdentifierPool(0);
// 手动设置一个即将溢出的世代
const id = pool.checkOut();
// 通过反射访问私有成员来模拟溢出情况
const generations = (pool as any)._generations;
generations.set(0, 65535); // 设置为最大值
pool.checkIn(id);
const newId = pool.checkOut();
// 世代应该重置为1而不是0
expect(newId >> 16).toBe(1);
});
});
// 测试错误处理
describe('错误处理测试', () => {
test('超过最大索引数应该抛出错误', () => {
// 创建一个模拟的池,直接设置到达到限制
const pool = new IdentifierPool();
// 通过反射设置到达到限制(65536会触发错误)
(pool as any)._nextAvailableIndex = 65536;
expect(() => {
pool.checkOut();
}).toThrow('实体索引已达到框架设计限制');
});
test('应该能处理边界值', () => {
const pool = new IdentifierPool();
const id = pool.checkOut();
expect(id).toBe(65536); // 世代1,索引0
// 回收并重新分配
pool.checkIn(id);
jest.advanceTimersByTime(200);
const newId = pool.checkOut();
expect(newId).toBe(131072); // 世代2,索引0
});
});
// 测试动态扩展
describe('动态内存扩展测试', () => {
test('应该能动态扩展内存', () => {
const pool = new IdentifierPool(0, 10); // 小的扩展块用于测试
// 分配超过初始块大小的ID
const ids: number[] = [];
for (let i = 0; i < 25; i++) {
ids.push(pool.checkOut());
}
expect(ids.length).toBe(25);
// 验证所有ID都是唯一的
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(25);
// 检查内存扩展统计
const stats = pool.getStats();
expect(stats.memoryExpansions).toBeGreaterThan(1);
expect(stats.generationStorageSize).toBeGreaterThanOrEqual(25);
});
test('内存扩展应该按块进行', () => {
const blockSize = 5;
const pool = new IdentifierPool(0, blockSize);
// 分配第一个块
for (let i = 0; i < blockSize; i++) {
pool.checkOut();
}
let stats = pool.getStats();
const initialExpansions = stats.memoryExpansions;
// 分配一个会触发新块的ID
pool.checkOut();
stats = pool.getStats();
expect(stats.memoryExpansions).toBe(initialExpansions + 1);
});
});
// 测试性能和内存
describe('性能和内存测试', () => {
test('应该能处理大量ID分配', () => {
const count = 10000; // 增加测试规模
const ids: number[] = [];
const startTime = performance.now();
for (let i = 0; i < count; i++) {
ids.push(pool.checkOut());
}
const endTime = performance.now();
const duration = endTime - startTime;
expect(ids.length).toBe(count);
expect(duration).toBeLessThan(1000); // 10k个ID应该在1秒内完成
// 验证所有ID都是唯一的
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(count);
});
test('应该能处理大量回收操作', () => {
const count = 5000; // 增加测试规模
const ids: number[] = [];
// 分配ID
for (let i = 0; i < count; i++) {
ids.push(pool.checkOut());
}
// 回收ID
const startTime = performance.now();
for (const id of ids) {
pool.checkIn(id);
}
const endTime = performance.now();
const duration = endTime - startTime;
expect(duration).toBeLessThan(500); // 5k个回收应该在500ms内完成
const stats = pool.getStats();
expect(stats.pendingRecycle).toBe(count);
expect(stats.currentActive).toBe(0);
});
test('内存使用应该是合理的', () => {
const stats = pool.getStats();
const initialMemory = stats.memoryUsage;
// 分配大量ID
for (let i = 0; i < 5000; i++) {
pool.checkOut();
}
const newStats = pool.getStats();
const memoryIncrease = newStats.memoryUsage - initialMemory;
// 内存增长应该是合理的(动态分配应该更高效)
expect(memoryIncrease).toBeLessThan(5000 * 50); // 每个ID少于50字节
});
});
// 测试并发安全性(模拟)
describe('并发安全性测试', () => {
test('应该能处理并发分配', async () => {
const promises: Promise<number>[] = [];
// 模拟并发分配
for (let i = 0; i < 1000; i++) {
promises.push(Promise.resolve(pool.checkOut()));
}
const ids = await Promise.all(promises);
// 所有ID应该是唯一的
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(1000);
});
test('应该能处理并发回收', async () => {
const ids: number[] = [];
// 先分配一些ID
for (let i = 0; i < 500; i++) {
ids.push(pool.checkOut());
}
// 模拟并发回收
const promises = ids.map(id => Promise.resolve(pool.checkIn(id)));
const results = await Promise.all(promises);
// 所有回收操作都应该成功
expect(results.every(result => result === true)).toBe(true);
});
});
// 测试统计信息
describe('统计信息测试', () => {
test('统计信息应该准确反映池状态', () => {
// 分配一些ID
const ids = [pool.checkOut(), pool.checkOut(), pool.checkOut()];
let stats = pool.getStats();
expect(stats.totalAllocated).toBe(3);
expect(stats.currentActive).toBe(3);
expect(stats.currentlyFree).toBe(0);
expect(stats.pendingRecycle).toBe(0);
// 回收一个ID
pool.checkIn(ids[0]);
stats = pool.getStats();
expect(stats.totalRecycled).toBe(1);
expect(stats.currentActive).toBe(2);
expect(stats.pendingRecycle).toBe(1);
// 强制处理延迟回收
pool.forceProcessDelayedRecycle();
stats = pool.getStats();
expect(stats.pendingRecycle).toBe(0);
expect(stats.currentlyFree).toBe(1);
});
test('应该正确计算平均世代版本', () => {
const pool = new IdentifierPool(0); // 无延迟
// 分配、回收、再分配来增加世代
const id1 = pool.checkOut();
pool.checkIn(id1);
const id2 = pool.checkOut(); // 这会触发世代递增
const stats = pool.getStats();
expect(stats.averageGeneration).toBeGreaterThan(1);
});
});
});
@@ -0,0 +1,578 @@
import { Component } from '../../../src/ECS/Component';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ComponentType } from '../../../src/ECS/Core/ComponentStorage';
class Position extends Component {
public x: number = 0;
public y: number = 0;
constructor(...args: unknown[]) {
super();
const [x = 0, y = 0] = args as [number?, number?];
this.x = x;
this.y = y;
}
}
class Velocity extends Component {
public vx: number = 0;
public vy: number = 0;
constructor(...args: unknown[]) {
super();
const [vx = 0, vy = 0] = args as [number?, number?];
this.vx = vx;
this.vy = vy;
}
}
class Health extends Component {
public hp: number = 100;
constructor(...args: unknown[]) {
super();
const [hp = 100] = args as [number?];
this.hp = hp;
}
}
class Shield extends Component {
public value: number = 50;
constructor(...args: unknown[]) {
super();
const [value = 50] = args as [number?];
this.value = value;
}
}
class Dead extends Component {}
class Weapon extends Component {
public damage: number = 10;
constructor(...args: unknown[]) {
super();
const [damage = 10] = args as [number?];
this.damage = damage;
}
}
describe('Matcher', () => {
describe('静态工厂方法', () => {
test('all() 应该创建包含所有指定组件的条件', () => {
const matcher = Matcher.all(Position, Velocity);
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(2);
expect(condition.all).toContain(Position);
expect(condition.all).toContain(Velocity);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
});
test('any() 应该创建包含任一指定组件的条件', () => {
const matcher = Matcher.any(Health, Shield);
const condition = matcher.getCondition();
expect(condition.any).toHaveLength(2);
expect(condition.any).toContain(Health);
expect(condition.any).toContain(Shield);
expect(condition.all).toHaveLength(0);
expect(condition.none).toHaveLength(0);
});
test('none() 应该创建排除指定组件的条件', () => {
const matcher = Matcher.none(Dead, Weapon);
const condition = matcher.getCondition();
expect(condition.none).toHaveLength(2);
expect(condition.none).toContain(Dead);
expect(condition.none).toContain(Weapon);
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
});
test('byTag() 应该创建标签查询条件', () => {
const matcher = Matcher.byTag(123);
const condition = matcher.getCondition();
expect(condition.tag).toBe(123);
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
});
test('byName() 应该创建名称查询条件', () => {
const matcher = Matcher.byName('TestEntity');
const condition = matcher.getCondition();
expect(condition.name).toBe('TestEntity');
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
});
test('byComponent() 应该创建单组件查询条件', () => {
const matcher = Matcher.byComponent(Position);
const condition = matcher.getCondition();
expect(condition.component).toBe(Position);
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
});
test('complex() 应该创建空的复杂查询构建器', () => {
const matcher = Matcher.complex();
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
expect(condition.tag).toBeUndefined();
expect(condition.name).toBeUndefined();
expect(condition.component).toBeUndefined();
});
test('empty() 应该创建空匹配器', () => {
const matcher = Matcher.empty();
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
expect(condition.tag).toBeUndefined();
expect(condition.name).toBeUndefined();
expect(condition.component).toBeUndefined();
});
});
describe('实例方法', () => {
test('all() 应该添加到 all 条件数组', () => {
const matcher = Matcher.empty();
matcher.all(Position, Velocity);
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(2);
expect(condition.all).toContain(Position);
expect(condition.all).toContain(Velocity);
});
test('any() 应该添加到 any 条件数组', () => {
const matcher = Matcher.empty();
matcher.any(Health, Shield);
const condition = matcher.getCondition();
expect(condition.any).toHaveLength(2);
expect(condition.any).toContain(Health);
expect(condition.any).toContain(Shield);
});
test('none() 应该添加到 none 条件数组', () => {
const matcher = Matcher.empty();
matcher.none(Dead);
const condition = matcher.getCondition();
expect(condition.none).toHaveLength(1);
expect(condition.none).toContain(Dead);
});
test('exclude() 应该是 none() 的别名', () => {
const matcher = Matcher.empty();
matcher.exclude(Dead, Weapon);
const condition = matcher.getCondition();
expect(condition.none).toHaveLength(2);
expect(condition.none).toContain(Dead);
expect(condition.none).toContain(Weapon);
});
test('one() 应该是 any() 的别名', () => {
const matcher = Matcher.empty();
matcher.one(Health, Shield);
const condition = matcher.getCondition();
expect(condition.any).toHaveLength(2);
expect(condition.any).toContain(Health);
expect(condition.any).toContain(Shield);
});
});
describe('链式调用', () => {
test('应该支持复杂的链式调用', () => {
const matcher = Matcher.all(Position)
.any(Health, Shield)
.none(Dead)
.withTag(100)
.withName('Player');
const condition = matcher.getCondition();
expect(condition.all).toContain(Position);
expect(condition.any).toContain(Health);
expect(condition.any).toContain(Shield);
expect(condition.none).toContain(Dead);
expect(condition.tag).toBe(100);
expect(condition.name).toBe('Player');
});
test('多次调用同一方法应该累积组件', () => {
const matcher = Matcher.empty()
.all(Position)
.all(Velocity)
.any(Health)
.any(Shield)
.none(Dead)
.none(Weapon);
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(2);
expect(condition.any).toHaveLength(2);
expect(condition.none).toHaveLength(2);
});
});
describe('条件管理方法', () => {
test('withTag() 应该设置标签条件', () => {
const matcher = Matcher.empty().withTag(42);
const condition = matcher.getCondition();
expect(condition.tag).toBe(42);
});
test('withName() 应该设置名称条件', () => {
const matcher = Matcher.empty().withName('Enemy');
const condition = matcher.getCondition();
expect(condition.name).toBe('Enemy');
});
test('withComponent() 应该设置单组件条件', () => {
const matcher = Matcher.empty().withComponent(Position);
const condition = matcher.getCondition();
expect(condition.component).toBe(Position);
});
test('withoutTag() 应该移除标签条件', () => {
const matcher = Matcher.byTag(123).withoutTag();
const condition = matcher.getCondition();
expect(condition.tag).toBeUndefined();
});
test('withoutName() 应该移除名称条件', () => {
const matcher = Matcher.byName('Test').withoutName();
const condition = matcher.getCondition();
expect(condition.name).toBeUndefined();
});
test('withoutComponent() 应该移除单组件条件', () => {
const matcher = Matcher.byComponent(Position).withoutComponent();
const condition = matcher.getCondition();
expect(condition.component).toBeUndefined();
});
test('覆盖条件应该替换之前的值', () => {
const matcher = Matcher.byTag(100)
.withTag(200)
.withName('First')
.withName('Second')
.withComponent(Position)
.withComponent(Velocity);
const condition = matcher.getCondition();
expect(condition.tag).toBe(200);
expect(condition.name).toBe('Second');
expect(condition.component).toBe(Velocity);
});
});
describe('工具方法', () => {
test('isEmpty() 应该正确判断空条件', () => {
const emptyMatcher = Matcher.empty();
expect(emptyMatcher.isEmpty()).toBe(true);
const nonEmptyMatcher = Matcher.all(Position);
expect(nonEmptyMatcher.isEmpty()).toBe(false);
});
test('isEmpty() 应该检查所有条件类型', () => {
expect(Matcher.all(Position).isEmpty()).toBe(false);
expect(Matcher.any(Health).isEmpty()).toBe(false);
expect(Matcher.none(Dead).isEmpty()).toBe(false);
expect(Matcher.byTag(1).isEmpty()).toBe(false);
expect(Matcher.byName('test').isEmpty()).toBe(false);
expect(Matcher.byComponent(Position).isEmpty()).toBe(false);
});
test('reset() 应该清空所有条件', () => {
const matcher = Matcher.all(Position, Velocity)
.any(Health, Shield)
.none(Dead)
.withTag(123)
.withName('Test')
.withComponent(Weapon);
matcher.reset();
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
expect(condition.tag).toBeUndefined();
expect(condition.name).toBeUndefined();
expect(condition.component).toBeUndefined();
expect(matcher.isEmpty()).toBe(true);
});
test('clone() 应该创建独立的副本', () => {
const original = Matcher.all(Position, Velocity)
.any(Health)
.none(Dead)
.withTag(100)
.withName('Original')
.withComponent(Weapon);
const cloned = original.clone();
const originalCondition = original.getCondition();
const clonedCondition = cloned.getCondition();
expect(clonedCondition.all).toEqual(originalCondition.all);
expect(clonedCondition.any).toEqual(originalCondition.any);
expect(clonedCondition.none).toEqual(originalCondition.none);
expect(clonedCondition.tag).toBe(originalCondition.tag);
expect(clonedCondition.name).toBe(originalCondition.name);
expect(clonedCondition.component).toBe(originalCondition.component);
// 修改克隆不应影响原对象
cloned.all(Shield).withTag(200);
expect(original.getCondition().all).not.toContain(Shield);
expect(original.getCondition().tag).toBe(100);
});
test('toString() 应该生成可读的字符串表示', () => {
const matcher = Matcher.all(Position, Velocity)
.any(Health, Shield)
.none(Dead)
.withTag(123)
.withName('TestEntity')
.withComponent(Weapon);
const str = matcher.toString();
expect(str).toContain('all(Position, Velocity)');
expect(str).toContain('any(Health, Shield)');
expect(str).toContain('none(Dead)');
expect(str).toContain('tag(123)');
expect(str).toContain('name(TestEntity)');
expect(str).toContain('component(Weapon)');
expect(str).toContain('Matcher[');
expect(str).toContain(' & ');
});
test('toString() 应该处理空条件', () => {
const emptyMatcher = Matcher.empty();
const str = emptyMatcher.toString();
expect(str).toBe('Matcher[]');
});
test('toString() 应该处理部分条件', () => {
const matcher = Matcher.all(Position).withTag(42);
const str = matcher.toString();
expect(str).toContain('all(Position)');
expect(str).toContain('tag(42)');
expect(str).not.toContain('any(');
expect(str).not.toContain('none(');
});
});
describe('getCondition() 返回值', () => {
test('应该返回只读的条件副本', () => {
const matcher = Matcher.all(Position, Velocity);
const condition1 = matcher.getCondition();
const condition2 = matcher.getCondition();
// 应该是不同的对象实例
expect(condition1).not.toBe(condition2);
// 但内容应该相同
expect(condition1.all).toEqual(condition2.all);
});
test('修改返回的条件不应影响原 Matcher', () => {
const matcher = Matcher.all(Position);
const condition = matcher.getCondition();
// 尝试修改返回的条件
condition.all.push(Velocity as ComponentType);
// 原 Matcher 不应被影响
const freshCondition = matcher.getCondition();
expect(freshCondition.all).toHaveLength(1);
expect(freshCondition.all).toContain(Position);
expect(freshCondition.all).not.toContain(Velocity);
});
});
describe('边界情况', () => {
test('应该处理空参数调用', () => {
const matcher = Matcher.empty();
matcher.all();
matcher.any();
matcher.none();
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
});
test('应该处理重复的组件类型', () => {
const matcher = Matcher.all(Position, Position, Velocity);
const condition = matcher.getCondition();
expect(condition.all).toHaveLength(3);
expect(condition.all.filter(t => t === Position)).toHaveLength(2);
});
test('应该处理标签值为0的情况', () => {
const matcher = Matcher.byTag(0);
const condition = matcher.getCondition();
expect(condition.tag).toBe(0);
expect(matcher.isEmpty()).toBe(false);
});
test('应该处理空字符串名称', () => {
const matcher = Matcher.byName('');
const condition = matcher.getCondition();
expect(condition.name).toBe('');
expect(matcher.isEmpty()).toBe(false);
});
test('reset() 应该返回自身以支持链式调用', () => {
const matcher = Matcher.all(Position);
const result = matcher.reset();
expect(result).toBe(matcher);
});
test('所有实例方法都应该返回自身以支持链式调用', () => {
const matcher = Matcher.empty();
expect(matcher.all(Position)).toBe(matcher);
expect(matcher.any(Health)).toBe(matcher);
expect(matcher.none(Dead)).toBe(matcher);
expect(matcher.exclude(Weapon)).toBe(matcher);
expect(matcher.one(Shield)).toBe(matcher);
expect(matcher.withTag(1)).toBe(matcher);
expect(matcher.withName('test')).toBe(matcher);
expect(matcher.withComponent(Position)).toBe(matcher);
expect(matcher.withoutTag()).toBe(matcher);
expect(matcher.withoutName()).toBe(matcher);
expect(matcher.withoutComponent()).toBe(matcher);
});
});
describe('nothing() 匹配器', () => {
test('nothing() 应该创建不匹配任何实体的匹配器', () => {
const matcher = Matcher.nothing();
const condition = matcher.getCondition();
expect(condition.matchNothing).toBe(true);
expect(condition.all).toHaveLength(0);
expect(condition.any).toHaveLength(0);
expect(condition.none).toHaveLength(0);
});
test('isNothing() 应该正确判断 nothing 匹配器', () => {
const nothingMatcher = Matcher.nothing();
expect(nothingMatcher.isNothing()).toBe(true);
const normalMatcher = Matcher.all(Position);
expect(normalMatcher.isNothing()).toBe(false);
const emptyMatcher = Matcher.empty();
expect(emptyMatcher.isNothing()).toBe(false);
});
test('isEmpty() 不应该将 nothing 匹配器视为空', () => {
const nothingMatcher = Matcher.nothing();
// nothing 匹配器有明确的语义,不应该算作空
expect(nothingMatcher.isEmpty()).toBe(false);
});
test('toString() 应该正确处理 nothing 匹配器', () => {
const matcher = Matcher.nothing();
const str = matcher.toString();
expect(str).toBe('Matcher[nothing]');
});
test('clone() 应该正确复制 nothing 匹配器', () => {
const original = Matcher.nothing();
const cloned = original.clone();
expect(cloned.isNothing()).toBe(true);
expect(cloned.getCondition().matchNothing).toBe(true);
});
test('reset() 应该清除 matchNothing 标志', () => {
const matcher = Matcher.nothing();
expect(matcher.isNothing()).toBe(true);
matcher.reset();
expect(matcher.isNothing()).toBe(false);
expect(matcher.isEmpty()).toBe(true);
});
});
describe('类型安全性', () => {
test('ComponentType 应该正确工作', () => {
// 这个测试主要是确保类型编译正确
const matcher = Matcher.all(Position as ComponentType<Position>);
const condition = matcher.getCondition();
expect(condition.all).toContain(Position);
});
test('应该支持泛型组件类型', () => {
class GenericComponent<T> extends Component {
public data: T;
constructor(data: T, ...args: unknown[]) {
super();
this.data = data;
}
}
class StringComponent extends GenericComponent<string> {
constructor(...args: unknown[]) {
super(args[0] as string || 'default');
}
}
class NumberComponent extends GenericComponent<number> {
constructor(...args: unknown[]) {
super(args[0] as number || 0);
}
}
const matcher = Matcher.all(StringComponent, NumberComponent);
const condition = matcher.getCondition();
expect(condition.all).toContain(StringComponent);
expect(condition.all).toContain(NumberComponent);
});
});
});
@@ -0,0 +1,241 @@
import { SparseSet } from '../../../src/ECS/Utils/SparseSet';
describe('SparseSet', () => {
let sparseSet: SparseSet<number>;
beforeEach(() => {
sparseSet = new SparseSet<number>();
});
describe('基本操作', () => {
it('应该能添加元素', () => {
expect(sparseSet.add(1)).toBe(true);
expect(sparseSet.add(2)).toBe(true);
expect(sparseSet.size).toBe(2);
});
it('应该防止重复添加', () => {
expect(sparseSet.add(1)).toBe(true);
expect(sparseSet.add(1)).toBe(false);
expect(sparseSet.size).toBe(1);
});
it('应该能移除元素', () => {
sparseSet.add(1);
sparseSet.add(2);
expect(sparseSet.remove(1)).toBe(true);
expect(sparseSet.size).toBe(1);
expect(sparseSet.has(1)).toBe(false);
expect(sparseSet.has(2)).toBe(true);
});
it('应该处理移除不存在的元素', () => {
expect(sparseSet.remove(99)).toBe(false);
});
it('应该能检查元素存在性', () => {
sparseSet.add(42);
expect(sparseSet.has(42)).toBe(true);
expect(sparseSet.has(99)).toBe(false);
});
});
describe('索引操作', () => {
it('应该返回正确的索引', () => {
sparseSet.add(10);
sparseSet.add(20);
sparseSet.add(30);
expect(sparseSet.getIndex(10)).toBe(0);
expect(sparseSet.getIndex(20)).toBe(1);
expect(sparseSet.getIndex(30)).toBe(2);
});
it('应该能根据索引获取元素', () => {
sparseSet.add(100);
sparseSet.add(200);
expect(sparseSet.getByIndex(0)).toBe(100);
expect(sparseSet.getByIndex(1)).toBe(200);
expect(sparseSet.getByIndex(999)).toBeUndefined();
});
it('移除中间元素后应该保持紧凑性', () => {
sparseSet.add(1);
sparseSet.add(2);
sparseSet.add(3);
// 移除中间元素
sparseSet.remove(2);
// 最后一个元素应该移动到中间
expect(sparseSet.getByIndex(0)).toBe(1);
expect(sparseSet.getByIndex(1)).toBe(3);
expect(sparseSet.size).toBe(2);
});
});
describe('遍历操作', () => {
beforeEach(() => {
sparseSet.add(10);
sparseSet.add(20);
sparseSet.add(30);
});
it('应该能正确遍历', () => {
const items: number[] = [];
const indices: number[] = [];
sparseSet.forEach((item, index) => {
items.push(item);
indices.push(index);
});
expect(items).toEqual([10, 20, 30]);
expect(indices).toEqual([0, 1, 2]);
});
it('应该能映射元素', () => {
const doubled = sparseSet.map(x => x * 2);
expect(doubled).toEqual([20, 40, 60]);
});
it('应该能过滤元素', () => {
const filtered = sparseSet.filter(x => x > 15);
expect(filtered).toEqual([20, 30]);
});
it('应该能查找元素', () => {
const found = sparseSet.find(x => x > 15);
expect(found).toBe(20);
const notFound = sparseSet.find(x => x > 100);
expect(notFound).toBeUndefined();
});
it('应该能检查存在性', () => {
expect(sparseSet.some(x => x > 25)).toBe(true);
expect(sparseSet.some(x => x > 100)).toBe(false);
});
it('应该能检查全部条件', () => {
expect(sparseSet.every(x => x > 0)).toBe(true);
expect(sparseSet.every(x => x > 15)).toBe(false);
});
});
describe('数据获取', () => {
beforeEach(() => {
sparseSet.add(1);
sparseSet.add(2);
sparseSet.add(3);
});
it('应该返回只读数组副本', () => {
const array = sparseSet.getDenseArray();
expect(array).toEqual([1, 2, 3]);
// 尝试修改应该不影响原数据
expect(() => {
(array as any).push(4);
}).not.toThrow();
expect(sparseSet.size).toBe(3);
});
it('应该能转换为数组', () => {
const array = sparseSet.toArray();
expect(array).toEqual([1, 2, 3]);
});
it('应该能转换为Set', () => {
const set = sparseSet.toSet();
expect(set).toEqual(new Set([1, 2, 3]));
});
});
describe('工具方法', () => {
it('应该能检查空状态', () => {
expect(sparseSet.isEmpty).toBe(true);
sparseSet.add(1);
expect(sparseSet.isEmpty).toBe(false);
});
it('应该能清空数据', () => {
sparseSet.add(1);
sparseSet.add(2);
sparseSet.clear();
expect(sparseSet.size).toBe(0);
expect(sparseSet.isEmpty).toBe(true);
expect(sparseSet.has(1)).toBe(false);
});
it('应该提供内存统计', () => {
sparseSet.add(1);
sparseSet.add(2);
const stats = sparseSet.getMemoryStats();
expect(stats.denseArraySize).toBeGreaterThan(0);
expect(stats.sparseMapSize).toBeGreaterThan(0);
expect(stats.totalMemory).toBe(stats.denseArraySize + stats.sparseMapSize);
});
it('应该能验证数据结构完整性', () => {
sparseSet.add(1);
sparseSet.add(2);
sparseSet.add(3);
sparseSet.remove(2);
expect(sparseSet.validate()).toBe(true);
});
});
describe('性能场景', () => {
it('应该处理大量数据操作', () => {
const items = Array.from({ length: 1000 }, (_, i) => i);
// 批量添加
for (const item of items) {
sparseSet.add(item);
}
expect(sparseSet.size).toBe(1000);
// 批量移除偶数
for (let i = 0; i < 1000; i += 2) {
sparseSet.remove(i);
}
expect(sparseSet.size).toBe(500);
// 验证只剩奇数
const remaining = sparseSet.toArray().sort((a, b) => a - b);
for (let i = 0; i < remaining.length; i++) {
expect(remaining[i] % 2).toBe(1);
}
});
it('应该保持O(1)访问性能', () => {
// 添加大量元素
for (let i = 0; i < 1000; i++) {
sparseSet.add(i);
}
// 随机访问应该很快
const start = performance.now();
for (let i = 0; i < 100; i++) {
const randomItem = Math.floor(Math.random() * 1000);
sparseSet.has(randomItem);
sparseSet.getIndex(randomItem);
}
const duration = performance.now() - start;
// 应该在很短时间内完成
expect(duration).toBeLessThan(10);
});
});
});
@@ -0,0 +1,528 @@
import { World, IWorldConfig, IGlobalSystem } from '../../src/ECS/World';
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 { IService } from '../../src/Core/ServiceContainer';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试用组件
@ECSComponent('WorldTest_TestComponent')
class TestComponent extends Component {
public value: number = 0;
constructor(value: number = 0) {
super();
this.value = value;
}
}
@ECSComponent('WorldTest_PlayerComponent')
class PlayerComponent extends Component {
public playerId: string;
constructor(playerId: string) {
super();
this.playerId = playerId;
}
}
// 测试用全局系统
class TestGlobalSystem implements IGlobalSystem {
public readonly name = 'TestGlobalSystem';
public updateCount: number = 0;
public initialize(): void {
// 初始化逻辑
}
public update(): void {
this.updateCount++;
}
public reset(): void {
this.updateCount = 0;
}
public destroy(): void {
// 销毁逻辑
}
}
class TestSceneSystem extends EntitySystem {
public updateCount = 0;
constructor() {
super(Matcher.empty().all(PlayerComponent));
}
protected override process(): void {
this.updateCount++;
}
}
// 测试用服务
class TestWorldService implements IService {
public disposed = false;
public value = 'test';
dispose(): void {
this.disposed = true;
}
}
// 测试用Scene
class TestScene extends Scene {
public initializeCalled = false;
public beginCalled = false;
public endCalled = false;
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
public override begin(): void {
this.beginCalled = true;
super.begin();
}
public override end(): void {
this.endCalled = true;
super.end();
}
}
describe('World', () => {
let world: World;
beforeEach(() => {
world = new World({ name: 'TestWorld' });
});
afterEach(() => {
if (world) {
world.destroy();
}
});
describe('基础功能', () => {
test('创建World时应该设置正确的配置', () => {
const config: IWorldConfig = {
name: 'GameWorld',
debug: true,
maxScenes: 5,
autoCleanup: false
};
const testWorld = new World(config);
expect(testWorld.name).toBe('GameWorld');
expect(testWorld.sceneCount).toBe(0);
expect(testWorld.isActive).toBe(false);
expect(testWorld.createdAt).toBeGreaterThan(0);
testWorld.destroy();
});
test('默认配置应该正确', () => {
const defaultWorld = new World();
expect(defaultWorld.name).toBe('World');
expect(defaultWorld.sceneCount).toBe(0);
expect(defaultWorld.isActive).toBe(false);
defaultWorld.destroy();
});
});
describe('Scene管理', () => {
test('创建Scene应该成功', () => {
const scene = world.createScene('test-scene');
expect(scene).toBeDefined();
expect(world.sceneCount).toBe(1);
expect(world.getSceneIds()).toContain('test-scene');
});
test('创建Scene时传入自定义Scene实例', () => {
const customScene = new TestScene();
const scene = world.createScene('custom-scene', customScene);
expect(scene).toBe(customScene);
expect(scene.initializeCalled).toBe(true);
expect(world.sceneCount).toBe(1);
});
test('空的Scene name应该抛出错误', () => {
expect(() => {
world.createScene('');
}).toThrow('Scene name不能为空');
expect(() => {
world.createScene(' ');
}).toThrow('Scene name不能为空');
});
test('重复的Scene ID应该抛出错误', () => {
world.createScene('duplicate');
expect(() => {
world.createScene('duplicate');
}).toThrow("Scene name 'duplicate' 已存在于World 'TestWorld' 中");
});
test('超出最大Scene数量限制应该抛出错误', () => {
const limitedWorld = new World({ maxScenes: 2 });
limitedWorld.createScene('scene1');
limitedWorld.createScene('scene2');
expect(() => {
limitedWorld.createScene('scene3');
}).toThrow("World 'World' 已达到最大Scene数量限制: 2");
limitedWorld.destroy();
});
test('获取Scene应该正确', () => {
const scene = world.createScene('get-test');
const retrievedScene = world.getScene('get-test');
expect(retrievedScene).toBe(scene);
});
test('获取不存在的Scene应该返回null', () => {
const scene = world.getScene('non-existent');
expect(scene).toBeNull();
});
test('移除Scene应该正确清理', () => {
const testScene = new TestScene();
world.createScene('remove-test', testScene);
world.setSceneActive('remove-test', true);
const removed = world.removeScene('remove-test');
expect(removed).toBe(true);
expect(world.sceneCount).toBe(0);
expect(world.getScene('remove-test')).toBeNull();
expect(testScene.endCalled).toBe(true);
});
test('移除不存在的Scene应该返回false', () => {
const removed = world.removeScene('non-existent');
expect(removed).toBe(false);
});
test('获取所有Scene应该正确', () => {
const scene1 = world.createScene('scene1');
const scene2 = world.createScene('scene2');
const allScenes = world.getAllScenes();
expect(allScenes).toHaveLength(2);
expect(allScenes).toContain(scene1);
expect(allScenes).toContain(scene2);
});
});
describe('Scene激活管理', () => {
test('激活Scene应该正确', () => {
const testScene = new TestScene();
world.createScene('active-test', testScene);
world.setSceneActive('active-test', true);
expect(world.isSceneActive('active-test')).toBe(true);
expect(world.getActiveSceneCount()).toBe(1);
expect(testScene.beginCalled).toBe(true);
});
test('停用Scene应该正确', () => {
world.createScene('deactive-test');
world.setSceneActive('deactive-test', true);
world.setSceneActive('deactive-test', false);
expect(world.isSceneActive('deactive-test')).toBe(false);
expect(world.getActiveSceneCount()).toBe(0);
});
test('激活不存在的Scene应该记录警告', () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
world.setSceneActive('non-existent', true);
// 注意:这里需要检查具体的日志实现,可能需要调整
consoleSpy.mockRestore();
});
});
describe('全局System管理', () => {
test('添加全局System应该成功', () => {
const globalSystem = new TestGlobalSystem();
const addedSystem = world.addGlobalSystem(globalSystem);
expect(addedSystem).toBe(globalSystem);
expect(world.getGlobalSystem(TestGlobalSystem)).toBe(globalSystem);
});
test('重复添加相同System应该返回原System', () => {
const globalSystem = new TestGlobalSystem();
const firstAdd = world.addGlobalSystem(globalSystem);
const secondAdd = world.addGlobalSystem(globalSystem);
expect(firstAdd).toBe(secondAdd);
expect(firstAdd).toBe(globalSystem);
});
test('移除全局System应该成功', () => {
const globalSystem = new TestGlobalSystem();
world.addGlobalSystem(globalSystem);
const removed = world.removeGlobalSystem(globalSystem);
expect(removed).toBe(true);
expect(world.getGlobalSystem(TestGlobalSystem)).toBeNull();
});
test('移除不存在的System应该返回false', () => {
const globalSystem = new TestGlobalSystem();
const removed = world.removeGlobalSystem(globalSystem);
expect(removed).toBe(false);
});
test('获取不存在的System类型应该返回null', () => {
const system = world.getGlobalSystem(TestGlobalSystem);
expect(system).toBeNull();
});
});
describe('World生命周期', () => {
test('启动World应该正确', () => {
const globalSystem = new TestGlobalSystem();
world.addGlobalSystem(globalSystem);
world.start();
expect(world.isActive).toBe(true);
});
test('重复启动World应该无效果', () => {
world.start();
const firstActive = world.isActive;
world.start();
expect(world.isActive).toBe(firstActive);
});
test('停止World应该停用所有Scene', () => {
const testScene = new TestScene();
world.createScene('stop-test', testScene);
world.setSceneActive('stop-test', true);
world.start();
world.stop();
expect(world.isActive).toBe(false);
expect(world.isSceneActive('stop-test')).toBe(false);
});
test('销毁World应该清理所有资源', () => {
const testScene = new TestScene();
const globalSystem = new TestGlobalSystem();
world.createScene('destroy-test', testScene);
world.addGlobalSystem(globalSystem);
world.start();
world.destroy();
expect(world.sceneCount).toBe(0);
expect(world.isActive).toBe(false);
expect(testScene.endCalled).toBe(true);
});
});
describe('更新逻辑', () => {
test('updateGlobalSystems应该更新全局系统', () => {
const globalSystem = new TestGlobalSystem();
world.addGlobalSystem(globalSystem);
world.start();
// 创建测试Scene
const scene = world.createScene('update-test');
world.setSceneActive('update-test', true);
// 直接测试全局系统更新
world.updateGlobalSystems();
// 验证全局System被正确调用
expect(globalSystem.updateCount).toBeGreaterThan(0);
});
test('未激活的World不应该更新', () => {
const globalSystem = new TestGlobalSystem();
world.addGlobalSystem(globalSystem);
// 不启动World
world.updateGlobalSystems();
expect(globalSystem.updateCount).toBe(0);
});
test('updateScenes应该更新激活的Scene', () => {
const scene1 = world.createScene('scene1');
const scene2 = world.createScene('scene2');
scene1.addEntityProcessor(new TestSceneSystem());
scene2.addEntityProcessor(new TestSceneSystem());
world.start();
world.setSceneActive('scene1', true);
// scene2保持未激活
world.updateScenes();
// 这里需要根据具体的Scene更新实现来验证
// 由于Scene.update()的具体实现可能不同,这里主要测试调用不出错
expect(() => world.updateScenes()).not.toThrow();
});
});
describe('状态和统计', () => {
test('获取World状态应该正确', () => {
world.createScene('status-scene1');
world.createScene('status-scene2');
world.setSceneActive('status-scene1', true);
world.addGlobalSystem(new TestGlobalSystem());
world.start();
const status = world.getStatus();
expect(status.name).toBe('TestWorld');
expect(status.isActive).toBe(true);
expect(status.sceneCount).toBe(2);
expect(status.activeSceneCount).toBe(1);
expect(status.globalSystemCount).toBe(1);
expect(status.createdAt).toBeGreaterThan(0);
expect(status.scenes).toHaveLength(2);
const activeScene = status.scenes.find(s => s.id === 'status-scene1');
expect(activeScene?.isActive).toBe(true);
const inactiveScene = status.scenes.find(s => s.id === 'status-scene2');
expect(inactiveScene?.isActive).toBe(false);
});
test('获取World统计应该包含基本信息', () => {
world.addGlobalSystem(new TestGlobalSystem());
const scene = world.createScene('stats-scene');
const entity = scene.createEntity('stats-entity');
entity.addComponent(new TestComponent());
const stats = world.getStats();
expect(stats).toHaveProperty('totalEntities');
expect(stats).toHaveProperty('totalSystems');
expect(stats).toHaveProperty('memoryUsage');
expect(stats).toHaveProperty('performance');
expect(stats.totalSystems).toBeGreaterThanOrEqual(1);
});
});
describe('自动清理功能', () => {
test('自动清理应该移除空闲Scene', async () => {
// 创建一个启用自动清理的World
const autoCleanWorld = new World({
name: 'AutoCleanWorld',
autoCleanup: true,
maxScenes: 10
});
// 创建一个空Scene
autoCleanWorld.createScene('empty-scene');
autoCleanWorld.start();
// 手动触发清理检查
autoCleanWorld.updateScenes();
// 由于清理策略基于时间,这里主要测试不会出错
expect(() => autoCleanWorld.updateScenes()).not.toThrow();
autoCleanWorld.destroy();
});
});
describe('服务容器', () => {
test('应该能访问World级别的服务容器', () => {
const services = world.services;
expect(services).toBeDefined();
expect(services).toHaveProperty('registerSingleton');
expect(services).toHaveProperty('resolve');
});
test('应该能在World服务容器中注册和解析服务', () => {
world.services.registerSingleton(TestWorldService);
const service = world.services.resolve(TestWorldService);
expect(service).toBeDefined();
expect(service.value).toBe('test');
expect(service.disposed).toBe(false);
});
test('World销毁时应该清理服务容器中的服务', () => {
const service = new TestWorldService();
world.services.registerInstance(TestWorldService, service);
world.destroy();
expect(service.disposed).toBe(true);
});
test('World服务容器应该独立于Scene服务容器', () => {
const scene = world.createScene('test-scene');
world.services.registerSingleton(TestWorldService);
expect(world.services.isRegistered(TestWorldService)).toBe(true);
expect(scene.services.isRegistered(TestWorldService)).toBe(false);
});
});
describe('错误处理', () => {
test('Scene name为空时应该抛出错误', () => {
expect(() => {
world.createScene('');
}).toThrow('Scene name不能为空');
});
test('极限情况下的资源管理', () => {
// 创建大量Scene
for (let i = 0; i < 5; i++) {
world.createScene(`scene_${i}`);
world.setSceneActive(`scene_${i}`, true);
}
// 添加多个全局System
for (let i = 0; i < 3; i++) {
world.addGlobalSystem(new TestGlobalSystem());
}
world.start();
// 测试批量清理
expect(() => world.destroy()).not.toThrow();
});
});
});
@@ -0,0 +1,717 @@
import { WorldManager, IWorldManagerConfig } from '../../src/ECS/WorldManager';
import { IWorldConfig, World } from '../../src/ECS/World';
import { Component } from '../../src/ECS/Component';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试用组件
@ECSComponent('WorldMgr_TestComponent')
class TestComponent extends Component {
public value: number = 0;
constructor(value: number = 0) {
super();
this.value = value;
}
}
// 测试用全局系统
class TestGlobalSystem {
public readonly name = 'TestGlobalSystem';
public updateCount: number = 0;
public initialize(): void {
// 初始化
}
public update(): void {
this.updateCount++;
}
public reset(): void {
this.updateCount = 0;
}
public destroy(): void {
// 销毁
}
}
describe('WorldManager', () => {
let worldManager: WorldManager;
beforeEach(() => {
// WorldManager不再是单例,直接创建新实例
worldManager = new WorldManager();
});
afterEach(() => {
// 清理所有World
if (worldManager) {
const worldIds = worldManager.getWorldIds();
worldIds.forEach((id) => {
worldManager.removeWorld(id);
});
// 清理定时器
worldManager.destroy();
}
});
describe('实例化', () => {
test('可以创建多个独立的WorldManager实例', () => {
const manager1 = new WorldManager();
const manager2 = new WorldManager();
expect(manager1).not.toBe(manager2);
manager1.createWorld('world1');
expect(manager2.getWorld('world1')).toBeNull();
// 清理
manager1.destroy();
manager2.destroy();
});
test('使用配置创建实例应该正确', () => {
const config: IWorldManagerConfig = {
maxWorlds: 10,
autoCleanup: true,
debug: false
};
const instance = new WorldManager(config);
expect(instance).toBeDefined();
expect(instance.worldCount).toBe(0);
// 清理
instance.destroy();
});
});
describe('World管理', () => {
test('创建World应该成功', () => {
const world = worldManager.createWorld('test-world');
expect(world).toBeDefined();
expect(world.name).toBe('test-world');
expect(worldManager.getWorld('test-world')).toBeDefined();
expect(worldManager.getWorldIds()).toContain('test-world');
});
test('创建World时传入配置应该正确', () => {
const worldConfig: IWorldConfig = {
name: 'ConfiguredWorld',
debug: true,
maxScenes: 5,
autoCleanup: false
};
const world = worldManager.createWorld('configured-world', worldConfig);
expect(world.name).toBe('configured-world');
});
test('空的World name应该抛出错误', () => {
expect(() => {
worldManager.createWorld('');
}).toThrow('World name不能为空');
expect(() => {
worldManager.createWorld(' ');
}).toThrow('World name不能为空');
});
test('重复的World ID应该抛出错误', () => {
worldManager.createWorld('duplicate-world');
expect(() => {
worldManager.createWorld('duplicate-world');
}).toThrow("World name 'duplicate-world' 已存在");
});
test('超出最大World数量应该抛出错误', () => {
const limitedManager = new WorldManager({ maxWorlds: 2 });
limitedManager.createWorld('world1');
limitedManager.createWorld('world2');
expect(() => {
limitedManager.createWorld('world3');
}).toThrow('已达到最大World数量限制: 2');
// 清理
limitedManager.destroy();
});
test('获取World应该正确', () => {
const world = worldManager.createWorld('get-world');
const retrievedWorld = worldManager.getWorld('get-world');
expect(retrievedWorld).toBe(world);
});
test('获取不存在的World应该返回null', () => {
const world = worldManager.getWorld('non-existent');
expect(world).toBeNull();
});
test('检查World存在性应该正确', () => {
expect(worldManager.getWorld('non-existent')).toBeNull();
worldManager.createWorld('exists');
expect(worldManager.getWorld('exists')).toBeDefined();
});
test('销毁World应该正确清理', () => {
const world = worldManager.createWorld('destroy-world');
world.start();
const destroyed = worldManager.removeWorld('destroy-world');
expect(destroyed).toBe(true);
expect(worldManager.getWorld('destroy-world')).toBeNull();
});
test('销毁不存在的World应该返回false', () => {
const destroyed = worldManager.removeWorld('non-existent');
expect(destroyed).toBe(false);
});
test('获取所有World ID应该正确', () => {
worldManager.createWorld('world1');
worldManager.createWorld('world2');
worldManager.createWorld('world3');
const worldIds = worldManager.getWorldIds();
expect(worldIds).toHaveLength(3);
expect(worldIds).toContain('world1');
expect(worldIds).toContain('world2');
expect(worldIds).toContain('world3');
});
});
describe('活跃World管理', () => {
test('启动World应该加入活跃列表', () => {
const world = worldManager.createWorld('active-world');
worldManager.setWorldActive('active-world', true);
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(1);
expect(activeWorlds[0]).toBe(world);
});
test('停止World应该从活跃列表移除', () => {
worldManager.createWorld('inactive-world');
worldManager.setWorldActive('inactive-world', true);
worldManager.setWorldActive('inactive-world', false);
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(0);
});
test('销毁激活的World应该从活跃列表移除', () => {
worldManager.createWorld('destroy-active');
worldManager.setWorldActive('destroy-active', true);
worldManager.removeWorld('destroy-active');
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(0);
});
test('多个World的激活状态应该独立管理', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
const world3 = worldManager.createWorld('world3');
worldManager.setWorldActive('world1', true);
worldManager.setWorldActive('world3', true);
// world2 保持未启动
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(2);
expect(activeWorlds).toContain(world1);
expect(activeWorlds).toContain(world3);
expect(activeWorlds).not.toContain(world2);
});
});
describe('统计和监控', () => {
test('获取WorldManager状态应该正确', () => {
worldManager.createWorld('status-world1');
worldManager.createWorld('status-world2');
worldManager.setWorldActive('status-world2', true);
const status = worldManager.getStats();
expect(status.totalWorlds).toBe(2);
expect(status.activeWorlds).toBe(1);
expect(status.config.maxWorlds).toBeGreaterThan(0);
expect(status.memoryUsage).toBeGreaterThanOrEqual(0);
expect(status.isRunning).toBeDefined();
});
test('获取所有World统计应该包含详细信息', () => {
const world1 = worldManager.createWorld('stats-world1');
worldManager.createWorld('stats-world2');
// 为world1添加一些内容
const scene1 = world1.createScene('scene1');
scene1.createEntity('entity1');
worldManager.setWorldActive('stats-world1', true);
// world2保持空
const allStats = worldManager.getDetailedStatus().worlds;
expect(allStats).toHaveLength(2);
const world1Stats = allStats.find((stat) => stat.id === 'stats-world1');
const world2Stats = allStats.find((stat) => stat.id === 'stats-world2');
expect(world1Stats).toBeDefined();
expect(world2Stats).toBeDefined();
expect(world1Stats?.isActive).toBe(true);
expect(world2Stats?.isActive).toBe(false);
});
test('空WorldManager的统计应该正确', () => {
const status = worldManager.getStats();
const allStats = worldManager.getDetailedStatus().worlds;
expect(status.totalWorlds).toBe(0);
expect(status.activeWorlds).toBe(0);
expect(allStats).toHaveLength(0);
});
});
describe('清理功能', () => {
test('清理空闲World应该移除符合条件的World', () => {
// 创建一个空的World
worldManager.createWorld('empty-world');
// 创建一个有内容的World
const fullWorld = worldManager.createWorld('full-world');
const scene = fullWorld.createScene('scene');
scene.createEntity('entity');
fullWorld.start();
// 执行清理
const cleanedCount = worldManager.cleanup();
// 由于清理逻辑可能基于时间或其他条件,这里主要测试不会出错
expect(cleanedCount).toBeGreaterThanOrEqual(0);
expect(() => worldManager.cleanup()).not.toThrow();
});
test('应该在指定帧数后自动执行清理', () => {
jest.useFakeTimers();
const manager = new WorldManager({
autoCleanup: true,
cleanupFrameInterval: 10 // 10 帧后清理
});
manager.createWorld('test');
// 模拟一个空的老 World (超过 10 分钟)
jest.advanceTimersByTime(11 * 60 * 1000);
// 执行 9 帧,不应该清理
for (let i = 0; i < 9; i++) {
manager.updateAll();
}
expect(manager.getWorld('test')).not.toBeNull();
// 第 10 帧,应该清理
manager.updateAll();
expect(manager.getWorld('test')).toBeNull();
jest.useRealTimers();
manager.destroy();
});
test('autoCleanup=false 时不应该自动清理', () => {
jest.useFakeTimers();
const manager = new WorldManager({
autoCleanup: false
});
manager.createWorld('test');
jest.advanceTimersByTime(20 * 60 * 1000);
// 执行多帧
for (let i = 0; i < 2000; i++) {
manager.updateAll();
}
// 不应该清理
expect(manager.getWorld('test')).not.toBeNull();
jest.useRealTimers();
manager.destroy();
});
test('清理后计数器应该重置', () => {
jest.useFakeTimers();
const manager = new WorldManager({
autoCleanup: true,
cleanupFrameInterval: 10
});
manager.createWorld('world1');
jest.advanceTimersByTime(11 * 60 * 1000);
// 第 10 帧触发清理
for (let i = 0; i < 10; i++) {
manager.updateAll();
}
// world1 被清理
expect(manager.getWorld('world1')).toBeNull();
// 创建新 World
manager.createWorld('world2');
jest.advanceTimersByTime(11 * 60 * 1000);
// 又要等 10 帧才能清理 (计数器已重置)
for (let i = 0; i < 9; i++) {
manager.updateAll();
}
expect(manager.getWorld('world2')).not.toBeNull();
manager.updateAll();
expect(manager.getWorld('world2')).toBeNull();
jest.useRealTimers();
manager.destroy();
});
test('应该使用配置的 cleanupFrameInterval', () => {
const manager = new WorldManager({
autoCleanup: true,
cleanupFrameInterval: 5
});
expect(manager.config.cleanupFrameInterval).toBe(5);
manager.destroy();
});
});
describe('World更新协调', () => {
test('更新所有活跃World应该正确', () => {
const world1 = worldManager.createWorld('update-world1');
const world2 = worldManager.createWorld('update-world2');
worldManager.createWorld('update-world3');
// 添加一些内容到World中
const scene1 = world1.createScene('scene1');
const scene2 = world2.createScene('scene2');
scene1.createEntity('entity1');
scene2.createEntity('entity2');
// 启动部分World
worldManager.setWorldActive('update-world1', true);
worldManager.setWorldActive('update-world2', true);
// world3保持未启动
// 手动调用更新(通常由Core.update()调用)
const activeWorlds = worldManager.getActiveWorlds();
expect(() => {
activeWorlds.forEach((world) => {
world.updateGlobalSystems();
world.updateScenes();
});
}).not.toThrow();
expect(activeWorlds).toHaveLength(2);
});
test('updateAll 应该只更新活跃的 World', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
const system1 = new TestGlobalSystem();
const system2 = new TestGlobalSystem();
world1.addGlobalSystem(system1);
world2.addGlobalSystem(system2);
worldManager.setWorldActive('world1', true);
// world2 保持未激活
worldManager.updateAll();
expect(system1.updateCount).toBe(1); // world1 被更新
expect(system2.updateCount).toBe(0); // world2 未被更新
});
test('isRunning=false 时 updateAll 不应该更新任何 World', () => {
const world = worldManager.createWorld('world');
const system = new TestGlobalSystem();
world.addGlobalSystem(system);
worldManager.setWorldActive('world', true);
worldManager.stopAll(); // isRunning = false
worldManager.updateAll();
expect(system.updateCount).toBe(0);
});
test('updateAll 应该正确处理多个活跃和非活跃 World', () => {
const worlds: World[] = [];
const systems: TestGlobalSystem[] = [];
// 创建 5 个 World
for (let i = 0; i < 5; i++) {
const world = worldManager.createWorld(`world${i}`);
const system = new TestGlobalSystem();
world.addGlobalSystem(system);
worlds.push(world);
systems.push(system);
}
// 激活第 0, 2, 4 个
worldManager.setWorldActive('world0', true);
worldManager.setWorldActive('world2', true);
worldManager.setWorldActive('world4', true);
worldManager.updateAll();
expect(systems[0].updateCount).toBe(1); // 活跃
expect(systems[1].updateCount).toBe(0); // 非活跃
expect(systems[2].updateCount).toBe(1); // 活跃
expect(systems[3].updateCount).toBe(0); // 非活跃
expect(systems[4].updateCount).toBe(1); // 活跃
});
});
describe('边界情况和错误处理', () => {
test('World ID为空字符串应该抛出错误', () => {
expect(() => {
worldManager.createWorld('');
}).toThrow();
});
test('World ID为null或undefined应该抛出错误', () => {
expect(() => {
worldManager.createWorld(null as unknown as string);
}).toThrow();
expect(() => {
worldManager.createWorld(undefined as unknown as string);
}).toThrow();
});
test('极限情况下的大量World管理', () => {
const worldCount = 50;
const worldIds: string[] = [];
// 创建大量World
for (let i = 0; i < worldCount; i++) {
const worldId = `mass-world-${i}`;
worldIds.push(worldId);
expect(() => {
worldManager.createWorld(worldId);
}).not.toThrow();
}
expect(worldManager.getWorldIds()).toHaveLength(worldCount);
// 启动一半的World
for (let i = 0; i < worldCount / 2; i++) {
worldManager.setWorldActive(worldIds[i], true);
}
expect(worldManager.getActiveWorlds()).toHaveLength(worldCount / 2);
// 批量清理
worldIds.forEach((id) => {
expect(() => {
worldManager.removeWorld(id);
}).not.toThrow();
});
expect(worldManager.getWorldIds()).toHaveLength(0);
});
test('销毁后获取World应该返回null', () => {
worldManager.createWorld('temp-world');
worldManager.removeWorld('temp-world');
expect(worldManager.getWorld('temp-world')).toBeNull();
});
});
describe('内存管理', () => {
test('销毁所有World后内存应该被释放', () => {
// 创建多个World并添加内容
for (let i = 0; i < 10; i++) {
const world = worldManager.createWorld(`memory-world-${i}`);
const scene = world.createScene('scene');
// 添加一些实体和系统
for (let j = 0; j < 5; j++) {
const entity = scene.createEntity(`entity-${j}`);
entity.addComponent(new TestComponent(j));
}
world.addGlobalSystem(new TestGlobalSystem());
worldManager.setWorldActive(`memory-world-${i}`, true);
}
const beforeCleanup = worldManager.getStats();
expect(beforeCleanup.totalWorlds).toBe(10);
expect(beforeCleanup.activeWorlds).toBe(10);
// 清理所有World
const worldIds = worldManager.getWorldIds();
worldIds.forEach((id) => {
worldManager.removeWorld(id);
});
const afterCleanup = worldManager.getStats();
expect(afterCleanup.totalWorlds).toBe(0);
expect(afterCleanup.activeWorlds).toBe(0);
});
});
describe('配置验证', () => {
test('无效的maxWorlds配置应该按传入值使用', () => {
const invalidConfig: IWorldManagerConfig = {
maxWorlds: -1,
autoCleanup: true,
debug: true
};
const manager = new WorldManager(invalidConfig);
expect(manager.getStats().config.maxWorlds).toBe(-1);
manager.destroy();
});
test('配置应该正确应用于新实例', () => {
const config: IWorldManagerConfig = {
maxWorlds: 3,
autoCleanup: true,
debug: true
};
const manager = new WorldManager(config);
// 创建到限制数量的World
manager.createWorld('world1');
manager.createWorld('world2');
manager.createWorld('world3');
// 第四个应该失败
expect(() => {
manager.createWorld('world4');
}).toThrow();
// 清理
manager.destroy();
});
});
describe('isWorldActive()', () => {
test('应该正确返回 World 激活状态', () => {
worldManager.createWorld('test');
// 初始未激活
expect(worldManager.isWorldActive('test')).toBe(false);
// 激活后
worldManager.setWorldActive('test', true);
expect(worldManager.isWorldActive('test')).toBe(true);
// 停用后
worldManager.setWorldActive('test', false);
expect(worldManager.isWorldActive('test')).toBe(false);
});
test('不存在的 World 应该返回 false', () => {
expect(worldManager.isWorldActive('non-existent')).toBe(false);
});
test('应该与 world.isActive 保持一致', () => {
const world = worldManager.createWorld('test');
expect(worldManager.isWorldActive('test')).toBe(world.isActive);
worldManager.setWorldActive('test', true);
expect(worldManager.isWorldActive('test')).toBe(world.isActive);
worldManager.setWorldActive('test', false);
expect(worldManager.isWorldActive('test')).toBe(world.isActive);
});
});
describe('activeWorldCount', () => {
test('应该正确返回激活的 World 数量', () => {
expect(worldManager.activeWorldCount).toBe(0);
worldManager.createWorld('world1');
worldManager.createWorld('world2');
worldManager.createWorld('world3');
expect(worldManager.activeWorldCount).toBe(0);
worldManager.setWorldActive('world1', true);
expect(worldManager.activeWorldCount).toBe(1);
worldManager.setWorldActive('world2', true);
expect(worldManager.activeWorldCount).toBe(2);
worldManager.setWorldActive('world1', false);
expect(worldManager.activeWorldCount).toBe(1);
});
test('应该与 getActiveWorlds().length 一致', () => {
worldManager.createWorld('world1');
worldManager.createWorld('world2');
worldManager.setWorldActive('world1', true);
expect(worldManager.activeWorldCount).toBe(worldManager.getActiveWorlds().length);
});
});
describe('getAllWorlds()', () => {
test('应该返回所有 World', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
const world3 = worldManager.createWorld('world3');
const allWorlds = worldManager.getAllWorlds();
expect(allWorlds).toHaveLength(3);
expect(allWorlds).toContain(world1);
expect(allWorlds).toContain(world2);
expect(allWorlds).toContain(world3);
});
test('空 WorldManager 应该返回空数组', () => {
const allWorlds = worldManager.getAllWorlds();
expect(allWorlds).toHaveLength(0);
});
test('返回的数组修改不应该影响内部状态', () => {
worldManager.createWorld('world1');
const allWorlds = worldManager.getAllWorlds();
allWorlds.push({} as unknown as World);
expect(worldManager.getAllWorlds()).toHaveLength(1);
});
});
});