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:
@@ -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);
|
||||
|
||||
// 第一次 update:system.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
|
||||
// 需要同时注册到 GlobalComponentRegistry(ArchetypeSystem 使用)和 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-45,Number.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user