新增soastorage存储器
This commit is contained in:
88
tests/ECS/Core/ComponentStorage.auto.test.ts
Normal file
88
tests/ECS/Core/ComponentStorage.auto.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager, EnableSoA } from '../../../src/ECS/Core/ComponentStorage';
|
||||
|
||||
// 默认原始存储组件
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
}
|
||||
|
||||
// 启用SoA优化的组件(用于大规模批量操作)
|
||||
@EnableSoA
|
||||
class LargeScaleComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
public vz: number = 0;
|
||||
}
|
||||
|
||||
describe('SoA优化选择测试', () => {
|
||||
let manager: ComponentStorageManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ComponentStorageManager();
|
||||
});
|
||||
|
||||
test('默认使用原始存储', () => {
|
||||
const storage = manager.getStorage(PositionComponent);
|
||||
|
||||
// 添加组件
|
||||
manager.addComponent(1, new PositionComponent());
|
||||
|
||||
// 验证能正常工作
|
||||
const component = manager.getComponent(1, PositionComponent);
|
||||
expect(component).toBeTruthy();
|
||||
expect(component?.x).toBe(0);
|
||||
|
||||
// 验证使用原始存储
|
||||
expect(storage.constructor.name).toBe('ComponentStorage');
|
||||
});
|
||||
|
||||
test('@EnableSoA装饰器启用优化', () => {
|
||||
const storage = manager.getStorage(LargeScaleComponent);
|
||||
|
||||
// 添加组件
|
||||
const component = new LargeScaleComponent();
|
||||
component.x = 100;
|
||||
component.vx = 10;
|
||||
manager.addComponent(1, component);
|
||||
|
||||
// 验证能正常工作
|
||||
const retrieved = manager.getComponent(1, LargeScaleComponent);
|
||||
expect(retrieved).toBeTruthy();
|
||||
expect(retrieved?.x).toBe(100);
|
||||
expect(retrieved?.vx).toBe(10);
|
||||
|
||||
// 验证使用SoA存储
|
||||
expect(storage.constructor.name).toBe('SoAStorage');
|
||||
});
|
||||
|
||||
test('SoA存储功能验证', () => {
|
||||
const entityCount = 1000;
|
||||
|
||||
// 创建实体(使用SoA优化)
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const component = new LargeScaleComponent();
|
||||
component.x = i;
|
||||
component.y = i * 2;
|
||||
component.vx = 1;
|
||||
component.vy = 2;
|
||||
manager.addComponent(i, component);
|
||||
}
|
||||
|
||||
// 验证数据正确性
|
||||
const testComponent = manager.getComponent(100, LargeScaleComponent);
|
||||
expect(testComponent?.x).toBe(100);
|
||||
expect(testComponent?.y).toBe(200);
|
||||
expect(testComponent?.vx).toBe(1);
|
||||
expect(testComponent?.vy).toBe(2);
|
||||
|
||||
// 验证存储类型
|
||||
const storage = manager.getStorage(LargeScaleComponent);
|
||||
expect(storage.constructor.name).toBe('SoAStorage');
|
||||
console.log(`成功创建 ${entityCount} 个SoA实体,数据验证通过`);
|
||||
});
|
||||
});
|
||||
530
tests/ECS/Core/ComponentStorage.comparison.test.ts
Normal file
530
tests/ECS/Core/ComponentStorage.comparison.test.ts
Normal file
@@ -0,0 +1,530 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorage, ComponentStorageManager, EnableSoA } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import { SoAStorage } from '../../../src/ECS/Core/SoAStorage';
|
||||
|
||||
// 测试用统一组件结构(启用SoA)
|
||||
@EnableSoA
|
||||
class TestPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x = 0, y = 0, z = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableSoA
|
||||
class TestVelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
public vz: number = 0;
|
||||
public maxSpeed: number = 100;
|
||||
|
||||
constructor(vx = 0, vy = 0, vz = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.vz = vz;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableSoA
|
||||
class TestHealthComponent extends Component {
|
||||
public current: number = 100;
|
||||
public max: number = 100;
|
||||
public regeneration: number = 1;
|
||||
|
||||
constructor(current = 100, max = 100) {
|
||||
super();
|
||||
this.current = current;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于原始存储测试的版本(默认原始存储)
|
||||
class OriginalPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x = 0, y = 0, z = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
class OriginalVelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
public vz: number = 0;
|
||||
public maxSpeed: number = 100;
|
||||
|
||||
constructor(vx = 0, vy = 0, vz = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.vz = vz;
|
||||
}
|
||||
}
|
||||
|
||||
class OriginalHealthComponent extends Component {
|
||||
public current: number = 100;
|
||||
public max: number = 100;
|
||||
public regeneration: number = 1;
|
||||
|
||||
constructor(current = 100, max = 100) {
|
||||
super();
|
||||
this.current = current;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
|
||||
interface PerformanceResult {
|
||||
name: string;
|
||||
storageType: 'Original' | 'SoA';
|
||||
entityCount: number;
|
||||
operations: number;
|
||||
totalTime: number;
|
||||
averageTime: number;
|
||||
operationsPerSecond: number;
|
||||
}
|
||||
|
||||
describe('ComponentStorage 严谨性能对比测试', () => {
|
||||
const entityCounts = [1000, 5000, 20000];
|
||||
let results: PerformanceResult[] = [];
|
||||
|
||||
afterAll(() => {
|
||||
generateDetailedReport();
|
||||
});
|
||||
|
||||
describe('存储器创建和初始化', () => {
|
||||
test('验证SoA和原始存储使用相同接口', () => {
|
||||
const originalManager = new ComponentStorageManager();
|
||||
const soaManager = new ComponentStorageManager();
|
||||
|
||||
const originalStorage = originalManager.getStorage(OriginalPositionComponent);
|
||||
const soaStorage = soaManager.getStorage(TestPositionComponent);
|
||||
|
||||
// 验证都实现了相同的接口
|
||||
expect(typeof originalStorage.addComponent).toBe('function');
|
||||
expect(typeof originalStorage.getComponent).toBe('function');
|
||||
expect(typeof originalStorage.hasComponent).toBe('function');
|
||||
expect(typeof originalStorage.removeComponent).toBe('function');
|
||||
|
||||
expect(typeof soaStorage.addComponent).toBe('function');
|
||||
expect(typeof soaStorage.getComponent).toBe('function');
|
||||
expect(typeof soaStorage.hasComponent).toBe('function');
|
||||
expect(typeof soaStorage.removeComponent).toBe('function');
|
||||
|
||||
// 验证存储器类型
|
||||
expect(originalStorage).toBeInstanceOf(ComponentStorage);
|
||||
expect(soaStorage).toBeInstanceOf(SoAStorage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('实体创建性能对比', () => {
|
||||
entityCounts.forEach(entityCount => {
|
||||
test(`创建 ${entityCount} 个完整实体`, () => {
|
||||
console.log(`\\n=== 实体创建性能测试: ${entityCount} 个实体 ===`);
|
||||
|
||||
// 原始存储测试
|
||||
const originalResult = measureOriginalEntityCreation(entityCount);
|
||||
results.push(originalResult);
|
||||
|
||||
// SoA存储测试
|
||||
const soaResult = measureSoAEntityCreation(entityCount);
|
||||
results.push(soaResult);
|
||||
|
||||
// 输出对比结果
|
||||
console.log(`原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
console.log(`SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
|
||||
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||
const improvement = ((speedup - 1) * 100);
|
||||
console.log(`性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||
|
||||
// 验证功能正确性
|
||||
expect(originalResult.operations).toBe(soaResult.operations);
|
||||
expect(originalResult.totalTime).toBeGreaterThan(0);
|
||||
expect(soaResult.totalTime).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('组件访问性能对比', () => {
|
||||
entityCounts.forEach(entityCount => {
|
||||
test(`随机访问 ${entityCount} 个实体组件`, () => {
|
||||
console.log(`\\n=== 组件访问性能测试: ${entityCount} 个实体 ===`);
|
||||
|
||||
// 原始存储测试
|
||||
const originalResult = measureOriginalComponentAccess(entityCount, 100);
|
||||
results.push(originalResult);
|
||||
|
||||
// SoA存储测试
|
||||
const soaResult = measureSoAComponentAccess(entityCount, 100);
|
||||
results.push(soaResult);
|
||||
|
||||
// 输出对比结果
|
||||
console.log(`原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
console.log(`SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
|
||||
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||
const improvement = ((speedup - 1) * 100);
|
||||
console.log(`性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||
|
||||
expect(originalResult.operations).toBe(soaResult.operations);
|
||||
expect(originalResult.totalTime).toBeGreaterThan(0);
|
||||
expect(soaResult.totalTime).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量更新性能对比(SoA优势场景)', () => {
|
||||
entityCounts.forEach(entityCount => {
|
||||
test(`批量更新 ${entityCount} 个实体`, () => {
|
||||
console.log(`\\n=== 批量更新性能测试: ${entityCount} 个实体 ===`);
|
||||
|
||||
// 原始存储测试
|
||||
const originalResult = measureOriginalBatchUpdate(entityCount, 50);
|
||||
results.push(originalResult);
|
||||
|
||||
// SoA存储测试(向量化操作)
|
||||
const soaResult = measureSoABatchUpdate(entityCount, 50);
|
||||
results.push(soaResult);
|
||||
|
||||
// 输出对比结果
|
||||
console.log(`原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
console.log(`SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
|
||||
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||
const improvement = ((speedup - 1) * 100);
|
||||
console.log(`性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||
|
||||
// 这是SoA的优势场景,应该有性能提升
|
||||
if (entityCount > 5000) {
|
||||
expect(speedup).toBeGreaterThan(1.0); // SoA应该更快
|
||||
}
|
||||
|
||||
expect(originalResult.operations).toBe(soaResult.operations);
|
||||
expect(originalResult.totalTime).toBeGreaterThan(0);
|
||||
expect(soaResult.totalTime).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 测试辅助函数
|
||||
function measureOriginalEntityCreation(entityCount: number): PerformanceResult {
|
||||
const manager = new ComponentStorageManager();
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
manager.addComponent(i, new OriginalPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
manager.addComponent(i, new OriginalVelocityComponent(
|
||||
(Math.random() - 0.5) * 20,
|
||||
(Math.random() - 0.5) * 20,
|
||||
(Math.random() - 0.5) * 10
|
||||
));
|
||||
if (i % 2 === 0) {
|
||||
manager.addComponent(i, new OriginalHealthComponent(
|
||||
80 + Math.random() * 20,
|
||||
100
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
const operations = entityCount * 2.5; // 平均每个实体2.5个组件
|
||||
|
||||
return {
|
||||
name: 'Entity Creation',
|
||||
storageType: 'Original',
|
||||
entityCount,
|
||||
operations,
|
||||
totalTime,
|
||||
averageTime: totalTime / operations,
|
||||
operationsPerSecond: operations / (totalTime / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
function measureSoAEntityCreation(entityCount: number): PerformanceResult {
|
||||
const manager = new ComponentStorageManager();
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
manager.addComponent(i, new TestPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
manager.addComponent(i, new TestVelocityComponent(
|
||||
(Math.random() - 0.5) * 20,
|
||||
(Math.random() - 0.5) * 20,
|
||||
(Math.random() - 0.5) * 10
|
||||
));
|
||||
if (i % 2 === 0) {
|
||||
manager.addComponent(i, new TestHealthComponent(
|
||||
80 + Math.random() * 20,
|
||||
100
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
const operations = entityCount * 2.5;
|
||||
|
||||
return {
|
||||
name: 'Entity Creation',
|
||||
storageType: 'SoA',
|
||||
entityCount,
|
||||
operations,
|
||||
totalTime,
|
||||
averageTime: totalTime / operations,
|
||||
operationsPerSecond: operations / (totalTime / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
function measureOriginalComponentAccess(entityCount: number, iterations: number): PerformanceResult {
|
||||
const manager = new ComponentStorageManager();
|
||||
|
||||
// 预创建实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
manager.addComponent(i, new OriginalPositionComponent(i, i, i));
|
||||
manager.addComponent(i, new OriginalVelocityComponent(1, 1, 1));
|
||||
if (i % 2 === 0) {
|
||||
manager.addComponent(i, new OriginalHealthComponent(100, 100));
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let iter = 0; iter < iterations; iter++) {
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const pos = manager.getComponent(i, OriginalPositionComponent);
|
||||
const vel = manager.getComponent(i, OriginalVelocityComponent);
|
||||
|
||||
if (pos && vel) {
|
||||
// 模拟简单的读取操作
|
||||
const sum = pos.x + pos.y + pos.z + vel.vx + vel.vy + vel.vz;
|
||||
if (sum < 0) continue; // 防止优化
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
const operations = entityCount * iterations;
|
||||
|
||||
return {
|
||||
name: 'Component Access',
|
||||
storageType: 'Original',
|
||||
entityCount,
|
||||
operations,
|
||||
totalTime,
|
||||
averageTime: totalTime / operations,
|
||||
operationsPerSecond: operations / (totalTime / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
function measureSoAComponentAccess(entityCount: number, iterations: number): PerformanceResult {
|
||||
const manager = new ComponentStorageManager();
|
||||
|
||||
// 预创建实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
manager.addComponent(i, new TestPositionComponent(i, i, i));
|
||||
manager.addComponent(i, new TestVelocityComponent(1, 1, 1));
|
||||
if (i % 2 === 0) {
|
||||
manager.addComponent(i, new TestHealthComponent(100, 100));
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let iter = 0; iter < iterations; iter++) {
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const pos = manager.getComponent(i, TestPositionComponent);
|
||||
const vel = manager.getComponent(i, TestVelocityComponent);
|
||||
|
||||
if (pos && vel) {
|
||||
// 模拟简单的读取操作
|
||||
const sum = pos.x + pos.y + pos.z + vel.vx + vel.vy + vel.vz;
|
||||
if (sum < 0) continue; // 防止优化
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
const operations = entityCount * iterations;
|
||||
|
||||
return {
|
||||
name: 'Component Access',
|
||||
storageType: 'SoA',
|
||||
entityCount,
|
||||
operations,
|
||||
totalTime,
|
||||
averageTime: totalTime / operations,
|
||||
operationsPerSecond: operations / (totalTime / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
function measureOriginalBatchUpdate(entityCount: number, iterations: number): PerformanceResult {
|
||||
const manager = new ComponentStorageManager();
|
||||
|
||||
// 预创建实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
manager.addComponent(i, new OriginalPositionComponent(i, i, 0));
|
||||
manager.addComponent(i, new OriginalVelocityComponent(1, 1, 0));
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const deltaTime = 0.016;
|
||||
|
||||
for (let iter = 0; iter < iterations; iter++) {
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const pos = manager.getComponent(i, OriginalPositionComponent);
|
||||
const vel = manager.getComponent(i, OriginalVelocityComponent);
|
||||
|
||||
if (pos && vel) {
|
||||
// 物理更新
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
pos.z += vel.vz * deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
const operations = entityCount * iterations;
|
||||
|
||||
return {
|
||||
name: 'Batch Update',
|
||||
storageType: 'Original',
|
||||
entityCount,
|
||||
operations,
|
||||
totalTime,
|
||||
averageTime: totalTime / operations,
|
||||
operationsPerSecond: operations / (totalTime / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
function measureSoABatchUpdate(entityCount: number, iterations: number): PerformanceResult {
|
||||
const manager = new ComponentStorageManager();
|
||||
|
||||
// 预创建实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
manager.addComponent(i, new TestPositionComponent(i, i, 0));
|
||||
manager.addComponent(i, new TestVelocityComponent(1, 1, 0));
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const deltaTime = 0.016;
|
||||
|
||||
// 获取SoA存储器进行向量化操作
|
||||
const posStorage = manager.getStorage(TestPositionComponent) as SoAStorage<TestPositionComponent>;
|
||||
const velStorage = manager.getStorage(TestVelocityComponent) as SoAStorage<TestVelocityComponent>;
|
||||
|
||||
for (let iter = 0; iter < iterations; iter++) {
|
||||
// 使用向量化操作
|
||||
posStorage.performVectorizedOperation((posFields, activeIndices) => {
|
||||
const velFields = velStorage.getFieldArray('vx') ?
|
||||
new Map([
|
||||
['vx', velStorage.getFieldArray('vx')!],
|
||||
['vy', velStorage.getFieldArray('vy')!],
|
||||
['vz', velStorage.getFieldArray('vz')!]
|
||||
]) : new Map();
|
||||
|
||||
const posX = posFields.get('x') as Float32Array;
|
||||
const posY = posFields.get('y') as Float32Array;
|
||||
const posZ = posFields.get('z') as Float32Array;
|
||||
|
||||
const velX = velFields.get('vx') as Float32Array;
|
||||
const velY = velFields.get('vy') as Float32Array;
|
||||
const velZ = velFields.get('vz') as Float32Array;
|
||||
|
||||
// 向量化物理更新
|
||||
for (let j = 0; j < activeIndices.length; j++) {
|
||||
const idx = activeIndices[j];
|
||||
posX[idx] += velX[idx] * deltaTime;
|
||||
posY[idx] += velY[idx] * deltaTime;
|
||||
posZ[idx] += velZ[idx] * deltaTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
const operations = entityCount * iterations;
|
||||
|
||||
return {
|
||||
name: 'Batch Update',
|
||||
storageType: 'SoA',
|
||||
entityCount,
|
||||
operations,
|
||||
totalTime,
|
||||
averageTime: totalTime / operations,
|
||||
operationsPerSecond: operations / (totalTime / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
function generateDetailedReport(): void {
|
||||
console.log('\\n' + '='.repeat(80));
|
||||
console.log('ComponentStorage 严谨性能对比报告');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// 按测试类型分组
|
||||
const groupedResults = new Map<string, PerformanceResult[]>();
|
||||
|
||||
for (const result of results) {
|
||||
const key = `${result.name}-${result.entityCount}`;
|
||||
if (!groupedResults.has(key)) {
|
||||
groupedResults.set(key, []);
|
||||
}
|
||||
groupedResults.get(key)!.push(result);
|
||||
}
|
||||
|
||||
let totalOriginalTime = 0;
|
||||
let totalSoATime = 0;
|
||||
let testCount = 0;
|
||||
|
||||
for (const [key, testResults] of groupedResults.entries()) {
|
||||
console.log(`\\n${key}:`);
|
||||
|
||||
const originalResult = testResults.find(r => r.storageType === 'Original');
|
||||
const soaResult = testResults.find(r => r.storageType === 'SoA');
|
||||
|
||||
if (originalResult && soaResult) {
|
||||
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||
const improvement = ((speedup - 1) * 100);
|
||||
|
||||
console.log(` 原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
console.log(` SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||
console.log(` 性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||
|
||||
totalOriginalTime += originalResult.totalTime;
|
||||
totalSoATime += soaResult.totalTime;
|
||||
testCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (testCount > 0) {
|
||||
const overallSpeedup = totalOriginalTime / totalSoATime;
|
||||
const overallImprovement = ((overallSpeedup - 1) * 100);
|
||||
|
||||
console.log('\\n' + '='.repeat(80));
|
||||
console.log('总体性能对比:');
|
||||
console.log(` 原始存储总耗时: ${totalOriginalTime.toFixed(2)}ms`);
|
||||
console.log(` SoA存储总耗时: ${totalSoATime.toFixed(2)}ms`);
|
||||
console.log(` 总体性能对比: ${overallSpeedup.toFixed(2)}x ${overallImprovement > 0 ? '提升' : '下降'} ${Math.abs(overallImprovement).toFixed(1)}%`);
|
||||
console.log('\\n结论: SoA优化在批量操作场景中表现优异,在小规模随机访问场景中有轻微开销。');
|
||||
console.log('建议: 对于大规模游戏实体和批量系统更新,SoA优化能带来显著性能提升。');
|
||||
console.log('='.repeat(80));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { BigIntFactory } from '../../../src/ECS/Utils/BigIntCompatibility';
|
||||
|
||||
// 测试组件类
|
||||
// 测试组件类(默认使用原始存储)
|
||||
class TestComponent extends Component {
|
||||
constructor(public value: number = 0) {
|
||||
super();
|
||||
|
||||
252
tests/ECS/Core/SoAStorage.collections.test.ts
Normal file
252
tests/ECS/Core/SoAStorage.collections.test.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager, EnableSoA, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/ComponentStorage';
|
||||
|
||||
// 测试组件:使用集合类型装饰器
|
||||
@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('✅ 性能测试完成');
|
||||
});
|
||||
});
|
||||
307
tests/ECS/Core/SoAStorage.comprehensive.test.ts
Normal file
307
tests/ECS/Core/SoAStorage.comprehensive.test.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager, EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/ComponentStorage';
|
||||
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(Float32Array);
|
||||
|
||||
// 高精度字段不应该在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('✅ 向量化操作测试通过');
|
||||
});
|
||||
});
|
||||
171
tests/ECS/Core/SoAStorage.decorators.test.ts
Normal file
171
tests/ECS/Core/SoAStorage.decorators.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager, EnableSoA, HighPrecision, Float64, Int32 } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import { SoAStorage } 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(Float32Array);
|
||||
|
||||
// 高精度字段不应该在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('✅ 性能测试完成,数据完整性验证通过');
|
||||
});
|
||||
});
|
||||
128
tests/ECS/Core/SoAStorage.edge-case.test.ts
Normal file
128
tests/ECS/Core/SoAStorage.edge-case.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager, EnableSoA } from '../../../src/ECS/Core/ComponentStorage';
|
||||
|
||||
// 模拟复杂对象(如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');
|
||||
});
|
||||
});
|
||||
158
tests/ECS/Core/SoAStorage.types.test.ts
Normal file
158
tests/ECS/Core/SoAStorage.types.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager, EnableSoA, HighPrecision, Float64 } from '../../../src/ECS/Core/ComponentStorage';
|
||||
|
||||
// 包含所有基础类型的组件
|
||||
@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('✅ 边界情况测试通过');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user