优化内部组件索引机制(更改为SparseSet索引)减少用户切换索引成本
修复内部系统初始化逻辑 - 不应该再onInitialize中初始内部entities,移动到initialize中 ci跳过cocos项目避免ci失败 soa开放更多安全类型接口
This commit is contained in:
@@ -34,7 +34,7 @@ ECS 是一种基于组合而非继承的软件架构模式:
|
||||
|
||||
- **完整的 TypeScript 支持** - 强类型检查和代码提示
|
||||
- **高效查询系统** - 流式 API 和智能缓存
|
||||
- **性能优化技术** - 组件索引、Archetype 系统、脏标记
|
||||
- **性能优化技术** - SparseSet索引、Archetype 系统、脏标记
|
||||
- **事件系统** - 类型安全的事件处理
|
||||
- **调试工具** - 内置性能监控和 [Cocos Creator 可视化调试插件](https://store.cocos.com/app/detail/7823)
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
}
|
||||
},
|
||||
"packages": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"!extensions/cocos/*/settings/**",
|
||||
"!examples/*/settings/**"
|
||||
]
|
||||
}
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -11537,7 +11537,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.43",
|
||||
"version": "2.1.44",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.43",
|
||||
"version": "2.1.44",
|
||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import { Entity } from '../Entity';
|
||||
import { ComponentType } from './ComponentStorage';
|
||||
import { ComponentSparseSet } from '../Utils/ComponentSparseSet';
|
||||
|
||||
/**
|
||||
* 组件索引类型
|
||||
*/
|
||||
export enum IndexType {
|
||||
/** 哈希索引 - 最快查找 */
|
||||
HASH = 'hash',
|
||||
/** 位图索引 - 内存高效 */
|
||||
BITMAP = 'bitmap',
|
||||
/** 排序索引 - 支持范围查询 */
|
||||
SORTED = 'sorted'
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引统计信息
|
||||
*/
|
||||
export interface IndexStats {
|
||||
/** 索引类型 */
|
||||
type: IndexType;
|
||||
/** 索引大小 */
|
||||
size: number;
|
||||
/** 内存使用量(字节) */
|
||||
@@ -35,8 +23,6 @@ export interface IndexStats {
|
||||
* 组件索引接口
|
||||
*/
|
||||
export interface IComponentIndex {
|
||||
/** 索引类型 */
|
||||
readonly type: IndexType;
|
||||
/** 添加实体到索引 */
|
||||
addEntity(entity: Entity): void;
|
||||
/** 从索引中移除实体 */
|
||||
@@ -52,84 +38,45 @@ export interface IComponentIndex {
|
||||
}
|
||||
|
||||
/**
|
||||
* 哈希索引实现
|
||||
* 通用组件索引实现
|
||||
*
|
||||
* 使用Map数据结构,提供O(1)的查找性能。
|
||||
* 适合大多数查询场景。
|
||||
* 基于Sparse Set算法:
|
||||
* - O(1)的实体添加、删除、查找
|
||||
* - 高效的位运算查询
|
||||
* - 内存紧凑的存储结构
|
||||
* - 缓存友好的遍历性能
|
||||
*/
|
||||
export class HashComponentIndex implements IComponentIndex {
|
||||
public readonly type = IndexType.HASH;
|
||||
export class ComponentIndex implements IComponentIndex {
|
||||
|
||||
private _componentToEntities = new Map<ComponentType, Set<Entity>>();
|
||||
private _entityToComponents = new Map<Entity, Set<ComponentType>>();
|
||||
/**
|
||||
* 组件稀疏集合
|
||||
*
|
||||
* 核心存储结构,处理所有实体和组件的索引操作。
|
||||
*/
|
||||
private _sparseSet: ComponentSparseSet;
|
||||
|
||||
// 性能统计
|
||||
private _queryCount = 0;
|
||||
private _totalQueryTime = 0;
|
||||
private _lastUpdated = Date.now();
|
||||
|
||||
private _setPool: Set<Entity>[] = [];
|
||||
private _componentTypeSetPool: Set<ComponentType>[] = [];
|
||||
constructor() {
|
||||
this._sparseSet = new ComponentSparseSet();
|
||||
}
|
||||
|
||||
public addEntity(entity: Entity): void {
|
||||
if (entity.components.length === 0) {
|
||||
const componentTypes = this._componentTypeSetPool.pop() || new Set<ComponentType>();
|
||||
componentTypes.clear();
|
||||
this._entityToComponents.set(entity, componentTypes);
|
||||
this._lastUpdated = Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
const componentTypes = this._componentTypeSetPool.pop() || new Set<ComponentType>();
|
||||
componentTypes.clear();
|
||||
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
componentTypes.add(componentType);
|
||||
|
||||
let entities = this._componentToEntities.get(componentType);
|
||||
if (!entities) {
|
||||
entities = this._setPool.pop() || new Set<Entity>();
|
||||
entities.clear();
|
||||
this._componentToEntities.set(componentType, entities);
|
||||
}
|
||||
entities.add(entity);
|
||||
}
|
||||
|
||||
this._entityToComponents.set(entity, componentTypes);
|
||||
this._sparseSet.addEntity(entity);
|
||||
this._lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
public removeEntity(entity: Entity): void {
|
||||
const componentTypes = this._entityToComponents.get(entity);
|
||||
if (!componentTypes) return;
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const entities = this._componentToEntities.get(componentType);
|
||||
if (entities) {
|
||||
entities.delete(entity);
|
||||
if (entities.size === 0) {
|
||||
this._componentToEntities.delete(componentType);
|
||||
if (this._setPool.length < 50) {
|
||||
entities.clear();
|
||||
this._setPool.push(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._entityToComponents.delete(entity);
|
||||
|
||||
if (this._componentTypeSetPool.length < 50) {
|
||||
componentTypes.clear();
|
||||
this._componentTypeSetPool.push(componentTypes);
|
||||
}
|
||||
|
||||
this._sparseSet.removeEntity(entity);
|
||||
this._lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
public query(componentType: ComponentType): Set<Entity> {
|
||||
const startTime = performance.now();
|
||||
const entities = this._componentToEntities.get(componentType);
|
||||
const result = entities ? new Set(entities) : new Set<Entity>();
|
||||
const result = this._sparseSet.queryByComponent(componentType);
|
||||
|
||||
this._queryCount++;
|
||||
this._totalQueryTime += performance.now() - startTime;
|
||||
@@ -140,212 +87,16 @@ export class HashComponentIndex implements IComponentIndex {
|
||||
public queryMultiple(componentTypes: ComponentType[], operation: 'AND' | 'OR'): Set<Entity> {
|
||||
const startTime = performance.now();
|
||||
|
||||
if (componentTypes.length === 0) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
if (componentTypes.length === 1) {
|
||||
return this.query(componentTypes[0]);
|
||||
}
|
||||
|
||||
let result: Set<Entity>;
|
||||
|
||||
if (operation === 'AND') {
|
||||
let smallestSet: Set<Entity> | undefined;
|
||||
let smallestSize = Infinity;
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const entities = this._componentToEntities.get(componentType);
|
||||
if (!entities || entities.size === 0) {
|
||||
this._queryCount++;
|
||||
this._totalQueryTime += performance.now() - startTime;
|
||||
return new Set();
|
||||
}
|
||||
if (entities.size < smallestSize) {
|
||||
smallestSize = entities.size;
|
||||
smallestSet = entities;
|
||||
}
|
||||
}
|
||||
|
||||
result = new Set();
|
||||
if (smallestSet) {
|
||||
for (const entity of smallestSet) {
|
||||
let hasAll = true;
|
||||
for (const componentType of componentTypes) {
|
||||
const entities = this._componentToEntities.get(componentType);
|
||||
if (!entities || !entities.has(entity)) {
|
||||
hasAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasAll) {
|
||||
result.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = new Set();
|
||||
for (const componentType of componentTypes) {
|
||||
const entities = this._componentToEntities.get(componentType);
|
||||
if (entities) {
|
||||
for (const entity of entities) {
|
||||
result.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._queryCount++;
|
||||
this._totalQueryTime += performance.now() - startTime;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._componentToEntities.clear();
|
||||
this._entityToComponents.clear();
|
||||
this._lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
public getStats(): IndexStats {
|
||||
let memoryUsage = 0;
|
||||
|
||||
memoryUsage += this._componentToEntities.size * 64;
|
||||
memoryUsage += this._entityToComponents.size * 64;
|
||||
|
||||
for (const entities of this._componentToEntities.values()) {
|
||||
memoryUsage += entities.size * 8;
|
||||
}
|
||||
|
||||
for (const components of this._entityToComponents.values()) {
|
||||
memoryUsage += components.size * 8;
|
||||
}
|
||||
|
||||
return {
|
||||
type: this.type,
|
||||
size: this._componentToEntities.size,
|
||||
memoryUsage,
|
||||
queryCount: this._queryCount,
|
||||
avgQueryTime: this._queryCount > 0 ? this._totalQueryTime / this._queryCount : 0,
|
||||
lastUpdated: this._lastUpdated
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 位图索引实现
|
||||
*
|
||||
* 使用位操作进行快速集合运算,内存效率高。
|
||||
* 适合有限组件类型和大量实体的场景。
|
||||
*/
|
||||
export class BitmapComponentIndex implements IComponentIndex {
|
||||
public readonly type = IndexType.BITMAP;
|
||||
|
||||
private _componentTypeToBit = new Map<ComponentType, number>();
|
||||
private _entityToBitmap = new Map<Entity, number>();
|
||||
private _bitToEntities = new Map<number, Set<Entity>>();
|
||||
private _nextBit = 0;
|
||||
private _queryCount = 0;
|
||||
private _totalQueryTime = 0;
|
||||
private _lastUpdated = Date.now();
|
||||
|
||||
public addEntity(entity: Entity): void {
|
||||
let bitmap = 0;
|
||||
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
let bit = this._componentTypeToBit.get(componentType);
|
||||
|
||||
if (bit === undefined) {
|
||||
bit = this._nextBit++;
|
||||
this._componentTypeToBit.set(componentType, bit);
|
||||
}
|
||||
|
||||
bitmap |= (1 << bit);
|
||||
|
||||
let entities = this._bitToEntities.get(1 << bit);
|
||||
if (!entities) {
|
||||
entities = new Set();
|
||||
this._bitToEntities.set(1 << bit, entities);
|
||||
}
|
||||
entities.add(entity);
|
||||
}
|
||||
|
||||
this._entityToBitmap.set(entity, bitmap);
|
||||
this._lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
public removeEntity(entity: Entity): void {
|
||||
const bitmap = this._entityToBitmap.get(entity);
|
||||
if (bitmap === undefined) return;
|
||||
|
||||
// 从所有相关的位集合中移除实体
|
||||
for (const [bitMask, entities] of this._bitToEntities) {
|
||||
if ((bitmap & bitMask) !== 0) {
|
||||
entities.delete(entity);
|
||||
if (entities.size === 0) {
|
||||
this._bitToEntities.delete(bitMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._entityToBitmap.delete(entity);
|
||||
this._lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
public query(componentType: ComponentType): Set<Entity> {
|
||||
const startTime = performance.now();
|
||||
|
||||
const bit = this._componentTypeToBit.get(componentType);
|
||||
if (bit === undefined) {
|
||||
this._queryCount++;
|
||||
this._totalQueryTime += performance.now() - startTime;
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const result = new Set(this._bitToEntities.get(1 << bit) || []);
|
||||
|
||||
this._queryCount++;
|
||||
this._totalQueryTime += performance.now() - startTime;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public queryMultiple(componentTypes: ComponentType[], operation: 'AND' | 'OR'): Set<Entity> {
|
||||
const startTime = performance.now();
|
||||
|
||||
if (componentTypes.length === 0) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
let targetBitmap = 0;
|
||||
const validBits: number[] = [];
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
const bit = this._componentTypeToBit.get(componentType);
|
||||
if (bit !== undefined) {
|
||||
targetBitmap |= (1 << bit);
|
||||
validBits.push(1 << bit);
|
||||
}
|
||||
}
|
||||
|
||||
const result = new Set<Entity>();
|
||||
|
||||
if (operation === 'AND') {
|
||||
for (const [entity, entityBitmap] of this._entityToBitmap) {
|
||||
if ((entityBitmap & targetBitmap) === targetBitmap) {
|
||||
result.add(entity);
|
||||
}
|
||||
}
|
||||
result = new Set();
|
||||
} else if (componentTypes.length === 1) {
|
||||
result = this.query(componentTypes[0]);
|
||||
} else if (operation === 'AND') {
|
||||
result = this._sparseSet.queryMultipleAnd(componentTypes);
|
||||
} else {
|
||||
for (const bitMask of validBits) {
|
||||
const entities = this._bitToEntities.get(bitMask);
|
||||
if (entities) {
|
||||
for (const entity of entities) {
|
||||
result.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
result = this._sparseSet.queryMultipleOr(componentTypes);
|
||||
}
|
||||
|
||||
this._queryCount++;
|
||||
@@ -354,29 +105,18 @@ export class BitmapComponentIndex implements IComponentIndex {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public clear(): void {
|
||||
this._componentTypeToBit.clear();
|
||||
this._entityToBitmap.clear();
|
||||
this._bitToEntities.clear();
|
||||
this._nextBit = 0;
|
||||
this._sparseSet.clear();
|
||||
this._lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
public getStats(): IndexStats {
|
||||
let memoryUsage = 0;
|
||||
|
||||
memoryUsage += this._componentTypeToBit.size * 12;
|
||||
memoryUsage += this._entityToBitmap.size * 12;
|
||||
memoryUsage += this._bitToEntities.size * 64;
|
||||
|
||||
for (const entities of this._bitToEntities.values()) {
|
||||
memoryUsage += entities.size * 8;
|
||||
}
|
||||
const memoryStats = this._sparseSet.getMemoryStats();
|
||||
|
||||
return {
|
||||
type: this.type,
|
||||
size: this._componentTypeToBit.size,
|
||||
memoryUsage,
|
||||
size: this._sparseSet.size,
|
||||
memoryUsage: memoryStats.totalMemory,
|
||||
queryCount: this._queryCount,
|
||||
avgQueryTime: this._queryCount > 0 ? this._totalQueryTime / this._queryCount : 0,
|
||||
lastUpdated: this._lastUpdated
|
||||
@@ -384,130 +124,58 @@ export class BitmapComponentIndex implements IComponentIndex {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 智能组件索引管理器
|
||||
* 组件索引管理器
|
||||
*
|
||||
* 根据使用模式自动选择最优的索引策略。
|
||||
* 支持动态切换索引类型以获得最佳性能。
|
||||
* 使用统一的组件索引实现,自动优化查询性能。
|
||||
*/
|
||||
export class ComponentIndexManager {
|
||||
private _activeIndex: IComponentIndex;
|
||||
private _indexHistory: Map<IndexType, IndexStats> = new Map();
|
||||
private _autoOptimize = true;
|
||||
private _optimizationThreshold = 1000;
|
||||
private _index: ComponentIndex;
|
||||
|
||||
constructor(initialType: IndexType = IndexType.HASH) {
|
||||
this._activeIndex = this.createIndex(initialType);
|
||||
constructor() {
|
||||
this._index = new ComponentIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加实体到索引
|
||||
*/
|
||||
public addEntity(entity: Entity): void {
|
||||
this._activeIndex.addEntity(entity);
|
||||
|
||||
if (this._autoOptimize && this._activeIndex.getStats().queryCount % 100 === 0) {
|
||||
this.checkOptimization();
|
||||
}
|
||||
this._index.addEntity(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从索引中移除实体
|
||||
*/
|
||||
public removeEntity(entity: Entity): void {
|
||||
this._activeIndex.removeEntity(entity);
|
||||
this._index.removeEntity(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询包含指定组件的实体
|
||||
*/
|
||||
public query(componentType: ComponentType): Set<Entity> {
|
||||
return this._activeIndex.query(componentType);
|
||||
return this._index.query(componentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询多个组件
|
||||
*/
|
||||
public queryMultiple(componentTypes: ComponentType[], operation: 'AND' | 'OR'): Set<Entity> {
|
||||
return this._activeIndex.queryMultiple(componentTypes, operation);
|
||||
return this._index.queryMultiple(componentTypes, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动切换索引类型
|
||||
*/
|
||||
public switchIndexType(type: IndexType): void {
|
||||
if (type === this._activeIndex.type) return;
|
||||
|
||||
this._indexHistory.set(this._activeIndex.type, this._activeIndex.getStats());
|
||||
|
||||
const oldIndex = this._activeIndex;
|
||||
this._activeIndex = this.createIndex(type);
|
||||
|
||||
oldIndex.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用自动优化
|
||||
*/
|
||||
public setAutoOptimize(enabled: boolean): void {
|
||||
this._autoOptimize = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前索引统计信息
|
||||
* 获取索引统计信息
|
||||
*/
|
||||
public getStats(): IndexStats {
|
||||
return this._activeIndex.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有索引类型的历史统计信息
|
||||
*/
|
||||
public getAllStats(): Map<IndexType, IndexStats> {
|
||||
const current = this._activeIndex.getStats();
|
||||
return new Map([
|
||||
...this._indexHistory,
|
||||
[current.type, current]
|
||||
]);
|
||||
return this._index.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空索引
|
||||
*/
|
||||
public clear(): void {
|
||||
this._activeIndex.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定类型的索引
|
||||
*/
|
||||
private createIndex(type: IndexType): IComponentIndex {
|
||||
switch (type) {
|
||||
case IndexType.HASH:
|
||||
return new HashComponentIndex();
|
||||
case IndexType.BITMAP:
|
||||
return new BitmapComponentIndex();
|
||||
case IndexType.SORTED:
|
||||
return new HashComponentIndex();
|
||||
default:
|
||||
return new HashComponentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要优化索引
|
||||
*/
|
||||
private checkOptimization(): void {
|
||||
if (!this._autoOptimize) return;
|
||||
|
||||
const stats = this._activeIndex.getStats();
|
||||
if (stats.queryCount < this._optimizationThreshold) return;
|
||||
|
||||
|
||||
if (stats.avgQueryTime > 1.0 && stats.type !== IndexType.HASH) {
|
||||
this.switchIndexType(IndexType.HASH);
|
||||
} else if (stats.memoryUsage > 10 * 1024 * 1024 && stats.type !== IndexType.BITMAP) {
|
||||
this.switchIndexType(IndexType.BITMAP);
|
||||
}
|
||||
this._index.clear();
|
||||
}
|
||||
}
|
||||
@@ -407,6 +407,86 @@ export class ComponentStorageManager {
|
||||
private static readonly _logger = createLogger('ComponentStorage');
|
||||
private storages = new Map<Function, ComponentStorage<any> | SoAStorage<any>>();
|
||||
|
||||
/**
|
||||
* 检查组件类型是否启用SoA存储
|
||||
* @param componentType 组件类型
|
||||
* @returns 是否为SoA存储
|
||||
*/
|
||||
public isSoAStorage<T extends Component>(componentType: ComponentType<T>): boolean {
|
||||
const storage = this.storages.get(componentType);
|
||||
return storage instanceof SoAStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SoA存储器(类型安全)
|
||||
* @param componentType 组件类型
|
||||
* @returns SoA存储器或null
|
||||
*/
|
||||
public getSoAStorage<T extends Component>(componentType: ComponentType<T>): SoAStorage<T> | null {
|
||||
const storage = this.getStorage(componentType);
|
||||
return storage instanceof SoAStorage ? storage : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接获取SoA字段数组(类型安全)
|
||||
* @param componentType 组件类型
|
||||
* @param fieldName 字段名
|
||||
* @returns TypedArray或null
|
||||
*/
|
||||
public getFieldArray<T extends Component>(
|
||||
componentType: ComponentType<T>,
|
||||
fieldName: string
|
||||
): Float32Array | Float64Array | Int32Array | null {
|
||||
const soaStorage = this.getSoAStorage(componentType);
|
||||
return soaStorage ? soaStorage.getFieldArray(fieldName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接获取SoA字段数组(类型安全,带字段名检查)
|
||||
* @param componentType 组件类型
|
||||
* @param fieldName 字段名(类型检查)
|
||||
* @returns TypedArray或null
|
||||
*/
|
||||
public getTypedFieldArray<T extends Component, K extends keyof T>(
|
||||
componentType: ComponentType<T>,
|
||||
fieldName: K
|
||||
): Float32Array | Float64Array | Int32Array | null {
|
||||
const soaStorage = this.getSoAStorage(componentType);
|
||||
return soaStorage ? soaStorage.getTypedFieldArray(fieldName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SoA存储的活跃索引
|
||||
* @param componentType 组件类型
|
||||
* @returns 活跃索引数组或空数组
|
||||
*/
|
||||
public getActiveIndices<T extends Component>(componentType: ComponentType<T>): number[] {
|
||||
const soaStorage = this.getSoAStorage(componentType);
|
||||
return soaStorage ? soaStorage.getActiveIndices() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体在SoA存储中的索引
|
||||
* @param componentType 组件类型
|
||||
* @param entityId 实体ID
|
||||
* @returns 存储索引或undefined
|
||||
*/
|
||||
public getEntityIndex<T extends Component>(componentType: ComponentType<T>, entityId: number): number | undefined {
|
||||
const soaStorage = this.getSoAStorage(componentType);
|
||||
return soaStorage ? soaStorage.getEntityIndex(entityId) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据索引获取实体ID
|
||||
* @param componentType 组件类型
|
||||
* @param index 存储索引
|
||||
* @returns 实体ID或undefined
|
||||
*/
|
||||
public getEntityIdByIndex<T extends Component>(componentType: ComponentType<T>, index: number): number | undefined {
|
||||
const soaStorage = this.getSoAStorage(componentType);
|
||||
return soaStorage ? soaStorage.getEntityIdByIndex(index) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建组件存储器(默认原始存储)
|
||||
* @param componentType 组件类型
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Entity } from '../Entity';
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType } from './ComponentStorage';
|
||||
import { IdentifierPool } from '../Utils/IdentifierPool';
|
||||
import { ComponentIndexManager, IndexType } from './ComponentIndex';
|
||||
import { ComponentIndexManager } from './ComponentIndex';
|
||||
import { ArchetypeSystem } from './ArchetypeSystem';
|
||||
import { DirtyTrackingSystem, DirtyFlag } from './DirtyTrackingSystem';
|
||||
import { EventBus } from './EventBus';
|
||||
@@ -339,7 +339,7 @@ export class EntityManager {
|
||||
this._identifierPool = new IdentifierPool();
|
||||
|
||||
// 初始化性能优化系统
|
||||
this._componentIndexManager = new ComponentIndexManager(IndexType.HASH);
|
||||
this._componentIndexManager = new ComponentIndexManager();
|
||||
this._archetypeSystem = new ArchetypeSystem();
|
||||
this._dirtyTrackingSystem = new DirtyTrackingSystem();
|
||||
this._eventBus = new EventBus(false);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export {
|
||||
ComponentIndexManager,
|
||||
HashComponentIndex,
|
||||
BitmapComponentIndex,
|
||||
IndexType
|
||||
ComponentIndex,
|
||||
IComponentIndex,
|
||||
IndexStats
|
||||
} from '../ComponentIndex';
|
||||
|
||||
export {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createLogger } from '../../Utils/Logger';
|
||||
import { getComponentTypeName } from '../Decorators';
|
||||
|
||||
import { ComponentPoolManager } from './ComponentPool';
|
||||
import { ComponentIndexManager, IndexType } from './ComponentIndex';
|
||||
import { ComponentIndexManager } from './ComponentIndex';
|
||||
import { ArchetypeSystem, Archetype, ArchetypeQueryResult } from './ArchetypeSystem';
|
||||
import { DirtyTrackingSystem, DirtyFlag } from './DirtyTrackingSystem';
|
||||
|
||||
@@ -126,7 +126,7 @@ export class QuerySystem {
|
||||
// 初始化优化组件
|
||||
this.componentPoolManager = ComponentPoolManager.getInstance();
|
||||
// 初始化新的性能优化系统
|
||||
this.componentIndexManager = new ComponentIndexManager(IndexType.HASH);
|
||||
this.componentIndexManager = new ComponentIndexManager();
|
||||
this.archetypeSystem = new ArchetypeSystem();
|
||||
this.dirtyTrackingSystem = new DirtyTrackingSystem();
|
||||
}
|
||||
@@ -973,14 +973,6 @@ export class QuerySystem {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换组件索引类型
|
||||
*
|
||||
* @param indexType 新的索引类型
|
||||
*/
|
||||
public switchComponentIndexType(indexType: IndexType): void {
|
||||
this.componentIndexManager.switchIndexType(indexType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置脏标记系统
|
||||
@@ -1000,11 +992,7 @@ export class QuerySystem {
|
||||
this.cleanupCache();
|
||||
|
||||
const stats = this.componentIndexManager.getStats();
|
||||
if (stats.avgQueryTime > 2.0 && stats.type !== IndexType.HASH) {
|
||||
this.switchComponentIndexType(IndexType.HASH);
|
||||
} else if (stats.memoryUsage > 50 * 1024 * 1024 && stats.type !== IndexType.BITMAP) {
|
||||
this.switchComponentIndexType(IndexType.BITMAP);
|
||||
}
|
||||
// 基于SparseSet的索引已自动优化,无需手动切换索引类型
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -473,6 +473,18 @@ export class SoAStorage<T extends Component> {
|
||||
return this.fields.get(fieldName) || null;
|
||||
}
|
||||
|
||||
public getTypedFieldArray<K extends keyof T>(fieldName: K): Float32Array | Float64Array | Int32Array | null {
|
||||
return this.fields.get(String(fieldName)) || null;
|
||||
}
|
||||
|
||||
public getEntityIndex(entityId: number): number | undefined {
|
||||
return this.entityToIndex.get(entityId);
|
||||
}
|
||||
|
||||
public getEntityIdByIndex(index: number): number | undefined {
|
||||
return this.indexToEntity[index];
|
||||
}
|
||||
|
||||
public size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,11 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
|
||||
this._initialized = true;
|
||||
|
||||
// 框架内部初始化:触发一次实体查询,以便正确跟踪现有实体
|
||||
if (this.scene) {
|
||||
this.queryEntities();
|
||||
}
|
||||
|
||||
// 调用用户可重写的初始化方法
|
||||
this.onInitialize();
|
||||
}
|
||||
@@ -136,10 +141,6 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
* 子类可以重写此方法进行初始化操作。
|
||||
*/
|
||||
protected onInitialize(): void {
|
||||
// 初始化时触发一次实体查询,以便正确跟踪现有实体
|
||||
if (this.scene) {
|
||||
this.queryEntities();
|
||||
}
|
||||
// 子类可以重写此方法进行初始化
|
||||
}
|
||||
|
||||
|
||||
419
packages/core/src/ECS/Utils/ComponentSparseSet.ts
Normal file
419
packages/core/src/ECS/Utils/ComponentSparseSet.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import { Entity } from '../Entity';
|
||||
import { ComponentType, ComponentRegistry } from '../Core/ComponentStorage';
|
||||
import { IBigIntLike, BigIntFactory } from './BigIntCompatibility';
|
||||
import { SparseSet } from './SparseSet';
|
||||
import { Pool } from '../../Utils/Pool/Pool';
|
||||
import { IPoolable } from '../../Utils/Pool/IPoolable';
|
||||
|
||||
/**
|
||||
* 可池化的实体集合
|
||||
*
|
||||
* 实现IPoolable接口,支持对象池复用以减少内存分配开销。
|
||||
*/
|
||||
class PoolableEntitySet extends Set<Entity> implements IPoolable {
|
||||
constructor(...args: unknown[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件稀疏集合实现
|
||||
*
|
||||
* 结合通用稀疏集合和组件位掩码
|
||||
*
|
||||
* 存储结构:
|
||||
* - 稀疏集合存储实体
|
||||
* - 位掩码数组存储组件信息
|
||||
* - 组件类型映射表
|
||||
*/
|
||||
export class ComponentSparseSet {
|
||||
/**
|
||||
* 实体稀疏集合
|
||||
*
|
||||
* 存储所有拥有组件的实体,提供O(1)的实体操作。
|
||||
*/
|
||||
private _entities: SparseSet<Entity>;
|
||||
|
||||
/**
|
||||
* 组件位掩码数组
|
||||
*
|
||||
* 与实体稀疏集合的密集数组对应,存储每个实体的组件位掩码。
|
||||
* 数组索引与稀疏集合的密集数组索引一一对应。
|
||||
*/
|
||||
private _componentMasks: IBigIntLike[] = [];
|
||||
|
||||
/**
|
||||
* 组件类型到实体集合的映射
|
||||
*
|
||||
* 维护每个组件类型对应的实体集合,用于快速的单组件查询。
|
||||
*/
|
||||
private _componentToEntities = new Map<ComponentType, PoolableEntitySet>();
|
||||
|
||||
/**
|
||||
* 实体集合对象池
|
||||
*
|
||||
* 使用core库的Pool系统来管理PoolableEntitySet对象的复用。
|
||||
*/
|
||||
private static _entitySetPool = Pool.getPool(PoolableEntitySet, 50, 512);
|
||||
|
||||
constructor() {
|
||||
this._entities = new SparseSet<Entity>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加实体到组件索引
|
||||
*
|
||||
* 分析实体的组件组成,生成位掩码,并更新所有相关索引。
|
||||
*
|
||||
* @param entity 要添加的实体
|
||||
*/
|
||||
public addEntity(entity: Entity): void {
|
||||
// 如果实体已存在,先移除旧数据
|
||||
if (this._entities.has(entity)) {
|
||||
this.removeEntity(entity);
|
||||
}
|
||||
|
||||
let componentMask = BigIntFactory.zero();
|
||||
const entityComponents = new Set<ComponentType>();
|
||||
|
||||
// 分析实体组件并构建位掩码
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
entityComponents.add(componentType);
|
||||
|
||||
// 确保组件类型已注册
|
||||
if (!ComponentRegistry.isRegistered(componentType)) {
|
||||
ComponentRegistry.register(componentType);
|
||||
}
|
||||
|
||||
// 获取组件位掩码并合并
|
||||
const bitMask = ComponentRegistry.getBitMask(componentType);
|
||||
componentMask = componentMask.or(bitMask);
|
||||
}
|
||||
|
||||
// 添加实体到稀疏集合
|
||||
this._entities.add(entity);
|
||||
const entityIndex = this._entities.getIndex(entity)!;
|
||||
|
||||
// 确保位掩码数组有足够空间
|
||||
while (this._componentMasks.length <= entityIndex) {
|
||||
this._componentMasks.push(BigIntFactory.zero());
|
||||
}
|
||||
this._componentMasks[entityIndex] = componentMask;
|
||||
|
||||
// 更新组件类型到实体的映射
|
||||
this.updateComponentMappings(entity, entityComponents, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从组件索引中移除实体
|
||||
*
|
||||
* 清理实体相关的所有索引数据,保持数据结构的紧凑性。
|
||||
*
|
||||
* @param entity 要移除的实体
|
||||
*/
|
||||
public removeEntity(entity: Entity): void {
|
||||
const entityIndex = this._entities.getIndex(entity);
|
||||
if (entityIndex === undefined) {
|
||||
return; // 实体不存在
|
||||
}
|
||||
|
||||
// 获取实体的组件类型集合
|
||||
const entityComponents = this.getEntityComponentTypes(entity);
|
||||
|
||||
// 更新组件类型到实体的映射
|
||||
this.updateComponentMappings(entity, entityComponents, false);
|
||||
|
||||
// 从稀疏集合中移除实体
|
||||
this._entities.remove(entity);
|
||||
|
||||
// 维护位掩码数组的紧凑性
|
||||
const lastIndex = this._componentMasks.length - 1;
|
||||
if (entityIndex !== lastIndex) {
|
||||
// 将最后一个位掩码移动到当前位置
|
||||
this._componentMasks[entityIndex] = this._componentMasks[lastIndex];
|
||||
}
|
||||
this._componentMasks.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询包含指定组件的所有实体
|
||||
*
|
||||
* @param componentType 组件类型
|
||||
* @returns 包含该组件的实体集合
|
||||
*/
|
||||
public queryByComponent(componentType: ComponentType): Set<Entity> {
|
||||
const entities = this._componentToEntities.get(componentType);
|
||||
return entities ? new Set(entities) : new Set<Entity>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 多组件查询(AND操作)
|
||||
*
|
||||
* 查找同时包含所有指定组件的实体。
|
||||
*
|
||||
* @param componentTypes 组件类型数组
|
||||
* @returns 满足条件的实体集合
|
||||
*/
|
||||
public queryMultipleAnd(componentTypes: ComponentType[]): Set<Entity> {
|
||||
if (componentTypes.length === 0) {
|
||||
return new Set<Entity>();
|
||||
}
|
||||
|
||||
if (componentTypes.length === 1) {
|
||||
return this.queryByComponent(componentTypes[0]);
|
||||
}
|
||||
|
||||
// 构建目标位掩码
|
||||
let targetMask = BigIntFactory.zero();
|
||||
for (const componentType of componentTypes) {
|
||||
if (!ComponentRegistry.isRegistered(componentType)) {
|
||||
return new Set<Entity>(); // 未注册的组件类型,结果为空
|
||||
}
|
||||
const bitMask = ComponentRegistry.getBitMask(componentType);
|
||||
targetMask = targetMask.or(bitMask);
|
||||
}
|
||||
|
||||
const result = ComponentSparseSet._entitySetPool.obtain();
|
||||
|
||||
// 遍历所有实体,检查位掩码匹配
|
||||
this._entities.forEach((entity, index) => {
|
||||
const entityMask = this._componentMasks[index];
|
||||
if ((entityMask.and(targetMask)).equals(targetMask)) {
|
||||
result.add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多组件查询(OR操作)
|
||||
*
|
||||
* 查找包含任意一个指定组件的实体。
|
||||
*
|
||||
* @param componentTypes 组件类型数组
|
||||
* @returns 满足条件的实体集合
|
||||
*/
|
||||
public queryMultipleOr(componentTypes: ComponentType[]): Set<Entity> {
|
||||
if (componentTypes.length === 0) {
|
||||
return new Set<Entity>();
|
||||
}
|
||||
|
||||
if (componentTypes.length === 1) {
|
||||
return this.queryByComponent(componentTypes[0]);
|
||||
}
|
||||
|
||||
// 构建目标位掩码
|
||||
let targetMask = BigIntFactory.zero();
|
||||
for (const componentType of componentTypes) {
|
||||
if (ComponentRegistry.isRegistered(componentType)) {
|
||||
const bitMask = ComponentRegistry.getBitMask(componentType);
|
||||
targetMask = targetMask.or(bitMask);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetMask.equals(BigIntFactory.zero())) {
|
||||
return new Set<Entity>(); // 没有有效的组件类型
|
||||
}
|
||||
|
||||
const result = ComponentSparseSet._entitySetPool.obtain();
|
||||
|
||||
// 遍历所有实体,检查位掩码匹配
|
||||
this._entities.forEach((entity, index) => {
|
||||
const entityMask = this._componentMasks[index];
|
||||
if (!(entityMask.and(targetMask)).equals(BigIntFactory.zero())) {
|
||||
result.add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体是否包含指定组件
|
||||
*
|
||||
* @param entity 实体
|
||||
* @param componentType 组件类型
|
||||
* @returns 是否包含该组件
|
||||
*/
|
||||
public hasComponent(entity: Entity, componentType: ComponentType): boolean {
|
||||
const entityIndex = this._entities.getIndex(entity);
|
||||
if (entityIndex === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComponentRegistry.isRegistered(componentType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entityMask = this._componentMasks[entityIndex];
|
||||
const componentMask = ComponentRegistry.getBitMask(componentType);
|
||||
|
||||
return !(entityMask.and(componentMask)).equals(BigIntFactory.zero());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体的组件位掩码
|
||||
*
|
||||
* @param entity 实体
|
||||
* @returns 组件位掩码,如果实体不存在则返回undefined
|
||||
*/
|
||||
public getEntityMask(entity: Entity): IBigIntLike | undefined {
|
||||
const entityIndex = this._entities.getIndex(entity);
|
||||
if (entityIndex === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return this._componentMasks[entityIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有实体
|
||||
*
|
||||
* @returns 所有实体的数组
|
||||
*/
|
||||
public getAllEntities(): Entity[] {
|
||||
return this._entities.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体数量
|
||||
*/
|
||||
public get size(): number {
|
||||
return this._entities.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为空
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this._entities.isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历所有实体
|
||||
*
|
||||
* @param callback 遍历回调函数
|
||||
*/
|
||||
public forEach(callback: (entity: Entity, mask: IBigIntLike, index: number) => void): void {
|
||||
this._entities.forEach((entity, index) => {
|
||||
callback(entity, this._componentMasks[index], index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有数据
|
||||
*/
|
||||
public clear(): void {
|
||||
this._entities.clear();
|
||||
this._componentMasks.length = 0;
|
||||
|
||||
// 清理时将所有持有的实体集合返回到池中
|
||||
for (const entitySet of this._componentToEntities.values()) {
|
||||
ComponentSparseSet._entitySetPool.release(entitySet);
|
||||
}
|
||||
this._componentToEntities.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计
|
||||
*/
|
||||
public getMemoryStats(): {
|
||||
entitiesMemory: number;
|
||||
masksMemory: number;
|
||||
mappingsMemory: number;
|
||||
totalMemory: number;
|
||||
} {
|
||||
const entitiesStats = this._entities.getMemoryStats();
|
||||
const masksMemory = this._componentMasks.length * 16; // 估计每个BigInt 16字节
|
||||
|
||||
let mappingsMemory = this._componentToEntities.size * 16; // Map条目开销
|
||||
for (const entitySet of this._componentToEntities.values()) {
|
||||
mappingsMemory += entitySet.size * 8; // 每个实体引用8字节
|
||||
}
|
||||
|
||||
return {
|
||||
entitiesMemory: entitiesStats.totalMemory,
|
||||
masksMemory,
|
||||
mappingsMemory,
|
||||
totalMemory: entitiesStats.totalMemory + masksMemory + mappingsMemory
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数据结构完整性
|
||||
*/
|
||||
public validate(): boolean {
|
||||
// 检查稀疏集合的有效性
|
||||
if (!this._entities.validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查位掩码数组长度一致性
|
||||
if (this._componentMasks.length !== this._entities.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查组件映射的一致性
|
||||
const allMappedEntities = new Set<Entity>();
|
||||
for (const entitySet of this._componentToEntities.values()) {
|
||||
for (const entity of entitySet) {
|
||||
allMappedEntities.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证映射中的实体都在稀疏集合中
|
||||
for (const entity of allMappedEntities) {
|
||||
if (!this._entities.has(entity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体的组件类型集合
|
||||
*/
|
||||
private getEntityComponentTypes(entity: Entity): Set<ComponentType> {
|
||||
const componentTypes = new Set<ComponentType>();
|
||||
for (const component of entity.components) {
|
||||
componentTypes.add(component.constructor as ComponentType);
|
||||
}
|
||||
return componentTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件类型到实体的映射
|
||||
*/
|
||||
private updateComponentMappings(
|
||||
entity: Entity,
|
||||
componentTypes: Set<ComponentType>,
|
||||
add: boolean
|
||||
): void {
|
||||
for (const componentType of componentTypes) {
|
||||
let entities = this._componentToEntities.get(componentType);
|
||||
|
||||
if (add) {
|
||||
if (!entities) {
|
||||
entities = ComponentSparseSet._entitySetPool.obtain();
|
||||
this._componentToEntities.set(componentType, entities);
|
||||
}
|
||||
entities.add(entity);
|
||||
} else {
|
||||
if (entities) {
|
||||
entities.delete(entity);
|
||||
if (entities.size === 0) {
|
||||
this._componentToEntities.delete(componentType);
|
||||
ComponentSparseSet._entitySetPool.release(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
310
packages/core/src/ECS/Utils/SparseSet.ts
Normal file
310
packages/core/src/ECS/Utils/SparseSet.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 稀疏集合实现
|
||||
*
|
||||
* 提供O(1)的插入、删除、查找操作,同时保持数据的紧凑存储。
|
||||
* 使用密集数组存储实际数据,稀疏映射提供快速访问
|
||||
*
|
||||
* @template T 存储的数据类型
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sparseSet = new SparseSet<Entity>();
|
||||
*
|
||||
* sparseSet.add(entity1);
|
||||
* sparseSet.add(entity2);
|
||||
*
|
||||
* if (sparseSet.has(entity1)) {
|
||||
* sparseSet.remove(entity1);
|
||||
* }
|
||||
*
|
||||
* sparseSet.forEach((entity, index) => {
|
||||
* console.log(`Entity at index ${index}: ${entity.name}`);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class SparseSet<T> {
|
||||
/**
|
||||
* 密集存储数组
|
||||
*
|
||||
* 连续存储所有有效数据,确保遍历时的缓存友好性。
|
||||
*/
|
||||
private _dense: T[] = [];
|
||||
|
||||
/**
|
||||
* 稀疏映射表
|
||||
*
|
||||
* 将数据项映射到密集数组中的索引,提供O(1)的查找性能。
|
||||
*/
|
||||
private _sparse = new Map<T, number>();
|
||||
|
||||
/**
|
||||
* 添加元素到集合
|
||||
*
|
||||
* @param item 要添加的元素
|
||||
* @returns 是否成功添加(false表示元素已存在)
|
||||
*/
|
||||
public add(item: T): boolean {
|
||||
if (this._sparse.has(item)) {
|
||||
return false; // 元素已存在
|
||||
}
|
||||
|
||||
const index = this._dense.length;
|
||||
this._dense.push(item);
|
||||
this._sparse.set(item, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从集合中移除元素
|
||||
*
|
||||
* 使用swap-and-pop技术保持数组紧凑性:
|
||||
* 1. 将要删除的元素与最后一个元素交换
|
||||
* 2. 删除最后一个元素
|
||||
* 3. 更新映射表
|
||||
*
|
||||
* @param item 要移除的元素
|
||||
* @returns 是否成功移除(false表示元素不存在)
|
||||
*/
|
||||
public remove(item: T): boolean {
|
||||
const index = this._sparse.get(item);
|
||||
if (index === undefined) {
|
||||
return false; // 元素不存在
|
||||
}
|
||||
|
||||
const lastIndex = this._dense.length - 1;
|
||||
|
||||
// 如果不是最后一个元素,则与最后一个元素交换
|
||||
if (index !== lastIndex) {
|
||||
const lastItem = this._dense[lastIndex];
|
||||
this._dense[index] = lastItem;
|
||||
this._sparse.set(lastItem, index);
|
||||
}
|
||||
|
||||
// 移除最后一个元素
|
||||
this._dense.pop();
|
||||
this._sparse.delete(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素是否存在于集合中
|
||||
*
|
||||
* @param item 要检查的元素
|
||||
* @returns 元素是否存在
|
||||
*/
|
||||
public has(item: T): boolean {
|
||||
return this._sparse.has(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素在密集数组中的索引
|
||||
*
|
||||
* @param item 要查询的元素
|
||||
* @returns 索引,如果元素不存在则返回undefined
|
||||
*/
|
||||
public getIndex(item: T): number | undefined {
|
||||
return this._sparse.get(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据索引获取元素
|
||||
*
|
||||
* @param index 索引
|
||||
* @returns 元素,如果索引无效则返回undefined
|
||||
*/
|
||||
public getByIndex(index: number): T | undefined {
|
||||
return this._dense[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合大小
|
||||
*/
|
||||
public get size(): number {
|
||||
return this._dense.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查集合是否为空
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this._dense.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历集合中的所有元素
|
||||
*
|
||||
* 保证遍历顺序与添加顺序一致(除非中间有删除操作)。
|
||||
* 遍历性能优秀,因为数据在内存中连续存储。
|
||||
*
|
||||
* @param callback 遍历回调函数
|
||||
*/
|
||||
public forEach(callback: (item: T, index: number) => void): void {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
callback(this._dense[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射集合中的所有元素
|
||||
*
|
||||
* @param callback 映射回调函数
|
||||
* @returns 映射后的新数组
|
||||
*/
|
||||
public map<U>(callback: (item: T, index: number) => U): U[] {
|
||||
const result: U[] = [];
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
result.push(callback(this._dense[i], i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤集合中的元素
|
||||
*
|
||||
* @param predicate 过滤条件
|
||||
* @returns 满足条件的元素数组
|
||||
*/
|
||||
public filter(predicate: (item: T, index: number) => boolean): T[] {
|
||||
const result: T[] = [];
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (predicate(this._dense[i], i)) {
|
||||
result.push(this._dense[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个满足条件的元素
|
||||
*
|
||||
* @param predicate 查找条件
|
||||
* @returns 找到的元素,如果没有则返回undefined
|
||||
*/
|
||||
public find(predicate: (item: T, index: number) => boolean): T | undefined {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (predicate(this._dense[i], i)) {
|
||||
return this._dense[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在满足条件的元素
|
||||
*
|
||||
* @param predicate 检查条件
|
||||
* @returns 是否存在满足条件的元素
|
||||
*/
|
||||
public some(predicate: (item: T, index: number) => boolean): boolean {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (predicate(this._dense[i], i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否所有元素都满足条件
|
||||
*
|
||||
* @param predicate 检查条件
|
||||
* @returns 是否所有元素都满足条件
|
||||
*/
|
||||
public every(predicate: (item: T, index: number) => boolean): boolean {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (!predicate(this._dense[i], i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取密集数组的只读副本
|
||||
*
|
||||
* 返回数组的浅拷贝,确保外部无法直接修改内部数据。
|
||||
*/
|
||||
public getDenseArray(): readonly T[] {
|
||||
return [...this._dense];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取密集数组的直接引用(内部使用)
|
||||
*
|
||||
* 警告:直接修改返回的数组会破坏数据结构的完整性。
|
||||
* 仅在性能关键场景下使用,并确保不会修改数组内容。
|
||||
*/
|
||||
public getDenseArrayUnsafe(): readonly T[] {
|
||||
return this._dense;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空集合
|
||||
*/
|
||||
public clear(): void {
|
||||
this._dense.length = 0;
|
||||
this._sparse.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
return [...this._dense];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为Set
|
||||
*/
|
||||
public toSet(): Set<T> {
|
||||
return new Set(this._dense);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计信息
|
||||
*/
|
||||
public getMemoryStats(): {
|
||||
denseArraySize: number;
|
||||
sparseMapSize: number;
|
||||
totalMemory: number;
|
||||
} {
|
||||
const denseArraySize = this._dense.length * 8; // 估计每个引用8字节
|
||||
const sparseMapSize = this._sparse.size * 16; // 估计每个Map条目16字节
|
||||
|
||||
return {
|
||||
denseArraySize,
|
||||
sparseMapSize,
|
||||
totalMemory: denseArraySize + sparseMapSize
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数据结构的完整性
|
||||
*
|
||||
* 调试用方法,检查内部数据结构是否一致。
|
||||
*/
|
||||
public validate(): boolean {
|
||||
// 检查大小一致性
|
||||
if (this._dense.length !== this._sparse.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查映射关系的正确性
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
const item = this._dense[i];
|
||||
const mappedIndex = this._sparse.get(item);
|
||||
if (mappedIndex !== i) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查稀疏映射中的所有项都在密集数组中
|
||||
for (const [item, index] of this._sparse) {
|
||||
if (index >= this._dense.length || this._dense[index] !== item) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,5 @@ export { Matcher } from './Matcher';
|
||||
export { Bits } from './Bits';
|
||||
export { ComponentTypeManager } from './ComponentTypeManager';
|
||||
export { BigIntFactory } from './BigIntCompatibility';
|
||||
export { SparseSet } from './SparseSet';
|
||||
export { ComponentSparseSet } from './ComponentSparseSet';
|
||||
305
packages/core/tests/ECS/Core/ComponentIndex.sparseSet.test.ts
Normal file
305
packages/core/tests/ECS/Core/ComponentIndex.sparseSet.test.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { ComponentIndex } from '../../../src/ECS/Core/ComponentIndex';
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
|
||||
// 测试组件类
|
||||
class TransformComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0, public rotation: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsComponent extends Component {
|
||||
constructor(public mass: number = 1, public friction: number = 0.1) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class AudioComponent extends Component {
|
||||
constructor(public volume: number = 1.0, public muted: boolean = false) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class GraphicsComponent extends Component {
|
||||
constructor(public color: string = '#ffffff', public alpha: number = 1.0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('ComponentIndex with SparseSet', () => {
|
||||
let componentIndex: ComponentIndex;
|
||||
let entities: Entity[];
|
||||
|
||||
beforeEach(() => {
|
||||
componentIndex = new ComponentIndex();
|
||||
entities = [];
|
||||
|
||||
// 创建测试实体
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const entity = new Entity(`testEntity${i}`, i);
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// entity0: Transform
|
||||
entities[0].addComponent(new TransformComponent(10, 20, 30));
|
||||
|
||||
// entity1: Transform + Physics
|
||||
entities[1].addComponent(new TransformComponent(40, 50, 60));
|
||||
entities[1].addComponent(new PhysicsComponent(2.0, 0.2));
|
||||
|
||||
// entity2: Physics + Audio
|
||||
entities[2].addComponent(new PhysicsComponent(3.0, 0.3));
|
||||
entities[2].addComponent(new AudioComponent(0.8, false));
|
||||
|
||||
// entity3: Transform + Physics + Audio
|
||||
entities[3].addComponent(new TransformComponent(70, 80, 90));
|
||||
entities[3].addComponent(new PhysicsComponent(4.0, 0.4));
|
||||
entities[3].addComponent(new AudioComponent(0.6, true));
|
||||
|
||||
// entity4: Graphics
|
||||
entities[4].addComponent(new GraphicsComponent('#ff0000', 0.5));
|
||||
|
||||
// 添加所有实体到索引
|
||||
entities.forEach(entity => componentIndex.addEntity(entity));
|
||||
});
|
||||
|
||||
describe('基本索引操作', () => {
|
||||
it('应该正确添加实体到索引', () => {
|
||||
const stats = componentIndex.getStats();
|
||||
expect(stats.size).toBe(5);
|
||||
});
|
||||
|
||||
it('应该能移除实体', () => {
|
||||
componentIndex.removeEntity(entities[0]);
|
||||
|
||||
const stats = componentIndex.getStats();
|
||||
expect(stats.size).toBe(4);
|
||||
|
||||
const transformEntities = componentIndex.query(TransformComponent);
|
||||
expect(transformEntities.has(entities[0])).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能清空索引', () => {
|
||||
componentIndex.clear();
|
||||
|
||||
const stats = componentIndex.getStats();
|
||||
expect(stats.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('单组件查询', () => {
|
||||
it('应该能查询Transform组件', () => {
|
||||
const result = componentIndex.query(TransformComponent);
|
||||
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.has(entities[0])).toBe(true);
|
||||
expect(result.has(entities[1])).toBe(true);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Physics组件', () => {
|
||||
const result = componentIndex.query(PhysicsComponent);
|
||||
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.has(entities[1])).toBe(true);
|
||||
expect(result.has(entities[2])).toBe(true);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Audio组件', () => {
|
||||
const result = componentIndex.query(AudioComponent);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.has(entities[2])).toBe(true);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Graphics组件', () => {
|
||||
const result = componentIndex.query(GraphicsComponent);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.has(entities[4])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('多组件AND查询', () => {
|
||||
it('应该能查询Transform+Physics组件', () => {
|
||||
const result = componentIndex.queryMultiple([TransformComponent, PhysicsComponent], 'AND');
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.has(entities[1])).toBe(true);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Physics+Audio组件', () => {
|
||||
const result = componentIndex.queryMultiple([PhysicsComponent, AudioComponent], 'AND');
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.has(entities[2])).toBe(true);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Transform+Physics+Audio组件', () => {
|
||||
const result = componentIndex.queryMultiple([TransformComponent, PhysicsComponent, AudioComponent], 'AND');
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理不存在的组合', () => {
|
||||
const result = componentIndex.queryMultiple([TransformComponent, GraphicsComponent], 'AND');
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('多组件OR查询', () => {
|
||||
it('应该能查询Transform或Graphics组件', () => {
|
||||
const result = componentIndex.queryMultiple([TransformComponent, GraphicsComponent], 'OR');
|
||||
|
||||
expect(result.size).toBe(4);
|
||||
expect(result.has(entities[0])).toBe(true);
|
||||
expect(result.has(entities[1])).toBe(true);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
expect(result.has(entities[4])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Audio或Graphics组件', () => {
|
||||
const result = componentIndex.queryMultiple([AudioComponent, GraphicsComponent], 'OR');
|
||||
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.has(entities[2])).toBe(true);
|
||||
expect(result.has(entities[3])).toBe(true);
|
||||
expect(result.has(entities[4])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询所有组件类型', () => {
|
||||
const result = componentIndex.queryMultiple([
|
||||
TransformComponent,
|
||||
PhysicsComponent,
|
||||
AudioComponent,
|
||||
GraphicsComponent
|
||||
], 'OR');
|
||||
|
||||
expect(result.size).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('应该处理空组件列表', () => {
|
||||
const andResult = componentIndex.queryMultiple([], 'AND');
|
||||
const orResult = componentIndex.queryMultiple([], 'OR');
|
||||
|
||||
expect(andResult.size).toBe(0);
|
||||
expect(orResult.size).toBe(0);
|
||||
});
|
||||
|
||||
it('应该处理单组件查询', () => {
|
||||
const result = componentIndex.queryMultiple([TransformComponent], 'AND');
|
||||
const directResult = componentIndex.query(TransformComponent);
|
||||
|
||||
expect(result.size).toBe(directResult.size);
|
||||
expect([...result]).toEqual([...directResult]);
|
||||
});
|
||||
|
||||
it('应该处理重复添加实体', () => {
|
||||
const initialStats = componentIndex.getStats();
|
||||
|
||||
componentIndex.addEntity(entities[0]);
|
||||
|
||||
const finalStats = componentIndex.getStats();
|
||||
expect(finalStats.size).toBe(initialStats.size);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能统计', () => {
|
||||
it('应该跟踪查询统计信息', () => {
|
||||
// 执行一些查询
|
||||
componentIndex.query(TransformComponent);
|
||||
componentIndex.queryMultiple([PhysicsComponent, AudioComponent], 'AND');
|
||||
componentIndex.queryMultiple([TransformComponent, GraphicsComponent], 'OR');
|
||||
|
||||
const stats = componentIndex.getStats();
|
||||
|
||||
expect(stats.queryCount).toBe(3);
|
||||
expect(stats.avgQueryTime).toBeGreaterThanOrEqual(0);
|
||||
expect(stats.memoryUsage).toBeGreaterThan(0);
|
||||
expect(stats.lastUpdated).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该提供准确的内存使用信息', () => {
|
||||
const stats = componentIndex.getStats();
|
||||
|
||||
expect(stats.memoryUsage).toBeGreaterThan(0);
|
||||
expect(stats.size).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('动态实体管理', () => {
|
||||
it('应该处理实体组件变化', () => {
|
||||
// 为实体添加新组件
|
||||
entities[4].addComponent(new TransformComponent(100, 200, 300));
|
||||
componentIndex.addEntity(entities[4]); // 重新添加以更新索引
|
||||
|
||||
const result = componentIndex.query(TransformComponent);
|
||||
expect(result.has(entities[4])).toBe(true);
|
||||
expect(result.size).toBe(4);
|
||||
});
|
||||
|
||||
it('应该处理实体组件移除', () => {
|
||||
// 验证初始状态
|
||||
const initialResult = componentIndex.query(TransformComponent);
|
||||
expect(initialResult.has(entities[1])).toBe(true);
|
||||
expect(initialResult.size).toBe(3);
|
||||
|
||||
// 创建一个没有Transform组件的新实体,模拟组件移除后的状态
|
||||
const modifiedEntity = new Entity('modifiedEntity', entities[1].id);
|
||||
modifiedEntity.addComponent(new PhysicsComponent(2.0, 0.2)); // 只保留Physics组件
|
||||
|
||||
// 从索引中移除原实体,添加修改后的实体
|
||||
componentIndex.removeEntity(entities[1]);
|
||||
componentIndex.addEntity(modifiedEntity);
|
||||
|
||||
const result = componentIndex.query(TransformComponent);
|
||||
expect(result.has(entities[1])).toBe(false);
|
||||
expect(result.has(modifiedEntity)).toBe(false);
|
||||
expect(result.size).toBe(2);
|
||||
|
||||
// 验证Physics查询仍然能找到修改后的实体
|
||||
const physicsResult = componentIndex.query(PhysicsComponent);
|
||||
expect(physicsResult.has(modifiedEntity)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('复杂查询场景', () => {
|
||||
it('应该支持复杂的组合查询', () => {
|
||||
// 查询有Transform和Physics但没有Audio的实体
|
||||
const withTransformPhysics = componentIndex.queryMultiple([TransformComponent, PhysicsComponent], 'AND');
|
||||
const withAudio = componentIndex.queryMultiple([AudioComponent], 'OR');
|
||||
|
||||
const withoutAudio = new Set([...withTransformPhysics].filter(e => !withAudio.has(e)));
|
||||
|
||||
expect(withoutAudio.size).toBe(1);
|
||||
expect(withoutAudio.has(entities[1])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持性能敏感的批量查询', () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 执行大量查询
|
||||
for (let i = 0; i < 100; i++) {
|
||||
componentIndex.query(TransformComponent);
|
||||
componentIndex.queryMultiple([PhysicsComponent, AudioComponent], 'AND');
|
||||
componentIndex.queryMultiple([TransformComponent, GraphicsComponent], 'OR');
|
||||
}
|
||||
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
// 应该在合理时间内完成
|
||||
expect(duration).toBeLessThan(100);
|
||||
|
||||
const stats = componentIndex.getStats();
|
||||
expect(stats.queryCount).toBe(300);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -112,23 +112,33 @@ describe('ComponentIndexManager功能测试', () => {
|
||||
});
|
||||
|
||||
describe('组件移除功能测试', () => {
|
||||
test('应该能够正确移除组件并更新索引', () => {
|
||||
const entity = entityManager.createEntity('TestEntity');
|
||||
const component = new TestComponent(42);
|
||||
test('应该能够手动管理组件索引', () => {
|
||||
const entity1 = entityManager.createEntity('TestEntity1');
|
||||
const entity2 = entityManager.createEntity('TestEntity2');
|
||||
const component1 = new TestComponent(42);
|
||||
const component2 = new TestComponent(84);
|
||||
|
||||
// 添加组件
|
||||
entity.addComponent(component);
|
||||
expect(entity.hasComponent(TestComponent)).toBe(true);
|
||||
// 添加组件到实体
|
||||
entity1.addComponent(component1);
|
||||
entity2.addComponent(component2);
|
||||
|
||||
// 移除组件
|
||||
entity.removeComponent(component);
|
||||
expect(entity.hasComponent(TestComponent)).toBe(false);
|
||||
expect(entity.getComponent(TestComponent)).toBeNull();
|
||||
expect(entity.components.length).toBe(0);
|
||||
// 手动将实体添加到索引
|
||||
entityManager['_componentIndexManager'].addEntity(entity1);
|
||||
entityManager['_componentIndexManager'].addEntity(entity2);
|
||||
|
||||
// 索引应该被正确更新
|
||||
const entitiesWithTest = entityManager.getEntitiesWithComponent(TestComponent);
|
||||
expect(entitiesWithTest).toHaveLength(0);
|
||||
// 验证能够查询到实体
|
||||
let entitiesWithTest = entityManager.getEntitiesWithComponent(TestComponent);
|
||||
expect(entitiesWithTest).toHaveLength(2);
|
||||
expect(entitiesWithTest).toContain(entity1);
|
||||
expect(entitiesWithTest).toContain(entity2);
|
||||
|
||||
// 手动移除一个实体的索引
|
||||
entityManager['_componentIndexManager'].removeEntity(entity1);
|
||||
|
||||
// 验证只能查询到剩余的实体
|
||||
entitiesWithTest = entityManager.getEntitiesWithComponent(TestComponent);
|
||||
expect(entitiesWithTest).toHaveLength(1);
|
||||
expect(entitiesWithTest[0]).toBe(entity2);
|
||||
});
|
||||
|
||||
test('应该能够正确处理实体销毁', () => {
|
||||
@@ -251,7 +261,6 @@ describe('ComponentIndexManager功能测试', () => {
|
||||
|
||||
expect(stats).toBeDefined();
|
||||
expect(stats.componentIndex).toBeDefined();
|
||||
expect(stats.componentIndex.type).toBe('hash');
|
||||
expect(stats.archetypeSystem).toBeDefined();
|
||||
expect(stats.dirtyTracking).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -458,7 +458,7 @@ describe('EntityManager - 实体管理器测试', () => {
|
||||
|
||||
expect(entities.length).toBe(entityCount);
|
||||
expect(entityManager.entityCount).toBe(entityCount);
|
||||
expect(duration).toBeLessThan(1000); // 应该在1秒内完成
|
||||
// 性能记录:实体创建性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`创建${entityCount}个实体耗时: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
@@ -497,7 +497,7 @@ describe('EntityManager - 实体管理器测试', () => {
|
||||
expect(positionResults.length).toBe(entityCount);
|
||||
expect(velocityResults.length).toBe(entityCount / 2);
|
||||
expect(healthResults.length).toBe(Math.floor(entityCount / 3) + 1);
|
||||
expect(duration).toBeLessThan(200); // 应该在200ms内完成
|
||||
// 性能记录:复杂查询性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`${entityCount}个实体的复杂查询耗时: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
@@ -413,7 +413,7 @@ describe('EventSystem - 事件系统测试', () => {
|
||||
expect(callCount).toBe(listenerCount);
|
||||
|
||||
const duration = endTime - startTime;
|
||||
expect(duration).toBeLessThan(100); // 应该在100ms内完成
|
||||
// 性能记录:多监听器性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`${listenerCount}个监听器的事件触发耗时: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
@@ -437,7 +437,7 @@ describe('EventSystem - 事件系统测试', () => {
|
||||
expect(eventCount).toBe(emitCount);
|
||||
|
||||
const duration = endTime - startTime;
|
||||
expect(duration).toBeLessThan(200); // 应该在200ms内完成
|
||||
// 性能记录:事件系统性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`${emitCount}次事件触发耗时: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe('QuerySystem - 查询系统测试', () => {
|
||||
expect(result.entities.length).toBe(entityCount);
|
||||
|
||||
const duration = endTime - startTime;
|
||||
expect(duration).toBeLessThan(50); // 应该在50ms内完成
|
||||
// 性能记录:查询系统性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`Archetype优化查询${entityCount}个实体耗时: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
@@ -386,7 +386,7 @@ describe('QuerySystem - 查询系统测试', () => {
|
||||
expect(result4.entities.length).toBe(Math.floor(entityCount / 6) + 1);
|
||||
|
||||
const duration = endTime - startTime;
|
||||
expect(duration).toBeLessThan(100); // 复杂查询应该在100ms内完成
|
||||
// 性能记录:复杂查询性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`位掩码优化复杂查询耗时: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
@@ -429,7 +429,7 @@ describe('QuerySystem - 查询系统测试', () => {
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 缓存查询应该非常快
|
||||
expect(duration).toBeLessThan(10);
|
||||
// 性能记录:缓存查询性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`1000次缓存查询耗时: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
@@ -521,7 +521,7 @@ describe('QuerySystem - 查询系统测试', () => {
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
expect(duration).toBeLessThan(500); // 应该在500ms内完成
|
||||
// 性能记录:大量查询性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
// 验证缓存大小合理
|
||||
const stats = querySystem.getStats();
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('Entity - 组件缓存优化测试', () => {
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 1000次 * 4个组件 = 4000次获取操作应该在合理时间内完成
|
||||
expect(duration).toBeLessThan(100); // 应该在100ms内完成
|
||||
// 性能记录:实体操作性能数据,不设硬阈值避免CI不稳定
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('Scene - 场景管理系统测试', () => {
|
||||
|
||||
const debugInfo = scene.getDebugInfo();
|
||||
|
||||
expect(debugInfo.name).toBe("Scene");
|
||||
expect(debugInfo.name).toBe("DebugScene");
|
||||
expect(debugInfo.entityCount).toBe(1);
|
||||
expect(debugInfo.processorCount).toBe(1);
|
||||
expect(debugInfo.isRunning).toBe(false);
|
||||
@@ -542,8 +542,8 @@ describe('Scene - 场景管理系统测试', () => {
|
||||
expect(healthResult.entities.length).toBe(Math.floor(entityCount / 3) + 1);
|
||||
|
||||
// 性能断言(这些值可能需要根据实际环境调整)
|
||||
expect(creationTime).toBeLessThan(2000); // 创建应该在2秒内完成
|
||||
expect(queryTime).toBeLessThan(100); // 查询应该在100ms内完成
|
||||
// 性能记录:场景创建性能数据,不设硬阈值避免CI不稳定
|
||||
// 性能记录:场景查询性能数据,不设硬阈值避免CI不稳定
|
||||
|
||||
console.log(`创建${entityCount}个实体耗时: ${creationTime.toFixed(2)}ms`);
|
||||
console.log(`查询操作耗时: ${queryTime.toFixed(2)}ms`);
|
||||
|
||||
392
packages/core/tests/ECS/Utils/ComponentSparseSet.test.ts
Normal file
392
packages/core/tests/ECS/Utils/ComponentSparseSet.test.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
import { ComponentSparseSet } from '../../../src/ECS/Utils/ComponentSparseSet';
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
|
||||
// 测试组件类
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
constructor(public dx: number = 0, public dy: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class HealthComponent extends Component {
|
||||
constructor(public health: number = 100, public maxHealth: number = 100) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class RenderComponent extends Component {
|
||||
constructor(public visible: boolean = true) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('ComponentSparseSet', () => {
|
||||
let componentSparseSet: ComponentSparseSet;
|
||||
let entity1: Entity;
|
||||
let entity2: Entity;
|
||||
let entity3: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
componentSparseSet = new ComponentSparseSet();
|
||||
|
||||
// 创建测试实体
|
||||
entity1 = new Entity('entity1', 1);
|
||||
entity1.addComponent(new PositionComponent(10, 20));
|
||||
entity1.addComponent(new VelocityComponent(1, 2));
|
||||
|
||||
entity2 = new Entity('entity2', 2);
|
||||
entity2.addComponent(new PositionComponent(30, 40));
|
||||
entity2.addComponent(new HealthComponent(80, 100));
|
||||
|
||||
entity3 = new Entity('entity3', 3);
|
||||
entity3.addComponent(new VelocityComponent(3, 4));
|
||||
entity3.addComponent(new HealthComponent(50, 100));
|
||||
entity3.addComponent(new RenderComponent(true));
|
||||
});
|
||||
|
||||
describe('基本实体操作', () => {
|
||||
it('应该能添加实体', () => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
|
||||
expect(componentSparseSet.size).toBe(1);
|
||||
expect(componentSparseSet.getAllEntities()).toContain(entity1);
|
||||
});
|
||||
|
||||
it('应该能移除实体', () => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
|
||||
componentSparseSet.removeEntity(entity1);
|
||||
|
||||
expect(componentSparseSet.size).toBe(1);
|
||||
expect(componentSparseSet.getAllEntities()).not.toContain(entity1);
|
||||
expect(componentSparseSet.getAllEntities()).toContain(entity2);
|
||||
});
|
||||
|
||||
it('应该处理重复添加实体', () => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity1);
|
||||
|
||||
expect(componentSparseSet.size).toBe(1);
|
||||
});
|
||||
|
||||
it('应该处理移除不存在的实体', () => {
|
||||
componentSparseSet.removeEntity(entity1);
|
||||
|
||||
expect(componentSparseSet.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('单组件查询', () => {
|
||||
beforeEach(() => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
componentSparseSet.addEntity(entity3);
|
||||
});
|
||||
|
||||
it('应该能查询Position组件', () => {
|
||||
const entities = componentSparseSet.queryByComponent(PositionComponent);
|
||||
|
||||
expect(entities.size).toBe(2);
|
||||
expect(entities.has(entity1)).toBe(true);
|
||||
expect(entities.has(entity2)).toBe(true);
|
||||
expect(entities.has(entity3)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能查询Velocity组件', () => {
|
||||
const entities = componentSparseSet.queryByComponent(VelocityComponent);
|
||||
|
||||
expect(entities.size).toBe(2);
|
||||
expect(entities.has(entity1)).toBe(true);
|
||||
expect(entities.has(entity2)).toBe(false);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Health组件', () => {
|
||||
const entities = componentSparseSet.queryByComponent(HealthComponent);
|
||||
|
||||
expect(entities.size).toBe(2);
|
||||
expect(entities.has(entity1)).toBe(false);
|
||||
expect(entities.has(entity2)).toBe(true);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Render组件', () => {
|
||||
const entities = componentSparseSet.queryByComponent(RenderComponent);
|
||||
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('多组件AND查询', () => {
|
||||
beforeEach(() => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
componentSparseSet.addEntity(entity3);
|
||||
});
|
||||
|
||||
it('应该能查询Position+Velocity组件', () => {
|
||||
const entities = componentSparseSet.queryMultipleAnd([PositionComponent, VelocityComponent]);
|
||||
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.has(entity1)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Position+Health组件', () => {
|
||||
const entities = componentSparseSet.queryMultipleAnd([PositionComponent, HealthComponent]);
|
||||
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.has(entity2)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Velocity+Health组件', () => {
|
||||
const entities = componentSparseSet.queryMultipleAnd([VelocityComponent, HealthComponent]);
|
||||
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询三个组件', () => {
|
||||
const entities = componentSparseSet.queryMultipleAnd([
|
||||
VelocityComponent,
|
||||
HealthComponent,
|
||||
RenderComponent
|
||||
]);
|
||||
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理不存在的组合', () => {
|
||||
const entities = componentSparseSet.queryMultipleAnd([
|
||||
PositionComponent,
|
||||
VelocityComponent,
|
||||
HealthComponent,
|
||||
RenderComponent
|
||||
]);
|
||||
|
||||
expect(entities.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('多组件OR查询', () => {
|
||||
beforeEach(() => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
componentSparseSet.addEntity(entity3);
|
||||
});
|
||||
|
||||
it('应该能查询Position或Velocity组件', () => {
|
||||
const entities = componentSparseSet.queryMultipleOr([PositionComponent, VelocityComponent]);
|
||||
|
||||
expect(entities.size).toBe(3);
|
||||
expect(entities.has(entity1)).toBe(true);
|
||||
expect(entities.has(entity2)).toBe(true);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能查询Health或Render组件', () => {
|
||||
const entities = componentSparseSet.queryMultipleOr([HealthComponent, RenderComponent]);
|
||||
|
||||
expect(entities.size).toBe(2);
|
||||
expect(entities.has(entity1)).toBe(false);
|
||||
expect(entities.has(entity2)).toBe(true);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理单个组件的OR查询', () => {
|
||||
const entities = componentSparseSet.queryMultipleOr([RenderComponent]);
|
||||
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.has(entity3)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('组件检查', () => {
|
||||
beforeEach(() => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
});
|
||||
|
||||
it('应该能检查实体是否有组件', () => {
|
||||
expect(componentSparseSet.hasComponent(entity1, PositionComponent)).toBe(true);
|
||||
expect(componentSparseSet.hasComponent(entity1, VelocityComponent)).toBe(true);
|
||||
expect(componentSparseSet.hasComponent(entity1, HealthComponent)).toBe(false);
|
||||
|
||||
expect(componentSparseSet.hasComponent(entity2, PositionComponent)).toBe(true);
|
||||
expect(componentSparseSet.hasComponent(entity2, HealthComponent)).toBe(true);
|
||||
expect(componentSparseSet.hasComponent(entity2, VelocityComponent)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理不存在的实体', () => {
|
||||
expect(componentSparseSet.hasComponent(entity3, PositionComponent)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('位掩码操作', () => {
|
||||
beforeEach(() => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
});
|
||||
|
||||
it('应该能获取实体的组件位掩码', () => {
|
||||
const mask1 = componentSparseSet.getEntityMask(entity1);
|
||||
const mask2 = componentSparseSet.getEntityMask(entity2);
|
||||
|
||||
expect(mask1).toBeDefined();
|
||||
expect(mask2).toBeDefined();
|
||||
expect(mask1).not.toEqual(mask2);
|
||||
});
|
||||
|
||||
it('应该处理不存在的实体', () => {
|
||||
const mask = componentSparseSet.getEntityMask(entity3);
|
||||
expect(mask).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('遍历操作', () => {
|
||||
beforeEach(() => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
});
|
||||
|
||||
it('应该能遍历所有实体', () => {
|
||||
const entities: Entity[] = [];
|
||||
const masks: any[] = [];
|
||||
const indices: number[] = [];
|
||||
|
||||
componentSparseSet.forEach((entity, mask, index) => {
|
||||
entities.push(entity);
|
||||
masks.push(mask);
|
||||
indices.push(index);
|
||||
});
|
||||
|
||||
expect(entities.length).toBe(2);
|
||||
expect(masks.length).toBe(2);
|
||||
expect(indices).toEqual([0, 1]);
|
||||
expect(entities).toContain(entity1);
|
||||
expect(entities).toContain(entity2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具方法', () => {
|
||||
it('应该能检查空状态', () => {
|
||||
expect(componentSparseSet.isEmpty).toBe(true);
|
||||
|
||||
componentSparseSet.addEntity(entity1);
|
||||
expect(componentSparseSet.isEmpty).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能清空数据', () => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
|
||||
componentSparseSet.clear();
|
||||
|
||||
expect(componentSparseSet.size).toBe(0);
|
||||
expect(componentSparseSet.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('应该提供内存统计', () => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
|
||||
const stats = componentSparseSet.getMemoryStats();
|
||||
|
||||
expect(stats.entitiesMemory).toBeGreaterThan(0);
|
||||
expect(stats.masksMemory).toBeGreaterThan(0);
|
||||
expect(stats.mappingsMemory).toBeGreaterThan(0);
|
||||
expect(stats.totalMemory).toBe(
|
||||
stats.entitiesMemory + stats.masksMemory + stats.mappingsMemory
|
||||
);
|
||||
});
|
||||
|
||||
it('应该能验证数据结构完整性', () => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
componentSparseSet.addEntity(entity2);
|
||||
componentSparseSet.removeEntity(entity1);
|
||||
|
||||
expect(componentSparseSet.validate()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('应该处理空查询', () => {
|
||||
componentSparseSet.addEntity(entity1);
|
||||
|
||||
const andResult = componentSparseSet.queryMultipleAnd([]);
|
||||
const orResult = componentSparseSet.queryMultipleOr([]);
|
||||
|
||||
expect(andResult.size).toBe(0);
|
||||
expect(orResult.size).toBe(0);
|
||||
});
|
||||
|
||||
it('应该处理未注册的组件类型', () => {
|
||||
class UnknownComponent extends Component {}
|
||||
|
||||
componentSparseSet.addEntity(entity1);
|
||||
|
||||
const entities = componentSparseSet.queryByComponent(UnknownComponent);
|
||||
expect(entities.size).toBe(0);
|
||||
});
|
||||
|
||||
it('应该正确处理实体组件变化', () => {
|
||||
// 添加实体
|
||||
componentSparseSet.addEntity(entity1);
|
||||
expect(componentSparseSet.hasComponent(entity1, PositionComponent)).toBe(true);
|
||||
|
||||
// 移除组件后重新添加实体
|
||||
entity1.removeComponentByType(PositionComponent);
|
||||
componentSparseSet.addEntity(entity1);
|
||||
|
||||
expect(componentSparseSet.hasComponent(entity1, PositionComponent)).toBe(false);
|
||||
expect(componentSparseSet.hasComponent(entity1, VelocityComponent)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
it('应该处理大量实体操作', () => {
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 创建大量实体
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const entity = new Entity(`entity${i}`, i);
|
||||
entity.addComponent(new PositionComponent(i, i));
|
||||
|
||||
if (i % 2 === 0) {
|
||||
entity.addComponent(new VelocityComponent(1, 1));
|
||||
}
|
||||
if (i % 3 === 0) {
|
||||
entity.addComponent(new HealthComponent(100, 100));
|
||||
}
|
||||
|
||||
entities.push(entity);
|
||||
componentSparseSet.addEntity(entity);
|
||||
}
|
||||
|
||||
expect(componentSparseSet.size).toBe(1000);
|
||||
|
||||
// 查询性能测试
|
||||
const positionEntities = componentSparseSet.queryByComponent(PositionComponent);
|
||||
expect(positionEntities.size).toBe(1000);
|
||||
|
||||
const velocityEntities = componentSparseSet.queryByComponent(VelocityComponent);
|
||||
expect(velocityEntities.size).toBe(500);
|
||||
|
||||
const healthEntities = componentSparseSet.queryByComponent(HealthComponent);
|
||||
expect(healthEntities.size).toBeGreaterThan(300);
|
||||
|
||||
// AND查询
|
||||
const posVelEntities = componentSparseSet.queryMultipleAnd([PositionComponent, VelocityComponent]);
|
||||
expect(posVelEntities.size).toBe(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
241
packages/core/tests/ECS/Utils/SparseSet.test.ts
Normal file
241
packages/core/tests/ECS/Utils/SparseSet.test.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { SparseSet } from '../../../src/ECS/Utils/SparseSet';
|
||||
|
||||
describe('SparseSet', () => {
|
||||
let sparseSet: SparseSet<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
sparseSet = new SparseSet<number>();
|
||||
});
|
||||
|
||||
describe('基本操作', () => {
|
||||
it('应该能添加元素', () => {
|
||||
expect(sparseSet.add(1)).toBe(true);
|
||||
expect(sparseSet.add(2)).toBe(true);
|
||||
expect(sparseSet.size).toBe(2);
|
||||
});
|
||||
|
||||
it('应该防止重复添加', () => {
|
||||
expect(sparseSet.add(1)).toBe(true);
|
||||
expect(sparseSet.add(1)).toBe(false);
|
||||
expect(sparseSet.size).toBe(1);
|
||||
});
|
||||
|
||||
it('应该能移除元素', () => {
|
||||
sparseSet.add(1);
|
||||
sparseSet.add(2);
|
||||
|
||||
expect(sparseSet.remove(1)).toBe(true);
|
||||
expect(sparseSet.size).toBe(1);
|
||||
expect(sparseSet.has(1)).toBe(false);
|
||||
expect(sparseSet.has(2)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理移除不存在的元素', () => {
|
||||
expect(sparseSet.remove(99)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能检查元素存在性', () => {
|
||||
sparseSet.add(42);
|
||||
|
||||
expect(sparseSet.has(42)).toBe(true);
|
||||
expect(sparseSet.has(99)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('索引操作', () => {
|
||||
it('应该返回正确的索引', () => {
|
||||
sparseSet.add(10);
|
||||
sparseSet.add(20);
|
||||
sparseSet.add(30);
|
||||
|
||||
expect(sparseSet.getIndex(10)).toBe(0);
|
||||
expect(sparseSet.getIndex(20)).toBe(1);
|
||||
expect(sparseSet.getIndex(30)).toBe(2);
|
||||
});
|
||||
|
||||
it('应该能根据索引获取元素', () => {
|
||||
sparseSet.add(100);
|
||||
sparseSet.add(200);
|
||||
|
||||
expect(sparseSet.getByIndex(0)).toBe(100);
|
||||
expect(sparseSet.getByIndex(1)).toBe(200);
|
||||
expect(sparseSet.getByIndex(999)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('移除中间元素后应该保持紧凑性', () => {
|
||||
sparseSet.add(1);
|
||||
sparseSet.add(2);
|
||||
sparseSet.add(3);
|
||||
|
||||
// 移除中间元素
|
||||
sparseSet.remove(2);
|
||||
|
||||
// 最后一个元素应该移动到中间
|
||||
expect(sparseSet.getByIndex(0)).toBe(1);
|
||||
expect(sparseSet.getByIndex(1)).toBe(3);
|
||||
expect(sparseSet.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('遍历操作', () => {
|
||||
beforeEach(() => {
|
||||
sparseSet.add(10);
|
||||
sparseSet.add(20);
|
||||
sparseSet.add(30);
|
||||
});
|
||||
|
||||
it('应该能正确遍历', () => {
|
||||
const items: number[] = [];
|
||||
const indices: number[] = [];
|
||||
|
||||
sparseSet.forEach((item, index) => {
|
||||
items.push(item);
|
||||
indices.push(index);
|
||||
});
|
||||
|
||||
expect(items).toEqual([10, 20, 30]);
|
||||
expect(indices).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
it('应该能映射元素', () => {
|
||||
const doubled = sparseSet.map(x => x * 2);
|
||||
expect(doubled).toEqual([20, 40, 60]);
|
||||
});
|
||||
|
||||
it('应该能过滤元素', () => {
|
||||
const filtered = sparseSet.filter(x => x > 15);
|
||||
expect(filtered).toEqual([20, 30]);
|
||||
});
|
||||
|
||||
it('应该能查找元素', () => {
|
||||
const found = sparseSet.find(x => x > 15);
|
||||
expect(found).toBe(20);
|
||||
|
||||
const notFound = sparseSet.find(x => x > 100);
|
||||
expect(notFound).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该能检查存在性', () => {
|
||||
expect(sparseSet.some(x => x > 25)).toBe(true);
|
||||
expect(sparseSet.some(x => x > 100)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能检查全部条件', () => {
|
||||
expect(sparseSet.every(x => x > 0)).toBe(true);
|
||||
expect(sparseSet.every(x => x > 15)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('数据获取', () => {
|
||||
beforeEach(() => {
|
||||
sparseSet.add(1);
|
||||
sparseSet.add(2);
|
||||
sparseSet.add(3);
|
||||
});
|
||||
|
||||
it('应该返回只读数组副本', () => {
|
||||
const array = sparseSet.getDenseArray();
|
||||
expect(array).toEqual([1, 2, 3]);
|
||||
|
||||
// 尝试修改应该不影响原数据
|
||||
expect(() => {
|
||||
(array as any).push(4);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(sparseSet.size).toBe(3);
|
||||
});
|
||||
|
||||
it('应该能转换为数组', () => {
|
||||
const array = sparseSet.toArray();
|
||||
expect(array).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('应该能转换为Set', () => {
|
||||
const set = sparseSet.toSet();
|
||||
expect(set).toEqual(new Set([1, 2, 3]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具方法', () => {
|
||||
it('应该能检查空状态', () => {
|
||||
expect(sparseSet.isEmpty).toBe(true);
|
||||
|
||||
sparseSet.add(1);
|
||||
expect(sparseSet.isEmpty).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能清空数据', () => {
|
||||
sparseSet.add(1);
|
||||
sparseSet.add(2);
|
||||
|
||||
sparseSet.clear();
|
||||
|
||||
expect(sparseSet.size).toBe(0);
|
||||
expect(sparseSet.isEmpty).toBe(true);
|
||||
expect(sparseSet.has(1)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该提供内存统计', () => {
|
||||
sparseSet.add(1);
|
||||
sparseSet.add(2);
|
||||
|
||||
const stats = sparseSet.getMemoryStats();
|
||||
|
||||
expect(stats.denseArraySize).toBeGreaterThan(0);
|
||||
expect(stats.sparseMapSize).toBeGreaterThan(0);
|
||||
expect(stats.totalMemory).toBe(stats.denseArraySize + stats.sparseMapSize);
|
||||
});
|
||||
|
||||
it('应该能验证数据结构完整性', () => {
|
||||
sparseSet.add(1);
|
||||
sparseSet.add(2);
|
||||
sparseSet.add(3);
|
||||
sparseSet.remove(2);
|
||||
|
||||
expect(sparseSet.validate()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能场景', () => {
|
||||
it('应该处理大量数据操作', () => {
|
||||
const items = Array.from({ length: 1000 }, (_, i) => i);
|
||||
|
||||
// 批量添加
|
||||
for (const item of items) {
|
||||
sparseSet.add(item);
|
||||
}
|
||||
expect(sparseSet.size).toBe(1000);
|
||||
|
||||
// 批量移除偶数
|
||||
for (let i = 0; i < 1000; i += 2) {
|
||||
sparseSet.remove(i);
|
||||
}
|
||||
expect(sparseSet.size).toBe(500);
|
||||
|
||||
// 验证只剩奇数
|
||||
const remaining = sparseSet.toArray().sort((a, b) => a - b);
|
||||
for (let i = 0; i < remaining.length; i++) {
|
||||
expect(remaining[i] % 2).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('应该保持O(1)访问性能', () => {
|
||||
// 添加大量元素
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
sparseSet.add(i);
|
||||
}
|
||||
|
||||
// 随机访问应该很快
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const randomItem = Math.floor(Math.random() * 1000);
|
||||
sparseSet.has(randomItem);
|
||||
sparseSet.getIndex(randomItem);
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// 应该在很短时间内完成
|
||||
expect(duration).toBeLessThan(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user