冗余测试合并
This commit is contained in:
@@ -1,253 +0,0 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import { EnableSoA, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/SoAStorage';
|
||||
|
||||
// 测试组件:使用集合类型装饰器
|
||||
@EnableSoA
|
||||
class CollectionsComponent extends Component {
|
||||
// 序列化Map存储
|
||||
@SerializeMap
|
||||
public playerStats: Map<string, number> = new Map();
|
||||
|
||||
// 序列化Set存储
|
||||
@SerializeSet
|
||||
public achievements: Set<string> = new Set();
|
||||
|
||||
// 序列化Array存储
|
||||
@SerializeArray
|
||||
public inventory: string[] = [];
|
||||
|
||||
// 深拷贝对象存储
|
||||
@DeepCopy
|
||||
public config: { settings: { volume: number } } = { settings: { volume: 0.5 } };
|
||||
|
||||
// 普通对象(引用存储)
|
||||
public metadata: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('SoA集合类型装饰器测试', () => {
|
||||
let manager: ComponentStorageManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ComponentStorageManager();
|
||||
});
|
||||
|
||||
test('验证Map序列化存储', () => {
|
||||
console.log('\\n=== 测试Map序列化存储 ===');
|
||||
|
||||
const component = new CollectionsComponent();
|
||||
|
||||
// 设置Map数据
|
||||
component.playerStats.set('health', 100);
|
||||
component.playerStats.set('mana', 50);
|
||||
component.playerStats.set('experience', 1250);
|
||||
|
||||
console.log('原始Map数据:', {
|
||||
size: component.playerStats.size,
|
||||
entries: Array.from(component.playerStats.entries())
|
||||
});
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||
|
||||
console.log('取回Map数据:', {
|
||||
size: retrieved?.playerStats.size,
|
||||
entries: Array.from(retrieved?.playerStats.entries() || [])
|
||||
});
|
||||
|
||||
// 验证Map数据完整性
|
||||
expect(retrieved?.playerStats).toBeInstanceOf(Map);
|
||||
expect(retrieved?.playerStats.size).toBe(3);
|
||||
expect(retrieved?.playerStats.get('health')).toBe(100);
|
||||
expect(retrieved?.playerStats.get('mana')).toBe(50);
|
||||
expect(retrieved?.playerStats.get('experience')).toBe(1250);
|
||||
|
||||
console.log('✅ Map序列化存储验证通过');
|
||||
});
|
||||
|
||||
test('验证Set序列化存储', () => {
|
||||
console.log('\\n=== 测试Set序列化存储 ===');
|
||||
|
||||
const component = new CollectionsComponent();
|
||||
|
||||
// 设置Set数据
|
||||
component.achievements.add('first_kill');
|
||||
component.achievements.add('level_10');
|
||||
component.achievements.add('boss_defeated');
|
||||
|
||||
console.log('原始Set数据:', {
|
||||
size: component.achievements.size,
|
||||
values: Array.from(component.achievements)
|
||||
});
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||
|
||||
console.log('取回Set数据:', {
|
||||
size: retrieved?.achievements.size,
|
||||
values: Array.from(retrieved?.achievements || [])
|
||||
});
|
||||
|
||||
// 验证Set数据完整性
|
||||
expect(retrieved?.achievements).toBeInstanceOf(Set);
|
||||
expect(retrieved?.achievements.size).toBe(3);
|
||||
expect(retrieved?.achievements.has('first_kill')).toBe(true);
|
||||
expect(retrieved?.achievements.has('level_10')).toBe(true);
|
||||
expect(retrieved?.achievements.has('boss_defeated')).toBe(true);
|
||||
|
||||
console.log('✅ Set序列化存储验证通过');
|
||||
});
|
||||
|
||||
test('验证Array序列化存储', () => {
|
||||
console.log('\\n=== 测试Array序列化存储 ===');
|
||||
|
||||
const component = new CollectionsComponent();
|
||||
|
||||
// 设置Array数据
|
||||
component.inventory.push('sword', 'shield', 'potion');
|
||||
|
||||
console.log('原始Array数据:', component.inventory);
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||
|
||||
console.log('取回Array数据:', retrieved?.inventory);
|
||||
|
||||
// 验证Array数据完整性
|
||||
expect(Array.isArray(retrieved?.inventory)).toBe(true);
|
||||
expect(retrieved?.inventory.length).toBe(3);
|
||||
expect(retrieved?.inventory).toEqual(['sword', 'shield', 'potion']);
|
||||
|
||||
console.log('✅ Array序列化存储验证通过');
|
||||
});
|
||||
|
||||
test('验证深拷贝对象存储', () => {
|
||||
console.log('\\n=== 测试深拷贝对象存储 ===');
|
||||
|
||||
const component = new CollectionsComponent();
|
||||
const originalConfig = component.config;
|
||||
|
||||
// 修改配置
|
||||
component.config.settings.volume = 0.8;
|
||||
|
||||
console.log('原始配置:', component.config);
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||
|
||||
console.log('取回配置:', retrieved?.config);
|
||||
|
||||
// 验证深拷贝
|
||||
expect(retrieved?.config).toEqual(component.config);
|
||||
expect(retrieved?.config).not.toBe(originalConfig); // 不是同一个引用
|
||||
expect(retrieved?.config.settings.volume).toBe(0.8);
|
||||
|
||||
// 修改原始对象不应该影响取回的对象
|
||||
component.config.settings.volume = 0.3;
|
||||
expect(retrieved?.config.settings.volume).toBe(0.8); // 保持不变
|
||||
|
||||
console.log('✅ 深拷贝对象存储验证通过');
|
||||
});
|
||||
|
||||
test('对比普通对象存储(引用存储)', () => {
|
||||
console.log('\\n=== 测试普通对象存储(引用存储)===');
|
||||
|
||||
const component = new CollectionsComponent();
|
||||
const sharedObject = { data: 'shared' };
|
||||
component.metadata = sharedObject;
|
||||
|
||||
console.log('原始metadata:', component.metadata);
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||
|
||||
console.log('取回metadata:', retrieved?.metadata);
|
||||
|
||||
// 验证引用存储
|
||||
expect(retrieved?.metadata).toBe(sharedObject); // 是同一个引用
|
||||
expect(retrieved?.metadata.data).toBe('shared');
|
||||
|
||||
console.log('✅ 普通对象存储验证通过');
|
||||
});
|
||||
|
||||
test('复杂场景:多种类型混合使用', () => {
|
||||
console.log('\\n=== 测试复杂场景 ===');
|
||||
|
||||
const component = new CollectionsComponent();
|
||||
|
||||
// 设置复杂数据
|
||||
component.playerStats.set('level', 25);
|
||||
component.playerStats.set('gold', 5000);
|
||||
|
||||
component.achievements.add('explorer');
|
||||
component.achievements.add('warrior');
|
||||
|
||||
component.inventory.push('legendary_sword', 'magic_potion');
|
||||
|
||||
component.config = {
|
||||
settings: {
|
||||
volume: 0.75
|
||||
}
|
||||
};
|
||||
|
||||
component.metadata = { timestamp: Date.now() };
|
||||
|
||||
console.log('复杂数据设置完成');
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||
|
||||
// 全面验证
|
||||
expect(retrieved?.playerStats.get('level')).toBe(25);
|
||||
expect(retrieved?.achievements.has('explorer')).toBe(true);
|
||||
expect(retrieved?.inventory).toContain('legendary_sword');
|
||||
expect(retrieved?.config.settings.volume).toBe(0.75);
|
||||
expect(retrieved?.metadata).toBeDefined();
|
||||
|
||||
console.log('✅ 复杂场景验证通过');
|
||||
});
|
||||
|
||||
test('性能测试:序列化 vs 深拷贝', () => {
|
||||
console.log('\\n=== 性能对比测试 ===');
|
||||
|
||||
const entityCount = 100;
|
||||
|
||||
// 准备测试数据
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const component = new CollectionsComponent();
|
||||
|
||||
// 设置数据
|
||||
component.playerStats.set('id', i);
|
||||
component.playerStats.set('score', i * 100);
|
||||
|
||||
component.achievements.add(`achievement_${i}`);
|
||||
component.inventory.push(`item_${i}`);
|
||||
|
||||
component.config = { settings: { volume: i / entityCount } };
|
||||
|
||||
manager.addComponent(i, component);
|
||||
}
|
||||
|
||||
const createTime = performance.now() - startTime;
|
||||
|
||||
// 读取测试
|
||||
const readStartTime = performance.now();
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const component = manager.getComponent(i, CollectionsComponent);
|
||||
expect(component?.playerStats.get('id')).toBe(i);
|
||||
}
|
||||
const readTime = performance.now() - readStartTime;
|
||||
|
||||
console.log(`创建${entityCount}个复杂组件: ${createTime.toFixed(2)}ms`);
|
||||
console.log(`读取${entityCount}个复杂组件: ${readTime.toFixed(2)}ms`);
|
||||
console.log(`平均每个组件: ${((createTime + readTime) / entityCount).toFixed(4)}ms`);
|
||||
|
||||
console.log('✅ 性能测试完成');
|
||||
});
|
||||
});
|
||||
631
packages/core/tests/ECS/Core/SoAStorage.complete.test.ts
Normal file
631
packages/core/tests/ECS/Core/SoAStorage.complete.test.ts
Normal file
@@ -0,0 +1,631 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import {
|
||||
EnableSoA,
|
||||
HighPrecision,
|
||||
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;
|
||||
|
||||
@HighPrecision
|
||||
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 {
|
||||
@HighPrecision
|
||||
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('@HighPrecision应该保持高精度数值', () => {
|
||||
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')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,308 +0,0 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import { EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/SoAStorage';
|
||||
import { SoAStorage } from '../../../src/ECS/Core/SoAStorage';
|
||||
|
||||
// 综合测试组件,覆盖所有装饰器
|
||||
@EnableSoA
|
||||
class ComprehensiveComponent extends Component {
|
||||
@HighPrecision
|
||||
public bigIntId: number = BigInt(Number.MAX_SAFE_INTEGER + 1) as any;
|
||||
|
||||
@Float64
|
||||
public preciseValue: number = Math.PI;
|
||||
|
||||
@Int32
|
||||
public intValue: number = -2147483648;
|
||||
|
||||
@SerializeMap
|
||||
public gameMap: Map<string, any> = new Map();
|
||||
|
||||
@SerializeSet
|
||||
public flags: Set<number> = new Set();
|
||||
|
||||
@SerializeArray
|
||||
public items: any[] = [];
|
||||
|
||||
@DeepCopy
|
||||
public nestedConfig: any = { deep: { nested: { value: 42 } } };
|
||||
|
||||
// 未装饰的字段
|
||||
public normalFloat: number = 1.23;
|
||||
public flag: boolean = true;
|
||||
public text: string = 'default';
|
||||
public complexObject: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('SoA存储综合测试覆盖', () => {
|
||||
let manager: ComponentStorageManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ComponentStorageManager();
|
||||
});
|
||||
|
||||
test('验证所有装饰器类型的存储和检索', () => {
|
||||
console.log('\\n=== 综合装饰器测试 ===');
|
||||
|
||||
const component = new ComprehensiveComponent();
|
||||
|
||||
// 设置复杂数据
|
||||
component.gameMap.set('player1', { level: 10, gold: 500 });
|
||||
component.gameMap.set('player2', { level: 15, gold: 1200 });
|
||||
|
||||
component.flags.add(1);
|
||||
component.flags.add(2);
|
||||
component.flags.add(4);
|
||||
|
||||
component.items.push({ type: 'weapon', name: 'sword' });
|
||||
component.items.push({ type: 'armor', name: 'shield' });
|
||||
|
||||
component.nestedConfig.deep.nested.value = 999;
|
||||
component.complexObject = { reference: 'shared' };
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, ComprehensiveComponent);
|
||||
|
||||
// 验证所有类型
|
||||
expect(retrieved?.bigIntId).toBe(component.bigIntId);
|
||||
expect(retrieved?.preciseValue).toBeCloseTo(Math.PI, 15);
|
||||
expect(retrieved?.intValue).toBe(-2147483648);
|
||||
|
||||
expect(retrieved?.gameMap).toBeInstanceOf(Map);
|
||||
expect(retrieved?.gameMap.get('player1')).toEqual({ level: 10, gold: 500 });
|
||||
|
||||
expect(retrieved?.flags).toBeInstanceOf(Set);
|
||||
expect(retrieved?.flags.has(2)).toBe(true);
|
||||
|
||||
expect(retrieved?.items).toEqual(component.items);
|
||||
expect(retrieved?.nestedConfig.deep.nested.value).toBe(999);
|
||||
|
||||
// 深拷贝验证
|
||||
expect(retrieved?.nestedConfig).not.toBe(component.nestedConfig);
|
||||
|
||||
console.log('✅ 综合装饰器测试通过');
|
||||
});
|
||||
|
||||
test('测试存储器内存统计和容量管理', () => {
|
||||
console.log('\\n=== 存储器管理测试 ===');
|
||||
|
||||
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||
|
||||
// 添加多个组件
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const component = new ComprehensiveComponent();
|
||||
component.intValue = i * 100;
|
||||
component.preciseValue = i * Math.PI;
|
||||
manager.addComponent(i, component);
|
||||
}
|
||||
|
||||
// 检查统计信息
|
||||
const stats = storage.getStats();
|
||||
console.log('存储统计:', {
|
||||
size: stats.size,
|
||||
capacity: stats.capacity,
|
||||
memoryUsage: stats.memoryUsage,
|
||||
fieldCount: stats.fieldStats.size
|
||||
});
|
||||
|
||||
expect(stats.size).toBe(5);
|
||||
expect(stats.capacity).toBeGreaterThanOrEqual(5);
|
||||
expect(stats.memoryUsage).toBeGreaterThan(0);
|
||||
|
||||
// 测试压缩
|
||||
storage.removeComponent(2);
|
||||
storage.removeComponent(4);
|
||||
|
||||
const statsBeforeCompact = storage.getStats();
|
||||
storage.compact();
|
||||
const statsAfterCompact = storage.getStats();
|
||||
|
||||
expect(statsAfterCompact.size).toBe(3);
|
||||
console.log('压缩前后对比:', {
|
||||
before: statsBeforeCompact.size,
|
||||
after: statsAfterCompact.size
|
||||
});
|
||||
|
||||
console.log('✅ 存储器管理测试通过');
|
||||
});
|
||||
|
||||
test('测试序列化错误处理', () => {
|
||||
console.log('\\n=== 序列化错误处理测试 ===');
|
||||
|
||||
// 创建包含循环引用的对象
|
||||
const component = new ComprehensiveComponent();
|
||||
const cyclicObject: any = { name: 'test' };
|
||||
cyclicObject.self = cyclicObject; // 循环引用
|
||||
|
||||
// 这应该不会崩溃,而是优雅处理
|
||||
component.items.push(cyclicObject);
|
||||
|
||||
expect(() => {
|
||||
manager.addComponent(1, component);
|
||||
}).not.toThrow();
|
||||
|
||||
const retrieved = manager.getComponent(1, ComprehensiveComponent);
|
||||
expect(retrieved).toBeDefined();
|
||||
|
||||
console.log('✅ 序列化错误处理测试通过');
|
||||
});
|
||||
|
||||
test('测试大容量扩展和性能', () => {
|
||||
console.log('\\n=== 大容量性能测试 ===');
|
||||
|
||||
const startTime = performance.now();
|
||||
const entityCount = 2000;
|
||||
|
||||
// 创建大量实体
|
||||
for (let i = 1; i <= entityCount; i++) {
|
||||
const component = new ComprehensiveComponent();
|
||||
component.intValue = i;
|
||||
component.preciseValue = i * 0.1;
|
||||
component.gameMap.set(`key${i}`, i);
|
||||
component.flags.add(i % 10);
|
||||
component.items.push(`item${i}`);
|
||||
|
||||
manager.addComponent(i, component);
|
||||
}
|
||||
|
||||
const createTime = performance.now() - startTime;
|
||||
|
||||
// 随机访问测试
|
||||
const readStartTime = performance.now();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const randomId = Math.floor(Math.random() * entityCount) + 1;
|
||||
const component = manager.getComponent(randomId, ComprehensiveComponent);
|
||||
expect(component?.intValue).toBe(randomId);
|
||||
}
|
||||
const readTime = performance.now() - readStartTime;
|
||||
|
||||
console.log(`创建${entityCount}个组件: ${createTime.toFixed(2)}ms`);
|
||||
console.log(`随机读取100次: ${readTime.toFixed(2)}ms`);
|
||||
console.log(`平均创建时间: ${(createTime / entityCount).toFixed(4)}ms/组件`);
|
||||
|
||||
// 验证存储统计
|
||||
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||
const stats = storage.getStats();
|
||||
|
||||
expect(stats.size).toBe(entityCount);
|
||||
expect(stats.capacity).toBeGreaterThanOrEqual(entityCount);
|
||||
|
||||
console.log('✅ 大容量性能测试通过');
|
||||
});
|
||||
|
||||
test('测试空值和边界处理', () => {
|
||||
console.log('\\n=== 空值边界测试 ===');
|
||||
|
||||
const component = new ComprehensiveComponent();
|
||||
|
||||
// 设置各种边界值
|
||||
component.gameMap.set('null', null);
|
||||
component.gameMap.set('undefined', undefined);
|
||||
component.gameMap.set('empty', '');
|
||||
component.gameMap.set('zero', 0);
|
||||
component.gameMap.set('false', false);
|
||||
|
||||
component.flags.add(0);
|
||||
component.items.push(null, undefined, '', 0, false);
|
||||
|
||||
component.nestedConfig = null;
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, ComprehensiveComponent);
|
||||
|
||||
// 验证边界值处理
|
||||
expect(retrieved?.gameMap.get('null')).toBe(null);
|
||||
expect(retrieved?.gameMap.get('undefined')).toBe(null); // JSON序列化会将undefined转为null
|
||||
expect(retrieved?.gameMap.get('empty')).toBe('');
|
||||
expect(retrieved?.gameMap.get('zero')).toBe(0);
|
||||
expect(retrieved?.gameMap.get('false')).toBe(false);
|
||||
|
||||
expect(retrieved?.flags.has(0)).toBe(true);
|
||||
expect(retrieved?.items).toEqual([null, null, '', 0, false]); // undefined序列化为null
|
||||
expect(retrieved?.nestedConfig).toBe(null);
|
||||
|
||||
console.log('✅ 空值边界测试通过');
|
||||
});
|
||||
|
||||
test('测试不同TypedArray类型的字段访问', () => {
|
||||
console.log('\\n=== TypedArray字段测试 ===');
|
||||
|
||||
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||
|
||||
// 添加测试数据
|
||||
const component = new ComprehensiveComponent();
|
||||
manager.addComponent(1, component);
|
||||
|
||||
// 检查不同类型的TypedArray
|
||||
const preciseArray = storage.getFieldArray('preciseValue');
|
||||
const intArray = storage.getFieldArray('intValue');
|
||||
const normalArray = storage.getFieldArray('normalFloat');
|
||||
const flagArray = storage.getFieldArray('flag');
|
||||
|
||||
expect(preciseArray).toBeInstanceOf(Float64Array);
|
||||
expect(intArray).toBeInstanceOf(Int32Array);
|
||||
expect(normalArray).toBeInstanceOf(Float32Array);
|
||||
expect(flagArray).toBeInstanceOf(Uint8Array);
|
||||
|
||||
// 高精度字段不应该在TypedArray中
|
||||
const bigIntArray = storage.getFieldArray('bigIntId');
|
||||
expect(bigIntArray).toBeNull();
|
||||
|
||||
console.log('TypedArray类型验证:', {
|
||||
preciseValue: preciseArray?.constructor.name,
|
||||
intValue: intArray?.constructor.name,
|
||||
normalFloat: normalArray?.constructor.name,
|
||||
flag: flagArray?.constructor.name,
|
||||
bigIntId: bigIntArray ? 'Found' : 'null (正确)'
|
||||
});
|
||||
|
||||
console.log('✅ TypedArray字段测试通过');
|
||||
});
|
||||
|
||||
test('测试向量化批量操作', () => {
|
||||
console.log('\\n=== 向量化操作测试 ===');
|
||||
|
||||
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||
|
||||
// 添加测试数据
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const component = new ComprehensiveComponent();
|
||||
component.normalFloat = i;
|
||||
component.intValue = i * 10;
|
||||
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; // 乘以2
|
||||
intArray[idx] += 5; // 加5
|
||||
}
|
||||
});
|
||||
|
||||
expect(operationExecuted).toBe(true);
|
||||
|
||||
// 验证批量操作结果
|
||||
const component = manager.getComponent(5, ComprehensiveComponent);
|
||||
expect(component?.normalFloat).toBe(10); // 5 * 2
|
||||
expect(component?.intValue).toBe(55); // 50 + 5
|
||||
|
||||
console.log('✅ 向量化操作测试通过');
|
||||
});
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import { SoAStorage, EnableSoA, HighPrecision, Float64, Int32 } from '../../../src/ECS/Core/SoAStorage';
|
||||
|
||||
// 测试组件:使用不同的数值类型装饰器
|
||||
@EnableSoA
|
||||
class DecoratedComponent extends Component {
|
||||
// 默认Float32Array存储
|
||||
public normalFloat: number = 3.14;
|
||||
|
||||
// 高精度存储(作为复杂对象)
|
||||
@HighPrecision
|
||||
public highPrecisionNumber: number = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// Float64Array存储
|
||||
@Float64
|
||||
public preciseFloat: number = Math.PI;
|
||||
|
||||
// Int32Array存储
|
||||
@Int32
|
||||
public integerValue: number = 42;
|
||||
|
||||
// 布尔值(默认Float32Array)
|
||||
public flag: boolean = true;
|
||||
|
||||
// 字符串(专门数组)
|
||||
public text: string = 'hello';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('SoA数值类型装饰器测试', () => {
|
||||
let manager: ComponentStorageManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ComponentStorageManager();
|
||||
});
|
||||
|
||||
test('验证不同装饰器的存储类型', () => {
|
||||
console.log('\\n=== 测试装饰器存储类型 ===');
|
||||
|
||||
const component = new DecoratedComponent();
|
||||
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
|
||||
component.preciseFloat = Math.PI;
|
||||
component.integerValue = 999999;
|
||||
component.normalFloat = 2.718;
|
||||
|
||||
console.log('原始数据:', {
|
||||
normalFloat: component.normalFloat,
|
||||
highPrecisionNumber: component.highPrecisionNumber,
|
||||
preciseFloat: component.preciseFloat,
|
||||
integerValue: component.integerValue,
|
||||
flag: component.flag,
|
||||
text: component.text
|
||||
});
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, DecoratedComponent);
|
||||
|
||||
console.log('\\n取回数据:', {
|
||||
normalFloat: retrieved?.normalFloat,
|
||||
highPrecisionNumber: retrieved?.highPrecisionNumber,
|
||||
preciseFloat: retrieved?.preciseFloat,
|
||||
integerValue: retrieved?.integerValue,
|
||||
flag: retrieved?.flag,
|
||||
text: retrieved?.text
|
||||
});
|
||||
|
||||
// 验证精度保持
|
||||
expect(retrieved?.normalFloat).toBeCloseTo(2.718, 5); // Float32精度
|
||||
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER); // 高精度保持
|
||||
expect(retrieved?.preciseFloat).toBeCloseTo(Math.PI, 15); // Float64精度
|
||||
expect(retrieved?.integerValue).toBe(999999); // 整数保持
|
||||
expect(retrieved?.flag).toBe(true);
|
||||
expect(retrieved?.text).toBe('hello');
|
||||
|
||||
console.log('✅ 所有装饰器类型验证通过');
|
||||
});
|
||||
|
||||
test('验证存储器内部结构', () => {
|
||||
console.log('\\n=== 测试存储器内部结构 ===');
|
||||
|
||||
const component = new DecoratedComponent();
|
||||
manager.addComponent(1, component);
|
||||
|
||||
const storage = manager.getStorage(DecoratedComponent) as SoAStorage<DecoratedComponent>;
|
||||
|
||||
// 检查TypedArray字段
|
||||
const normalFloatArray = storage.getFieldArray('normalFloat');
|
||||
const preciseFloatArray = storage.getFieldArray('preciseFloat');
|
||||
const integerArray = storage.getFieldArray('integerValue');
|
||||
const flagArray = storage.getFieldArray('flag');
|
||||
|
||||
console.log('存储类型:', {
|
||||
normalFloat: normalFloatArray?.constructor.name,
|
||||
preciseFloat: preciseFloatArray?.constructor.name,
|
||||
integerValue: integerArray?.constructor.name,
|
||||
flag: flagArray?.constructor.name
|
||||
});
|
||||
|
||||
// 验证存储类型
|
||||
expect(normalFloatArray).toBeInstanceOf(Float32Array);
|
||||
expect(preciseFloatArray).toBeInstanceOf(Float64Array);
|
||||
expect(integerArray).toBeInstanceOf(Int32Array);
|
||||
expect(flagArray).toBeInstanceOf(Uint8Array);
|
||||
|
||||
// 高精度字段不应该在TypedArray中
|
||||
const highPrecisionArray = storage.getFieldArray('highPrecisionNumber');
|
||||
expect(highPrecisionArray).toBeNull();
|
||||
|
||||
console.log('✅ 存储器内部结构验证通过');
|
||||
});
|
||||
|
||||
test('测试边界值精度', () => {
|
||||
console.log('\\n=== 测试边界值精度 ===');
|
||||
|
||||
const component = new DecoratedComponent();
|
||||
|
||||
// 测试极限值
|
||||
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
|
||||
component.preciseFloat = Number.MIN_VALUE;
|
||||
component.normalFloat = 16777217; // 超出Float32精度
|
||||
component.integerValue = -2147483648; // Int32最小值
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, DecoratedComponent);
|
||||
|
||||
console.log('边界值测试结果:', {
|
||||
highPrecision: retrieved?.highPrecisionNumber === Number.MAX_SAFE_INTEGER,
|
||||
preciseFloat: retrieved?.preciseFloat === Number.MIN_VALUE,
|
||||
normalFloat: retrieved?.normalFloat, // 可能有精度损失
|
||||
integerValue: retrieved?.integerValue === -2147483648
|
||||
});
|
||||
|
||||
// 验证高精度保持
|
||||
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(retrieved?.preciseFloat).toBe(Number.MIN_VALUE);
|
||||
expect(retrieved?.integerValue).toBe(-2147483648);
|
||||
|
||||
console.log('✅ 边界值精度测试通过');
|
||||
});
|
||||
|
||||
test('性能对比:装饰器 vs 自动检测', () => {
|
||||
console.log('\\n=== 性能对比测试 ===');
|
||||
|
||||
const entityCount = 1000;
|
||||
|
||||
// 使用装饰器的组件
|
||||
const startTime = performance.now();
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const component = new DecoratedComponent();
|
||||
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
|
||||
component.preciseFloat = Math.PI * i;
|
||||
component.integerValue = i;
|
||||
manager.addComponent(i, component);
|
||||
}
|
||||
const decoratorTime = performance.now() - startTime;
|
||||
|
||||
console.log(`装饰器方式: ${decoratorTime.toFixed(2)}ms`);
|
||||
console.log(`平均每个组件: ${(decoratorTime / entityCount).toFixed(4)}ms`);
|
||||
|
||||
// 验证数据完整性
|
||||
const sample = manager.getComponent(500, DecoratedComponent);
|
||||
expect(sample?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(sample?.integerValue).toBe(500);
|
||||
|
||||
console.log('✅ 性能测试完成,数据完整性验证通过');
|
||||
});
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import { EnableSoA } from '../../../src/ECS/Core/SoAStorage';
|
||||
|
||||
// 模拟复杂对象(如cocos的node节点)
|
||||
class MockNode {
|
||||
public name: string;
|
||||
public active: boolean;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `Node(${this.name})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 包含复杂属性的组件
|
||||
@EnableSoA
|
||||
class ProblematicComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public node: MockNode | null = null;
|
||||
public callback: Function | null = null;
|
||||
public data: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.node = new MockNode('test');
|
||||
this.callback = () => console.log('test');
|
||||
this.data = { complex: 'object' };
|
||||
}
|
||||
}
|
||||
|
||||
// 安全的数值组件
|
||||
@EnableSoA
|
||||
class SafeComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public active: boolean = true;
|
||||
}
|
||||
|
||||
describe('SoA边界情况和复杂属性测试', () => {
|
||||
let manager: ComponentStorageManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ComponentStorageManager();
|
||||
});
|
||||
|
||||
test('包含复杂对象的组件会有什么问题', () => {
|
||||
console.log('\\n=== 测试复杂对象处理 ===');
|
||||
|
||||
// 创建包含复杂属性的组件
|
||||
const originalComponent = new ProblematicComponent();
|
||||
console.log('原始组件:', {
|
||||
x: originalComponent.x,
|
||||
y: originalComponent.y,
|
||||
node: originalComponent.node?.name,
|
||||
callback: typeof originalComponent.callback,
|
||||
data: originalComponent.data
|
||||
});
|
||||
|
||||
// 添加到SoA存储
|
||||
manager.addComponent(1, originalComponent);
|
||||
|
||||
// 获取组件看看发生了什么
|
||||
const retrievedComponent = manager.getComponent(1, ProblematicComponent);
|
||||
console.log('取回的组件:', {
|
||||
x: retrievedComponent?.x,
|
||||
y: retrievedComponent?.y,
|
||||
node: retrievedComponent?.node,
|
||||
callback: retrievedComponent?.callback,
|
||||
data: retrievedComponent?.data
|
||||
});
|
||||
|
||||
// 验证数据完整性
|
||||
expect(retrievedComponent?.x).toBe(0);
|
||||
expect(retrievedComponent?.y).toBe(0);
|
||||
|
||||
// 复杂对象的问题
|
||||
console.log('\\n⚠️ 问题发现:');
|
||||
console.log('- node对象:', retrievedComponent?.node);
|
||||
console.log('- callback函数:', retrievedComponent?.callback);
|
||||
console.log('- data对象:', retrievedComponent?.data);
|
||||
|
||||
// 复杂属性现在应该正确保存
|
||||
expect(retrievedComponent?.node?.name).toBe('test'); // 应该保持原始值
|
||||
expect(retrievedComponent?.callback).toBe(originalComponent.callback); // 应该是同一个函数
|
||||
expect(retrievedComponent?.data).toEqual({ complex: 'object' }); // 应该保持原始数据
|
||||
|
||||
console.log('✅ 修复成功:复杂对象现在能正确处理!');
|
||||
});
|
||||
|
||||
test('纯数值组件工作正常', () => {
|
||||
console.log('\\n=== 测试纯数值组件 ===');
|
||||
|
||||
const safeComponent = new SafeComponent();
|
||||
safeComponent.x = 100;
|
||||
safeComponent.y = 200;
|
||||
safeComponent.active = false;
|
||||
|
||||
manager.addComponent(1, safeComponent);
|
||||
const retrieved = manager.getComponent(1, SafeComponent);
|
||||
|
||||
console.log('纯数值组件正常工作:', {
|
||||
x: retrieved?.x,
|
||||
y: retrieved?.y,
|
||||
active: retrieved?.active
|
||||
});
|
||||
|
||||
expect(retrieved?.x).toBe(100);
|
||||
expect(retrieved?.y).toBe(200);
|
||||
expect(retrieved?.active).toBe(false);
|
||||
});
|
||||
|
||||
test('SoA是否能检测到不适合的组件类型', () => {
|
||||
console.log('\\n=== 测试类型检测 ===');
|
||||
|
||||
// 当前实现会静默忽略复杂字段
|
||||
// 这是一个潜在的问题!
|
||||
const storage = manager.getStorage(ProblematicComponent);
|
||||
console.log('存储类型:', storage.constructor.name);
|
||||
|
||||
// SoA存储应该能警告或拒绝不适合的组件
|
||||
expect(storage.constructor.name).toBe('SoAStorage');
|
||||
});
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import { EnableSoA, HighPrecision, Float64 } from '../../../src/ECS/Core/SoAStorage';
|
||||
|
||||
// 包含所有基础类型的组件
|
||||
@EnableSoA
|
||||
class AllTypesComponent extends Component {
|
||||
// 数值类型
|
||||
public intNumber: number = 42;
|
||||
public floatNumber: number = 3.14;
|
||||
public zeroNumber: number = 0;
|
||||
|
||||
// 布尔类型
|
||||
public trueBoolean: boolean = true;
|
||||
public falseBoolean: boolean = false;
|
||||
|
||||
// 字符串类型
|
||||
public emptyString: string = '';
|
||||
public normalString: string = 'hello';
|
||||
public longString: string = 'this is a long string with spaces and 123 numbers!';
|
||||
|
||||
// 其他基础类型
|
||||
public nullValue: null = null;
|
||||
public undefinedValue: undefined = undefined;
|
||||
|
||||
// 复杂类型
|
||||
public arrayValue: number[] = [1, 2, 3];
|
||||
public objectValue: { name: string } = { name: 'test' };
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 边界测试专用组件
|
||||
@EnableSoA
|
||||
class BoundaryTestComponent extends Component {
|
||||
// 高精度大整数
|
||||
@HighPrecision
|
||||
public maxInt: number = 0;
|
||||
|
||||
// 高精度小浮点数
|
||||
@Float64
|
||||
public minFloat: number = 0;
|
||||
|
||||
// 普通数值
|
||||
public normalNumber: number = 0;
|
||||
|
||||
// 字符串测试
|
||||
public testString: string = '';
|
||||
public longString: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('SoA所有数据类型处理测试', () => {
|
||||
let manager: ComponentStorageManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ComponentStorageManager();
|
||||
});
|
||||
|
||||
test('验证所有基础类型的处理', () => {
|
||||
console.log('\\n=== 测试所有数据类型 ===');
|
||||
|
||||
// 创建包含各种类型的组件
|
||||
const originalComponent = new AllTypesComponent();
|
||||
originalComponent.normalString = 'modified string';
|
||||
originalComponent.longString = '测试中文字符串 with emoji 🎉';
|
||||
originalComponent.intNumber = 999;
|
||||
originalComponent.floatNumber = 2.718;
|
||||
originalComponent.trueBoolean = false;
|
||||
originalComponent.falseBoolean = true;
|
||||
|
||||
console.log('原始组件数据:', {
|
||||
intNumber: originalComponent.intNumber,
|
||||
floatNumber: originalComponent.floatNumber,
|
||||
trueBoolean: originalComponent.trueBoolean,
|
||||
falseBoolean: originalComponent.falseBoolean,
|
||||
emptyString: `"${originalComponent.emptyString}"`,
|
||||
normalString: `"${originalComponent.normalString}"`,
|
||||
longString: `"${originalComponent.longString}"`,
|
||||
arrayValue: originalComponent.arrayValue,
|
||||
objectValue: originalComponent.objectValue
|
||||
});
|
||||
|
||||
// 存储到SoA
|
||||
manager.addComponent(1, originalComponent);
|
||||
|
||||
// 获取并验证
|
||||
const retrievedComponent = manager.getComponent(1, AllTypesComponent);
|
||||
|
||||
console.log('\\n取回的组件数据:', {
|
||||
intNumber: retrievedComponent?.intNumber,
|
||||
floatNumber: retrievedComponent?.floatNumber,
|
||||
trueBoolean: retrievedComponent?.trueBoolean,
|
||||
falseBoolean: retrievedComponent?.falseBoolean,
|
||||
emptyString: `"${retrievedComponent?.emptyString}"`,
|
||||
normalString: `"${retrievedComponent?.normalString}"`,
|
||||
longString: `"${retrievedComponent?.longString}"`,
|
||||
arrayValue: retrievedComponent?.arrayValue,
|
||||
objectValue: retrievedComponent?.objectValue
|
||||
});
|
||||
|
||||
// 验证数值类型
|
||||
expect(retrievedComponent?.intNumber).toBe(999);
|
||||
expect(retrievedComponent?.floatNumber).toBeCloseTo(2.718);
|
||||
|
||||
// 验证布尔类型
|
||||
expect(retrievedComponent?.trueBoolean).toBe(false);
|
||||
expect(retrievedComponent?.falseBoolean).toBe(true);
|
||||
|
||||
// 验证字符串类型
|
||||
expect(retrievedComponent?.emptyString).toBe('');
|
||||
expect(retrievedComponent?.normalString).toBe('modified string');
|
||||
expect(retrievedComponent?.longString).toBe('测试中文字符串 with emoji 🎉');
|
||||
|
||||
// 验证复杂类型
|
||||
expect(retrievedComponent?.arrayValue).toEqual([1, 2, 3]);
|
||||
expect(retrievedComponent?.objectValue).toEqual({ name: 'test' });
|
||||
|
||||
console.log('\\n✅ 所有类型验证完成');
|
||||
});
|
||||
|
||||
test('边界情况测试', () => {
|
||||
console.log('\\n=== 边界情况测试 ===');
|
||||
|
||||
const component = new BoundaryTestComponent();
|
||||
|
||||
// 特殊数值
|
||||
component.maxInt = Number.MAX_SAFE_INTEGER;
|
||||
component.minFloat = Number.MIN_VALUE;
|
||||
component.normalNumber = -0;
|
||||
|
||||
// 特殊字符串
|
||||
component.testString = '\\n\\t\\r"\'\\\\'; // 转义字符
|
||||
component.longString = 'a'.repeat(1000); // 长字符串
|
||||
|
||||
manager.addComponent(1, component);
|
||||
const retrieved = manager.getComponent(1, BoundaryTestComponent);
|
||||
|
||||
console.log('边界情况结果:', {
|
||||
maxInt: retrieved?.maxInt,
|
||||
minFloat: retrieved?.minFloat,
|
||||
negativeZero: retrieved?.normalNumber,
|
||||
escapeStr: retrieved?.testString,
|
||||
longStr: retrieved?.longString?.length
|
||||
});
|
||||
|
||||
expect(retrieved?.maxInt).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(retrieved?.minFloat).toBe(Number.MIN_VALUE);
|
||||
expect(retrieved?.testString).toBe('\\n\\t\\r"\'\\\\');
|
||||
expect(retrieved?.longString).toBe('a'.repeat(1000));
|
||||
|
||||
console.log('✅ 边界情况测试通过');
|
||||
});
|
||||
});
|
||||
512
packages/core/tests/ECS/Core/SystemInitialization.test.ts
Normal file
512
packages/core/tests/ECS/Core/SystemInitialization.test.ts
Normal file
@@ -0,0 +1,512 @@
|
||||
import { Scene } from '../../../src/ECS/Scene';
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
|
||||
import { Matcher } from '../../../src/ECS/Utils/Matcher';
|
||||
import { ComponentTypeManager } from '../../../src/ECS/Utils/ComponentTypeManager';
|
||||
|
||||
/**
|
||||
* System初始化测试套件
|
||||
*
|
||||
* 测试覆盖:
|
||||
* - 系统初始化时序问题(先添加实体 vs 先添加系统)
|
||||
* - 系统重复初始化防护
|
||||
* - 动态组件修改响应
|
||||
* - 系统生命周期管理
|
||||
*/
|
||||
|
||||
// 测试组件
|
||||
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;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
const [health = 100] = args as [number?];
|
||||
this.health = health;
|
||||
}
|
||||
}
|
||||
|
||||
class TagComponent extends Component {
|
||||
public tag: string;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
const [tag = ''] = args as [string?];
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
ComponentTypeManager.instance.reset();
|
||||
scene = new Scene();
|
||||
scene.name = 'InitializationTestScene';
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,500 +0,0 @@
|
||||
import { Scene } from '../../../src/ECS/Scene';
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
|
||||
import { Matcher } from '../../../src/ECS/Utils/Matcher';
|
||||
|
||||
// 测试组件
|
||||
class PositionComponent extends Component {
|
||||
public x: number;
|
||||
public y: number;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
this.x = (args[0] as number) ?? 0;
|
||||
this.y = (args[1] as number) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number;
|
||||
public vy: number;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
this.vx = (args[0] as number) ?? 0;
|
||||
this.vy = (args[1] as number) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public health: number;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
this.health = (args[0] as number) ?? 100;
|
||||
}
|
||||
}
|
||||
|
||||
class TagComponent extends Component {
|
||||
public tag: string;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
this.tag = (args[0] as string) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// 测试系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
public processedEntities: Entity[] = [];
|
||||
public initializeCalled = false;
|
||||
public onAddedEntities: Entity[] = [];
|
||||
public onRemovedEntities: Entity[] = [];
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
public override initialize(): void {
|
||||
this.initializeCalled = true;
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
protected override onAdded(entity: Entity): void {
|
||||
this.onAddedEntities.push(entity);
|
||||
}
|
||||
|
||||
protected override onRemoved(entity: Entity): void {
|
||||
this.onRemovedEntities.push(entity);
|
||||
}
|
||||
|
||||
protected override process(entities: Entity[]): void {
|
||||
this.processedEntities = [...entities];
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent)!;
|
||||
const velocity = entity.getComponent(VelocityComponent)!;
|
||||
position.x += velocity.vx;
|
||||
position.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
public processedEntities: Entity[] = [];
|
||||
public initializeCalled = false;
|
||||
public onAddedEntities: Entity[] = [];
|
||||
public onRemovedEntities: Entity[] = [];
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
public override initialize(): void {
|
||||
this.initializeCalled = true;
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
protected override onAdded(entity: Entity): void {
|
||||
this.onAddedEntities.push(entity);
|
||||
}
|
||||
|
||||
protected override onRemoved(entity: Entity): void {
|
||||
this.onRemovedEntities.push(entity);
|
||||
}
|
||||
|
||||
protected override process(entities: Entity[]): void {
|
||||
this.processedEntities = [...entities];
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent)!;
|
||||
if (health.health <= 0) {
|
||||
entity.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MultiComponentSystem extends EntitySystem {
|
||||
public processedEntities: Entity[] = [];
|
||||
public initializeCalled = false;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent, HealthComponent, TagComponent));
|
||||
}
|
||||
|
||||
public override initialize(): void {
|
||||
this.initializeCalled = true;
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
protected override process(entities: Entity[]): void {
|
||||
this.processedEntities = [...entities];
|
||||
}
|
||||
}
|
||||
|
||||
describe('ECS系统初始化时序问题深度测试', () => {
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
scene = new Scene();
|
||||
scene.name = "InitializeTestScene";
|
||||
});
|
||||
|
||||
describe('基础时序问题测试', () => {
|
||||
test('先添加实体再添加系统 - 系统应该正确初始化', () => {
|
||||
// 创建实体并添加组件
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(10, 20));
|
||||
player.addComponent(new VelocityComponent(1, 1));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
|
||||
const enemy = scene.createEntity("Enemy");
|
||||
enemy.addComponent(new PositionComponent(50, 60));
|
||||
enemy.addComponent(new VelocityComponent(-1, 0));
|
||||
enemy.addComponent(new HealthComponent(80));
|
||||
|
||||
// 验证实体已创建
|
||||
expect(scene.entities.count).toBe(2);
|
||||
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
const healthSystem = new HealthSystem();
|
||||
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
scene.addEntityProcessor(healthSystem);
|
||||
|
||||
// 验证initialize方法被调用
|
||||
expect(movementSystem.initializeCalled).toBe(true);
|
||||
expect(healthSystem.initializeCalled).toBe(true);
|
||||
|
||||
// 验证系统正确识别已存在的实体
|
||||
expect(movementSystem.entities.length).toBe(2);
|
||||
expect(healthSystem.entities.length).toBe(2);
|
||||
|
||||
// 验证onAdded回调被正确调用
|
||||
expect(movementSystem.onAddedEntities.length).toBe(2);
|
||||
expect(movementSystem.onAddedEntities).toContain(player);
|
||||
expect(movementSystem.onAddedEntities).toContain(enemy);
|
||||
|
||||
// 运行更新确认处理
|
||||
scene.update();
|
||||
expect(movementSystem.processedEntities.length).toBe(2);
|
||||
expect(healthSystem.processedEntities.length).toBe(2);
|
||||
|
||||
// 检查移动逻辑是否生效
|
||||
const playerPos = player.getComponent(PositionComponent)!;
|
||||
expect(playerPos.x).toBe(11);
|
||||
expect(playerPos.y).toBe(21);
|
||||
});
|
||||
|
||||
test('先添加系统再添加实体 - 正常工作', () => {
|
||||
// 先添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
const healthSystem = new HealthSystem();
|
||||
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
scene.addEntityProcessor(healthSystem);
|
||||
|
||||
// 验证initialize被调用,但没有发现实体
|
||||
expect(movementSystem.initializeCalled).toBe(true);
|
||||
expect(healthSystem.initializeCalled).toBe(true);
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
expect(healthSystem.entities.length).toBe(0);
|
||||
|
||||
// 创建实体
|
||||
const player = scene.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(10, 20));
|
||||
player.addComponent(new VelocityComponent(1, 1));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
|
||||
// 系统应该自动识别新实体
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
expect(healthSystem.entities.length).toBe(1);
|
||||
expect(movementSystem.onAddedEntities.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('复杂场景的时序测试', () => {
|
||||
test('部分匹配实体的初始化', () => {
|
||||
// 创建不同类型的实体
|
||||
const fullEntity = scene.createEntity("FullEntity");
|
||||
fullEntity.addComponent(new PositionComponent(0, 0));
|
||||
fullEntity.addComponent(new VelocityComponent(1, 1));
|
||||
fullEntity.addComponent(new HealthComponent(100));
|
||||
|
||||
const partialEntity1 = scene.createEntity("PartialEntity1");
|
||||
partialEntity1.addComponent(new PositionComponent(10, 10));
|
||||
partialEntity1.addComponent(new HealthComponent(50));
|
||||
// 缺少VelocityComponent
|
||||
|
||||
const partialEntity2 = scene.createEntity("PartialEntity2");
|
||||
partialEntity2.addComponent(new PositionComponent(20, 20));
|
||||
partialEntity2.addComponent(new VelocityComponent(2, 2));
|
||||
// 缺少HealthComponent
|
||||
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
const healthSystem = new HealthSystem();
|
||||
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
scene.addEntityProcessor(healthSystem);
|
||||
|
||||
// 验证选择性匹配
|
||||
expect(movementSystem.entities).toContain(fullEntity);
|
||||
expect(movementSystem.entities).not.toContain(partialEntity1);
|
||||
expect(movementSystem.entities).toContain(partialEntity2);
|
||||
expect(movementSystem.entities.length).toBe(2);
|
||||
|
||||
expect(healthSystem.entities).toContain(fullEntity);
|
||||
expect(healthSystem.entities).toContain(partialEntity1);
|
||||
expect(healthSystem.entities).not.toContain(partialEntity2);
|
||||
expect(healthSystem.entities.length).toBe(2);
|
||||
});
|
||||
|
||||
test('多组件要求系统的初始化', () => {
|
||||
// 创建具有不同组件组合的实体
|
||||
const entity1 = scene.createEntity("Entity1");
|
||||
entity1.addComponent(new PositionComponent(0, 0));
|
||||
entity1.addComponent(new HealthComponent(100));
|
||||
entity1.addComponent(new TagComponent("player"));
|
||||
|
||||
const entity2 = scene.createEntity("Entity2");
|
||||
entity2.addComponent(new PositionComponent(10, 10));
|
||||
entity2.addComponent(new HealthComponent(80));
|
||||
// 缺少TagComponent
|
||||
|
||||
const entity3 = scene.createEntity("Entity3");
|
||||
entity3.addComponent(new PositionComponent(20, 20));
|
||||
entity3.addComponent(new TagComponent("enemy"));
|
||||
// 缺少HealthComponent
|
||||
|
||||
// 添加要求三个组件的系统
|
||||
const multiSystem = new MultiComponentSystem();
|
||||
scene.addEntityProcessor(multiSystem);
|
||||
|
||||
// 只有entity1应该匹配
|
||||
expect(multiSystem.entities.length).toBe(1);
|
||||
expect(multiSystem.entities).toContain(entity1);
|
||||
expect(multiSystem.entities).not.toContain(entity2);
|
||||
expect(multiSystem.entities).not.toContain(entity3);
|
||||
});
|
||||
|
||||
test('批量实体创建后的系统初始化', () => {
|
||||
// 批量创建实体
|
||||
const entities = scene.createEntities(10, "BatchEntity");
|
||||
|
||||
// 为所有实体添加组件
|
||||
entities.forEach((entity, index) => {
|
||||
entity.addComponent(new PositionComponent(index * 10, index * 10));
|
||||
entity.addComponent(new VelocityComponent(index, index));
|
||||
if (index % 2 === 0) {
|
||||
entity.addComponent(new HealthComponent(100 - index * 10));
|
||||
}
|
||||
});
|
||||
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
const healthSystem = new HealthSystem();
|
||||
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
scene.addEntityProcessor(healthSystem);
|
||||
|
||||
// 验证系统正确处理批量实体
|
||||
expect(movementSystem.entities.length).toBe(10); // 所有实体都有Position+Velocity
|
||||
expect(healthSystem.entities.length).toBe(5); // 只有偶数索引的实体有Health
|
||||
|
||||
// 验证onAdded回调被正确调用
|
||||
expect(movementSystem.onAddedEntities.length).toBe(10);
|
||||
expect(healthSystem.onAddedEntities.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('动态组件修改后的系统响应', () => {
|
||||
test('运行时添加组件 - 系统自动响应', () => {
|
||||
const movementSystem = new MovementSystem();
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
|
||||
// 创建只有位置组件的实体
|
||||
const entity = scene.createEntity("TestEntity");
|
||||
entity.addComponent(new PositionComponent(0, 0));
|
||||
|
||||
// 系统不应该匹配
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
|
||||
// 添加速度组件
|
||||
entity.addComponent(new VelocityComponent(5, 5));
|
||||
|
||||
// 系统应该立即识别
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
expect(movementSystem.entities).toContain(entity);
|
||||
expect(movementSystem.onAddedEntities).toContain(entity);
|
||||
});
|
||||
|
||||
test('运行时移除组件 - 系统自动响应', () => {
|
||||
const movementSystem = new MovementSystem();
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
|
||||
// 创建完整的可移动实体
|
||||
const entity = scene.createEntity("MovableEntity");
|
||||
entity.addComponent(new PositionComponent(0, 0));
|
||||
entity.addComponent(new VelocityComponent(5, 5));
|
||||
|
||||
// 系统应该识别
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
|
||||
// 移除速度组件
|
||||
const velocityComponent = entity.getComponent(VelocityComponent);
|
||||
if (velocityComponent) {
|
||||
entity.removeComponent(velocityComponent);
|
||||
}
|
||||
|
||||
// 系统应该移除实体
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
expect(movementSystem.onRemovedEntities).toContain(entity);
|
||||
});
|
||||
|
||||
test('复杂的组件添加移除序列', () => {
|
||||
const movementSystem = new MovementSystem();
|
||||
const healthSystem = new HealthSystem();
|
||||
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
scene.addEntityProcessor(healthSystem);
|
||||
|
||||
const entity = scene.createEntity("ComplexEntity");
|
||||
|
||||
// 初始状态:无组件
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
expect(healthSystem.entities.length).toBe(0);
|
||||
|
||||
// 添加位置组件
|
||||
entity.addComponent(new PositionComponent(0, 0));
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
expect(healthSystem.entities.length).toBe(0);
|
||||
|
||||
// 添加健康组件
|
||||
entity.addComponent(new HealthComponent(100));
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
expect(healthSystem.entities.length).toBe(1);
|
||||
|
||||
// 添加速度组件
|
||||
entity.addComponent(new VelocityComponent(1, 1));
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
expect(healthSystem.entities.length).toBe(1);
|
||||
|
||||
// 移除健康组件
|
||||
const healthComponent = entity.getComponent(HealthComponent);
|
||||
if (healthComponent) {
|
||||
entity.removeComponent(healthComponent);
|
||||
}
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
expect(healthSystem.entities.length).toBe(0);
|
||||
|
||||
// 移除位置组件
|
||||
const positionComponent = entity.getComponent(PositionComponent);
|
||||
if (positionComponent) {
|
||||
entity.removeComponent(positionComponent);
|
||||
}
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
expect(healthSystem.entities.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('系统重复添加和移除测试', () => {
|
||||
test('重复添加同一个系统 - 应该忽略', () => {
|
||||
const movementSystem = new MovementSystem();
|
||||
|
||||
// 第一次添加
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
expect(scene.entityProcessors.count).toBe(1);
|
||||
expect(movementSystem.initializeCalled).toBe(true);
|
||||
|
||||
// 重置标志
|
||||
movementSystem.initializeCalled = false;
|
||||
|
||||
// 第二次添加同一个系统
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
expect(scene.entityProcessors.count).toBe(1); // 没有增加
|
||||
expect(movementSystem.initializeCalled).toBe(false); // initialize不应该再次调用
|
||||
});
|
||||
|
||||
test('添加后移除再添加 - 应该重新初始化', () => {
|
||||
const entity = scene.createEntity("TestEntity");
|
||||
entity.addComponent(new PositionComponent(0, 0));
|
||||
entity.addComponent(new VelocityComponent(1, 1));
|
||||
|
||||
const movementSystem = new MovementSystem();
|
||||
|
||||
// 第一次添加
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
expect(movementSystem.initializeCalled).toBe(true);
|
||||
|
||||
// 移除系统
|
||||
scene.removeEntityProcessor(movementSystem);
|
||||
expect(scene.entityProcessors.count).toBe(0);
|
||||
|
||||
// 重置状态
|
||||
movementSystem.initializeCalled = false;
|
||||
movementSystem.onAddedEntities = [];
|
||||
|
||||
// 重新添加
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
expect(movementSystem.initializeCalled).toBe(true);
|
||||
expect(movementSystem.onAddedEntities.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('空场景和空系统的边界情况', () => {
|
||||
test('空场景添加系统 - 不应该出错', () => {
|
||||
const movementSystem = new MovementSystem();
|
||||
|
||||
expect(() => {
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(movementSystem.initializeCalled).toBe(true);
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
});
|
||||
|
||||
test('有实体但没有匹配组件 - 系统应该为空', () => {
|
||||
// 创建只有健康组件的实体
|
||||
const entity = scene.createEntity("HealthOnlyEntity");
|
||||
entity.addComponent(new HealthComponent(100));
|
||||
|
||||
// 添加移动系统(需要Position+Velocity)
|
||||
const movementSystem = new MovementSystem();
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
|
||||
expect(movementSystem.entities.length).toBe(0);
|
||||
expect(movementSystem.onAddedEntities.length).toBe(0);
|
||||
});
|
||||
|
||||
test('实体被禁用 - 系统仍应包含但不处理', () => {
|
||||
const entity = scene.createEntity("DisabledEntity");
|
||||
entity.addComponent(new PositionComponent(0, 0));
|
||||
entity.addComponent(new VelocityComponent(1, 1));
|
||||
|
||||
const movementSystem = new MovementSystem();
|
||||
scene.addEntityProcessor(movementSystem);
|
||||
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
|
||||
// 禁用实体
|
||||
entity.enabled = false;
|
||||
|
||||
// 系统仍然包含实体,但处理时应该跳过
|
||||
expect(movementSystem.entities.length).toBe(1);
|
||||
|
||||
scene.update();
|
||||
// 处理逻辑中应该检查enabled状态
|
||||
// 由于实体被禁用,位置不应该改变(这取决于系统实现)
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scene.destroyAllEntities();
|
||||
const processors = [...scene.entityProcessors.processors];
|
||||
processors.forEach(processor => scene.removeEntityProcessor(processor));
|
||||
});
|
||||
});
|
||||
@@ -1,140 +0,0 @@
|
||||
import { Scene } from '../../../src/ECS/Scene';
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
|
||||
import { ComponentTypeManager } from '../../../src/ECS/Utils/ComponentTypeManager';
|
||||
|
||||
class TestComponent extends Component {
|
||||
public value: number = 0;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
const [value = 0] = args as [number?];
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class TrackingSystem extends EntitySystem {
|
||||
public initializeCallCount = 0;
|
||||
public onChangedCallCount = 0;
|
||||
public trackedEntities: Entity[] = [];
|
||||
|
||||
public override initialize(): void {
|
||||
// 必须先调用父类的initialize来检查防重复逻辑
|
||||
const wasInitialized = (this as any)._initialized;
|
||||
super.initialize();
|
||||
|
||||
// 只有在真正执行初始化时才增加计数和处理实体
|
||||
if (!wasInitialized) {
|
||||
this.initializeCallCount++;
|
||||
|
||||
// 处理所有现有实体
|
||||
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('系统多次初始化问题测试', () => {
|
||||
let scene: Scene;
|
||||
let system: TrackingSystem;
|
||||
|
||||
beforeEach(() => {
|
||||
ComponentTypeManager.instance.reset();
|
||||
scene = new Scene();
|
||||
system = new TrackingSystem();
|
||||
});
|
||||
|
||||
test('系统被多次添加到场景 - 应该防止重复初始化', () => {
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
entity.addComponent(new TestComponent(10));
|
||||
|
||||
// 第一次添加系统
|
||||
scene.addEntityProcessor(system);
|
||||
expect(system.initializeCallCount).toBe(1);
|
||||
expect(system.trackedEntities.length).toBe(1);
|
||||
expect(system.onChangedCallCount).toBe(1);
|
||||
|
||||
// 再次添加同一个系统 - 应该被忽略
|
||||
scene.addEntityProcessor(system);
|
||||
expect(system.initializeCallCount).toBe(1); // 不应该增加
|
||||
expect(system.trackedEntities.length).toBe(1); // 实体不应该重复
|
||||
expect(system.onChangedCallCount).toBe(1); // onChanged不应该重复调用
|
||||
});
|
||||
|
||||
test('手动多次调用initialize - 应该防止重复处理', () => {
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
entity.addComponent(new TestComponent(10));
|
||||
|
||||
scene.addEntityProcessor(system);
|
||||
expect(system.initializeCallCount).toBe(1);
|
||||
expect(system.trackedEntities.length).toBe(1);
|
||||
expect(system.onChangedCallCount).toBe(1);
|
||||
|
||||
// 手动再次调用initialize - 应该被防止
|
||||
system.initialize();
|
||||
expect(system.initializeCallCount).toBe(1); // 不应该增加
|
||||
expect(system.onChangedCallCount).toBe(1); // onChanged不应该重复调用
|
||||
expect(system.trackedEntities.length).toBe(1);
|
||||
});
|
||||
|
||||
test('系统被移除后重新添加 - 应该重新初始化', () => {
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
entity.addComponent(new TestComponent(10));
|
||||
|
||||
// 添加系统
|
||||
scene.addEntityProcessor(system);
|
||||
expect(system.initializeCallCount).toBe(1);
|
||||
expect(system.trackedEntities.length).toBe(1);
|
||||
|
||||
// 移除系统
|
||||
scene.removeEntityProcessor(system);
|
||||
|
||||
// 重新添加系统 - 应该重新初始化
|
||||
scene.addEntityProcessor(system);
|
||||
expect(system.initializeCallCount).toBe(2); // 应该重新初始化
|
||||
expect(system.trackedEntities.length).toBe(1);
|
||||
});
|
||||
|
||||
test('多个实体的重复初始化应该被防止', () => {
|
||||
// 创建多个实体
|
||||
const entities = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const entity = scene.createEntity(`Entity${i}`);
|
||||
entity.addComponent(new TestComponent(i));
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
scene.addEntityProcessor(system);
|
||||
expect(system.initializeCallCount).toBe(1);
|
||||
expect(system.trackedEntities.length).toBe(5);
|
||||
expect(system.onChangedCallCount).toBe(5);
|
||||
|
||||
// 手动再次初始化 - 应该被防止
|
||||
system.initialize();
|
||||
expect(system.initializeCallCount).toBe(1); // 不应该增加
|
||||
expect(system.onChangedCallCount).toBe(5); // 不应该重复处理
|
||||
expect(system.trackedEntities.length).toBe(5);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user