From aa33cad4fa6de24bb6aa2e3a55fef0d905767271 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 30 Sep 2025 11:00:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=A9=E5=B1=95typedarray=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=EF=BC=8C=E5=85=81=E8=AE=B8=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=8E=A8=E6=96=AD@AutoTyped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/ECS/Core/ComponentStorage.ts | 13 +- packages/core/src/ECS/Core/SoAStorage.ts | 327 +++++++++++++++++- .../core/src/ECS/Core/StorageDecorators.ts | 49 +++ packages/core/src/ECS/index.ts | 3 +- .../tests/ECS/Core/AutoTypeInference.test.ts | 219 ++++++++++++ .../Core/EnhancedTypedArraySupport.test.ts | 214 ++++++++++++ 6 files changed, 801 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/ECS/Core/StorageDecorators.ts create mode 100644 packages/core/tests/ECS/Core/AutoTypeInference.test.ts create mode 100644 packages/core/tests/ECS/Core/EnhancedTypedArraySupport.test.ts diff --git a/packages/core/src/ECS/Core/ComponentStorage.ts b/packages/core/src/ECS/Core/ComponentStorage.ts index 45c43690..04f5c254 100644 --- a/packages/core/src/ECS/Core/ComponentStorage.ts +++ b/packages/core/src/ECS/Core/ComponentStorage.ts @@ -1,12 +1,11 @@ import { Component } from '../Component'; import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility'; -import { SoAStorage, EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from './SoAStorage'; +import { SoAStorage, SupportedTypedArray } from './SoAStorage'; import { createLogger } from '../../Utils/Logger'; import { getComponentTypeName } from '../Decorators'; import { ComponentRegistry, ComponentType } from './ComponentStorage/ComponentRegistry'; -// 重新导出装饰器和核心类型 -export { EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy }; +// 导出核心类型 export { ComponentRegistry, ComponentType }; @@ -203,9 +202,9 @@ export class ComponentStorageManager { * @returns TypedArray或null */ public getFieldArray( - componentType: ComponentType, + componentType: ComponentType, fieldName: string - ): Float32Array | Float64Array | Int32Array | null { + ): SupportedTypedArray | null { const soaStorage = this.getSoAStorage(componentType); return soaStorage ? soaStorage.getFieldArray(fieldName) : null; } @@ -217,9 +216,9 @@ export class ComponentStorageManager { * @returns TypedArray或null */ public getTypedFieldArray( - componentType: ComponentType, + componentType: ComponentType, fieldName: K - ): Float32Array | Float64Array | Int32Array | null { + ): SupportedTypedArray | null { const soaStorage = this.getSoAStorage(componentType); return soaStorage ? soaStorage.getTypedFieldArray(fieldName) : null; } diff --git a/packages/core/src/ECS/Core/SoAStorage.ts b/packages/core/src/ECS/Core/SoAStorage.ts index 00d776d8..0f4b826c 100644 --- a/packages/core/src/ECS/Core/SoAStorage.ts +++ b/packages/core/src/ECS/Core/SoAStorage.ts @@ -60,6 +60,78 @@ export function Int32(target: any, propertyKey: string | symbol): void { target.constructor.__int32Fields.add(key); } +/** + * 32位无符号整数装饰器 + * 标记字段使用Uint32Array存储(适用于无符号整数,如ID、标志位等) + */ +export function Uint32(target: any, propertyKey: string | symbol): void { + const key = String(propertyKey); + if (!target.constructor.__uint32Fields) { + target.constructor.__uint32Fields = new Set(); + } + target.constructor.__uint32Fields.add(key); +} + +/** + * 16位整数装饰器 + * 标记字段使用Int16Array存储(适用于小范围整数) + */ +export function Int16(target: any, propertyKey: string | symbol): void { + const key = String(propertyKey); + if (!target.constructor.__int16Fields) { + target.constructor.__int16Fields = new Set(); + } + target.constructor.__int16Fields.add(key); +} + +/** + * 16位无符号整数装饰器 + * 标记字段使用Uint16Array存储(适用于小范围无符号整数) + */ +export function Uint16(target: any, propertyKey: string | symbol): void { + const key = String(propertyKey); + if (!target.constructor.__uint16Fields) { + target.constructor.__uint16Fields = new Set(); + } + target.constructor.__uint16Fields.add(key); +} + +/** + * 8位整数装饰器 + * 标记字段使用Int8Array存储(适用于很小的整数值) + */ +export function Int8(target: any, propertyKey: string | symbol): void { + const key = String(propertyKey); + if (!target.constructor.__int8Fields) { + target.constructor.__int8Fields = new Set(); + } + target.constructor.__int8Fields.add(key); +} + +/** + * 8位无符号整数装饰器 + * 标记字段使用Uint8Array存储(适用于字节值、布尔标志等) + */ +export function Uint8(target: any, propertyKey: string | symbol): void { + const key = String(propertyKey); + if (!target.constructor.__uint8Fields) { + target.constructor.__uint8Fields = new Set(); + } + target.constructor.__uint8Fields.add(key); +} + +/** + * 8位夹紧整数装饰器 + * 标记字段使用Uint8ClampedArray存储(适用于颜色值等需要夹紧的数据) + */ +export function Uint8Clamped(target: any, propertyKey: string | symbol): void { + const key = String(propertyKey); + if (!target.constructor.__uint8ClampedFields) { + target.constructor.__uint8ClampedFields = new Set(); + } + target.constructor.__uint8ClampedFields.add(key); +} + /** * 序列化Map装饰器 @@ -109,13 +181,161 @@ export function DeepCopy(target: any, propertyKey: string | symbol): void { target.constructor.__deepCopyFields.add(key); } +/** + * 自动类型推断装饰器 + * 根据字段的默认值和数值范围自动选择最优的TypedArray类型 + * + * @param options 类型推断选项 + * @param options.minValue 数值的最小值(用于范围优化) + * @param options.maxValue 数值的最大值(用于范围优化) + * @param options.precision 是否需要浮点精度(true: 使用浮点数组, false: 使用整数数组) + * @param options.signed 是否需要符号位(仅在整数模式下有效) + */ +export function AutoTyped(options?: { + minValue?: number; + maxValue?: number; + precision?: boolean; + signed?: boolean; +}) { + return function (target: any, propertyKey: string | symbol): void { + const key = String(propertyKey); + if (!target.constructor.__autoTypedFields) { + target.constructor.__autoTypedFields = new Map(); + } + target.constructor.__autoTypedFields.set(key, options || {}); + }; +} + +/** + * 自动类型推断器 + * 根据数值类型和范围自动选择最优的TypedArray类型 + */ +export class TypeInference { + /** + * 根据数值范围推断最优的TypedArray类型 + */ + public static inferOptimalType(value: any, options: { + minValue?: number; + maxValue?: number; + precision?: boolean; + signed?: boolean; + } = {}): string { + const type = typeof value; + + if (type === 'boolean') { + return 'uint8'; // 布尔值使用最小的无符号整数 + } + + if (type !== 'number') { + return 'float32'; // 非数值类型默认使用Float32 + } + + const { minValue, maxValue, precision, signed } = options; + + // 如果显式要求精度,使用浮点数 + if (precision === true) { + // 检查是否需要双精度 + if (Math.abs(value) > 3.4028235e+38 || (minValue !== undefined && Math.abs(minValue) > 3.4028235e+38) || (maxValue !== undefined && Math.abs(maxValue) > 3.4028235e+38)) { + return 'float64'; + } + return 'float32'; + } + + // 如果显式禁用精度,或者是整数值,尝试使用整数数组 + if (precision === false || Number.isInteger(value)) { + const actualMin = minValue !== undefined ? minValue : value; + const actualMax = maxValue !== undefined ? maxValue : value; + const needsSigned = signed !== false && (actualMin < 0 || value < 0); + + // 根据范围选择最小的整数类型 + if (needsSigned) { + // 有符号整数 + if (actualMin >= -128 && actualMax <= 127) { + return 'int8'; + } else if (actualMin >= -32768 && actualMax <= 32767) { + return 'int16'; + } else if (actualMin >= -2147483648 && actualMax <= 2147483647) { + return 'int32'; + } else { + return 'float64'; // 超出int32范围,使用双精度浮点 + } + } else { + // 无符号整数 + if (actualMax <= 255) { + return 'uint8'; + } else if (actualMax <= 65535) { + return 'uint16'; + } else if (actualMax <= 4294967295) { + return 'uint32'; + } else { + return 'float64'; // 超出uint32范围,使用双精度浮点 + } + } + } + + // 默认情况:检查是否为小数 + if (!Number.isInteger(value)) { + return 'float32'; + } + + // 整数值,但没有指定范围,根据值的大小选择 + if (value >= 0 && value <= 255) { + return 'uint8'; + } else if (value >= -128 && value <= 127) { + return 'int8'; + } else if (value >= 0 && value <= 65535) { + return 'uint16'; + } else if (value >= -32768 && value <= 32767) { + return 'int16'; + } else if (value >= 0 && value <= 4294967295) { + return 'uint32'; + } else if (value >= -2147483648 && value <= 2147483647) { + return 'int32'; + } else { + return 'float64'; + } + } + + /** + * 根据推断的类型名创建对应的TypedArray构造函数 + */ + public static getTypedArrayConstructor(typeName: string): typeof Float32Array | typeof Float64Array | typeof Int32Array | typeof Uint32Array | typeof Int16Array | typeof Uint16Array | typeof Int8Array | typeof Uint8Array | typeof Uint8ClampedArray { + switch (typeName) { + case 'float32': return Float32Array; + case 'float64': return Float64Array; + case 'int32': return Int32Array; + case 'uint32': return Uint32Array; + case 'int16': return Int16Array; + case 'uint16': return Uint16Array; + case 'int8': return Int8Array; + case 'uint8': return Uint8Array; + case 'uint8clamped': return Uint8ClampedArray; + default: return Float32Array; + } + } +} + +/** + * SoA存储器支持的TypedArray类型 + */ +export type SupportedTypedArray = + | Float32Array + | Float64Array + | Int32Array + | Uint32Array + | Int16Array + | Uint16Array + | Int8Array + | Uint8Array + | Uint8ClampedArray; + /** * SoA存储器(需要装饰器启用) * 使用Structure of Arrays存储模式,在大规模批量操作时提供优异性能 */ export class SoAStorage { private static readonly _logger = createLogger('SoAStorage'); - private fields = new Map(); + private fields = new Map(); private stringFields = new Map(); // 专门存储字符串 private serializedFields = new Map(); // 序列化存储Map/Set/Array private complexFields = new Map>(); // 存储复杂对象 @@ -137,6 +357,13 @@ export class SoAStorage { const float64Fields = (componentType as any).__float64Fields || new Set(); const float32Fields = (componentType as any).__float32Fields || new Set(); const int32Fields = (componentType as any).__int32Fields || new Set(); + const uint32Fields = (componentType as any).__uint32Fields || new Set(); + const int16Fields = (componentType as any).__int16Fields || new Set(); + const uint16Fields = (componentType as any).__uint16Fields || new Set(); + const int8Fields = (componentType as any).__int8Fields || new Set(); + const uint8Fields = (componentType as any).__uint8Fields || new Set(); + const uint8ClampedFields = (componentType as any).__uint8ClampedFields || new Set(); + const autoTypedFields = (componentType as any).__autoTypedFields || new Map(); const serializeMapFields = (componentType as any).__serializeMapFields || new Set(); const serializeSetFields = (componentType as any).__serializeSetFields || new Set(); const serializeArrayFields = (componentType as any).__serializeArrayFields || new Set(); @@ -151,22 +378,52 @@ export class SoAStorage { if (highPrecisionFields.has(key)) { // 标记为高精度,作为复杂对象处理 // 不添加到fields,会在updateComponentAtIndex中自动添加到complexFields + } else if (autoTypedFields.has(key)) { + // 使用自动类型推断 + const options = autoTypedFields.get(key); + const inferredType = TypeInference.inferOptimalType(value, options); + const ArrayConstructor = TypeInference.getTypedArrayConstructor(inferredType); + this.fields.set(key, new ArrayConstructor(this._capacity)); + SoAStorage._logger.info(`字段 ${key} 自动推断为 ${inferredType} 类型,值: ${value}, 选项:`, options); } else if (float64Fields.has(key)) { - // 使用Float64Array存储 + // 使用Float64Array存储(高精度浮点数) this.fields.set(key, new Float64Array(this._capacity)); } else if (int32Fields.has(key)) { - // 使用Int32Array存储 + // 使用Int32Array存储(32位有符号整数) this.fields.set(key, new Int32Array(this._capacity)); + } else if (uint32Fields.has(key)) { + // 使用Uint32Array存储(32位无符号整数) + this.fields.set(key, new Uint32Array(this._capacity)); + } else if (int16Fields.has(key)) { + // 使用Int16Array存储(16位有符号整数) + this.fields.set(key, new Int16Array(this._capacity)); + } else if (uint16Fields.has(key)) { + // 使用Uint16Array存储(16位无符号整数) + this.fields.set(key, new Uint16Array(this._capacity)); + } else if (int8Fields.has(key)) { + // 使用Int8Array存储(8位有符号整数) + this.fields.set(key, new Int8Array(this._capacity)); + } else if (uint8Fields.has(key)) { + // 使用Uint8Array存储(8位无符号整数) + this.fields.set(key, new Uint8Array(this._capacity)); + } else if (uint8ClampedFields.has(key)) { + // 使用Uint8ClampedArray存储(8位夹紧无符号整数) + this.fields.set(key, new Uint8ClampedArray(this._capacity)); } else if (float32Fields.has(key)) { - // 使用Float32Array存储 + // 使用Float32Array存储(32位浮点数) this.fields.set(key, new Float32Array(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)); + // 布尔值默认使用Uint8Array存储为0/1(更节省内存) + if (uint8Fields.has(key) || (!float32Fields.has(key) && !float64Fields.has(key))) { + this.fields.set(key, new Uint8Array(this._capacity)); + } else { + // 兼容性:如果显式指定浮点类型则使用原有方式 + this.fields.set(key, new Float32Array(this._capacity)); + } } else if (type === 'string') { // 字符串专门处理 this.stringFields.set(key, new Array(this._capacity)); @@ -430,16 +687,32 @@ export class SoAStorage { private resize(newCapacity: number): void { // 调整数值字段的TypedArray for (const [fieldName, oldArray] of this.fields.entries()) { - let newArray: Float32Array | Float64Array | Int32Array; - + let newArray: SupportedTypedArray; + if (oldArray instanceof Float32Array) { newArray = new Float32Array(newCapacity); } else if (oldArray instanceof Float64Array) { newArray = new Float64Array(newCapacity); - } else { + } else if (oldArray instanceof Int32Array) { newArray = new Int32Array(newCapacity); + } else if (oldArray instanceof Uint32Array) { + newArray = new Uint32Array(newCapacity); + } else if (oldArray instanceof Int16Array) { + newArray = new Int16Array(newCapacity); + } else if (oldArray instanceof Uint16Array) { + newArray = new Uint16Array(newCapacity); + } else if (oldArray instanceof Int8Array) { + newArray = new Int8Array(newCapacity); + } else if (oldArray instanceof Uint8Array) { + newArray = new Uint8Array(newCapacity); + } else if (oldArray instanceof Uint8ClampedArray) { + newArray = new Uint8ClampedArray(newCapacity); + } else { + // 默认回退到Float32Array + newArray = new Float32Array(newCapacity); + SoAStorage._logger.warn(`未知的TypedArray类型用于字段 ${fieldName},回退到Float32Array`); } - + newArray.set(oldArray); this.fields.set(fieldName, newArray); } @@ -469,11 +742,11 @@ export class SoAStorage { return Array.from(this.entityToIndex.values()); } - public getFieldArray(fieldName: string): Float32Array | Float64Array | Int32Array | null { + public getFieldArray(fieldName: string): SupportedTypedArray | null { return this.fields.get(fieldName) || null; } - - public getTypedFieldArray(fieldName: K): Float32Array | Float64Array | Int32Array | null { + + public getTypedFieldArray(fieldName: K): SupportedTypedArray | null { return this.fields.get(String(fieldName)) || null; } @@ -566,16 +839,38 @@ export class SoAStorage { 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 { + } else if (array instanceof Int32Array) { bytesPerElement = 4; typeName = 'int32'; + } else if (array instanceof Uint32Array) { + bytesPerElement = 4; + typeName = 'uint32'; + } else if (array instanceof Int16Array) { + bytesPerElement = 2; + typeName = 'int16'; + } else if (array instanceof Uint16Array) { + bytesPerElement = 2; + typeName = 'uint16'; + } else if (array instanceof Int8Array) { + bytesPerElement = 1; + typeName = 'int8'; + } else if (array instanceof Uint8Array) { + bytesPerElement = 1; + typeName = 'uint8'; + } else if (array instanceof Uint8ClampedArray) { + bytesPerElement = 1; + typeName = 'uint8clamped'; + } else { + // 默认回退 + bytesPerElement = 4; + typeName = 'unknown'; } const memory = array.length * bytesPerElement; @@ -603,7 +898,7 @@ export class SoAStorage { * 执行向量化批量操作 * @param operation 操作函数,接收字段数组和活跃索引 */ - public performVectorizedOperation(operation: (fieldArrays: Map, activeIndices: number[]) => void): void { + public performVectorizedOperation(operation: (fieldArrays: Map, activeIndices: number[]) => void): void { const activeIndices = this.getActiveIndices(); operation(this.fields, activeIndices); } diff --git a/packages/core/src/ECS/Core/StorageDecorators.ts b/packages/core/src/ECS/Core/StorageDecorators.ts new file mode 100644 index 00000000..b613db8f --- /dev/null +++ b/packages/core/src/ECS/Core/StorageDecorators.ts @@ -0,0 +1,49 @@ +/** + * 统一的存储装饰器导出文件 + * + * 用户可以从这里导入所有的SoA存储装饰器,而不需要知道内部实现细节 + * + * @example + * ```typescript + * import { EnableSoA, Float32, Int16, Uint8 } from './ECS/Core/StorageDecorators'; + * + * @EnableSoA + * class TransformComponent extends Component { + * @Float32 x: number = 0; + * @Float32 y: number = 0; + * @Int16 layer: number = 0; + * @Uint8 visible: boolean = true; + * } + * ``` + */ + +// 从SoAStorage导入所有装饰器和类型 +export { + // 启用装饰器 + EnableSoA, + + // 数值类型装饰器 + HighPrecision, + Float64, + Float32, + Int32, + Uint32, + Int16, + Uint16, + Int8, + Uint8, + Uint8Clamped, + + // 自动类型推断 + AutoTyped, + TypeInference, + + // 序列化装饰器 + SerializeMap, + SerializeSet, + SerializeArray, + DeepCopy, + + // 类型定义 + SupportedTypedArray +} from './SoAStorage'; \ No newline at end of file diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index d7de5eb7..25a5eaa5 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -10,4 +10,5 @@ export { World, IWorldConfig } from './World'; export { WorldManager, IWorldManagerConfig } from './WorldManager'; export * from './Core/Events'; export * from './Core/Query'; -export * from './Core/Storage'; \ No newline at end of file +export * from './Core/Storage'; +export * from './Core/StorageDecorators'; \ No newline at end of file diff --git a/packages/core/tests/ECS/Core/AutoTypeInference.test.ts b/packages/core/tests/ECS/Core/AutoTypeInference.test.ts new file mode 100644 index 00000000..428cd65c --- /dev/null +++ b/packages/core/tests/ECS/Core/AutoTypeInference.test.ts @@ -0,0 +1,219 @@ +import { Component } from '../../../src/ECS/Component'; +import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage'; +import { EnableSoA, AutoTyped, TypeInference } from '../../../src/ECS/Core/StorageDecorators'; + +// 测试自动类型推断 +@EnableSoA +class AutoInferredComponent extends Component { + // 基于默认值的自动推断 + @AutoTyped() health: number = 100; // 应该推断为uint8 + @AutoTyped() score: number = 1000000; // 应该推断为uint32 + @AutoTyped() position: number = 3.14; // 应该推断为float32 + @AutoTyped() temperature: number = -40; // 应该推断为int8 + + // 基于范围的精确推断 + @AutoTyped({ minValue: 0, maxValue: 255 }) + level: number = 1; // 应该推断为uint8 + + @AutoTyped({ minValue: -1000, maxValue: 1000 }) + velocity: number = 0; // 应该推断为int16 + + @AutoTyped({ minValue: 0, maxValue: 100000, precision: false }) + experience: number = 0; // 应该推断为uint32(禁用精度) + + @AutoTyped({ precision: true }) + exactValue: number = 42; // 应该推断为float32(强制精度) + + @AutoTyped({ minValue: 0, maxValue: 10, signed: false }) + difficulty: number = 5; // 应该推断为uint8(无符号) +} + +// 传统手动指定类型的组件(用于对比) +@EnableSoA +class ManualTypedComponent extends Component { + health: number = 100; // 默认Float32Array + score: number = 1000000; // 默认Float32Array + position: number = 3.14; // 默认Float32Array + temperature: number = -40; // 默认Float32Array + level: number = 1; // 默认Float32Array + velocity: number = 0; // 默认Float32Array + experience: number = 0; // 默认Float32Array + exactValue: number = 42; // 默认Float32Array + difficulty: number = 5; // 默认Float32Array +} + +describe('AutoType Inference', () => { + let storageManager: ComponentStorageManager; + + beforeEach(() => { + storageManager = new ComponentStorageManager(); + }); + + test('TypeInference.inferOptimalType 应该正确推断基础类型', () => { + // 布尔值 + expect(TypeInference.inferOptimalType(true)).toBe('uint8'); + expect(TypeInference.inferOptimalType(false)).toBe('uint8'); + + // 小整数 + expect(TypeInference.inferOptimalType(100)).toBe('uint8'); + expect(TypeInference.inferOptimalType(-50)).toBe('int8'); + + // 大整数 + expect(TypeInference.inferOptimalType(1000)).toBe('uint16'); + expect(TypeInference.inferOptimalType(-1000)).toBe('int16'); + + // 浮点数 + expect(TypeInference.inferOptimalType(3.14)).toBe('float32'); + expect(TypeInference.inferOptimalType(-3.14)).toBe('float32'); + + // 非数值 + expect(TypeInference.inferOptimalType('string')).toBe('float32'); + expect(TypeInference.inferOptimalType(null)).toBe('float32'); + }); + + test('TypeInference.inferOptimalType 应该根据范围推断类型', () => { + // 指定范围的无符号整数 + expect(TypeInference.inferOptimalType(100, { minValue: 0, maxValue: 255 })).toBe('uint8'); + expect(TypeInference.inferOptimalType(1000, { minValue: 0, maxValue: 65535 })).toBe('uint16'); + + // 指定范围的有符号整数 + expect(TypeInference.inferOptimalType(-50, { minValue: -128, maxValue: 127 })).toBe('int8'); + expect(TypeInference.inferOptimalType(-1000, { minValue: -32768, maxValue: 32767 })).toBe('int16'); + + // 强制使用浮点精度 + expect(TypeInference.inferOptimalType(42, { precision: true })).toBe('float32'); + expect(TypeInference.inferOptimalType(42, { precision: true, maxValue: 1e40 })).toBe('float64'); + + // 强制禁用精度 + expect(TypeInference.inferOptimalType(42.5, { precision: false, minValue: 0, maxValue: 255 })).toBe('uint8'); + }); + + test('应该为自动推断的字段创建正确的TypedArray', () => { + const storage = storageManager.getSoAStorage(AutoInferredComponent); + expect(storage).not.toBeNull(); + + // 验证推断的类型 + expect(storage!.getFieldArray('health')).toBeInstanceOf(Uint8Array); + expect(storage!.getFieldArray('score')).toBeInstanceOf(Uint32Array); + expect(storage!.getFieldArray('position')).toBeInstanceOf(Float32Array); + expect(storage!.getFieldArray('temperature')).toBeInstanceOf(Int8Array); + expect(storage!.getFieldArray('level')).toBeInstanceOf(Uint8Array); + expect(storage!.getFieldArray('velocity')).toBeInstanceOf(Int16Array); + expect(storage!.getFieldArray('experience')).toBeInstanceOf(Uint32Array); + expect(storage!.getFieldArray('exactValue')).toBeInstanceOf(Float32Array); + expect(storage!.getFieldArray('difficulty')).toBeInstanceOf(Uint8Array); + }); + + test('应该正确存储和检索自动推断类型的值', () => { + const entityId = 1; + const component = new AutoInferredComponent(); + + // 设置测试值 + component.health = 255; // Uint8 最大值 + component.score = 4000000000; // 大的Uint32值 + component.position = 3.14159; + component.temperature = -128; // Int8 最小值 + component.level = 200; + component.velocity = -500; + component.experience = 50000; + component.exactValue = Math.PI; + component.difficulty = 10; + + storageManager.addComponent(entityId, component); + const retrieved = storageManager.getComponent(entityId, AutoInferredComponent); + + expect(retrieved).not.toBeNull(); + expect(retrieved!.health).toBe(255); + expect(retrieved!.score).toBe(4000000000); + expect(retrieved!.position).toBeCloseTo(3.14159, 5); + expect(retrieved!.temperature).toBe(-128); + expect(retrieved!.level).toBe(200); + expect(retrieved!.velocity).toBe(-500); + expect(retrieved!.experience).toBe(50000); + expect(retrieved!.exactValue).toBeCloseTo(Math.PI, 5); + expect(retrieved!.difficulty).toBe(10); + }); + + test('自动推断应该比手动类型使用更少内存', () => { + const autoStorage = storageManager.getSoAStorage(AutoInferredComponent); + const manualStorage = storageManager.getSoAStorage(ManualTypedComponent); + + const autoStats = autoStorage!.getStats(); + const manualStats = manualStorage!.getStats(); + + // 自动推断应该使用更少的内存 + expect(autoStats.memoryUsage).toBeLessThan(manualStats.memoryUsage); + + // 验证特定字段的内存使用 + expect(autoStats.fieldStats.get('health').memory).toBe(1000); // Uint8: 1 byte per element + expect(manualStats.fieldStats.get('health').memory).toBe(4000); // Float32: 4 bytes per element + + expect(autoStats.fieldStats.get('temperature').memory).toBe(1000); // Int8: 1 byte per element + expect(manualStats.fieldStats.get('temperature').memory).toBe(4000); // Float32: 4 bytes per element + }); + + test('应该处理边界值', () => { + @EnableSoA + class BoundaryTestComponent extends Component { + @AutoTyped({ minValue: 0, maxValue: 255 }) + maxUint8: number = 255; + + @AutoTyped({ minValue: -128, maxValue: 127 }) + minInt8: number = -128; + + @AutoTyped({ minValue: 0, maxValue: 65535 }) + maxUint16: number = 65535; + + @AutoTyped({ minValue: -32768, maxValue: 32767 }) + minInt16: number = -32768; + } + + const storage = storageManager.getSoAStorage(BoundaryTestComponent); + + expect(storage!.getFieldArray('maxUint8')).toBeInstanceOf(Uint8Array); + expect(storage!.getFieldArray('minInt8')).toBeInstanceOf(Int8Array); + expect(storage!.getFieldArray('maxUint16')).toBeInstanceOf(Uint16Array); + expect(storage!.getFieldArray('minInt16')).toBeInstanceOf(Int16Array); + + const entityId = 1; + const component = new BoundaryTestComponent(); + + storageManager.addComponent(entityId, component); + const retrieved = storageManager.getComponent(entityId, BoundaryTestComponent); + + expect(retrieved!.maxUint8).toBe(255); + expect(retrieved!.minInt8).toBe(-128); + expect(retrieved!.maxUint16).toBe(65535); + expect(retrieved!.minInt16).toBe(-32768); + }); + + test('应该输出推断日志信息', () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + + // 创建新的存储以触发推断 + const newStorageManager = new ComponentStorageManager(); + newStorageManager.getSoAStorage(AutoInferredComponent); + + // 验证日志输出(注意:实际的日志可能通过logger系统输出) + // 这里主要验证不会出错 + expect(() => { + const component = new AutoInferredComponent(); + newStorageManager.addComponent(999, component); + }).not.toThrow(); + + consoleSpy.mockRestore(); + }); + + test('计算内存节省百分比', () => { + const autoStorage = storageManager.getSoAStorage(AutoInferredComponent); + const manualStorage = storageManager.getSoAStorage(ManualTypedComponent); + + const autoStats = autoStorage!.getStats(); + const manualStats = manualStorage!.getStats(); + + const memorySaved = ((manualStats.memoryUsage - autoStats.memoryUsage) / manualStats.memoryUsage) * 100; + + // 自动推断应该节省显著的内存 + expect(memorySaved).toBeGreaterThan(20); // 至少节省20%的内存 + }); +}); \ No newline at end of file diff --git a/packages/core/tests/ECS/Core/EnhancedTypedArraySupport.test.ts b/packages/core/tests/ECS/Core/EnhancedTypedArraySupport.test.ts new file mode 100644 index 00000000..e1bf809a --- /dev/null +++ b/packages/core/tests/ECS/Core/EnhancedTypedArraySupport.test.ts @@ -0,0 +1,214 @@ +import { Component } from '../../../src/ECS/Component'; +import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage'; +import { + EnableSoA, + Float32, + Float64, + Int32, + Uint32, + Int16, + Uint16, + Int8, + Uint8, + Uint8Clamped +} from '../../../src/ECS/Core/StorageDecorators'; + +// 测试组件 - 展示所有TypedArray类型 +@EnableSoA +class EnhancedTestComponent extends Component { + @Float32 x: number = 0; + @Float32 y: number = 0; + @Float64 preciseX: number = 0; + @Int32 count: number = 0; + @Uint32 entityId: number = 0; // 避免与基类id冲突 + @Int16 layer: number = 0; + @Uint16 priority: number = 0; + @Int8 direction: number = 0; + @Uint8 flags: number = 0; + @Uint8Clamped alpha: number = 255; +} + +// 简单组件用于对比 +class SimpleComponent extends Component { + value: number = 42; +} + +describe('Enhanced TypedArray Support', () => { + let storageManager: ComponentStorageManager; + + beforeEach(() => { + storageManager = new ComponentStorageManager(); + }); + + test('应该为所有TypedArray类型创建正确的存储', () => { + const storage = storageManager.getSoAStorage(EnhancedTestComponent); + expect(storage).not.toBeNull(); + + // 测试不同类型的字段数组 + const xArray = storage!.getFieldArray('x'); // Float32Array + const preciseXArray = storage!.getFieldArray('preciseX'); // Float64Array + const countArray = storage!.getFieldArray('count'); // Int32Array + const entityIdArray = storage!.getFieldArray('entityId'); // Uint32Array + const layerArray = storage!.getFieldArray('layer'); // Int16Array + const priorityArray = storage!.getFieldArray('priority'); // Uint16Array + const directionArray = storage!.getFieldArray('direction'); // Int8Array + const flagsArray = storage!.getFieldArray('flags'); // Uint8Array + const alphaArray = storage!.getFieldArray('alpha'); // Uint8ClampedArray + + expect(xArray).toBeInstanceOf(Float32Array); + expect(preciseXArray).toBeInstanceOf(Float64Array); + expect(countArray).toBeInstanceOf(Int32Array); + expect(entityIdArray).toBeInstanceOf(Uint32Array); + expect(layerArray).toBeInstanceOf(Int16Array); + expect(priorityArray).toBeInstanceOf(Uint16Array); + expect(directionArray).toBeInstanceOf(Int8Array); + expect(flagsArray).toBeInstanceOf(Uint8Array); + expect(alphaArray).toBeInstanceOf(Uint8ClampedArray); + }); + + test('应该正确存储和检索不同类型的数值', () => { + const entityId = 1; + const component = new EnhancedTestComponent(); + + // 设置测试值 + component.x = 3.14159; + component.preciseX = Math.PI; + component.count = -123456; + component.entityId = 4294967295; // 最大Uint32值 + component.layer = -32768; // 最小Int16值 + component.priority = 65535; // 最大Uint16值 + component.direction = -128; // 最小Int8值 + component.flags = 255; // 最大Uint8值 + component.alpha = 300; // 会被Clamped到255 + + storageManager.addComponent(entityId, component); + const retrieved = storageManager.getComponent(entityId, EnhancedTestComponent); + + expect(retrieved).not.toBeNull(); + expect(retrieved!.x).toBeCloseTo(3.14159, 5); + expect(retrieved!.preciseX).toBeCloseTo(Math.PI, 10); + expect(retrieved!.count).toBe(-123456); + expect(retrieved!.entityId).toBe(4294967295); + expect(retrieved!.layer).toBe(-32768); + expect(retrieved!.priority).toBe(65535); + expect(retrieved!.direction).toBe(-128); + expect(retrieved!.flags).toBe(255); + expect(retrieved!.alpha).toBe(255); // Clamped + }); + + test('应该正确计算内存使用量', () => { + const storage = storageManager.getSoAStorage(EnhancedTestComponent); + const stats = storage!.getStats(); + + expect(stats.fieldStats.get('x').type).toBe('float32'); + expect(stats.fieldStats.get('x').memory).toBe(4000); // 1000 * 4 bytes + + expect(stats.fieldStats.get('preciseX').type).toBe('float64'); + expect(stats.fieldStats.get('preciseX').memory).toBe(8000); // 1000 * 8 bytes + + expect(stats.fieldStats.get('count').type).toBe('int32'); + expect(stats.fieldStats.get('count').memory).toBe(4000); // 1000 * 4 bytes + + expect(stats.fieldStats.get('entityId').type).toBe('uint32'); + expect(stats.fieldStats.get('entityId').memory).toBe(4000); // 1000 * 4 bytes + + expect(stats.fieldStats.get('layer').type).toBe('int16'); + expect(stats.fieldStats.get('layer').memory).toBe(2000); // 1000 * 2 bytes + + expect(stats.fieldStats.get('priority').type).toBe('uint16'); + expect(stats.fieldStats.get('priority').memory).toBe(2000); // 1000 * 2 bytes + + expect(stats.fieldStats.get('direction').type).toBe('int8'); + expect(stats.fieldStats.get('direction').memory).toBe(1000); // 1000 * 1 byte + + expect(stats.fieldStats.get('flags').type).toBe('uint8'); + expect(stats.fieldStats.get('flags').memory).toBe(1000); // 1000 * 1 byte + + expect(stats.fieldStats.get('alpha').type).toBe('uint8clamped'); + expect(stats.fieldStats.get('alpha').memory).toBe(1000); // 1000 * 1 byte + }); + + test('应该支持向量化批量操作', () => { + const storage = storageManager.getSoAStorage(EnhancedTestComponent); + + // 添加测试数据 + for (let i = 0; i < 100; i++) { + const component = new EnhancedTestComponent(); + component.x = i; + component.y = i * 2; + storageManager.addComponent(i, component); + } + + // 执行向量化操作 + let processedCount = 0; + storage!.performVectorizedOperation((fieldArrays, activeIndices) => { + const xArray = fieldArrays.get('x') as Float32Array; + const yArray = fieldArrays.get('y') as Float32Array; + + // 批量处理:x = x + y + for (const index of activeIndices) { + xArray[index] = xArray[index] + yArray[index]; + processedCount++; + } + }); + + expect(processedCount).toBe(100); + + // 验证结果 + for (let i = 0; i < 100; i++) { + const component = storageManager.getComponent(i, EnhancedTestComponent); + expect(component!.x).toBe(i + i * 2); // x + y + } + }); + + test('布尔值应该默认使用Uint8Array', () => { + @EnableSoA + class BooleanTestComponent extends Component { + visible: boolean = true; + @Float32 enabledFloat: boolean = true; // 显式指定Float32 + } + + const storage = storageManager.getSoAStorage(BooleanTestComponent); + + const visibleArray = storage!.getFieldArray('visible'); + const enabledFloatArray = storage!.getFieldArray('enabledFloat'); + + expect(visibleArray).toBeInstanceOf(Uint8Array); + expect(enabledFloatArray).toBeInstanceOf(Float32Array); + }); + + test('内存使用量对比:优化后应该更节省内存', () => { + @EnableSoA + class OptimizedComponent extends Component { + @Uint8 flag1: number = 0; + @Uint8 flag2: number = 0; + @Uint8 flag3: number = 0; + @Uint8 flag4: number = 0; + } + + @EnableSoA + class UnoptimizedComponent extends Component { + @Float32 flag1: number = 0; + @Float32 flag2: number = 0; + @Float32 flag3: number = 0; + @Float32 flag4: number = 0; + } + + const optimizedStorage = storageManager.getSoAStorage(OptimizedComponent); + const unoptimizedStorage = storageManager.getSoAStorage(UnoptimizedComponent); + + const optimizedStats = optimizedStorage!.getStats(); + const unoptimizedStats = unoptimizedStorage!.getStats(); + + // 优化版本应该使用更少的内存(4个Uint8 vs 4个Float32) + expect(optimizedStats.memoryUsage).toBeLessThan(unoptimizedStats.memoryUsage); + + // 验证优化后的单个字段确实更小 + expect(optimizedStats.fieldStats.get('flag1').memory).toBe(1000); // Uint8: 1000 bytes + expect(unoptimizedStats.fieldStats.get('flag1').memory).toBe(4000); // Float32: 4000 bytes + + // 计算节省的内存百分比 + const memorySaved = ((unoptimizedStats.memoryUsage - optimizedStats.memoryUsage) / unoptimizedStats.memoryUsage) * 100; + expect(memorySaved).toBeGreaterThan(50); // 应该节省超过50%的内存 + }); +}); \ No newline at end of file