改成 SparseSet+SwapRemove 的致密存储
This commit is contained in:
@@ -203,15 +203,12 @@ export class ComponentRegistry {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 高性能组件存储器
|
* 高性能组件存储器
|
||||||
* 使用SoA(Structure of Arrays)模式存储组件
|
|
||||||
*/
|
*/
|
||||||
export class ComponentStorage<T extends Component> {
|
export class ComponentStorage<T extends Component> {
|
||||||
private components: (T | null)[] = [];
|
private dense: T[] = [];
|
||||||
|
private entityIds: number[] = [];
|
||||||
private entityToIndex = new Map<number, number>();
|
private entityToIndex = new Map<number, number>();
|
||||||
private indexToEntity: number[] = [];
|
|
||||||
private freeIndices: number[] = [];
|
|
||||||
private componentType: ComponentType<T>;
|
private componentType: ComponentType<T>;
|
||||||
private _size = 0;
|
|
||||||
|
|
||||||
constructor(componentType: ComponentType<T>) {
|
constructor(componentType: ComponentType<T>) {
|
||||||
this.componentType = componentType;
|
this.componentType = componentType;
|
||||||
@@ -233,22 +230,11 @@ export class ComponentStorage<T extends Component> {
|
|||||||
throw new Error(`Entity ${entityId} already has component ${getComponentTypeName(this.componentType)}`);
|
throw new Error(`Entity ${entityId} already has component ${getComponentTypeName(this.componentType)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let index: number;
|
// 末尾插入到致密数组
|
||||||
|
const index = this.dense.length;
|
||||||
if (this.freeIndices.length > 0) {
|
this.dense.push(component);
|
||||||
// 重用空闲索引
|
this.entityIds.push(entityId);
|
||||||
index = this.freeIndices.pop()!;
|
|
||||||
this.components[index] = component;
|
|
||||||
this.indexToEntity[index] = entityId;
|
|
||||||
} else {
|
|
||||||
// 添加到末尾
|
|
||||||
index = this.components.length;
|
|
||||||
this.components.push(component);
|
|
||||||
this.indexToEntity.push(entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.entityToIndex.set(entityId, index);
|
this.entityToIndex.set(entityId, index);
|
||||||
this._size++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,7 +244,7 @@ export class ComponentStorage<T extends Component> {
|
|||||||
*/
|
*/
|
||||||
public getComponent(entityId: number): T | null {
|
public getComponent(entityId: number): T | null {
|
||||||
const index = this.entityToIndex.get(entityId);
|
const index = this.entityToIndex.get(entityId);
|
||||||
return index !== undefined ? this.components[index] : null;
|
return index !== undefined ? this.dense[index] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,11 +267,25 @@ export class ComponentStorage<T extends Component> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const component = this.components[index];
|
const component = this.dense[index];
|
||||||
|
const lastIndex = this.dense.length - 1;
|
||||||
|
|
||||||
|
if (index !== lastIndex) {
|
||||||
|
// 将末尾元素交换到要删除的位置
|
||||||
|
const lastComponent = this.dense[lastIndex];
|
||||||
|
const lastEntityId = this.entityIds[lastIndex];
|
||||||
|
|
||||||
|
this.dense[index] = lastComponent;
|
||||||
|
this.entityIds[index] = lastEntityId;
|
||||||
|
|
||||||
|
// 更新被交换元素的映射
|
||||||
|
this.entityToIndex.set(lastEntityId, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除末尾元素
|
||||||
|
this.dense.pop();
|
||||||
|
this.entityIds.pop();
|
||||||
this.entityToIndex.delete(entityId);
|
this.entityToIndex.delete(entityId);
|
||||||
this.components[index] = null;
|
|
||||||
this.freeIndices.push(index);
|
|
||||||
this._size--;
|
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
@@ -295,49 +295,36 @@ export class ComponentStorage<T extends Component> {
|
|||||||
* @param callback 回调函数
|
* @param callback 回调函数
|
||||||
*/
|
*/
|
||||||
public forEach(callback: (component: T, entityId: number, index: number) => void): void {
|
public forEach(callback: (component: T, entityId: number, index: number) => void): void {
|
||||||
for (let i = 0; i < this.components.length; i++) {
|
for (let i = 0; i < this.dense.length; i++) {
|
||||||
const component = this.components[i];
|
callback(this.dense[i], this.entityIds[i], i);
|
||||||
if (component) {
|
|
||||||
callback(component, this.indexToEntity[i], i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有组件(密集数组)
|
* 获取所有组件
|
||||||
* @returns 组件数组
|
* @returns 组件数组
|
||||||
*/
|
*/
|
||||||
public getDenseArray(): { components: T[]; entityIds: number[] } {
|
public getDenseArray(): { components: T[]; entityIds: number[] } {
|
||||||
const components: T[] = [];
|
return {
|
||||||
const entityIds: number[] = [];
|
components: [...this.dense],
|
||||||
|
entityIds: [...this.entityIds]
|
||||||
for (let i = 0; i < this.components.length; i++) {
|
};
|
||||||
const component = this.components[i];
|
|
||||||
if (component) {
|
|
||||||
components.push(component);
|
|
||||||
entityIds.push(this.indexToEntity[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { components, entityIds };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空所有组件
|
* 清空所有组件
|
||||||
*/
|
*/
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this.components.length = 0;
|
this.dense.length = 0;
|
||||||
|
this.entityIds.length = 0;
|
||||||
this.entityToIndex.clear();
|
this.entityToIndex.clear();
|
||||||
this.indexToEntity.length = 0;
|
|
||||||
this.freeIndices.length = 0;
|
|
||||||
this._size = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取组件数量
|
* 获取组件数量
|
||||||
*/
|
*/
|
||||||
public get size(): number {
|
public get size(): number {
|
||||||
return this._size;
|
return this.dense.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,34 +334,6 @@ export class ComponentStorage<T extends Component> {
|
|||||||
return this.componentType;
|
return this.componentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 压缩存储(移除空洞)
|
|
||||||
*/
|
|
||||||
public compact(): void {
|
|
||||||
if (this.freeIndices.length === 0) {
|
|
||||||
return; // 没有空洞,无需压缩
|
|
||||||
}
|
|
||||||
|
|
||||||
const newComponents: T[] = [];
|
|
||||||
const newIndexToEntity: number[] = [];
|
|
||||||
const newEntityToIndex = new Map<number, number>();
|
|
||||||
|
|
||||||
let newIndex = 0;
|
|
||||||
for (let i = 0; i < this.components.length; i++) {
|
|
||||||
const component = this.components[i];
|
|
||||||
if (component) {
|
|
||||||
newComponents[newIndex] = component;
|
|
||||||
newIndexToEntity[newIndex] = this.indexToEntity[i];
|
|
||||||
newEntityToIndex.set(this.indexToEntity[i], newIndex);
|
|
||||||
newIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.components = newComponents;
|
|
||||||
this.indexToEntity = newIndexToEntity;
|
|
||||||
this.entityToIndex = newEntityToIndex;
|
|
||||||
this.freeIndices.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取存储统计信息
|
* 获取存储统计信息
|
||||||
@@ -385,10 +344,10 @@ export class ComponentStorage<T extends Component> {
|
|||||||
freeSlots: number;
|
freeSlots: number;
|
||||||
fragmentation: number;
|
fragmentation: number;
|
||||||
} {
|
} {
|
||||||
const totalSlots = this.components.length;
|
const totalSlots = this.dense.length;
|
||||||
const usedSlots = this._size;
|
const usedSlots = this.dense.length;
|
||||||
const freeSlots = this.freeIndices.length;
|
const freeSlots = 0; // 永远无空洞
|
||||||
const fragmentation = totalSlots > 0 ? freeSlots / totalSlots : 0;
|
const fragmentation = 0; // 永远无碎片
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalSlots,
|
totalSlots,
|
||||||
@@ -586,14 +545,6 @@ export class ComponentStorageManager {
|
|||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 压缩所有存储器
|
|
||||||
*/
|
|
||||||
public compactAll(): void {
|
|
||||||
for (const storage of this.storages.values()) {
|
|
||||||
storage.compact();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有存储器的统计信息
|
* 获取所有存储器的统计信息
|
||||||
|
|||||||
@@ -364,12 +364,6 @@ export class Scene implements IScene {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 压缩组件存储(清理碎片)
|
|
||||||
*/
|
|
||||||
public compactComponentStorage(): void {
|
|
||||||
this.componentStorageManager.compactAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取场景的调试信息
|
* 获取场景的调试信息
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ describe('ComponentStorage - 组件存储器测试', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('内存管理和优化', () => {
|
describe('内存管理和优化', () => {
|
||||||
test('应该能够重用空闲索引', () => {
|
test('应该维持紧凑存储', () => {
|
||||||
const component1 = new TestComponent(100);
|
const component1 = new TestComponent(100);
|
||||||
const component2 = new TestComponent(200);
|
const component2 = new TestComponent(200);
|
||||||
const component3 = new TestComponent(300);
|
const component3 = new TestComponent(300);
|
||||||
@@ -311,68 +311,37 @@ describe('ComponentStorage - 组件存储器测试', () => {
|
|||||||
storage.addComponent(2, component2);
|
storage.addComponent(2, component2);
|
||||||
storage.addComponent(3, component3);
|
storage.addComponent(3, component3);
|
||||||
|
|
||||||
// 移除中间的组件
|
// 移除中间的组件,稀疏集合会自动保持紧凑
|
||||||
storage.removeComponent(2);
|
storage.removeComponent(2);
|
||||||
|
|
||||||
// 添加新组件应该重用空闲索引
|
// 添加新组件
|
||||||
const component4 = new TestComponent(400);
|
const component4 = new TestComponent(400);
|
||||||
storage.addComponent(4, component4);
|
storage.addComponent(4, component4);
|
||||||
|
|
||||||
expect(storage.size).toBe(3);
|
expect(storage.size).toBe(3);
|
||||||
expect(storage.getComponent(4)).toBe(component4);
|
expect(storage.getComponent(4)).toBe(component4);
|
||||||
});
|
|
||||||
|
|
||||||
test('应该能够压缩存储', () => {
|
// 验证存储保持紧凑
|
||||||
// 添加多个组件
|
const stats = storage.getStats();
|
||||||
storage.addComponent(1, new TestComponent(100));
|
|
||||||
storage.addComponent(2, new TestComponent(200));
|
|
||||||
storage.addComponent(3, new TestComponent(300));
|
|
||||||
storage.addComponent(4, new TestComponent(400));
|
|
||||||
|
|
||||||
// 移除部分组件创建空洞
|
|
||||||
storage.removeComponent(2);
|
|
||||||
storage.removeComponent(3);
|
|
||||||
|
|
||||||
let stats = storage.getStats();
|
|
||||||
expect(stats.freeSlots).toBe(2);
|
|
||||||
expect(stats.fragmentation).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// 压缩存储
|
|
||||||
storage.compact();
|
|
||||||
|
|
||||||
stats = storage.getStats();
|
|
||||||
expect(stats.freeSlots).toBe(0);
|
expect(stats.freeSlots).toBe(0);
|
||||||
expect(stats.fragmentation).toBe(0);
|
expect(stats.fragmentation).toBe(0);
|
||||||
expect(storage.size).toBe(2);
|
|
||||||
expect(storage.hasComponent(1)).toBe(true);
|
|
||||||
expect(storage.hasComponent(4)).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('没有空洞时压缩应该不做任何操作', () => {
|
|
||||||
storage.addComponent(1, new TestComponent(100));
|
|
||||||
storage.addComponent(2, new TestComponent(200));
|
|
||||||
|
|
||||||
const statsBefore = storage.getStats();
|
|
||||||
storage.compact();
|
|
||||||
const statsAfter = storage.getStats();
|
|
||||||
|
|
||||||
expect(statsBefore).toEqual(statsAfter);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('应该能够获取存储统计信息', () => {
|
test('应该能够获取存储统计信息', () => {
|
||||||
storage.addComponent(1, new TestComponent(100));
|
storage.addComponent(1, new TestComponent(100));
|
||||||
storage.addComponent(2, new TestComponent(200));
|
storage.addComponent(2, new TestComponent(200));
|
||||||
storage.addComponent(3, new TestComponent(300));
|
storage.addComponent(3, new TestComponent(300));
|
||||||
|
|
||||||
// 移除一个组件创建空洞
|
// 移除一个组件,稀疏集合会自动紧凑
|
||||||
storage.removeComponent(2);
|
storage.removeComponent(2);
|
||||||
|
|
||||||
const stats = storage.getStats();
|
const stats = storage.getStats();
|
||||||
|
|
||||||
expect(stats.totalSlots).toBe(3);
|
expect(stats.totalSlots).toBe(2); // 稀疏集合自动紧凑
|
||||||
expect(stats.usedSlots).toBe(2);
|
expect(stats.usedSlots).toBe(2);
|
||||||
expect(stats.freeSlots).toBe(1);
|
expect(stats.freeSlots).toBe(0); // 无空洞
|
||||||
expect(stats.fragmentation).toBeCloseTo(1/3);
|
expect(stats.fragmentation).toBe(0); // 无碎片
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -541,22 +510,6 @@ describe('ComponentStorageManager - 组件存储管理器测试', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('管理器级别操作', () => {
|
describe('管理器级别操作', () => {
|
||||||
test('应该能够压缩所有存储器', () => {
|
|
||||||
manager.addComponent(1, new TestComponent(100));
|
|
||||||
manager.addComponent(2, new TestComponent(200));
|
|
||||||
manager.addComponent(3, new TestComponent(300));
|
|
||||||
|
|
||||||
manager.addComponent(1, new PositionComponent(10, 20));
|
|
||||||
manager.addComponent(2, new PositionComponent(30, 40));
|
|
||||||
|
|
||||||
// 移除部分组件创建空洞
|
|
||||||
manager.removeComponent(2, TestComponent);
|
|
||||||
manager.removeComponent(1, PositionComponent);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
manager.compactAll();
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('应该能够获取所有存储器的统计信息', () => {
|
test('应该能够获取所有存储器的统计信息', () => {
|
||||||
manager.addComponent(1, new TestComponent(100));
|
manager.addComponent(1, new TestComponent(100));
|
||||||
|
|||||||
@@ -468,18 +468,6 @@ describe('Scene - 场景管理系统测试', () => {
|
|||||||
expect(parseFloat(stats.cacheStats.hitRate)).toBeGreaterThanOrEqual(0);
|
expect(parseFloat(stats.cacheStats.hitRate)).toBeGreaterThanOrEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('应该能够压缩组件存储', () => {
|
|
||||||
// 创建一些实体和组件
|
|
||||||
const entities = scene.createEntities(10, "Entity");
|
|
||||||
entities.forEach(entity => {
|
|
||||||
entity.addComponent(new PositionComponent(Math.random() * 100, Math.random() * 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 压缩组件存储应该不抛出异常
|
|
||||||
expect(() => {
|
|
||||||
scene.compactComponentStorage();
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('内存管理和性能', () => {
|
describe('内存管理和性能', () => {
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
import { ComponentStorage, ComponentRegistry, ComponentType } from '../../src/ECS/Core/ComponentStorage';
|
||||||
|
import { Component } from '../../src/ECS/Component';
|
||||||
|
|
||||||
|
// 测试组件类
|
||||||
|
class PerformanceTestComponent extends Component {
|
||||||
|
public value: number;
|
||||||
|
public x: number;
|
||||||
|
public y: number;
|
||||||
|
public active: boolean;
|
||||||
|
|
||||||
|
constructor(value = 0, x = 0, y = 0, active = true) {
|
||||||
|
super();
|
||||||
|
this.value = value;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟旧的基于holes的ComponentStorage实现
|
||||||
|
class LegacyComponentStorage<T extends Component> {
|
||||||
|
private components: (T | null)[] = [];
|
||||||
|
private freeIndices: number[] = [];
|
||||||
|
private entityToIndex = new Map<number, number>();
|
||||||
|
private componentType: ComponentType<T>;
|
||||||
|
|
||||||
|
constructor(componentType: ComponentType<T>) {
|
||||||
|
this.componentType = componentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addComponent(entityId: number, component: T): void {
|
||||||
|
if (this.entityToIndex.has(entityId)) {
|
||||||
|
throw new Error(`Entity ${entityId} already has component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let index: number;
|
||||||
|
if (this.freeIndices.length > 0) {
|
||||||
|
index = this.freeIndices.pop()!;
|
||||||
|
} else {
|
||||||
|
index = this.components.length;
|
||||||
|
this.components.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.components[index] = component;
|
||||||
|
this.entityToIndex.set(entityId, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getComponent(entityId: number): T | null {
|
||||||
|
const index = this.entityToIndex.get(entityId);
|
||||||
|
return index !== undefined ? this.components[index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasComponent(entityId: number): boolean {
|
||||||
|
return this.entityToIndex.has(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeComponent(entityId: number): T | null {
|
||||||
|
const index = this.entityToIndex.get(entityId);
|
||||||
|
if (index === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = this.components[index];
|
||||||
|
this.components[index] = null;
|
||||||
|
this.freeIndices.push(index);
|
||||||
|
this.entityToIndex.delete(entityId);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
public forEach(callback: (component: T, entityId: number, index: number) => void): void {
|
||||||
|
for (let i = 0; i < this.components.length; i++) {
|
||||||
|
const component = this.components[i];
|
||||||
|
if (component !== null) {
|
||||||
|
// 需要找到对应的entityId,这在holes存储中是低效的
|
||||||
|
for (const [entityId, compIndex] of this.entityToIndex) {
|
||||||
|
if (compIndex === i) {
|
||||||
|
callback(component, entityId, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this.components.length = 0;
|
||||||
|
this.freeIndices.length = 0;
|
||||||
|
this.entityToIndex.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get size(): number {
|
||||||
|
return this.entityToIndex.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ComponentStorage 性能对比测试', () => {
|
||||||
|
let newStorage: ComponentStorage<PerformanceTestComponent>;
|
||||||
|
let legacyStorage: LegacyComponentStorage<PerformanceTestComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ComponentRegistry.reset();
|
||||||
|
newStorage = new ComponentStorage(PerformanceTestComponent);
|
||||||
|
legacyStorage = new LegacyComponentStorage(PerformanceTestComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
const createComponent = (id: number) => new PerformanceTestComponent(id, id * 2, id * 3, true);
|
||||||
|
|
||||||
|
describe('基础操作性能对比', () => {
|
||||||
|
const entityCount = 10000;
|
||||||
|
|
||||||
|
test('批量添加组件性能', () => {
|
||||||
|
console.log(`\n=== 批量添加${entityCount}个组件性能对比 ===`);
|
||||||
|
|
||||||
|
// 测试新实现
|
||||||
|
const newStartTime = performance.now();
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
newStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
const newAddTime = performance.now() - newStartTime;
|
||||||
|
|
||||||
|
// 测试旧实现
|
||||||
|
const legacyStartTime = performance.now();
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
const legacyAddTime = performance.now() - legacyStartTime;
|
||||||
|
|
||||||
|
console.log(`旧实现(holes存储): ${legacyAddTime.toFixed(3)}ms`);
|
||||||
|
console.log(`新实现(稀疏集合): ${newAddTime.toFixed(3)}ms`);
|
||||||
|
console.log(`性能提升: ${((legacyAddTime - newAddTime) / legacyAddTime * 100).toFixed(1)}%`);
|
||||||
|
|
||||||
|
expect(newStorage.size).toBe(entityCount);
|
||||||
|
expect(legacyStorage.size).toBe(entityCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('批量查找组件性能', () => {
|
||||||
|
console.log(`\n=== 批量查找${entityCount}个组件性能对比 ===`);
|
||||||
|
|
||||||
|
// 先添加数据
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
newStorage.addComponent(i, createComponent(i));
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试新实现查找
|
||||||
|
const newStartTime = performance.now();
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
const component = newStorage.getComponent(i);
|
||||||
|
expect(component?.value).toBe(i);
|
||||||
|
}
|
||||||
|
const newGetTime = performance.now() - newStartTime;
|
||||||
|
|
||||||
|
// 测试旧实现查找
|
||||||
|
const legacyStartTime = performance.now();
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
const component = legacyStorage.getComponent(i);
|
||||||
|
expect(component?.value).toBe(i);
|
||||||
|
}
|
||||||
|
const legacyGetTime = performance.now() - legacyStartTime;
|
||||||
|
|
||||||
|
console.log(`旧实现(holes存储): ${legacyGetTime.toFixed(3)}ms`);
|
||||||
|
console.log(`新实现(稀疏集合): ${newGetTime.toFixed(3)}ms`);
|
||||||
|
console.log(`性能提升: ${((legacyGetTime - newGetTime) / legacyGetTime * 100).toFixed(1)}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('批量移除组件性能', () => {
|
||||||
|
console.log(`\n=== 批量移除${entityCount / 2}个组件性能对比 ===`);
|
||||||
|
|
||||||
|
// 先添加数据
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
newStorage.addComponent(i, createComponent(i));
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试新实现移除(移除奇数ID)
|
||||||
|
const newStartTime = performance.now();
|
||||||
|
for (let i = 1; i <= entityCount; i += 2) {
|
||||||
|
newStorage.removeComponent(i);
|
||||||
|
}
|
||||||
|
const newRemoveTime = performance.now() - newStartTime;
|
||||||
|
|
||||||
|
// 测试旧实现移除(移除奇数ID)
|
||||||
|
const legacyStartTime = performance.now();
|
||||||
|
for (let i = 1; i <= entityCount; i += 2) {
|
||||||
|
legacyStorage.removeComponent(i);
|
||||||
|
}
|
||||||
|
const legacyRemoveTime = performance.now() - legacyStartTime;
|
||||||
|
|
||||||
|
console.log(`旧实现(holes存储): ${legacyRemoveTime.toFixed(3)}ms`);
|
||||||
|
console.log(`新实现(稀疏集合): ${newRemoveTime.toFixed(3)}ms`);
|
||||||
|
console.log(`性能提升: ${((legacyRemoveTime - newRemoveTime) / legacyRemoveTime * 100).toFixed(1)}%`);
|
||||||
|
|
||||||
|
expect(newStorage.size).toBe(entityCount / 2);
|
||||||
|
expect(legacyStorage.size).toBe(entityCount / 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('遍历性能对比', () => {
|
||||||
|
const entityCount = 50000;
|
||||||
|
|
||||||
|
test('完整遍历性能 - 无碎片情况', () => {
|
||||||
|
console.log(`\n=== 完整遍历${entityCount}个组件性能对比(无碎片) ===`);
|
||||||
|
|
||||||
|
// 先添加数据
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
newStorage.addComponent(i, createComponent(i));
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
let newSum = 0;
|
||||||
|
let legacySum = 0;
|
||||||
|
|
||||||
|
// 测试新实现遍历
|
||||||
|
const newStartTime = performance.now();
|
||||||
|
newStorage.forEach((component) => {
|
||||||
|
newSum += component.value;
|
||||||
|
});
|
||||||
|
const newForEachTime = performance.now() - newStartTime;
|
||||||
|
|
||||||
|
// 测试旧实现遍历
|
||||||
|
const legacyStartTime = performance.now();
|
||||||
|
legacyStorage.forEach((component) => {
|
||||||
|
legacySum += component.value;
|
||||||
|
});
|
||||||
|
const legacyForEachTime = performance.now() - legacyStartTime;
|
||||||
|
|
||||||
|
console.log(`旧实现(holes存储): ${legacyForEachTime.toFixed(3)}ms`);
|
||||||
|
console.log(`新实现(稀疏集合): ${newForEachTime.toFixed(3)}ms`);
|
||||||
|
console.log(`性能提升: ${((legacyForEachTime - newForEachTime) / legacyForEachTime * 100).toFixed(1)}%`);
|
||||||
|
|
||||||
|
expect(newSum).toBe(legacySum);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('遍历性能 - 高碎片情况', () => {
|
||||||
|
console.log(`\n=== 遍历性能对比(高碎片情况,70%组件被移除) ===`);
|
||||||
|
|
||||||
|
// 添加大量数据
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
newStorage.addComponent(i, createComponent(i));
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除70%的组件,创建大量碎片
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
if (i % 10 < 7) { // 移除70%
|
||||||
|
newStorage.removeComponent(i);
|
||||||
|
legacyStorage.removeComponent(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`剩余组件数量: ${newStorage.size}`);
|
||||||
|
|
||||||
|
let newSum = 0;
|
||||||
|
let legacySum = 0;
|
||||||
|
|
||||||
|
// 测试新实现遍历(稀疏集合天然无碎片)
|
||||||
|
const newStartTime = performance.now();
|
||||||
|
newStorage.forEach((component) => {
|
||||||
|
newSum += component.value;
|
||||||
|
});
|
||||||
|
const newForEachTime = performance.now() - newStartTime;
|
||||||
|
|
||||||
|
// 测试旧实现遍历(需要跳过大量holes)
|
||||||
|
const legacyStartTime = performance.now();
|
||||||
|
legacyStorage.forEach((component) => {
|
||||||
|
legacySum += component.value;
|
||||||
|
});
|
||||||
|
const legacyForEachTime = performance.now() - legacyStartTime;
|
||||||
|
|
||||||
|
console.log(`旧实现(holes存储,70%空洞): ${legacyForEachTime.toFixed(3)}ms`);
|
||||||
|
console.log(`新实现(稀疏集合,无空洞): ${newForEachTime.toFixed(3)}ms`);
|
||||||
|
console.log(`性能提升: ${((legacyForEachTime - newForEachTime) / legacyForEachTime * 100).toFixed(1)}%`);
|
||||||
|
|
||||||
|
expect(newSum).toBe(legacySum);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('内存效率对比', () => {
|
||||||
|
const entityCount = 20000;
|
||||||
|
|
||||||
|
test('内存使用和碎片对比', () => {
|
||||||
|
console.log(`\n=== 内存效率对比 ===`);
|
||||||
|
|
||||||
|
// 添加组件
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
newStorage.addComponent(i, createComponent(i));
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除一半组件创建碎片
|
||||||
|
for (let i = 1; i <= entityCount; i += 2) {
|
||||||
|
newStorage.removeComponent(i);
|
||||||
|
legacyStorage.removeComponent(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStats = newStorage.getStats();
|
||||||
|
|
||||||
|
console.log(`=== 移除50%组件后的存储效率 ===`);
|
||||||
|
console.log(`新实现(稀疏集合):`);
|
||||||
|
console.log(` - 总槽位: ${newStats.totalSlots}`);
|
||||||
|
console.log(` - 使用槽位: ${newStats.usedSlots}`);
|
||||||
|
console.log(` - 空闲槽位: ${newStats.freeSlots}`);
|
||||||
|
console.log(` - 碎片率: ${(newStats.fragmentation * 100).toFixed(1)}%`);
|
||||||
|
console.log(` - 内存效率: ${((newStats.usedSlots / newStats.totalSlots) * 100).toFixed(1)}%`);
|
||||||
|
|
||||||
|
console.log(`旧实现(holes存储):`);
|
||||||
|
console.log(` - 总槽位: ${entityCount} (固定数组大小)`);
|
||||||
|
console.log(` - 使用槽位: ${entityCount / 2}`);
|
||||||
|
console.log(` - 空闲槽位: ${entityCount / 2}`);
|
||||||
|
console.log(` - 碎片率: 50.0%`);
|
||||||
|
console.log(` - 内存效率: 50.0%`);
|
||||||
|
|
||||||
|
// 验证新实现的优势
|
||||||
|
expect(newStats.fragmentation).toBe(0); // 稀疏集合无碎片
|
||||||
|
expect(newStats.totalSlots).toBe(newStats.usedSlots); // 完全紧凑
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('随机访问模式性能测试', () => {
|
||||||
|
const entityCount = 10000;
|
||||||
|
const operationCount = 5000;
|
||||||
|
|
||||||
|
test('混合操作性能对比', () => {
|
||||||
|
console.log(`\n=== 混合操作性能对比(${operationCount}次随机操作) ===`);
|
||||||
|
|
||||||
|
// 预先添加一些数据
|
||||||
|
for (let i = 1; i <= entityCount / 2; i++) {
|
||||||
|
newStorage.addComponent(i, createComponent(i));
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机操作序列
|
||||||
|
const operations: Array<{type: 'add' | 'get' | 'remove', entityId: number}> = [];
|
||||||
|
for (let i = 0; i < operationCount; i++) {
|
||||||
|
const type = Math.random() < 0.4 ? 'add' : Math.random() < 0.7 ? 'get' : 'remove';
|
||||||
|
const entityId = Math.floor(Math.random() * entityCount) + 1;
|
||||||
|
operations.push({ type, entityId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试新实现
|
||||||
|
const newStartTime = performance.now();
|
||||||
|
operations.forEach(op => {
|
||||||
|
try {
|
||||||
|
switch (op.type) {
|
||||||
|
case 'add':
|
||||||
|
if (!newStorage.hasComponent(op.entityId)) {
|
||||||
|
newStorage.addComponent(op.entityId, createComponent(op.entityId));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'get':
|
||||||
|
newStorage.getComponent(op.entityId);
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
newStorage.removeComponent(op.entityId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略重复添加等错误
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const newMixedTime = performance.now() - newStartTime;
|
||||||
|
|
||||||
|
// 重置旧实现状态
|
||||||
|
legacyStorage.clear();
|
||||||
|
for (let i = 1; i <= entityCount / 2; i++) {
|
||||||
|
legacyStorage.addComponent(i, createComponent(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试旧实现
|
||||||
|
const legacyStartTime = performance.now();
|
||||||
|
operations.forEach(op => {
|
||||||
|
try {
|
||||||
|
switch (op.type) {
|
||||||
|
case 'add':
|
||||||
|
if (!legacyStorage.hasComponent(op.entityId)) {
|
||||||
|
legacyStorage.addComponent(op.entityId, createComponent(op.entityId));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'get':
|
||||||
|
legacyStorage.getComponent(op.entityId);
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
legacyStorage.removeComponent(op.entityId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略重复添加等错误
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const legacyMixedTime = performance.now() - legacyStartTime;
|
||||||
|
|
||||||
|
console.log(`旧实现(holes存储): ${legacyMixedTime.toFixed(3)}ms`);
|
||||||
|
console.log(`新实现(稀疏集合): ${newMixedTime.toFixed(3)}ms`);
|
||||||
|
console.log(`性能提升: ${((legacyMixedTime - newMixedTime) / legacyMixedTime * 100).toFixed(1)}%`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user