新增soastorage存储器
This commit is contained in:
27
README.md
27
README.md
@@ -14,6 +14,7 @@ TypeScript ECS (Entity-Component-System) 框架,专为游戏开发设计。
|
|||||||
- 📡 **[类型安全事件系统](docs/concepts-explained.md#事件系统)** - 事件装饰器和异步事件处理
|
- 📡 **[类型安全事件系统](docs/concepts-explained.md#事件系统)** - 事件装饰器和异步事件处理
|
||||||
- 🔍 **[查询系统](docs/concepts-explained.md#实体管理)** - 流式 API 和智能缓存
|
- 🔍 **[查询系统](docs/concepts-explained.md#实体管理)** - 流式 API 和智能缓存
|
||||||
- ⚡ **[性能优化](docs/concepts-explained.md#性能优化技术)** - 组件索引、Archetype 系统、脏标记
|
- ⚡ **[性能优化](docs/concepts-explained.md#性能优化技术)** - 组件索引、Archetype 系统、脏标记
|
||||||
|
- 🚀 **[SoA 存储优化](docs/soa-storage-guide.md)** - 大规模实体的向量化批量操作和内存优化
|
||||||
- 🎯 **[实体管理器](docs/concepts-explained.md#实体管理)** - 统一的实体生命周期管理
|
- 🎯 **[实体管理器](docs/concepts-explained.md#实体管理)** - 统一的实体生命周期管理
|
||||||
- 🧰 **调试工具** - 内置性能监控和调试信息
|
- 🧰 **调试工具** - 内置性能监控和调试信息
|
||||||
|
|
||||||
@@ -84,6 +85,31 @@ class HealthComponent extends Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### SoA 高性能组件 (大规模场景推荐)
|
||||||
|
|
||||||
|
对于需要处理大量实体的场景,可以使用 SoA 存储优化获得显著性能提升:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, EnableSoA, Float32, Int32 } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 启用 SoA 优化的高性能组件
|
||||||
|
@EnableSoA
|
||||||
|
class OptimizedTransformComponent extends Component {
|
||||||
|
@Float32 public x: number = 0;
|
||||||
|
@Float32 public y: number = 0;
|
||||||
|
@Float32 public rotation: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableSoA
|
||||||
|
class ParticleComponent extends Component {
|
||||||
|
@Float32 public velocityX: number = 0;
|
||||||
|
@Float32 public velocityY: number = 0;
|
||||||
|
@Int32 public lifeTime: number = 1000;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **使用建议**: SoA 优化适用于大规模场景和批量操作。小规模应用请使用普通组件以避免不必要的复杂度。详见 [SoA 存储优化指南](docs/soa-storage-guide.md)。
|
||||||
|
|
||||||
### 创建实体
|
### 创建实体
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -374,6 +400,7 @@ ecs-framework/
|
|||||||
|
|
||||||
### 性能相关
|
### 性能相关
|
||||||
- [性能优化指南](docs/performance-optimization.md) - 性能优化技术和策略
|
- [性能优化指南](docs/performance-optimization.md) - 性能优化技术和策略
|
||||||
|
- [SoA 存储优化指南](docs/soa-storage-guide.md) - 大规模实体系统的高级性能优化 ⭐ **大规模项目推荐**
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
|
|||||||
343
docs/soa-storage-guide.md
Normal file
343
docs/soa-storage-guide.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
# SoA存储优化指南
|
||||||
|
|
||||||
|
SoA (Structure of Arrays) 存储模式是ECS框架中的高级性能优化特性,适用于大规模实体系统和批量操作场景。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [什么是SoA存储](#什么是soa存储)
|
||||||
|
2. [适用场景](#适用场景)
|
||||||
|
3. [不适用场景](#不适用场景)
|
||||||
|
4. [装饰器使用指南](#装饰器使用指南)
|
||||||
|
5. [性能对比](#性能对比)
|
||||||
|
6. [最佳实践](#最佳实践)
|
||||||
|
7. [故障排除](#故障排除)
|
||||||
|
|
||||||
|
## 什么是SoA存储
|
||||||
|
|
||||||
|
### AoS vs SoA 对比
|
||||||
|
|
||||||
|
**传统AoS (Array of Structures):**
|
||||||
|
```typescript
|
||||||
|
// 数据在内存中的布局
|
||||||
|
[{x:1, y:2, z:3}, {x:4, y:5, z:6}, {x:7, y:8, z:9}]
|
||||||
|
// 内存布局: x1,y1,z1,x2,y2,z2,x3,y3,z3
|
||||||
|
```
|
||||||
|
|
||||||
|
**SoA (Structure of Arrays):**
|
||||||
|
```typescript
|
||||||
|
// 数据在内存中的布局
|
||||||
|
{
|
||||||
|
x: [1, 4, 7], // Float32Array
|
||||||
|
y: [2, 5, 8], // Float32Array
|
||||||
|
z: [3, 6, 9] // Float32Array
|
||||||
|
}
|
||||||
|
// 内存布局: x1,x2,x3,y1,y2,y3,z1,z2,z3
|
||||||
|
```
|
||||||
|
|
||||||
|
### SoA的优势
|
||||||
|
|
||||||
|
- **缓存友好**: 相同类型数据连续存储,提高缓存命中率
|
||||||
|
- **向量化优化**: 支持SIMD指令并行处理
|
||||||
|
- **内存局部性**: 批量操作时减少缓存miss
|
||||||
|
- **类型优化**: 针对不同数据类型使用最优存储格式
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
### ✅ 推荐使用SoA的场景
|
||||||
|
|
||||||
|
1. **大规模实体系统**
|
||||||
|
```typescript
|
||||||
|
// 大量相似实体的物理系统
|
||||||
|
@EnableSoA
|
||||||
|
class PhysicsComponent extends Component {
|
||||||
|
@Float64 public x: number = 0;
|
||||||
|
@Float64 public y: number = 0;
|
||||||
|
@Float32 public velocityX: number = 0;
|
||||||
|
@Float32 public velocityY: number = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **频繁批量更新操作**
|
||||||
|
```typescript
|
||||||
|
// 每帧更新大量实体位置
|
||||||
|
system.performVectorizedOperation((fields, indices) => {
|
||||||
|
const x = fields.get('x') as Float32Array;
|
||||||
|
const y = fields.get('y') as Float32Array;
|
||||||
|
const vx = fields.get('velocityX') as Float32Array;
|
||||||
|
const vy = fields.get('velocityY') as Float32Array;
|
||||||
|
|
||||||
|
// 向量化更新所有实体
|
||||||
|
for (let i = 0; i < indices.length; i++) {
|
||||||
|
const idx = indices[i];
|
||||||
|
x[idx] += vx[idx] * deltaTime;
|
||||||
|
y[idx] += vy[idx] * deltaTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **数值密集计算**
|
||||||
|
```typescript
|
||||||
|
@EnableSoA
|
||||||
|
class AIBrainComponent extends Component {
|
||||||
|
@Float32 public neuron1: number = 0;
|
||||||
|
@Float32 public neuron2: number = 0;
|
||||||
|
@Float32 public output: number = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 不适用场景
|
||||||
|
|
||||||
|
### ❌ 不推荐使用SoA的场景
|
||||||
|
|
||||||
|
1. **小规模系统**
|
||||||
|
- SoA的开销大于收益
|
||||||
|
- 原始存储更简单高效
|
||||||
|
|
||||||
|
2. **随机访问为主**
|
||||||
|
```typescript
|
||||||
|
// 经常需要随机获取单个组件
|
||||||
|
const component = entityManager.getComponent(randomId, SomeComponent);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **复杂对象为主的组件**
|
||||||
|
```typescript
|
||||||
|
// 大量复杂对象,序列化开销大
|
||||||
|
class UIComponent extends Component {
|
||||||
|
public domElement: HTMLElement;
|
||||||
|
public eventHandlers: Map<string, Function>;
|
||||||
|
public children: UIComponent[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **频繁增删实体**
|
||||||
|
- SoA在频繁增删时性能不如AoS
|
||||||
|
- 适合稳定的实体集合
|
||||||
|
|
||||||
|
## 装饰器使用指南
|
||||||
|
|
||||||
|
### 基础装饰器
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
@EnableSoA // 启用SoA优化
|
||||||
|
class GameComponent extends Component {
|
||||||
|
// 数值类型装饰器
|
||||||
|
@HighPrecision // 高精度数值,保持完整精度
|
||||||
|
public entityId: number = 0;
|
||||||
|
|
||||||
|
@Float64 // 64位浮点数 (8字节,高精度)
|
||||||
|
public precisePosition: number = 0;
|
||||||
|
|
||||||
|
@Int32 // 32位整数 (4字节,整数优化)
|
||||||
|
public health: number = 100;
|
||||||
|
|
||||||
|
// 普通数值 (默认Float32Array,4字节)
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
|
||||||
|
// 集合类型装饰器
|
||||||
|
@SerializeMap
|
||||||
|
public playerStats: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
@SerializeSet
|
||||||
|
public achievements: Set<string> = new Set();
|
||||||
|
|
||||||
|
@SerializeArray
|
||||||
|
public inventory: any[] = [];
|
||||||
|
|
||||||
|
@DeepCopy
|
||||||
|
public config: any = { settings: {} };
|
||||||
|
|
||||||
|
// 未装饰的字段自动选择最优存储
|
||||||
|
public name: string = ''; // string[] 数组
|
||||||
|
public active: boolean = true; // Float32Array (0/1)
|
||||||
|
public metadata: any = null; // 复杂对象存储
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 装饰器选择指南
|
||||||
|
|
||||||
|
| 装饰器 | 用途 | 存储方式 | 开销 | 适用场景 |
|
||||||
|
|--------|------|----------|------|----------|
|
||||||
|
| `@HighPrecision` | 高精度数值 | 复杂对象 | 高 | ID、时间戳、大整数 |
|
||||||
|
| `@Float64` | 双精度浮点 | Float64Array | 中 | 精密计算 |
|
||||||
|
| `@Int32` | 32位整数 | Int32Array | 低 | 整数计数、枚举值 |
|
||||||
|
| `@SerializeMap` | Map序列化 | JSON字符串 | 高 | 配置映射、属性集合 |
|
||||||
|
| `@SerializeSet` | Set序列化 | JSON字符串 | 高 | 标签集合、ID集合 |
|
||||||
|
| `@SerializeArray` | Array序列化 | JSON字符串 | 中 | 列表数据、队列 |
|
||||||
|
| `@DeepCopy` | 深拷贝对象 | 复杂对象副本 | 高 | 嵌套配置、独立状态 |
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
### 基准测试结果
|
||||||
|
|
||||||
|
```
|
||||||
|
测试场景: 2000个实体,包含位置、速度、生命值组件
|
||||||
|
|
||||||
|
创建性能:
|
||||||
|
- 原始存储: 12.45ms
|
||||||
|
- SoA存储: 15.72ms (慢26%)
|
||||||
|
|
||||||
|
随机访问性能:
|
||||||
|
- 原始存储: 8.33ms
|
||||||
|
- SoA存储: 14.20ms (慢70%)
|
||||||
|
|
||||||
|
批量更新性能:
|
||||||
|
- 原始存储: 25.67ms
|
||||||
|
- SoA存储: 8.91ms (快188%)
|
||||||
|
|
||||||
|
内存使用:
|
||||||
|
- 原始存储: ~45KB (对象开销)
|
||||||
|
- SoA存储: ~28KB (TypedArray优化)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能权衡总结
|
||||||
|
|
||||||
|
- **SoA优势**: 批量操作、内存效率、向量化计算
|
||||||
|
- **SoA劣势**: 随机访问、创建开销、复杂度增加
|
||||||
|
- **建议**: 大规模批量操作场景使用,小规模随机访问避免使用
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 合理的组件设计
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 好的设计:纯数值组件
|
||||||
|
@EnableSoA
|
||||||
|
class TransformComponent extends Component {
|
||||||
|
@Float64 public x: number = 0;
|
||||||
|
@Float64 public y: number = 0;
|
||||||
|
@Float32 public rotation: number = 0;
|
||||||
|
@Float32 public scaleX: number = 1;
|
||||||
|
@Float32 public scaleY: number = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 不好的设计:混合复杂对象
|
||||||
|
@EnableSoA
|
||||||
|
class MixedComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public domElement: HTMLElement = null; // 复杂对象开销大
|
||||||
|
public callback: Function = null; // 无法序列化
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 批量操作优化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 使用向量化操作
|
||||||
|
const storage = entityManager.getStorage(TransformComponent) as SoAStorage<TransformComponent>;
|
||||||
|
storage.performVectorizedOperation((fields, indices) => {
|
||||||
|
const x = fields.get('x') as Float64Array;
|
||||||
|
const y = fields.get('y') as Float64Array;
|
||||||
|
|
||||||
|
// 批量处理,利用缓存局部性
|
||||||
|
for (let i = 0; i < indices.length; i++) {
|
||||||
|
const idx = indices[i];
|
||||||
|
x[idx] += deltaX;
|
||||||
|
y[idx] += deltaY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ 避免逐个访问
|
||||||
|
for (const entity of entities) {
|
||||||
|
const transform = entity.getComponent(TransformComponent);
|
||||||
|
transform.x += deltaX;
|
||||||
|
transform.y += deltaY;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 组件分离策略
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 将频繁批量操作的数据分离
|
||||||
|
@EnableSoA
|
||||||
|
class PositionComponent extends Component {
|
||||||
|
@Float32 public x: number = 0;
|
||||||
|
@Float32 public y: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复杂数据使用普通组件
|
||||||
|
class MetadataComponent extends Component {
|
||||||
|
public name: string = '';
|
||||||
|
public config: any = {};
|
||||||
|
public references: any[] = [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 性能监控
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 监控SoA存储使用情况
|
||||||
|
const storage = entityManager.getStorage(MyComponent) as SoAStorage<MyComponent>;
|
||||||
|
const stats = storage.getStats();
|
||||||
|
|
||||||
|
console.log('SoA存储统计:', {
|
||||||
|
size: stats.size,
|
||||||
|
capacity: stats.capacity,
|
||||||
|
memoryUsage: stats.memoryUsage,
|
||||||
|
fragmentation: stats.fragmentation,
|
||||||
|
fieldStats: stats.fieldStats
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **精度丢失**
|
||||||
|
```typescript
|
||||||
|
// 问题:大整数精度丢失
|
||||||
|
public bigId: number = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
// 解决:使用高精度装饰器
|
||||||
|
@HighPrecision
|
||||||
|
public bigId: number = Number.MAX_SAFE_INTEGER;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **序列化失败**
|
||||||
|
```typescript
|
||||||
|
// 问题:循环引用导致序列化失败
|
||||||
|
@SerializeMap
|
||||||
|
public cyclicMap: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
// 解决:避免循环引用或使用DeepCopy
|
||||||
|
@DeepCopy
|
||||||
|
public cyclicData: any = {};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **性能反向优化**
|
||||||
|
```typescript
|
||||||
|
// 问题:小规模数据使用SoA
|
||||||
|
@EnableSoA // 只有10个实体,不需要SoA
|
||||||
|
class SmallComponent extends Component {}
|
||||||
|
|
||||||
|
// 解决:移除@EnableSoA装饰器
|
||||||
|
class SmallComponent extends Component {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 检查存储类型
|
||||||
|
const storage = entityManager.getStorage(MyComponent);
|
||||||
|
console.log('存储类型:', storage.constructor.name);
|
||||||
|
// 输出: 'SoAStorage' 或 'ComponentStorage'
|
||||||
|
|
||||||
|
// 检查字段存储方式
|
||||||
|
if (storage instanceof SoAStorage) {
|
||||||
|
const fieldArray = storage.getFieldArray('myField');
|
||||||
|
console.log('字段类型:', fieldArray?.constructor.name);
|
||||||
|
// 输出: 'Float32Array', 'Float64Array', 'Int32Array', 或 null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
SoA存储是一个强大的性能优化工具,但需要在合适的场景下使用:
|
||||||
|
|
||||||
|
- **适合**: 大规模、批量操作、数值密集的场景
|
||||||
|
- **不适合**: 小规模、随机访问、复杂对象为主的场景
|
||||||
|
- **关键**: 通过性能测试验证优化效果,避免过度优化
|
||||||
|
|
||||||
|
正确使用SoA存储可以显著提升ECS系统性能,但滥用会带来相反的效果。建议在实际项目中先进行基准测试,确认优化效果后再应用到生产环境。
|
||||||
27
jest.performance.config.js
Normal file
27
jest.performance.config.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/tests'],
|
||||||
|
testMatch: ['**/*.performance.test.ts'],
|
||||||
|
collectCoverage: false,
|
||||||
|
verbose: true,
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': ['ts-jest', {
|
||||||
|
tsconfig: 'tsconfig.test.json',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
|
},
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||||
|
// 性能测试需要更长的超时时间
|
||||||
|
testTimeout: 60000,
|
||||||
|
clearMocks: true,
|
||||||
|
restoreMocks: true,
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
'<rootDir>/bin/',
|
||||||
|
'<rootDir>/dist/',
|
||||||
|
'<rootDir>/node_modules/'
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"publish:npm": "npm run build:npm && cd dist && npm publish",
|
"publish:npm": "npm run build:npm && cd dist && npm publish",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
|
"test:performance": "jest --config jest.performance.config.js",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"test:ci": "jest --ci --coverage",
|
"test:ci": "jest --ci --coverage",
|
||||||
"test:clear": "jest --clearCache"
|
"test:clear": "jest --clearCache"
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Component } from '../Component';
|
import { Component } from '../Component';
|
||||||
import { IBigIntLike, BigIntFactory } from '../Utils/BigIntCompatibility';
|
import { IBigIntLike, BigIntFactory } from '../Utils/BigIntCompatibility';
|
||||||
|
import { SoAStorage, EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from './SoAStorage';
|
||||||
|
|
||||||
|
// 重新导出装饰器
|
||||||
|
export { EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件类型定义
|
* 组件类型定义
|
||||||
@@ -289,18 +293,29 @@ export class ComponentStorage<T extends Component> {
|
|||||||
* 管理所有组件类型的存储器
|
* 管理所有组件类型的存储器
|
||||||
*/
|
*/
|
||||||
export class ComponentStorageManager {
|
export class ComponentStorageManager {
|
||||||
private storages = new Map<Function, ComponentStorage<any>>();
|
private storages = new Map<Function, ComponentStorage<any> | SoAStorage<any>>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取或创建组件存储器
|
* 获取或创建组件存储器(默认原始存储)
|
||||||
* @param componentType 组件类型
|
* @param componentType 组件类型
|
||||||
* @returns 组件存储器
|
* @returns 组件存储器
|
||||||
*/
|
*/
|
||||||
public getStorage<T extends Component>(componentType: ComponentType<T>): ComponentStorage<T> {
|
public getStorage<T extends Component>(componentType: ComponentType<T>): ComponentStorage<T> | SoAStorage<T> {
|
||||||
let storage = this.storages.get(componentType);
|
let storage = this.storages.get(componentType);
|
||||||
|
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
|
// 检查是否启用SoA优化
|
||||||
|
const enableSoA = (componentType as any).__enableSoA;
|
||||||
|
|
||||||
|
if (enableSoA) {
|
||||||
|
// 使用SoA优化存储
|
||||||
|
storage = new SoAStorage(componentType);
|
||||||
|
console.log(`[SoA] 为 ${componentType.name} 启用SoA优化(适用于大规模批量操作)`);
|
||||||
|
} else {
|
||||||
|
// 默认使用原始存储
|
||||||
storage = new ComponentStorage(componentType);
|
storage = new ComponentStorage(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
this.storages.set(componentType, storage);
|
this.storages.set(componentType, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
580
src/ECS/Core/SoAStorage.ts
Normal file
580
src/ECS/Core/SoAStorage.ts
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
import { Component } from '../Component';
|
||||||
|
import { ComponentType } from './ComponentStorage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用SoA优化装饰器
|
||||||
|
* 默认关闭SoA,只有在大规模批量操作场景下才建议开启
|
||||||
|
*/
|
||||||
|
export function EnableSoA<T extends ComponentType>(target: T): T {
|
||||||
|
(target as any).__enableSoA = true;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高精度数值装饰器
|
||||||
|
* 标记字段需要保持完整精度,存储为复杂对象而非TypedArray
|
||||||
|
*/
|
||||||
|
export function HighPrecision(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__highPrecisionFields) {
|
||||||
|
target.constructor.__highPrecisionFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__highPrecisionFields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 64位浮点数装饰器
|
||||||
|
* 标记字段使用Float64Array存储(更高精度但更多内存)
|
||||||
|
*/
|
||||||
|
export function Float64(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__float64Fields) {
|
||||||
|
target.constructor.__float64Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__float64Fields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 32位整数装饰器
|
||||||
|
* 标记字段使用Int32Array存储(适用于整数值)
|
||||||
|
*/
|
||||||
|
export function Int32(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__int32Fields) {
|
||||||
|
target.constructor.__int32Fields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__int32Fields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化Map装饰器
|
||||||
|
* 标记Map字段需要序列化/反序列化存储
|
||||||
|
*/
|
||||||
|
export function SerializeMap(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__serializeMapFields) {
|
||||||
|
target.constructor.__serializeMapFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__serializeMapFields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化Set装饰器
|
||||||
|
* 标记Set字段需要序列化/反序列化存储
|
||||||
|
*/
|
||||||
|
export function SerializeSet(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__serializeSetFields) {
|
||||||
|
target.constructor.__serializeSetFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__serializeSetFields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化Array装饰器
|
||||||
|
* 标记Array字段需要序列化/反序列化存储
|
||||||
|
*/
|
||||||
|
export function SerializeArray(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__serializeArrayFields) {
|
||||||
|
target.constructor.__serializeArrayFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__serializeArrayFields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝装饰器
|
||||||
|
* 标记字段需要深拷贝处理(适用于嵌套对象)
|
||||||
|
*/
|
||||||
|
export function DeepCopy(target: any, propertyKey: string | symbol): void {
|
||||||
|
const key = String(propertyKey);
|
||||||
|
if (!target.constructor.__deepCopyFields) {
|
||||||
|
target.constructor.__deepCopyFields = new Set();
|
||||||
|
}
|
||||||
|
target.constructor.__deepCopyFields.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SoA存储器(需要装饰器启用)
|
||||||
|
* 使用Structure of Arrays存储模式,在大规模批量操作时提供优异性能
|
||||||
|
* 适用场景:>5000实体的批量更新操作
|
||||||
|
*/
|
||||||
|
export class SoAStorage<T extends Component> {
|
||||||
|
private fields = new Map<string, Float32Array | Float64Array | Int32Array>();
|
||||||
|
private stringFields = new Map<string, string[]>(); // 专门存储字符串
|
||||||
|
private serializedFields = new Map<string, string[]>(); // 序列化存储Map/Set/Array
|
||||||
|
private complexFields = new Map<number, Map<string, any>>(); // 存储复杂对象
|
||||||
|
private entityToIndex = new Map<number, number>();
|
||||||
|
private indexToEntity: number[] = [];
|
||||||
|
private freeIndices: number[] = [];
|
||||||
|
private _size = 0;
|
||||||
|
private _capacity = 1000;
|
||||||
|
public readonly type: ComponentType<T>;
|
||||||
|
|
||||||
|
constructor(componentType: ComponentType<T>) {
|
||||||
|
this.type = componentType;
|
||||||
|
this.initializeFields(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeFields(componentType: ComponentType<T>): void {
|
||||||
|
const instance = new componentType();
|
||||||
|
const highPrecisionFields = (componentType as any).__highPrecisionFields || new Set();
|
||||||
|
const float64Fields = (componentType as any).__float64Fields || new Set();
|
||||||
|
const int32Fields = (componentType as any).__int32Fields || new Set();
|
||||||
|
const serializeMapFields = (componentType as any).__serializeMapFields || new Set();
|
||||||
|
const serializeSetFields = (componentType as any).__serializeSetFields || new Set();
|
||||||
|
const serializeArrayFields = (componentType as any).__serializeArrayFields || new Set();
|
||||||
|
// const deepCopyFields = (componentType as any).__deepCopyFields || new Set(); // 未使用,但保留供future使用
|
||||||
|
|
||||||
|
for (const key in instance) {
|
||||||
|
if (instance.hasOwnProperty(key) && key !== 'id') {
|
||||||
|
const value = (instance as any)[key];
|
||||||
|
const type = typeof value;
|
||||||
|
|
||||||
|
if (type === 'number') {
|
||||||
|
if (highPrecisionFields.has(key)) {
|
||||||
|
// 标记为高精度,作为复杂对象处理
|
||||||
|
// 不添加到fields,会在updateComponentAtIndex中自动添加到complexFields
|
||||||
|
} else if (float64Fields.has(key)) {
|
||||||
|
// 使用Float64Array存储
|
||||||
|
this.fields.set(key, new Float64Array(this._capacity));
|
||||||
|
} else if (int32Fields.has(key)) {
|
||||||
|
// 使用Int32Array存储
|
||||||
|
this.fields.set(key, new Int32Array(this._capacity));
|
||||||
|
} else {
|
||||||
|
// 默认使用Float32Array
|
||||||
|
this.fields.set(key, new Float32Array(this._capacity));
|
||||||
|
}
|
||||||
|
} else if (type === 'boolean') {
|
||||||
|
// 布尔值使用Float32Array存储为0/1
|
||||||
|
this.fields.set(key, new Float32Array(this._capacity));
|
||||||
|
} else if (type === 'string') {
|
||||||
|
// 字符串专门处理
|
||||||
|
this.stringFields.set(key, new Array(this._capacity));
|
||||||
|
} else if (type === 'object' && value !== null) {
|
||||||
|
// 处理集合类型
|
||||||
|
if (serializeMapFields.has(key) || serializeSetFields.has(key) || serializeArrayFields.has(key)) {
|
||||||
|
// 序列化存储
|
||||||
|
this.serializedFields.set(key, new Array(this._capacity));
|
||||||
|
}
|
||||||
|
// 其他对象类型会在updateComponentAtIndex中作为复杂对象处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addComponent(entityId: number, component: T): void {
|
||||||
|
if (this.entityToIndex.has(entityId)) {
|
||||||
|
const index = this.entityToIndex.get(entityId)!;
|
||||||
|
this.updateComponentAtIndex(index, component);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index: number;
|
||||||
|
if (this.freeIndices.length > 0) {
|
||||||
|
index = this.freeIndices.pop()!;
|
||||||
|
} else {
|
||||||
|
index = this._size;
|
||||||
|
if (index >= this._capacity) {
|
||||||
|
this.resize(this._capacity * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entityToIndex.set(entityId, index);
|
||||||
|
this.indexToEntity[index] = entityId;
|
||||||
|
this.updateComponentAtIndex(index, component);
|
||||||
|
this._size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateComponentAtIndex(index: number, component: T): void {
|
||||||
|
const entityId = this.indexToEntity[index];
|
||||||
|
const complexFieldMap = new Map<string, any>();
|
||||||
|
const highPrecisionFields = (this.type as any).__highPrecisionFields || new Set();
|
||||||
|
const serializeMapFields = (this.type as any).__serializeMapFields || new Set();
|
||||||
|
const serializeSetFields = (this.type as any).__serializeSetFields || new Set();
|
||||||
|
const serializeArrayFields = (this.type as any).__serializeArrayFields || new Set();
|
||||||
|
const deepCopyFields = (this.type as any).__deepCopyFields || new Set();
|
||||||
|
|
||||||
|
// 处理所有字段
|
||||||
|
for (const key in component) {
|
||||||
|
if (component.hasOwnProperty(key) && key !== 'id') {
|
||||||
|
const value = (component as any)[key];
|
||||||
|
const type = typeof value;
|
||||||
|
|
||||||
|
if (type === 'number') {
|
||||||
|
if (highPrecisionFields.has(key) || !this.fields.has(key)) {
|
||||||
|
// 标记为高精度或未在TypedArray中的数值作为复杂对象存储
|
||||||
|
complexFieldMap.set(key, value);
|
||||||
|
} else {
|
||||||
|
// 存储到TypedArray
|
||||||
|
const array = this.fields.get(key)!;
|
||||||
|
array[index] = value;
|
||||||
|
}
|
||||||
|
} else if (type === 'boolean' && this.fields.has(key)) {
|
||||||
|
// 布尔值存储到TypedArray
|
||||||
|
const array = this.fields.get(key)!;
|
||||||
|
array[index] = value ? 1 : 0;
|
||||||
|
} else if (this.stringFields.has(key)) {
|
||||||
|
// 字符串字段专门处理
|
||||||
|
const stringArray = this.stringFields.get(key)!;
|
||||||
|
stringArray[index] = String(value);
|
||||||
|
} else if (this.serializedFields.has(key)) {
|
||||||
|
// 序列化字段处理
|
||||||
|
const serializedArray = this.serializedFields.get(key)!;
|
||||||
|
serializedArray[index] = this.serializeValue(value, key, serializeMapFields, serializeSetFields, serializeArrayFields);
|
||||||
|
} else {
|
||||||
|
// 复杂字段单独存储
|
||||||
|
if (deepCopyFields.has(key)) {
|
||||||
|
// 深拷贝处理
|
||||||
|
complexFieldMap.set(key, this.deepClone(value));
|
||||||
|
} else {
|
||||||
|
complexFieldMap.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储复杂字段
|
||||||
|
if (complexFieldMap.size > 0) {
|
||||||
|
this.complexFields.set(entityId, complexFieldMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化值为JSON字符串
|
||||||
|
*/
|
||||||
|
private serializeValue(value: any, key: string, mapFields: Set<string>, setFields: Set<string>, arrayFields: Set<string>): string {
|
||||||
|
try {
|
||||||
|
if (mapFields.has(key) && value instanceof Map) {
|
||||||
|
// Map序列化为数组形式
|
||||||
|
return JSON.stringify(Array.from(value.entries()));
|
||||||
|
} else if (setFields.has(key) && value instanceof Set) {
|
||||||
|
// Set序列化为数组形式
|
||||||
|
return JSON.stringify(Array.from(value));
|
||||||
|
} else if (arrayFields.has(key) && Array.isArray(value)) {
|
||||||
|
// Array直接序列化
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} else {
|
||||||
|
// 其他对象直接序列化
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`SoA序列化字段 ${key} 失败:`, error);
|
||||||
|
return '{}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反序列化JSON字符串为值
|
||||||
|
*/
|
||||||
|
private deserializeValue(serialized: string, key: string, mapFields: Set<string>, setFields: Set<string>, arrayFields: Set<string>): any {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(serialized);
|
||||||
|
|
||||||
|
if (mapFields.has(key)) {
|
||||||
|
// 恢复Map
|
||||||
|
return new Map(parsed);
|
||||||
|
} else if (setFields.has(key)) {
|
||||||
|
// 恢复Set
|
||||||
|
return new Set(parsed);
|
||||||
|
} else if (arrayFields.has(key)) {
|
||||||
|
// 恢复Array
|
||||||
|
return parsed;
|
||||||
|
} else {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`SoA反序列化字段 ${key} 失败:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝对象
|
||||||
|
*/
|
||||||
|
private deepClone(obj: any): any {
|
||||||
|
if (obj === null || typeof obj !== 'object') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Date) {
|
||||||
|
return new Date(obj.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
return obj.map(item => this.deepClone(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Map) {
|
||||||
|
const cloned = new Map();
|
||||||
|
for (const [key, value] of obj.entries()) {
|
||||||
|
cloned.set(key, this.deepClone(value));
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Set) {
|
||||||
|
const cloned = new Set();
|
||||||
|
for (const value of obj.values()) {
|
||||||
|
cloned.add(this.deepClone(value));
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通对象
|
||||||
|
const cloned: any = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
cloned[key] = this.deepClone(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getComponent(entityId: number): T | null {
|
||||||
|
const index = this.entityToIndex.get(entityId);
|
||||||
|
if (index === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建真正的组件实例以保持兼容性
|
||||||
|
const component = new this.type() as any;
|
||||||
|
const serializeMapFields = (this.type as any).__serializeMapFields || new Set();
|
||||||
|
const serializeSetFields = (this.type as any).__serializeSetFields || new Set();
|
||||||
|
const serializeArrayFields = (this.type as any).__serializeArrayFields || new Set();
|
||||||
|
|
||||||
|
// 恢复数值字段
|
||||||
|
for (const [fieldName, array] of this.fields.entries()) {
|
||||||
|
const value = array[index];
|
||||||
|
const fieldType = this.getFieldType(fieldName);
|
||||||
|
|
||||||
|
if (fieldType === 'boolean') {
|
||||||
|
component[fieldName] = value === 1;
|
||||||
|
} else {
|
||||||
|
component[fieldName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复字符串字段
|
||||||
|
for (const [fieldName, stringArray] of this.stringFields.entries()) {
|
||||||
|
component[fieldName] = stringArray[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复序列化字段
|
||||||
|
for (const [fieldName, serializedArray] of this.serializedFields.entries()) {
|
||||||
|
const serialized = serializedArray[index];
|
||||||
|
if (serialized) {
|
||||||
|
component[fieldName] = this.deserializeValue(serialized, fieldName, serializeMapFields, serializeSetFields, serializeArrayFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复复杂字段
|
||||||
|
const complexFieldMap = this.complexFields.get(entityId);
|
||||||
|
if (complexFieldMap) {
|
||||||
|
for (const [fieldName, value] of complexFieldMap.entries()) {
|
||||||
|
component[fieldName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return component as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFieldType(fieldName: string): string {
|
||||||
|
// 通过创建临时实例检查字段类型
|
||||||
|
const tempInstance = new this.type();
|
||||||
|
const value = (tempInstance as any)[fieldName];
|
||||||
|
return typeof value;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getComponent(entityId);
|
||||||
|
|
||||||
|
// 清理复杂字段
|
||||||
|
this.complexFields.delete(entityId);
|
||||||
|
|
||||||
|
this.entityToIndex.delete(entityId);
|
||||||
|
this.freeIndices.push(index);
|
||||||
|
this._size--;
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resize(newCapacity: number): void {
|
||||||
|
// 调整数值字段的TypedArray
|
||||||
|
for (const [fieldName, oldArray] of this.fields.entries()) {
|
||||||
|
let newArray: Float32Array | Float64Array | Int32Array;
|
||||||
|
|
||||||
|
if (oldArray instanceof Float32Array) {
|
||||||
|
newArray = new Float32Array(newCapacity);
|
||||||
|
} else if (oldArray instanceof Float64Array) {
|
||||||
|
newArray = new Float64Array(newCapacity);
|
||||||
|
} else {
|
||||||
|
newArray = new Int32Array(newCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
newArray.set(oldArray);
|
||||||
|
this.fields.set(fieldName, newArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整字符串字段的数组
|
||||||
|
for (const [fieldName, oldArray] of this.stringFields.entries()) {
|
||||||
|
const newArray = new Array(newCapacity);
|
||||||
|
for (let i = 0; i < oldArray.length; i++) {
|
||||||
|
newArray[i] = oldArray[i];
|
||||||
|
}
|
||||||
|
this.stringFields.set(fieldName, newArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整序列化字段的数组
|
||||||
|
for (const [fieldName, oldArray] of this.serializedFields.entries()) {
|
||||||
|
const newArray = new Array(newCapacity);
|
||||||
|
for (let i = 0; i < oldArray.length; i++) {
|
||||||
|
newArray[i] = oldArray[i];
|
||||||
|
}
|
||||||
|
this.serializedFields.set(fieldName, newArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._capacity = newCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveIndices(): number[] {
|
||||||
|
return Array.from(this.entityToIndex.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFieldArray(fieldName: string): Float32Array | Float64Array | Int32Array | null {
|
||||||
|
return this.fields.get(fieldName) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public size(): number {
|
||||||
|
return this._size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this.entityToIndex.clear();
|
||||||
|
this.indexToEntity = [];
|
||||||
|
this.freeIndices = [];
|
||||||
|
this.complexFields.clear();
|
||||||
|
this._size = 0;
|
||||||
|
|
||||||
|
// 重置数值字段数组
|
||||||
|
for (const array of this.fields.values()) {
|
||||||
|
array.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置字符串字段数组
|
||||||
|
for (const stringArray of this.stringFields.values()) {
|
||||||
|
for (let i = 0; i < stringArray.length; i++) {
|
||||||
|
stringArray[i] = undefined as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置序列化字段数组
|
||||||
|
for (const serializedArray of this.serializedFields.values()) {
|
||||||
|
for (let i = 0; i < serializedArray.length; i++) {
|
||||||
|
serializedArray[i] = undefined as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public compact(): void {
|
||||||
|
if (this.freeIndices.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeEntries = Array.from(this.entityToIndex.entries())
|
||||||
|
.sort((a, b) => a[1] - b[1]);
|
||||||
|
|
||||||
|
// 重新映射索引
|
||||||
|
const newEntityToIndex = new Map<number, number>();
|
||||||
|
const newIndexToEntity: number[] = [];
|
||||||
|
|
||||||
|
for (let newIndex = 0; newIndex < activeEntries.length; newIndex++) {
|
||||||
|
const [entityId, oldIndex] = activeEntries[newIndex];
|
||||||
|
|
||||||
|
newEntityToIndex.set(entityId, newIndex);
|
||||||
|
newIndexToEntity[newIndex] = entityId;
|
||||||
|
|
||||||
|
// 移动字段数据
|
||||||
|
if (newIndex !== oldIndex) {
|
||||||
|
// 移动数值字段
|
||||||
|
for (const [, array] of this.fields.entries()) {
|
||||||
|
array[newIndex] = array[oldIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动字符串字段
|
||||||
|
for (const [, stringArray] of this.stringFields.entries()) {
|
||||||
|
stringArray[newIndex] = stringArray[oldIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动序列化字段
|
||||||
|
for (const [, serializedArray] of this.serializedFields.entries()) {
|
||||||
|
serializedArray[newIndex] = serializedArray[oldIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entityToIndex = newEntityToIndex;
|
||||||
|
this.indexToEntity = newIndexToEntity;
|
||||||
|
this.freeIndices = [];
|
||||||
|
this._size = activeEntries.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStats(): any {
|
||||||
|
let totalMemory = 0;
|
||||||
|
const fieldStats = new Map<string, any>();
|
||||||
|
|
||||||
|
for (const [fieldName, array] of this.fields.entries()) {
|
||||||
|
let bytesPerElement: number;
|
||||||
|
let typeName: string;
|
||||||
|
|
||||||
|
if (array instanceof Float32Array) {
|
||||||
|
bytesPerElement = 4;
|
||||||
|
typeName = 'float32';
|
||||||
|
} else if (array instanceof Float64Array) {
|
||||||
|
bytesPerElement = 8;
|
||||||
|
typeName = 'float64';
|
||||||
|
} else {
|
||||||
|
bytesPerElement = 4;
|
||||||
|
typeName = 'int32';
|
||||||
|
}
|
||||||
|
|
||||||
|
const memory = array.length * bytesPerElement;
|
||||||
|
totalMemory += memory;
|
||||||
|
|
||||||
|
fieldStats.set(fieldName, {
|
||||||
|
size: this._size,
|
||||||
|
capacity: array.length,
|
||||||
|
type: typeName,
|
||||||
|
memory: memory
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: this._size,
|
||||||
|
capacity: this._capacity,
|
||||||
|
usedSlots: this._size, // 兼容原测试
|
||||||
|
fragmentation: this.freeIndices.length / this._capacity,
|
||||||
|
memoryUsage: totalMemory,
|
||||||
|
fieldStats: fieldStats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行向量化批量操作
|
||||||
|
* @param operation 操作函数,接收字段数组和活跃索引
|
||||||
|
*/
|
||||||
|
public performVectorizedOperation(operation: (fieldArrays: Map<string, Float32Array | Float64Array | Int32Array>, activeIndices: number[]) => void): void {
|
||||||
|
const activeIndices = this.getActiveIndices();
|
||||||
|
operation(this.fields, activeIndices);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
tests/ECS/Core/ComponentStorage.auto.test.ts
Normal file
88
tests/ECS/Core/ComponentStorage.auto.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ComponentStorageManager, EnableSoA } from '../../../src/ECS/Core/ComponentStorage';
|
||||||
|
|
||||||
|
// 默认原始存储组件
|
||||||
|
class PositionComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public z: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用SoA优化的组件(用于大规模批量操作)
|
||||||
|
@EnableSoA
|
||||||
|
class LargeScaleComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public z: number = 0;
|
||||||
|
public vx: number = 0;
|
||||||
|
public vy: number = 0;
|
||||||
|
public vz: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SoA优化选择测试', () => {
|
||||||
|
let manager: ComponentStorageManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new ComponentStorageManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('默认使用原始存储', () => {
|
||||||
|
const storage = manager.getStorage(PositionComponent);
|
||||||
|
|
||||||
|
// 添加组件
|
||||||
|
manager.addComponent(1, new PositionComponent());
|
||||||
|
|
||||||
|
// 验证能正常工作
|
||||||
|
const component = manager.getComponent(1, PositionComponent);
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
expect(component?.x).toBe(0);
|
||||||
|
|
||||||
|
// 验证使用原始存储
|
||||||
|
expect(storage.constructor.name).toBe('ComponentStorage');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('@EnableSoA装饰器启用优化', () => {
|
||||||
|
const storage = manager.getStorage(LargeScaleComponent);
|
||||||
|
|
||||||
|
// 添加组件
|
||||||
|
const component = new LargeScaleComponent();
|
||||||
|
component.x = 100;
|
||||||
|
component.vx = 10;
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
|
||||||
|
// 验证能正常工作
|
||||||
|
const retrieved = manager.getComponent(1, LargeScaleComponent);
|
||||||
|
expect(retrieved).toBeTruthy();
|
||||||
|
expect(retrieved?.x).toBe(100);
|
||||||
|
expect(retrieved?.vx).toBe(10);
|
||||||
|
|
||||||
|
// 验证使用SoA存储
|
||||||
|
expect(storage.constructor.name).toBe('SoAStorage');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SoA存储功能验证', () => {
|
||||||
|
const entityCount = 1000;
|
||||||
|
|
||||||
|
// 创建实体(使用SoA优化)
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const component = new LargeScaleComponent();
|
||||||
|
component.x = i;
|
||||||
|
component.y = i * 2;
|
||||||
|
component.vx = 1;
|
||||||
|
component.vy = 2;
|
||||||
|
manager.addComponent(i, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数据正确性
|
||||||
|
const testComponent = manager.getComponent(100, LargeScaleComponent);
|
||||||
|
expect(testComponent?.x).toBe(100);
|
||||||
|
expect(testComponent?.y).toBe(200);
|
||||||
|
expect(testComponent?.vx).toBe(1);
|
||||||
|
expect(testComponent?.vy).toBe(2);
|
||||||
|
|
||||||
|
// 验证存储类型
|
||||||
|
const storage = manager.getStorage(LargeScaleComponent);
|
||||||
|
expect(storage.constructor.name).toBe('SoAStorage');
|
||||||
|
console.log(`成功创建 ${entityCount} 个SoA实体,数据验证通过`);
|
||||||
|
});
|
||||||
|
});
|
||||||
530
tests/ECS/Core/ComponentStorage.comparison.test.ts
Normal file
530
tests/ECS/Core/ComponentStorage.comparison.test.ts
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ComponentStorage, ComponentStorageManager, EnableSoA } from '../../../src/ECS/Core/ComponentStorage';
|
||||||
|
import { SoAStorage } from '../../../src/ECS/Core/SoAStorage';
|
||||||
|
|
||||||
|
// 测试用统一组件结构(启用SoA)
|
||||||
|
@EnableSoA
|
||||||
|
class TestPositionComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x = 0, y = 0, z = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableSoA
|
||||||
|
class TestVelocityComponent extends Component {
|
||||||
|
public vx: number = 0;
|
||||||
|
public vy: number = 0;
|
||||||
|
public vz: number = 0;
|
||||||
|
public maxSpeed: number = 100;
|
||||||
|
|
||||||
|
constructor(vx = 0, vy = 0, vz = 0) {
|
||||||
|
super();
|
||||||
|
this.vx = vx;
|
||||||
|
this.vy = vy;
|
||||||
|
this.vz = vz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableSoA
|
||||||
|
class TestHealthComponent extends Component {
|
||||||
|
public current: number = 100;
|
||||||
|
public max: number = 100;
|
||||||
|
public regeneration: number = 1;
|
||||||
|
|
||||||
|
constructor(current = 100, max = 100) {
|
||||||
|
super();
|
||||||
|
this.current = current;
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于原始存储测试的版本(默认原始存储)
|
||||||
|
class OriginalPositionComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x = 0, y = 0, z = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OriginalVelocityComponent extends Component {
|
||||||
|
public vx: number = 0;
|
||||||
|
public vy: number = 0;
|
||||||
|
public vz: number = 0;
|
||||||
|
public maxSpeed: number = 100;
|
||||||
|
|
||||||
|
constructor(vx = 0, vy = 0, vz = 0) {
|
||||||
|
super();
|
||||||
|
this.vx = vx;
|
||||||
|
this.vy = vy;
|
||||||
|
this.vz = vz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OriginalHealthComponent extends Component {
|
||||||
|
public current: number = 100;
|
||||||
|
public max: number = 100;
|
||||||
|
public regeneration: number = 1;
|
||||||
|
|
||||||
|
constructor(current = 100, max = 100) {
|
||||||
|
super();
|
||||||
|
this.current = current;
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerformanceResult {
|
||||||
|
name: string;
|
||||||
|
storageType: 'Original' | 'SoA';
|
||||||
|
entityCount: number;
|
||||||
|
operations: number;
|
||||||
|
totalTime: number;
|
||||||
|
averageTime: number;
|
||||||
|
operationsPerSecond: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ComponentStorage 严谨性能对比测试', () => {
|
||||||
|
const entityCounts = [1000, 5000, 20000];
|
||||||
|
let results: PerformanceResult[] = [];
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
generateDetailedReport();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('存储器创建和初始化', () => {
|
||||||
|
test('验证SoA和原始存储使用相同接口', () => {
|
||||||
|
const originalManager = new ComponentStorageManager();
|
||||||
|
const soaManager = new ComponentStorageManager();
|
||||||
|
|
||||||
|
const originalStorage = originalManager.getStorage(OriginalPositionComponent);
|
||||||
|
const soaStorage = soaManager.getStorage(TestPositionComponent);
|
||||||
|
|
||||||
|
// 验证都实现了相同的接口
|
||||||
|
expect(typeof originalStorage.addComponent).toBe('function');
|
||||||
|
expect(typeof originalStorage.getComponent).toBe('function');
|
||||||
|
expect(typeof originalStorage.hasComponent).toBe('function');
|
||||||
|
expect(typeof originalStorage.removeComponent).toBe('function');
|
||||||
|
|
||||||
|
expect(typeof soaStorage.addComponent).toBe('function');
|
||||||
|
expect(typeof soaStorage.getComponent).toBe('function');
|
||||||
|
expect(typeof soaStorage.hasComponent).toBe('function');
|
||||||
|
expect(typeof soaStorage.removeComponent).toBe('function');
|
||||||
|
|
||||||
|
// 验证存储器类型
|
||||||
|
expect(originalStorage).toBeInstanceOf(ComponentStorage);
|
||||||
|
expect(soaStorage).toBeInstanceOf(SoAStorage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('实体创建性能对比', () => {
|
||||||
|
entityCounts.forEach(entityCount => {
|
||||||
|
test(`创建 ${entityCount} 个完整实体`, () => {
|
||||||
|
console.log(`\\n=== 实体创建性能测试: ${entityCount} 个实体 ===`);
|
||||||
|
|
||||||
|
// 原始存储测试
|
||||||
|
const originalResult = measureOriginalEntityCreation(entityCount);
|
||||||
|
results.push(originalResult);
|
||||||
|
|
||||||
|
// SoA存储测试
|
||||||
|
const soaResult = measureSoAEntityCreation(entityCount);
|
||||||
|
results.push(soaResult);
|
||||||
|
|
||||||
|
// 输出对比结果
|
||||||
|
console.log(`原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
console.log(`SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
|
||||||
|
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||||
|
const improvement = ((speedup - 1) * 100);
|
||||||
|
console.log(`性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||||
|
|
||||||
|
// 验证功能正确性
|
||||||
|
expect(originalResult.operations).toBe(soaResult.operations);
|
||||||
|
expect(originalResult.totalTime).toBeGreaterThan(0);
|
||||||
|
expect(soaResult.totalTime).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('组件访问性能对比', () => {
|
||||||
|
entityCounts.forEach(entityCount => {
|
||||||
|
test(`随机访问 ${entityCount} 个实体组件`, () => {
|
||||||
|
console.log(`\\n=== 组件访问性能测试: ${entityCount} 个实体 ===`);
|
||||||
|
|
||||||
|
// 原始存储测试
|
||||||
|
const originalResult = measureOriginalComponentAccess(entityCount, 100);
|
||||||
|
results.push(originalResult);
|
||||||
|
|
||||||
|
// SoA存储测试
|
||||||
|
const soaResult = measureSoAComponentAccess(entityCount, 100);
|
||||||
|
results.push(soaResult);
|
||||||
|
|
||||||
|
// 输出对比结果
|
||||||
|
console.log(`原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
console.log(`SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
|
||||||
|
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||||
|
const improvement = ((speedup - 1) * 100);
|
||||||
|
console.log(`性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||||
|
|
||||||
|
expect(originalResult.operations).toBe(soaResult.operations);
|
||||||
|
expect(originalResult.totalTime).toBeGreaterThan(0);
|
||||||
|
expect(soaResult.totalTime).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('批量更新性能对比(SoA优势场景)', () => {
|
||||||
|
entityCounts.forEach(entityCount => {
|
||||||
|
test(`批量更新 ${entityCount} 个实体`, () => {
|
||||||
|
console.log(`\\n=== 批量更新性能测试: ${entityCount} 个实体 ===`);
|
||||||
|
|
||||||
|
// 原始存储测试
|
||||||
|
const originalResult = measureOriginalBatchUpdate(entityCount, 50);
|
||||||
|
results.push(originalResult);
|
||||||
|
|
||||||
|
// SoA存储测试(向量化操作)
|
||||||
|
const soaResult = measureSoABatchUpdate(entityCount, 50);
|
||||||
|
results.push(soaResult);
|
||||||
|
|
||||||
|
// 输出对比结果
|
||||||
|
console.log(`原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
console.log(`SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
|
||||||
|
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||||
|
const improvement = ((speedup - 1) * 100);
|
||||||
|
console.log(`性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||||
|
|
||||||
|
// 这是SoA的优势场景,应该有性能提升
|
||||||
|
if (entityCount > 5000) {
|
||||||
|
expect(speedup).toBeGreaterThan(1.0); // SoA应该更快
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(originalResult.operations).toBe(soaResult.operations);
|
||||||
|
expect(originalResult.totalTime).toBeGreaterThan(0);
|
||||||
|
expect(soaResult.totalTime).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试辅助函数
|
||||||
|
function measureOriginalEntityCreation(entityCount: number): PerformanceResult {
|
||||||
|
const manager = new ComponentStorageManager();
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
manager.addComponent(i, new OriginalPositionComponent(
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 100
|
||||||
|
));
|
||||||
|
manager.addComponent(i, new OriginalVelocityComponent(
|
||||||
|
(Math.random() - 0.5) * 20,
|
||||||
|
(Math.random() - 0.5) * 20,
|
||||||
|
(Math.random() - 0.5) * 10
|
||||||
|
));
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
manager.addComponent(i, new OriginalHealthComponent(
|
||||||
|
80 + Math.random() * 20,
|
||||||
|
100
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const operations = entityCount * 2.5; // 平均每个实体2.5个组件
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Entity Creation',
|
||||||
|
storageType: 'Original',
|
||||||
|
entityCount,
|
||||||
|
operations,
|
||||||
|
totalTime,
|
||||||
|
averageTime: totalTime / operations,
|
||||||
|
operationsPerSecond: operations / (totalTime / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureSoAEntityCreation(entityCount: number): PerformanceResult {
|
||||||
|
const manager = new ComponentStorageManager();
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
manager.addComponent(i, new TestPositionComponent(
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 100
|
||||||
|
));
|
||||||
|
manager.addComponent(i, new TestVelocityComponent(
|
||||||
|
(Math.random() - 0.5) * 20,
|
||||||
|
(Math.random() - 0.5) * 20,
|
||||||
|
(Math.random() - 0.5) * 10
|
||||||
|
));
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
manager.addComponent(i, new TestHealthComponent(
|
||||||
|
80 + Math.random() * 20,
|
||||||
|
100
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const operations = entityCount * 2.5;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Entity Creation',
|
||||||
|
storageType: 'SoA',
|
||||||
|
entityCount,
|
||||||
|
operations,
|
||||||
|
totalTime,
|
||||||
|
averageTime: totalTime / operations,
|
||||||
|
operationsPerSecond: operations / (totalTime / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureOriginalComponentAccess(entityCount: number, iterations: number): PerformanceResult {
|
||||||
|
const manager = new ComponentStorageManager();
|
||||||
|
|
||||||
|
// 预创建实体
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
manager.addComponent(i, new OriginalPositionComponent(i, i, i));
|
||||||
|
manager.addComponent(i, new OriginalVelocityComponent(1, 1, 1));
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
manager.addComponent(i, new OriginalHealthComponent(100, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (let iter = 0; iter < iterations; iter++) {
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const pos = manager.getComponent(i, OriginalPositionComponent);
|
||||||
|
const vel = manager.getComponent(i, OriginalVelocityComponent);
|
||||||
|
|
||||||
|
if (pos && vel) {
|
||||||
|
// 模拟简单的读取操作
|
||||||
|
const sum = pos.x + pos.y + pos.z + vel.vx + vel.vy + vel.vz;
|
||||||
|
if (sum < 0) continue; // 防止优化
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const operations = entityCount * iterations;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Component Access',
|
||||||
|
storageType: 'Original',
|
||||||
|
entityCount,
|
||||||
|
operations,
|
||||||
|
totalTime,
|
||||||
|
averageTime: totalTime / operations,
|
||||||
|
operationsPerSecond: operations / (totalTime / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureSoAComponentAccess(entityCount: number, iterations: number): PerformanceResult {
|
||||||
|
const manager = new ComponentStorageManager();
|
||||||
|
|
||||||
|
// 预创建实体
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
manager.addComponent(i, new TestPositionComponent(i, i, i));
|
||||||
|
manager.addComponent(i, new TestVelocityComponent(1, 1, 1));
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
manager.addComponent(i, new TestHealthComponent(100, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (let iter = 0; iter < iterations; iter++) {
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const pos = manager.getComponent(i, TestPositionComponent);
|
||||||
|
const vel = manager.getComponent(i, TestVelocityComponent);
|
||||||
|
|
||||||
|
if (pos && vel) {
|
||||||
|
// 模拟简单的读取操作
|
||||||
|
const sum = pos.x + pos.y + pos.z + vel.vx + vel.vy + vel.vz;
|
||||||
|
if (sum < 0) continue; // 防止优化
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const operations = entityCount * iterations;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Component Access',
|
||||||
|
storageType: 'SoA',
|
||||||
|
entityCount,
|
||||||
|
operations,
|
||||||
|
totalTime,
|
||||||
|
averageTime: totalTime / operations,
|
||||||
|
operationsPerSecond: operations / (totalTime / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureOriginalBatchUpdate(entityCount: number, iterations: number): PerformanceResult {
|
||||||
|
const manager = new ComponentStorageManager();
|
||||||
|
|
||||||
|
// 预创建实体
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
manager.addComponent(i, new OriginalPositionComponent(i, i, 0));
|
||||||
|
manager.addComponent(i, new OriginalVelocityComponent(1, 1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
const deltaTime = 0.016;
|
||||||
|
|
||||||
|
for (let iter = 0; iter < iterations; iter++) {
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const pos = manager.getComponent(i, OriginalPositionComponent);
|
||||||
|
const vel = manager.getComponent(i, OriginalVelocityComponent);
|
||||||
|
|
||||||
|
if (pos && vel) {
|
||||||
|
// 物理更新
|
||||||
|
pos.x += vel.vx * deltaTime;
|
||||||
|
pos.y += vel.vy * deltaTime;
|
||||||
|
pos.z += vel.vz * deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const operations = entityCount * iterations;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Batch Update',
|
||||||
|
storageType: 'Original',
|
||||||
|
entityCount,
|
||||||
|
operations,
|
||||||
|
totalTime,
|
||||||
|
averageTime: totalTime / operations,
|
||||||
|
operationsPerSecond: operations / (totalTime / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureSoABatchUpdate(entityCount: number, iterations: number): PerformanceResult {
|
||||||
|
const manager = new ComponentStorageManager();
|
||||||
|
|
||||||
|
// 预创建实体
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
manager.addComponent(i, new TestPositionComponent(i, i, 0));
|
||||||
|
manager.addComponent(i, new TestVelocityComponent(1, 1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
const deltaTime = 0.016;
|
||||||
|
|
||||||
|
// 获取SoA存储器进行向量化操作
|
||||||
|
const posStorage = manager.getStorage(TestPositionComponent) as SoAStorage<TestPositionComponent>;
|
||||||
|
const velStorage = manager.getStorage(TestVelocityComponent) as SoAStorage<TestVelocityComponent>;
|
||||||
|
|
||||||
|
for (let iter = 0; iter < iterations; iter++) {
|
||||||
|
// 使用向量化操作
|
||||||
|
posStorage.performVectorizedOperation((posFields, activeIndices) => {
|
||||||
|
const velFields = velStorage.getFieldArray('vx') ?
|
||||||
|
new Map([
|
||||||
|
['vx', velStorage.getFieldArray('vx')!],
|
||||||
|
['vy', velStorage.getFieldArray('vy')!],
|
||||||
|
['vz', velStorage.getFieldArray('vz')!]
|
||||||
|
]) : new Map();
|
||||||
|
|
||||||
|
const posX = posFields.get('x') as Float32Array;
|
||||||
|
const posY = posFields.get('y') as Float32Array;
|
||||||
|
const posZ = posFields.get('z') as Float32Array;
|
||||||
|
|
||||||
|
const velX = velFields.get('vx') as Float32Array;
|
||||||
|
const velY = velFields.get('vy') as Float32Array;
|
||||||
|
const velZ = velFields.get('vz') as Float32Array;
|
||||||
|
|
||||||
|
// 向量化物理更新
|
||||||
|
for (let j = 0; j < activeIndices.length; j++) {
|
||||||
|
const idx = activeIndices[j];
|
||||||
|
posX[idx] += velX[idx] * deltaTime;
|
||||||
|
posY[idx] += velY[idx] * deltaTime;
|
||||||
|
posZ[idx] += velZ[idx] * deltaTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const operations = entityCount * iterations;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Batch Update',
|
||||||
|
storageType: 'SoA',
|
||||||
|
entityCount,
|
||||||
|
operations,
|
||||||
|
totalTime,
|
||||||
|
averageTime: totalTime / operations,
|
||||||
|
operationsPerSecond: operations / (totalTime / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDetailedReport(): void {
|
||||||
|
console.log('\\n' + '='.repeat(80));
|
||||||
|
console.log('ComponentStorage 严谨性能对比报告');
|
||||||
|
console.log('='.repeat(80));
|
||||||
|
|
||||||
|
// 按测试类型分组
|
||||||
|
const groupedResults = new Map<string, PerformanceResult[]>();
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const key = `${result.name}-${result.entityCount}`;
|
||||||
|
if (!groupedResults.has(key)) {
|
||||||
|
groupedResults.set(key, []);
|
||||||
|
}
|
||||||
|
groupedResults.get(key)!.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalOriginalTime = 0;
|
||||||
|
let totalSoATime = 0;
|
||||||
|
let testCount = 0;
|
||||||
|
|
||||||
|
for (const [key, testResults] of groupedResults.entries()) {
|
||||||
|
console.log(`\\n${key}:`);
|
||||||
|
|
||||||
|
const originalResult = testResults.find(r => r.storageType === 'Original');
|
||||||
|
const soaResult = testResults.find(r => r.storageType === 'SoA');
|
||||||
|
|
||||||
|
if (originalResult && soaResult) {
|
||||||
|
const speedup = originalResult.totalTime / soaResult.totalTime;
|
||||||
|
const improvement = ((speedup - 1) * 100);
|
||||||
|
|
||||||
|
console.log(` 原始存储: ${originalResult.totalTime.toFixed(2)}ms (${originalResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
console.log(` SoA存储: ${soaResult.totalTime.toFixed(2)}ms (${soaResult.operationsPerSecond.toFixed(0)} ops/sec)`);
|
||||||
|
console.log(` 性能对比: ${speedup.toFixed(2)}x ${improvement > 0 ? '提升' : '下降'} ${Math.abs(improvement).toFixed(1)}%`);
|
||||||
|
|
||||||
|
totalOriginalTime += originalResult.totalTime;
|
||||||
|
totalSoATime += soaResult.totalTime;
|
||||||
|
testCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testCount > 0) {
|
||||||
|
const overallSpeedup = totalOriginalTime / totalSoATime;
|
||||||
|
const overallImprovement = ((overallSpeedup - 1) * 100);
|
||||||
|
|
||||||
|
console.log('\\n' + '='.repeat(80));
|
||||||
|
console.log('总体性能对比:');
|
||||||
|
console.log(` 原始存储总耗时: ${totalOriginalTime.toFixed(2)}ms`);
|
||||||
|
console.log(` SoA存储总耗时: ${totalSoATime.toFixed(2)}ms`);
|
||||||
|
console.log(` 总体性能对比: ${overallSpeedup.toFixed(2)}x ${overallImprovement > 0 ? '提升' : '下降'} ${Math.abs(overallImprovement).toFixed(1)}%`);
|
||||||
|
console.log('\\n结论: SoA优化在批量操作场景中表现优异,在小规模随机访问场景中有轻微开销。');
|
||||||
|
console.log('建议: 对于大规模游戏实体和批量系统更新,SoA优化能带来显著性能提升。');
|
||||||
|
console.log('='.repeat(80));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { Component } from '../../../src/ECS/Component';
|
import { Component } from '../../../src/ECS/Component';
|
||||||
import { BigIntFactory } from '../../../src/ECS/Utils/BigIntCompatibility';
|
import { BigIntFactory } from '../../../src/ECS/Utils/BigIntCompatibility';
|
||||||
|
|
||||||
// 测试组件类
|
// 测试组件类(默认使用原始存储)
|
||||||
class TestComponent extends Component {
|
class TestComponent extends Component {
|
||||||
constructor(public value: number = 0) {
|
constructor(public value: number = 0) {
|
||||||
super();
|
super();
|
||||||
|
|||||||
252
tests/ECS/Core/SoAStorage.collections.test.ts
Normal file
252
tests/ECS/Core/SoAStorage.collections.test.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ComponentStorageManager, EnableSoA, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/ComponentStorage';
|
||||||
|
|
||||||
|
// 测试组件:使用集合类型装饰器
|
||||||
|
@EnableSoA
|
||||||
|
class CollectionsComponent extends Component {
|
||||||
|
// 序列化Map存储
|
||||||
|
@SerializeMap
|
||||||
|
public playerStats: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
// 序列化Set存储
|
||||||
|
@SerializeSet
|
||||||
|
public achievements: Set<string> = new Set();
|
||||||
|
|
||||||
|
// 序列化Array存储
|
||||||
|
@SerializeArray
|
||||||
|
public inventory: string[] = [];
|
||||||
|
|
||||||
|
// 深拷贝对象存储
|
||||||
|
@DeepCopy
|
||||||
|
public config: { settings: { volume: number } } = { settings: { volume: 0.5 } };
|
||||||
|
|
||||||
|
// 普通对象(引用存储)
|
||||||
|
public metadata: any = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SoA集合类型装饰器测试', () => {
|
||||||
|
let manager: ComponentStorageManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new ComponentStorageManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证Map序列化存储', () => {
|
||||||
|
console.log('\\n=== 测试Map序列化存储 ===');
|
||||||
|
|
||||||
|
const component = new CollectionsComponent();
|
||||||
|
|
||||||
|
// 设置Map数据
|
||||||
|
component.playerStats.set('health', 100);
|
||||||
|
component.playerStats.set('mana', 50);
|
||||||
|
component.playerStats.set('experience', 1250);
|
||||||
|
|
||||||
|
console.log('原始Map数据:', {
|
||||||
|
size: component.playerStats.size,
|
||||||
|
entries: Array.from(component.playerStats.entries())
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||||
|
|
||||||
|
console.log('取回Map数据:', {
|
||||||
|
size: retrieved?.playerStats.size,
|
||||||
|
entries: Array.from(retrieved?.playerStats.entries() || [])
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证Map数据完整性
|
||||||
|
expect(retrieved?.playerStats).toBeInstanceOf(Map);
|
||||||
|
expect(retrieved?.playerStats.size).toBe(3);
|
||||||
|
expect(retrieved?.playerStats.get('health')).toBe(100);
|
||||||
|
expect(retrieved?.playerStats.get('mana')).toBe(50);
|
||||||
|
expect(retrieved?.playerStats.get('experience')).toBe(1250);
|
||||||
|
|
||||||
|
console.log('✅ Map序列化存储验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证Set序列化存储', () => {
|
||||||
|
console.log('\\n=== 测试Set序列化存储 ===');
|
||||||
|
|
||||||
|
const component = new CollectionsComponent();
|
||||||
|
|
||||||
|
// 设置Set数据
|
||||||
|
component.achievements.add('first_kill');
|
||||||
|
component.achievements.add('level_10');
|
||||||
|
component.achievements.add('boss_defeated');
|
||||||
|
|
||||||
|
console.log('原始Set数据:', {
|
||||||
|
size: component.achievements.size,
|
||||||
|
values: Array.from(component.achievements)
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||||
|
|
||||||
|
console.log('取回Set数据:', {
|
||||||
|
size: retrieved?.achievements.size,
|
||||||
|
values: Array.from(retrieved?.achievements || [])
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证Set数据完整性
|
||||||
|
expect(retrieved?.achievements).toBeInstanceOf(Set);
|
||||||
|
expect(retrieved?.achievements.size).toBe(3);
|
||||||
|
expect(retrieved?.achievements.has('first_kill')).toBe(true);
|
||||||
|
expect(retrieved?.achievements.has('level_10')).toBe(true);
|
||||||
|
expect(retrieved?.achievements.has('boss_defeated')).toBe(true);
|
||||||
|
|
||||||
|
console.log('✅ Set序列化存储验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证Array序列化存储', () => {
|
||||||
|
console.log('\\n=== 测试Array序列化存储 ===');
|
||||||
|
|
||||||
|
const component = new CollectionsComponent();
|
||||||
|
|
||||||
|
// 设置Array数据
|
||||||
|
component.inventory.push('sword', 'shield', 'potion');
|
||||||
|
|
||||||
|
console.log('原始Array数据:', component.inventory);
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||||
|
|
||||||
|
console.log('取回Array数据:', retrieved?.inventory);
|
||||||
|
|
||||||
|
// 验证Array数据完整性
|
||||||
|
expect(Array.isArray(retrieved?.inventory)).toBe(true);
|
||||||
|
expect(retrieved?.inventory.length).toBe(3);
|
||||||
|
expect(retrieved?.inventory).toEqual(['sword', 'shield', 'potion']);
|
||||||
|
|
||||||
|
console.log('✅ Array序列化存储验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证深拷贝对象存储', () => {
|
||||||
|
console.log('\\n=== 测试深拷贝对象存储 ===');
|
||||||
|
|
||||||
|
const component = new CollectionsComponent();
|
||||||
|
const originalConfig = component.config;
|
||||||
|
|
||||||
|
// 修改配置
|
||||||
|
component.config.settings.volume = 0.8;
|
||||||
|
|
||||||
|
console.log('原始配置:', component.config);
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||||
|
|
||||||
|
console.log('取回配置:', retrieved?.config);
|
||||||
|
|
||||||
|
// 验证深拷贝
|
||||||
|
expect(retrieved?.config).toEqual(component.config);
|
||||||
|
expect(retrieved?.config).not.toBe(originalConfig); // 不是同一个引用
|
||||||
|
expect(retrieved?.config.settings.volume).toBe(0.8);
|
||||||
|
|
||||||
|
// 修改原始对象不应该影响取回的对象
|
||||||
|
component.config.settings.volume = 0.3;
|
||||||
|
expect(retrieved?.config.settings.volume).toBe(0.8); // 保持不变
|
||||||
|
|
||||||
|
console.log('✅ 深拷贝对象存储验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('对比普通对象存储(引用存储)', () => {
|
||||||
|
console.log('\\n=== 测试普通对象存储(引用存储)===');
|
||||||
|
|
||||||
|
const component = new CollectionsComponent();
|
||||||
|
const sharedObject = { data: 'shared' };
|
||||||
|
component.metadata = sharedObject;
|
||||||
|
|
||||||
|
console.log('原始metadata:', component.metadata);
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||||
|
|
||||||
|
console.log('取回metadata:', retrieved?.metadata);
|
||||||
|
|
||||||
|
// 验证引用存储
|
||||||
|
expect(retrieved?.metadata).toBe(sharedObject); // 是同一个引用
|
||||||
|
expect(retrieved?.metadata.data).toBe('shared');
|
||||||
|
|
||||||
|
console.log('✅ 普通对象存储验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('复杂场景:多种类型混合使用', () => {
|
||||||
|
console.log('\\n=== 测试复杂场景 ===');
|
||||||
|
|
||||||
|
const component = new CollectionsComponent();
|
||||||
|
|
||||||
|
// 设置复杂数据
|
||||||
|
component.playerStats.set('level', 25);
|
||||||
|
component.playerStats.set('gold', 5000);
|
||||||
|
|
||||||
|
component.achievements.add('explorer');
|
||||||
|
component.achievements.add('warrior');
|
||||||
|
|
||||||
|
component.inventory.push('legendary_sword', 'magic_potion');
|
||||||
|
|
||||||
|
component.config = {
|
||||||
|
settings: {
|
||||||
|
volume: 0.75
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
component.metadata = { timestamp: Date.now() };
|
||||||
|
|
||||||
|
console.log('复杂数据设置完成');
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, CollectionsComponent);
|
||||||
|
|
||||||
|
// 全面验证
|
||||||
|
expect(retrieved?.playerStats.get('level')).toBe(25);
|
||||||
|
expect(retrieved?.achievements.has('explorer')).toBe(true);
|
||||||
|
expect(retrieved?.inventory).toContain('legendary_sword');
|
||||||
|
expect(retrieved?.config.settings.volume).toBe(0.75);
|
||||||
|
expect(retrieved?.metadata).toBeDefined();
|
||||||
|
|
||||||
|
console.log('✅ 复杂场景验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('性能测试:序列化 vs 深拷贝', () => {
|
||||||
|
console.log('\\n=== 性能对比测试 ===');
|
||||||
|
|
||||||
|
const entityCount = 100;
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const component = new CollectionsComponent();
|
||||||
|
|
||||||
|
// 设置数据
|
||||||
|
component.playerStats.set('id', i);
|
||||||
|
component.playerStats.set('score', i * 100);
|
||||||
|
|
||||||
|
component.achievements.add(`achievement_${i}`);
|
||||||
|
component.inventory.push(`item_${i}`);
|
||||||
|
|
||||||
|
component.config = { settings: { volume: i / entityCount } };
|
||||||
|
|
||||||
|
manager.addComponent(i, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTime = performance.now() - startTime;
|
||||||
|
|
||||||
|
// 读取测试
|
||||||
|
const readStartTime = performance.now();
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const component = manager.getComponent(i, CollectionsComponent);
|
||||||
|
expect(component?.playerStats.get('id')).toBe(i);
|
||||||
|
}
|
||||||
|
const readTime = performance.now() - readStartTime;
|
||||||
|
|
||||||
|
console.log(`创建${entityCount}个复杂组件: ${createTime.toFixed(2)}ms`);
|
||||||
|
console.log(`读取${entityCount}个复杂组件: ${readTime.toFixed(2)}ms`);
|
||||||
|
console.log(`平均每个组件: ${((createTime + readTime) / entityCount).toFixed(4)}ms`);
|
||||||
|
|
||||||
|
console.log('✅ 性能测试完成');
|
||||||
|
});
|
||||||
|
});
|
||||||
307
tests/ECS/Core/SoAStorage.comprehensive.test.ts
Normal file
307
tests/ECS/Core/SoAStorage.comprehensive.test.ts
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ComponentStorageManager, EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/ComponentStorage';
|
||||||
|
import { SoAStorage } from '../../../src/ECS/Core/SoAStorage';
|
||||||
|
|
||||||
|
// 综合测试组件,覆盖所有装饰器
|
||||||
|
@EnableSoA
|
||||||
|
class ComprehensiveComponent extends Component {
|
||||||
|
@HighPrecision
|
||||||
|
public bigIntId: number = BigInt(Number.MAX_SAFE_INTEGER + 1) as any;
|
||||||
|
|
||||||
|
@Float64
|
||||||
|
public preciseValue: number = Math.PI;
|
||||||
|
|
||||||
|
@Int32
|
||||||
|
public intValue: number = -2147483648;
|
||||||
|
|
||||||
|
@SerializeMap
|
||||||
|
public gameMap: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
@SerializeSet
|
||||||
|
public flags: Set<number> = new Set();
|
||||||
|
|
||||||
|
@SerializeArray
|
||||||
|
public items: any[] = [];
|
||||||
|
|
||||||
|
@DeepCopy
|
||||||
|
public nestedConfig: any = { deep: { nested: { value: 42 } } };
|
||||||
|
|
||||||
|
// 未装饰的字段
|
||||||
|
public normalFloat: number = 1.23;
|
||||||
|
public flag: boolean = true;
|
||||||
|
public text: string = 'default';
|
||||||
|
public complexObject: any = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SoA存储综合测试覆盖', () => {
|
||||||
|
let manager: ComponentStorageManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new ComponentStorageManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证所有装饰器类型的存储和检索', () => {
|
||||||
|
console.log('\\n=== 综合装饰器测试 ===');
|
||||||
|
|
||||||
|
const component = new ComprehensiveComponent();
|
||||||
|
|
||||||
|
// 设置复杂数据
|
||||||
|
component.gameMap.set('player1', { level: 10, gold: 500 });
|
||||||
|
component.gameMap.set('player2', { level: 15, gold: 1200 });
|
||||||
|
|
||||||
|
component.flags.add(1);
|
||||||
|
component.flags.add(2);
|
||||||
|
component.flags.add(4);
|
||||||
|
|
||||||
|
component.items.push({ type: 'weapon', name: 'sword' });
|
||||||
|
component.items.push({ type: 'armor', name: 'shield' });
|
||||||
|
|
||||||
|
component.nestedConfig.deep.nested.value = 999;
|
||||||
|
component.complexObject = { reference: 'shared' };
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, ComprehensiveComponent);
|
||||||
|
|
||||||
|
// 验证所有类型
|
||||||
|
expect(retrieved?.bigIntId).toBe(component.bigIntId);
|
||||||
|
expect(retrieved?.preciseValue).toBeCloseTo(Math.PI, 15);
|
||||||
|
expect(retrieved?.intValue).toBe(-2147483648);
|
||||||
|
|
||||||
|
expect(retrieved?.gameMap).toBeInstanceOf(Map);
|
||||||
|
expect(retrieved?.gameMap.get('player1')).toEqual({ level: 10, gold: 500 });
|
||||||
|
|
||||||
|
expect(retrieved?.flags).toBeInstanceOf(Set);
|
||||||
|
expect(retrieved?.flags.has(2)).toBe(true);
|
||||||
|
|
||||||
|
expect(retrieved?.items).toEqual(component.items);
|
||||||
|
expect(retrieved?.nestedConfig.deep.nested.value).toBe(999);
|
||||||
|
|
||||||
|
// 深拷贝验证
|
||||||
|
expect(retrieved?.nestedConfig).not.toBe(component.nestedConfig);
|
||||||
|
|
||||||
|
console.log('✅ 综合装饰器测试通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('测试存储器内存统计和容量管理', () => {
|
||||||
|
console.log('\\n=== 存储器管理测试 ===');
|
||||||
|
|
||||||
|
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||||
|
|
||||||
|
// 添加多个组件
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
const component = new ComprehensiveComponent();
|
||||||
|
component.intValue = i * 100;
|
||||||
|
component.preciseValue = i * Math.PI;
|
||||||
|
manager.addComponent(i, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查统计信息
|
||||||
|
const stats = storage.getStats();
|
||||||
|
console.log('存储统计:', {
|
||||||
|
size: stats.size,
|
||||||
|
capacity: stats.capacity,
|
||||||
|
memoryUsage: stats.memoryUsage,
|
||||||
|
fieldCount: stats.fieldStats.size
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stats.size).toBe(5);
|
||||||
|
expect(stats.capacity).toBeGreaterThanOrEqual(5);
|
||||||
|
expect(stats.memoryUsage).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 测试压缩
|
||||||
|
storage.removeComponent(2);
|
||||||
|
storage.removeComponent(4);
|
||||||
|
|
||||||
|
const statsBeforeCompact = storage.getStats();
|
||||||
|
storage.compact();
|
||||||
|
const statsAfterCompact = storage.getStats();
|
||||||
|
|
||||||
|
expect(statsAfterCompact.size).toBe(3);
|
||||||
|
console.log('压缩前后对比:', {
|
||||||
|
before: statsBeforeCompact.size,
|
||||||
|
after: statsAfterCompact.size
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 存储器管理测试通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('测试序列化错误处理', () => {
|
||||||
|
console.log('\\n=== 序列化错误处理测试 ===');
|
||||||
|
|
||||||
|
// 创建包含循环引用的对象
|
||||||
|
const component = new ComprehensiveComponent();
|
||||||
|
const cyclicObject: any = { name: 'test' };
|
||||||
|
cyclicObject.self = cyclicObject; // 循环引用
|
||||||
|
|
||||||
|
// 这应该不会崩溃,而是优雅处理
|
||||||
|
component.items.push(cyclicObject);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
const retrieved = manager.getComponent(1, ComprehensiveComponent);
|
||||||
|
expect(retrieved).toBeDefined();
|
||||||
|
|
||||||
|
console.log('✅ 序列化错误处理测试通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('测试大容量扩展和性能', () => {
|
||||||
|
console.log('\\n=== 大容量性能测试 ===');
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
const entityCount = 2000;
|
||||||
|
|
||||||
|
// 创建大量实体
|
||||||
|
for (let i = 1; i <= entityCount; i++) {
|
||||||
|
const component = new ComprehensiveComponent();
|
||||||
|
component.intValue = i;
|
||||||
|
component.preciseValue = i * 0.1;
|
||||||
|
component.gameMap.set(`key${i}`, i);
|
||||||
|
component.flags.add(i % 10);
|
||||||
|
component.items.push(`item${i}`);
|
||||||
|
|
||||||
|
manager.addComponent(i, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTime = performance.now() - startTime;
|
||||||
|
|
||||||
|
// 随机访问测试
|
||||||
|
const readStartTime = performance.now();
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const randomId = Math.floor(Math.random() * entityCount) + 1;
|
||||||
|
const component = manager.getComponent(randomId, ComprehensiveComponent);
|
||||||
|
expect(component?.intValue).toBe(randomId);
|
||||||
|
}
|
||||||
|
const readTime = performance.now() - readStartTime;
|
||||||
|
|
||||||
|
console.log(`创建${entityCount}个组件: ${createTime.toFixed(2)}ms`);
|
||||||
|
console.log(`随机读取100次: ${readTime.toFixed(2)}ms`);
|
||||||
|
console.log(`平均创建时间: ${(createTime / entityCount).toFixed(4)}ms/组件`);
|
||||||
|
|
||||||
|
// 验证存储统计
|
||||||
|
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||||
|
const stats = storage.getStats();
|
||||||
|
|
||||||
|
expect(stats.size).toBe(entityCount);
|
||||||
|
expect(stats.capacity).toBeGreaterThanOrEqual(entityCount);
|
||||||
|
|
||||||
|
console.log('✅ 大容量性能测试通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('测试空值和边界处理', () => {
|
||||||
|
console.log('\\n=== 空值边界测试 ===');
|
||||||
|
|
||||||
|
const component = new ComprehensiveComponent();
|
||||||
|
|
||||||
|
// 设置各种边界值
|
||||||
|
component.gameMap.set('null', null);
|
||||||
|
component.gameMap.set('undefined', undefined);
|
||||||
|
component.gameMap.set('empty', '');
|
||||||
|
component.gameMap.set('zero', 0);
|
||||||
|
component.gameMap.set('false', false);
|
||||||
|
|
||||||
|
component.flags.add(0);
|
||||||
|
component.items.push(null, undefined, '', 0, false);
|
||||||
|
|
||||||
|
component.nestedConfig = null;
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, ComprehensiveComponent);
|
||||||
|
|
||||||
|
// 验证边界值处理
|
||||||
|
expect(retrieved?.gameMap.get('null')).toBe(null);
|
||||||
|
expect(retrieved?.gameMap.get('undefined')).toBe(null); // JSON序列化会将undefined转为null
|
||||||
|
expect(retrieved?.gameMap.get('empty')).toBe('');
|
||||||
|
expect(retrieved?.gameMap.get('zero')).toBe(0);
|
||||||
|
expect(retrieved?.gameMap.get('false')).toBe(false);
|
||||||
|
|
||||||
|
expect(retrieved?.flags.has(0)).toBe(true);
|
||||||
|
expect(retrieved?.items).toEqual([null, null, '', 0, false]); // undefined序列化为null
|
||||||
|
expect(retrieved?.nestedConfig).toBe(null);
|
||||||
|
|
||||||
|
console.log('✅ 空值边界测试通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('测试不同TypedArray类型的字段访问', () => {
|
||||||
|
console.log('\\n=== TypedArray字段测试 ===');
|
||||||
|
|
||||||
|
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||||
|
|
||||||
|
// 添加测试数据
|
||||||
|
const component = new ComprehensiveComponent();
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
|
||||||
|
// 检查不同类型的TypedArray
|
||||||
|
const preciseArray = storage.getFieldArray('preciseValue');
|
||||||
|
const intArray = storage.getFieldArray('intValue');
|
||||||
|
const normalArray = storage.getFieldArray('normalFloat');
|
||||||
|
const flagArray = storage.getFieldArray('flag');
|
||||||
|
|
||||||
|
expect(preciseArray).toBeInstanceOf(Float64Array);
|
||||||
|
expect(intArray).toBeInstanceOf(Int32Array);
|
||||||
|
expect(normalArray).toBeInstanceOf(Float32Array);
|
||||||
|
expect(flagArray).toBeInstanceOf(Float32Array);
|
||||||
|
|
||||||
|
// 高精度字段不应该在TypedArray中
|
||||||
|
const bigIntArray = storage.getFieldArray('bigIntId');
|
||||||
|
expect(bigIntArray).toBeNull();
|
||||||
|
|
||||||
|
console.log('TypedArray类型验证:', {
|
||||||
|
preciseValue: preciseArray?.constructor.name,
|
||||||
|
intValue: intArray?.constructor.name,
|
||||||
|
normalFloat: normalArray?.constructor.name,
|
||||||
|
flag: flagArray?.constructor.name,
|
||||||
|
bigIntId: bigIntArray ? 'Found' : 'null (正确)'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ TypedArray字段测试通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('测试向量化批量操作', () => {
|
||||||
|
console.log('\\n=== 向量化操作测试 ===');
|
||||||
|
|
||||||
|
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
|
||||||
|
|
||||||
|
// 添加测试数据
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
const component = new ComprehensiveComponent();
|
||||||
|
component.normalFloat = i;
|
||||||
|
component.intValue = i * 10;
|
||||||
|
manager.addComponent(i, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行向量化操作
|
||||||
|
let operationExecuted = false;
|
||||||
|
storage.performVectorizedOperation((fieldArrays, activeIndices) => {
|
||||||
|
operationExecuted = true;
|
||||||
|
|
||||||
|
const normalFloatArray = fieldArrays.get('normalFloat') as Float32Array;
|
||||||
|
const intArray = fieldArrays.get('intValue') as Int32Array;
|
||||||
|
|
||||||
|
expect(normalFloatArray).toBeInstanceOf(Float32Array);
|
||||||
|
expect(intArray).toBeInstanceOf(Int32Array);
|
||||||
|
expect(activeIndices.length).toBe(10);
|
||||||
|
|
||||||
|
// 批量修改数据
|
||||||
|
for (let i = 0; i < activeIndices.length; i++) {
|
||||||
|
const idx = activeIndices[i];
|
||||||
|
normalFloatArray[idx] *= 2; // 乘以2
|
||||||
|
intArray[idx] += 5; // 加5
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(operationExecuted).toBe(true);
|
||||||
|
|
||||||
|
// 验证批量操作结果
|
||||||
|
const component = manager.getComponent(5, ComprehensiveComponent);
|
||||||
|
expect(component?.normalFloat).toBe(10); // 5 * 2
|
||||||
|
expect(component?.intValue).toBe(55); // 50 + 5
|
||||||
|
|
||||||
|
console.log('✅ 向量化操作测试通过');
|
||||||
|
});
|
||||||
|
});
|
||||||
171
tests/ECS/Core/SoAStorage.decorators.test.ts
Normal file
171
tests/ECS/Core/SoAStorage.decorators.test.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ComponentStorageManager, EnableSoA, HighPrecision, Float64, Int32 } from '../../../src/ECS/Core/ComponentStorage';
|
||||||
|
import { SoAStorage } from '../../../src/ECS/Core/SoAStorage';
|
||||||
|
|
||||||
|
// 测试组件:使用不同的数值类型装饰器
|
||||||
|
@EnableSoA
|
||||||
|
class DecoratedComponent extends Component {
|
||||||
|
// 默认Float32Array存储
|
||||||
|
public normalFloat: number = 3.14;
|
||||||
|
|
||||||
|
// 高精度存储(作为复杂对象)
|
||||||
|
@HighPrecision
|
||||||
|
public highPrecisionNumber: number = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
// Float64Array存储
|
||||||
|
@Float64
|
||||||
|
public preciseFloat: number = Math.PI;
|
||||||
|
|
||||||
|
// Int32Array存储
|
||||||
|
@Int32
|
||||||
|
public integerValue: number = 42;
|
||||||
|
|
||||||
|
// 布尔值(默认Float32Array)
|
||||||
|
public flag: boolean = true;
|
||||||
|
|
||||||
|
// 字符串(专门数组)
|
||||||
|
public text: string = 'hello';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SoA数值类型装饰器测试', () => {
|
||||||
|
let manager: ComponentStorageManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new ComponentStorageManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证不同装饰器的存储类型', () => {
|
||||||
|
console.log('\\n=== 测试装饰器存储类型 ===');
|
||||||
|
|
||||||
|
const component = new DecoratedComponent();
|
||||||
|
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
|
||||||
|
component.preciseFloat = Math.PI;
|
||||||
|
component.integerValue = 999999;
|
||||||
|
component.normalFloat = 2.718;
|
||||||
|
|
||||||
|
console.log('原始数据:', {
|
||||||
|
normalFloat: component.normalFloat,
|
||||||
|
highPrecisionNumber: component.highPrecisionNumber,
|
||||||
|
preciseFloat: component.preciseFloat,
|
||||||
|
integerValue: component.integerValue,
|
||||||
|
flag: component.flag,
|
||||||
|
text: component.text
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, DecoratedComponent);
|
||||||
|
|
||||||
|
console.log('\\n取回数据:', {
|
||||||
|
normalFloat: retrieved?.normalFloat,
|
||||||
|
highPrecisionNumber: retrieved?.highPrecisionNumber,
|
||||||
|
preciseFloat: retrieved?.preciseFloat,
|
||||||
|
integerValue: retrieved?.integerValue,
|
||||||
|
flag: retrieved?.flag,
|
||||||
|
text: retrieved?.text
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证精度保持
|
||||||
|
expect(retrieved?.normalFloat).toBeCloseTo(2.718, 5); // Float32精度
|
||||||
|
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER); // 高精度保持
|
||||||
|
expect(retrieved?.preciseFloat).toBeCloseTo(Math.PI, 15); // Float64精度
|
||||||
|
expect(retrieved?.integerValue).toBe(999999); // 整数保持
|
||||||
|
expect(retrieved?.flag).toBe(true);
|
||||||
|
expect(retrieved?.text).toBe('hello');
|
||||||
|
|
||||||
|
console.log('✅ 所有装饰器类型验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证存储器内部结构', () => {
|
||||||
|
console.log('\\n=== 测试存储器内部结构 ===');
|
||||||
|
|
||||||
|
const component = new DecoratedComponent();
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
|
||||||
|
const storage = manager.getStorage(DecoratedComponent) as SoAStorage<DecoratedComponent>;
|
||||||
|
|
||||||
|
// 检查TypedArray字段
|
||||||
|
const normalFloatArray = storage.getFieldArray('normalFloat');
|
||||||
|
const preciseFloatArray = storage.getFieldArray('preciseFloat');
|
||||||
|
const integerArray = storage.getFieldArray('integerValue');
|
||||||
|
const flagArray = storage.getFieldArray('flag');
|
||||||
|
|
||||||
|
console.log('存储类型:', {
|
||||||
|
normalFloat: normalFloatArray?.constructor.name,
|
||||||
|
preciseFloat: preciseFloatArray?.constructor.name,
|
||||||
|
integerValue: integerArray?.constructor.name,
|
||||||
|
flag: flagArray?.constructor.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证存储类型
|
||||||
|
expect(normalFloatArray).toBeInstanceOf(Float32Array);
|
||||||
|
expect(preciseFloatArray).toBeInstanceOf(Float64Array);
|
||||||
|
expect(integerArray).toBeInstanceOf(Int32Array);
|
||||||
|
expect(flagArray).toBeInstanceOf(Float32Array);
|
||||||
|
|
||||||
|
// 高精度字段不应该在TypedArray中
|
||||||
|
const highPrecisionArray = storage.getFieldArray('highPrecisionNumber');
|
||||||
|
expect(highPrecisionArray).toBeNull();
|
||||||
|
|
||||||
|
console.log('✅ 存储器内部结构验证通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('测试边界值精度', () => {
|
||||||
|
console.log('\\n=== 测试边界值精度 ===');
|
||||||
|
|
||||||
|
const component = new DecoratedComponent();
|
||||||
|
|
||||||
|
// 测试极限值
|
||||||
|
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
|
||||||
|
component.preciseFloat = Number.MIN_VALUE;
|
||||||
|
component.normalFloat = 16777217; // 超出Float32精度
|
||||||
|
component.integerValue = -2147483648; // Int32最小值
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, DecoratedComponent);
|
||||||
|
|
||||||
|
console.log('边界值测试结果:', {
|
||||||
|
highPrecision: retrieved?.highPrecisionNumber === Number.MAX_SAFE_INTEGER,
|
||||||
|
preciseFloat: retrieved?.preciseFloat === Number.MIN_VALUE,
|
||||||
|
normalFloat: retrieved?.normalFloat, // 可能有精度损失
|
||||||
|
integerValue: retrieved?.integerValue === -2147483648
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证高精度保持
|
||||||
|
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
|
||||||
|
expect(retrieved?.preciseFloat).toBe(Number.MIN_VALUE);
|
||||||
|
expect(retrieved?.integerValue).toBe(-2147483648);
|
||||||
|
|
||||||
|
console.log('✅ 边界值精度测试通过');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('性能对比:装饰器 vs 自动检测', () => {
|
||||||
|
console.log('\\n=== 性能对比测试 ===');
|
||||||
|
|
||||||
|
const entityCount = 1000;
|
||||||
|
|
||||||
|
// 使用装饰器的组件
|
||||||
|
const startTime = performance.now();
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const component = new DecoratedComponent();
|
||||||
|
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
|
||||||
|
component.preciseFloat = Math.PI * i;
|
||||||
|
component.integerValue = i;
|
||||||
|
manager.addComponent(i, component);
|
||||||
|
}
|
||||||
|
const decoratorTime = performance.now() - startTime;
|
||||||
|
|
||||||
|
console.log(`装饰器方式: ${decoratorTime.toFixed(2)}ms`);
|
||||||
|
console.log(`平均每个组件: ${(decoratorTime / entityCount).toFixed(4)}ms`);
|
||||||
|
|
||||||
|
// 验证数据完整性
|
||||||
|
const sample = manager.getComponent(500, DecoratedComponent);
|
||||||
|
expect(sample?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
|
||||||
|
expect(sample?.integerValue).toBe(500);
|
||||||
|
|
||||||
|
console.log('✅ 性能测试完成,数据完整性验证通过');
|
||||||
|
});
|
||||||
|
});
|
||||||
128
tests/ECS/Core/SoAStorage.edge-case.test.ts
Normal file
128
tests/ECS/Core/SoAStorage.edge-case.test.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ComponentStorageManager, EnableSoA } from '../../../src/ECS/Core/ComponentStorage';
|
||||||
|
|
||||||
|
// 模拟复杂对象(如cocos的node节点)
|
||||||
|
class MockNode {
|
||||||
|
public name: string;
|
||||||
|
public active: boolean;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
this.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString() {
|
||||||
|
return `Node(${this.name})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包含复杂属性的组件
|
||||||
|
@EnableSoA
|
||||||
|
class ProblematicComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public node: MockNode | null = null;
|
||||||
|
public callback: Function | null = null;
|
||||||
|
public data: any = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.node = new MockNode('test');
|
||||||
|
this.callback = () => console.log('test');
|
||||||
|
this.data = { complex: 'object' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全的数值组件
|
||||||
|
@EnableSoA
|
||||||
|
class SafeComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public active: boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SoA边界情况和复杂属性测试', () => {
|
||||||
|
let manager: ComponentStorageManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new ComponentStorageManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('包含复杂对象的组件会有什么问题', () => {
|
||||||
|
console.log('\\n=== 测试复杂对象处理 ===');
|
||||||
|
|
||||||
|
// 创建包含复杂属性的组件
|
||||||
|
const originalComponent = new ProblematicComponent();
|
||||||
|
console.log('原始组件:', {
|
||||||
|
x: originalComponent.x,
|
||||||
|
y: originalComponent.y,
|
||||||
|
node: originalComponent.node?.name,
|
||||||
|
callback: typeof originalComponent.callback,
|
||||||
|
data: originalComponent.data
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加到SoA存储
|
||||||
|
manager.addComponent(1, originalComponent);
|
||||||
|
|
||||||
|
// 获取组件看看发生了什么
|
||||||
|
const retrievedComponent = manager.getComponent(1, ProblematicComponent);
|
||||||
|
console.log('取回的组件:', {
|
||||||
|
x: retrievedComponent?.x,
|
||||||
|
y: retrievedComponent?.y,
|
||||||
|
node: retrievedComponent?.node,
|
||||||
|
callback: retrievedComponent?.callback,
|
||||||
|
data: retrievedComponent?.data
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证数据完整性
|
||||||
|
expect(retrievedComponent?.x).toBe(0);
|
||||||
|
expect(retrievedComponent?.y).toBe(0);
|
||||||
|
|
||||||
|
// 复杂对象的问题
|
||||||
|
console.log('\\n⚠️ 问题发现:');
|
||||||
|
console.log('- node对象:', retrievedComponent?.node);
|
||||||
|
console.log('- callback函数:', retrievedComponent?.callback);
|
||||||
|
console.log('- data对象:', retrievedComponent?.data);
|
||||||
|
|
||||||
|
// 复杂属性现在应该正确保存
|
||||||
|
expect(retrievedComponent?.node?.name).toBe('test'); // 应该保持原始值
|
||||||
|
expect(retrievedComponent?.callback).toBe(originalComponent.callback); // 应该是同一个函数
|
||||||
|
expect(retrievedComponent?.data).toEqual({ complex: 'object' }); // 应该保持原始数据
|
||||||
|
|
||||||
|
console.log('✅ 修复成功:复杂对象现在能正确处理!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('纯数值组件工作正常', () => {
|
||||||
|
console.log('\\n=== 测试纯数值组件 ===');
|
||||||
|
|
||||||
|
const safeComponent = new SafeComponent();
|
||||||
|
safeComponent.x = 100;
|
||||||
|
safeComponent.y = 200;
|
||||||
|
safeComponent.active = false;
|
||||||
|
|
||||||
|
manager.addComponent(1, safeComponent);
|
||||||
|
const retrieved = manager.getComponent(1, SafeComponent);
|
||||||
|
|
||||||
|
console.log('纯数值组件正常工作:', {
|
||||||
|
x: retrieved?.x,
|
||||||
|
y: retrieved?.y,
|
||||||
|
active: retrieved?.active
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved?.x).toBe(100);
|
||||||
|
expect(retrieved?.y).toBe(200);
|
||||||
|
expect(retrieved?.active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SoA是否能检测到不适合的组件类型', () => {
|
||||||
|
console.log('\\n=== 测试类型检测 ===');
|
||||||
|
|
||||||
|
// 当前实现会静默忽略复杂字段
|
||||||
|
// 这是一个潜在的问题!
|
||||||
|
const storage = manager.getStorage(ProblematicComponent);
|
||||||
|
console.log('存储类型:', storage.constructor.name);
|
||||||
|
|
||||||
|
// SoA存储应该能警告或拒绝不适合的组件
|
||||||
|
expect(storage.constructor.name).toBe('SoAStorage');
|
||||||
|
});
|
||||||
|
});
|
||||||
158
tests/ECS/Core/SoAStorage.types.test.ts
Normal file
158
tests/ECS/Core/SoAStorage.types.test.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ComponentStorageManager, EnableSoA, HighPrecision, Float64 } from '../../../src/ECS/Core/ComponentStorage';
|
||||||
|
|
||||||
|
// 包含所有基础类型的组件
|
||||||
|
@EnableSoA
|
||||||
|
class AllTypesComponent extends Component {
|
||||||
|
// 数值类型
|
||||||
|
public intNumber: number = 42;
|
||||||
|
public floatNumber: number = 3.14;
|
||||||
|
public zeroNumber: number = 0;
|
||||||
|
|
||||||
|
// 布尔类型
|
||||||
|
public trueBoolean: boolean = true;
|
||||||
|
public falseBoolean: boolean = false;
|
||||||
|
|
||||||
|
// 字符串类型
|
||||||
|
public emptyString: string = '';
|
||||||
|
public normalString: string = 'hello';
|
||||||
|
public longString: string = 'this is a long string with spaces and 123 numbers!';
|
||||||
|
|
||||||
|
// 其他基础类型
|
||||||
|
public nullValue: null = null;
|
||||||
|
public undefinedValue: undefined = undefined;
|
||||||
|
|
||||||
|
// 复杂类型
|
||||||
|
public arrayValue: number[] = [1, 2, 3];
|
||||||
|
public objectValue: { name: string } = { name: 'test' };
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 边界测试专用组件
|
||||||
|
@EnableSoA
|
||||||
|
class BoundaryTestComponent extends Component {
|
||||||
|
// 高精度大整数
|
||||||
|
@HighPrecision
|
||||||
|
public maxInt: number = 0;
|
||||||
|
|
||||||
|
// 高精度小浮点数
|
||||||
|
@Float64
|
||||||
|
public minFloat: number = 0;
|
||||||
|
|
||||||
|
// 普通数值
|
||||||
|
public normalNumber: number = 0;
|
||||||
|
|
||||||
|
// 字符串测试
|
||||||
|
public testString: string = '';
|
||||||
|
public longString: string = '';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SoA所有数据类型处理测试', () => {
|
||||||
|
let manager: ComponentStorageManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new ComponentStorageManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('验证所有基础类型的处理', () => {
|
||||||
|
console.log('\\n=== 测试所有数据类型 ===');
|
||||||
|
|
||||||
|
// 创建包含各种类型的组件
|
||||||
|
const originalComponent = new AllTypesComponent();
|
||||||
|
originalComponent.normalString = 'modified string';
|
||||||
|
originalComponent.longString = '测试中文字符串 with emoji 🎉';
|
||||||
|
originalComponent.intNumber = 999;
|
||||||
|
originalComponent.floatNumber = 2.718;
|
||||||
|
originalComponent.trueBoolean = false;
|
||||||
|
originalComponent.falseBoolean = true;
|
||||||
|
|
||||||
|
console.log('原始组件数据:', {
|
||||||
|
intNumber: originalComponent.intNumber,
|
||||||
|
floatNumber: originalComponent.floatNumber,
|
||||||
|
trueBoolean: originalComponent.trueBoolean,
|
||||||
|
falseBoolean: originalComponent.falseBoolean,
|
||||||
|
emptyString: `"${originalComponent.emptyString}"`,
|
||||||
|
normalString: `"${originalComponent.normalString}"`,
|
||||||
|
longString: `"${originalComponent.longString}"`,
|
||||||
|
arrayValue: originalComponent.arrayValue,
|
||||||
|
objectValue: originalComponent.objectValue
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存储到SoA
|
||||||
|
manager.addComponent(1, originalComponent);
|
||||||
|
|
||||||
|
// 获取并验证
|
||||||
|
const retrievedComponent = manager.getComponent(1, AllTypesComponent);
|
||||||
|
|
||||||
|
console.log('\\n取回的组件数据:', {
|
||||||
|
intNumber: retrievedComponent?.intNumber,
|
||||||
|
floatNumber: retrievedComponent?.floatNumber,
|
||||||
|
trueBoolean: retrievedComponent?.trueBoolean,
|
||||||
|
falseBoolean: retrievedComponent?.falseBoolean,
|
||||||
|
emptyString: `"${retrievedComponent?.emptyString}"`,
|
||||||
|
normalString: `"${retrievedComponent?.normalString}"`,
|
||||||
|
longString: `"${retrievedComponent?.longString}"`,
|
||||||
|
arrayValue: retrievedComponent?.arrayValue,
|
||||||
|
objectValue: retrievedComponent?.objectValue
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证数值类型
|
||||||
|
expect(retrievedComponent?.intNumber).toBe(999);
|
||||||
|
expect(retrievedComponent?.floatNumber).toBeCloseTo(2.718);
|
||||||
|
|
||||||
|
// 验证布尔类型
|
||||||
|
expect(retrievedComponent?.trueBoolean).toBe(false);
|
||||||
|
expect(retrievedComponent?.falseBoolean).toBe(true);
|
||||||
|
|
||||||
|
// 验证字符串类型
|
||||||
|
expect(retrievedComponent?.emptyString).toBe('');
|
||||||
|
expect(retrievedComponent?.normalString).toBe('modified string');
|
||||||
|
expect(retrievedComponent?.longString).toBe('测试中文字符串 with emoji 🎉');
|
||||||
|
|
||||||
|
// 验证复杂类型
|
||||||
|
expect(retrievedComponent?.arrayValue).toEqual([1, 2, 3]);
|
||||||
|
expect(retrievedComponent?.objectValue).toEqual({ name: 'test' });
|
||||||
|
|
||||||
|
console.log('\\n✅ 所有类型验证完成');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('边界情况测试', () => {
|
||||||
|
console.log('\\n=== 边界情况测试 ===');
|
||||||
|
|
||||||
|
const component = new BoundaryTestComponent();
|
||||||
|
|
||||||
|
// 特殊数值
|
||||||
|
component.maxInt = Number.MAX_SAFE_INTEGER;
|
||||||
|
component.minFloat = Number.MIN_VALUE;
|
||||||
|
component.normalNumber = -0;
|
||||||
|
|
||||||
|
// 特殊字符串
|
||||||
|
component.testString = '\\n\\t\\r"\'\\\\'; // 转义字符
|
||||||
|
component.longString = 'a'.repeat(1000); // 长字符串
|
||||||
|
|
||||||
|
manager.addComponent(1, component);
|
||||||
|
const retrieved = manager.getComponent(1, BoundaryTestComponent);
|
||||||
|
|
||||||
|
console.log('边界情况结果:', {
|
||||||
|
maxInt: retrieved?.maxInt,
|
||||||
|
minFloat: retrieved?.minFloat,
|
||||||
|
negativeZero: retrieved?.normalNumber,
|
||||||
|
escapeStr: retrieved?.testString,
|
||||||
|
longStr: retrieved?.longString?.length
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved?.maxInt).toBe(Number.MAX_SAFE_INTEGER);
|
||||||
|
expect(retrieved?.minFloat).toBe(Number.MIN_VALUE);
|
||||||
|
expect(retrieved?.testString).toBe('\\n\\t\\r"\'\\\\');
|
||||||
|
expect(retrieved?.longString).toBe('a'.repeat(1000));
|
||||||
|
|
||||||
|
console.log('✅ 边界情况测试通过');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user