feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 * feat: 增强编辑器UI功能与跨平台支持 * fix: 修复CI测试和类型检查问题 * fix: 修复CI问题并提高测试覆盖率 * fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
||||
"test:clear": "jest --clearCache",
|
||||
"type-check": "tsc --noEmit",
|
||||
"type-check": "npx tsc --noEmit",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IComponent } from '../Types';
|
||||
import { Int32 } from './Core/SoAStorage';
|
||||
|
||||
/**
|
||||
* 游戏组件基类
|
||||
@@ -50,6 +51,7 @@ export abstract class Component implements IComponent {
|
||||
*
|
||||
* 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计。
|
||||
*/
|
||||
@Int32
|
||||
public entityId: number | null = null;
|
||||
|
||||
/**
|
||||
|
||||
108
packages/core/src/ECS/Core/SoASerializer.ts
Normal file
108
packages/core/src/ECS/Core/SoASerializer.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
|
||||
/**
|
||||
* SoA 序列化器
|
||||
* 负责复杂类型的序列化/反序列化和深拷贝
|
||||
*/
|
||||
export class SoASerializer {
|
||||
private static readonly _logger = createLogger('SoASerializer');
|
||||
|
||||
/**
|
||||
* 序列化值为 JSON 字符串
|
||||
*/
|
||||
public static serialize(
|
||||
value: unknown,
|
||||
fieldName: string,
|
||||
options: {
|
||||
isMap?: boolean;
|
||||
isSet?: boolean;
|
||||
isArray?: boolean;
|
||||
} = {}
|
||||
): string {
|
||||
try {
|
||||
if (options.isMap && value instanceof Map) {
|
||||
return JSON.stringify(Array.from(value.entries()));
|
||||
}
|
||||
if (options.isSet && value instanceof Set) {
|
||||
return JSON.stringify(Array.from(value));
|
||||
}
|
||||
if (options.isArray && Array.isArray(value)) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
} catch (error) {
|
||||
this._logger.warn(`SoA序列化字段 ${fieldName} 失败:`, error);
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化 JSON 字符串为值
|
||||
*/
|
||||
public static deserialize(
|
||||
serialized: string,
|
||||
fieldName: string,
|
||||
options: {
|
||||
isMap?: boolean;
|
||||
isSet?: boolean;
|
||||
isArray?: boolean;
|
||||
} = {}
|
||||
): unknown {
|
||||
try {
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
if (options.isMap) {
|
||||
return new Map(parsed);
|
||||
}
|
||||
if (options.isSet) {
|
||||
return new Set(parsed);
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
this._logger.warn(`SoA反序列化字段 ${fieldName} 失败:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝对象
|
||||
*/
|
||||
public static deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as T;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => this.deepClone(item)) as T;
|
||||
}
|
||||
|
||||
if (obj instanceof Map) {
|
||||
const cloned = new Map();
|
||||
for (const [key, value] of obj.entries()) {
|
||||
cloned.set(key, this.deepClone(value));
|
||||
}
|
||||
return cloned as T;
|
||||
}
|
||||
|
||||
if (obj instanceof Set) {
|
||||
const cloned = new Set();
|
||||
for (const value of obj.values()) {
|
||||
cloned.add(this.deepClone(value));
|
||||
}
|
||||
return cloned as T;
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
const cloned = {} as Record<string, unknown>;
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
cloned[key] = this.deepClone((obj as Record<string, unknown>)[key]);
|
||||
}
|
||||
}
|
||||
return cloned as T;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Component } from '../Component';
|
||||
import { ComponentType } from './ComponentStorage';
|
||||
import { createLogger } from '../../Utils/Logger';
|
||||
import {
|
||||
SoATypeRegistry,
|
||||
SupportedTypedArray,
|
||||
TypedArrayTypeName
|
||||
} from './SoATypeRegistry';
|
||||
import { SoASerializer } from './SoASerializer';
|
||||
|
||||
// 重新导出类型,保持向后兼容
|
||||
export { SupportedTypedArray, TypedArrayTypeName } from './SoATypeRegistry';
|
||||
export { SoATypeRegistry } from './SoATypeRegistry';
|
||||
export { SoASerializer } from './SoASerializer';
|
||||
|
||||
/**
|
||||
* 启用SoA优化装饰器
|
||||
@@ -12,18 +22,6 @@ export function EnableSoA<T extends ComponentType>(target: T): T {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 高精度数值装饰器
|
||||
* 标记字段需要保持完整精度,存储为复杂对象而非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存储(更高精度但更多内存)
|
||||
@@ -181,164 +179,16 @@ 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<T extends Component> {
|
||||
private static readonly _logger = createLogger('SoAStorage');
|
||||
private fields = new Map<string, SupportedTypedArray>();
|
||||
private stringFields = new Map<string, string[]>(); // 专门存储字符串
|
||||
private serializedFields = new Map<string, string[]>(); // 序列化存储Map/Set/Array
|
||||
private complexFields = new Map<number, Map<string, any>>(); // 存储复杂对象
|
||||
private stringFields = new Map<string, string[]>();
|
||||
private serializedFields = new Map<string, string[]>();
|
||||
private complexFields = new Map<number, Map<string, unknown>>();
|
||||
private entityToIndex = new Map<number, number>();
|
||||
private indexToEntity: number[] = [];
|
||||
private freeIndices: number[] = [];
|
||||
@@ -346,6 +196,13 @@ export class SoAStorage<T extends Component> {
|
||||
private _capacity = 1000;
|
||||
public readonly type: ComponentType<T>;
|
||||
|
||||
// 缓存字段类型信息,避免重复创建实例
|
||||
private fieldTypes = new Map<string, string>();
|
||||
// 缓存装饰器元数据
|
||||
private serializeMapFields: Set<string> = new Set();
|
||||
private serializeSetFields: Set<string> = new Set();
|
||||
private serializeArrayFields: Set<string> = new Set();
|
||||
|
||||
constructor(componentType: ComponentType<T>) {
|
||||
this.type = componentType;
|
||||
this.initializeFields(componentType);
|
||||
@@ -353,88 +210,85 @@ export class SoAStorage<T extends Component> {
|
||||
|
||||
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 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();
|
||||
// const deepCopyFields = (componentType as any).__deepCopyFields || new Set(); // 未使用,但保留供future使用
|
||||
const typeWithMeta = componentType as ComponentType<T> & {
|
||||
__float64Fields?: Set<string>;
|
||||
__float32Fields?: Set<string>;
|
||||
__int32Fields?: Set<string>;
|
||||
__uint32Fields?: Set<string>;
|
||||
__int16Fields?: Set<string>;
|
||||
__uint16Fields?: Set<string>;
|
||||
__int8Fields?: Set<string>;
|
||||
__uint8Fields?: Set<string>;
|
||||
__uint8ClampedFields?: Set<string>;
|
||||
__serializeMapFields?: Set<string>;
|
||||
__serializeSetFields?: Set<string>;
|
||||
__serializeArrayFields?: Set<string>;
|
||||
};
|
||||
|
||||
for (const key in instance) {
|
||||
if (instance.hasOwnProperty(key) && key !== 'id') {
|
||||
const value = (instance as any)[key];
|
||||
const type = typeof value;
|
||||
const float64Fields = typeWithMeta.__float64Fields || new Set<string>();
|
||||
const float32Fields = typeWithMeta.__float32Fields || new Set<string>();
|
||||
const int32Fields = typeWithMeta.__int32Fields || new Set<string>();
|
||||
const uint32Fields = typeWithMeta.__uint32Fields || new Set<string>();
|
||||
const int16Fields = typeWithMeta.__int16Fields || new Set<string>();
|
||||
const uint16Fields = typeWithMeta.__uint16Fields || new Set<string>();
|
||||
const int8Fields = typeWithMeta.__int8Fields || new Set<string>();
|
||||
const uint8Fields = typeWithMeta.__uint8Fields || new Set<string>();
|
||||
const uint8ClampedFields = typeWithMeta.__uint8ClampedFields || new Set<string>();
|
||||
|
||||
if (type === 'number') {
|
||||
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存储(高精度浮点数)
|
||||
this.fields.set(key, new Float64Array(this._capacity));
|
||||
} else if (int32Fields.has(key)) {
|
||||
// 使用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存储(32位浮点数)
|
||||
this.fields.set(key, new Float32Array(this._capacity));
|
||||
} else {
|
||||
// 默认使用Float32Array
|
||||
this.fields.set(key, new Float32Array(this._capacity));
|
||||
}
|
||||
// 缓存装饰器元数据
|
||||
this.serializeMapFields = typeWithMeta.__serializeMapFields || new Set<string>();
|
||||
this.serializeSetFields = typeWithMeta.__serializeSetFields || new Set<string>();
|
||||
this.serializeArrayFields = typeWithMeta.__serializeArrayFields || new Set<string>();
|
||||
|
||||
// 先收集所有有装饰器的字段,避免重复遍历
|
||||
const decoratedFields = new Map<string, string>(); // fieldName -> arrayType
|
||||
|
||||
// 处理各类型装饰器标记的字段
|
||||
for (const key of float64Fields) decoratedFields.set(key, 'float64');
|
||||
for (const key of float32Fields) decoratedFields.set(key, 'float32');
|
||||
for (const key of int32Fields) decoratedFields.set(key, 'int32');
|
||||
for (const key of uint32Fields) decoratedFields.set(key, 'uint32');
|
||||
for (const key of int16Fields) decoratedFields.set(key, 'int16');
|
||||
for (const key of uint16Fields) decoratedFields.set(key, 'uint16');
|
||||
for (const key of int8Fields) decoratedFields.set(key, 'int8');
|
||||
for (const key of uint8Fields) decoratedFields.set(key, 'uint8');
|
||||
for (const key of uint8ClampedFields) decoratedFields.set(key, 'uint8clamped');
|
||||
|
||||
// 只遍历实例自身的属性(不包括原型链),跳过 id
|
||||
const instanceKeys = Object.keys(instance).filter(key => key !== 'id');
|
||||
|
||||
for (const key of instanceKeys) {
|
||||
const value = (instance as Record<string, unknown>)[key];
|
||||
const type = typeof value;
|
||||
|
||||
// 跳过函数(通常不会出现在 Object.keys 中,但以防万一)
|
||||
if (type === 'function') continue;
|
||||
|
||||
// 检查装饰器类型
|
||||
const decoratorType = decoratedFields.get(key);
|
||||
const effectiveType = decoratorType ? 'number' : type;
|
||||
this.fieldTypes.set(key, effectiveType);
|
||||
|
||||
if (decoratorType) {
|
||||
// 有装饰器标记的数字字段
|
||||
const ArrayConstructor = SoATypeRegistry.getConstructor(decoratorType as TypedArrayTypeName);
|
||||
this.fields.set(key, new ArrayConstructor(this._capacity));
|
||||
} else if (type === 'number') {
|
||||
// 无装饰器的数字字段,默认使用 Float32Array
|
||||
this.fields.set(key, new Float32Array(this._capacity));
|
||||
} else if (type === 'boolean') {
|
||||
// 布尔值默认使用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));
|
||||
}
|
||||
// 布尔值使用 Uint8Array 存储为 0/1
|
||||
this.fields.set(key, new Uint8Array(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)) {
|
||||
if (this.serializeMapFields.has(key) || this.serializeSetFields.has(key) || this.serializeArrayFields.has(key)) {
|
||||
// 序列化存储
|
||||
this.serializedFields.set(key, new Array(this._capacity));
|
||||
}
|
||||
// 其他对象类型会在updateComponentAtIndex中作为复杂对象处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,12 +351,16 @@ export class SoAStorage<T extends Component> {
|
||||
} else if (this.serializedFields.has(key)) {
|
||||
// 序列化字段处理
|
||||
const serializedArray = this.serializedFields.get(key)!;
|
||||
serializedArray[index] = this.serializeValue(value, key, serializeMapFields, serializeSetFields, serializeArrayFields);
|
||||
serializedArray[index] = SoASerializer.serialize(value, key, {
|
||||
isMap: serializeMapFields.has(key),
|
||||
isSet: serializeSetFields.has(key),
|
||||
isArray: serializeArrayFields.has(key)
|
||||
});
|
||||
} else {
|
||||
// 复杂字段单独存储
|
||||
if (deepCopyFields.has(key)) {
|
||||
// 深拷贝处理
|
||||
complexFieldMap.set(key, this.deepClone(value));
|
||||
complexFieldMap.set(key, SoASerializer.deepClone(value));
|
||||
} else {
|
||||
complexFieldMap.set(key, value);
|
||||
}
|
||||
@@ -516,96 +374,6 @@ export class SoAStorage<T extends Component> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化值为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) {
|
||||
SoAStorage._logger.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) {
|
||||
SoAStorage._logger.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);
|
||||
@@ -613,11 +381,162 @@ export class SoAStorage<T extends Component> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建真正的组件实例以保持兼容性
|
||||
// 返回 Proxy,直接操作底层 TypedArray
|
||||
return this.createProxyView(entityId, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组件的 Proxy 视图
|
||||
* 读写操作直接映射到底层 TypedArray,无数据复制
|
||||
*/
|
||||
private createProxyView(entityId: number, index: number): T {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
|
||||
// Proxy handler 类型定义
|
||||
const handler: ProxyHandler<Record<string, unknown>> = {
|
||||
get(_, prop: string | symbol) {
|
||||
const propStr = String(prop);
|
||||
|
||||
// TypedArray 字段
|
||||
const array = self.fields.get(propStr);
|
||||
if (array) {
|
||||
const fieldType = self.getFieldType(propStr);
|
||||
if (fieldType === 'boolean') {
|
||||
return array[index] === 1;
|
||||
}
|
||||
return array[index];
|
||||
}
|
||||
|
||||
// 字符串字段
|
||||
const stringArray = self.stringFields.get(propStr);
|
||||
if (stringArray) {
|
||||
return stringArray[index];
|
||||
}
|
||||
|
||||
// 序列化字段
|
||||
const serializedArray = self.serializedFields.get(propStr);
|
||||
if (serializedArray) {
|
||||
const serialized = serializedArray[index];
|
||||
if (serialized) {
|
||||
return SoASerializer.deserialize(serialized, propStr, {
|
||||
isMap: self.serializeMapFields.has(propStr),
|
||||
isSet: self.serializeSetFields.has(propStr),
|
||||
isArray: self.serializeArrayFields.has(propStr)
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 复杂字段
|
||||
const complexFieldMap = self.complexFields.get(entityId);
|
||||
if (complexFieldMap?.has(propStr)) {
|
||||
return complexFieldMap.get(propStr);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
set(_, prop: string | symbol, value) {
|
||||
const propStr = String(prop);
|
||||
|
||||
// entityId 是只读的
|
||||
if (propStr === 'entityId') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TypedArray 字段
|
||||
const array = self.fields.get(propStr);
|
||||
if (array) {
|
||||
const fieldType = self.getFieldType(propStr);
|
||||
if (fieldType === 'boolean') {
|
||||
array[index] = value ? 1 : 0;
|
||||
} else {
|
||||
array[index] = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 字符串字段
|
||||
const stringArray = self.stringFields.get(propStr);
|
||||
if (stringArray) {
|
||||
stringArray[index] = String(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 序列化字段
|
||||
if (self.serializedFields.has(propStr)) {
|
||||
const serializedArray = self.serializedFields.get(propStr)!;
|
||||
serializedArray[index] = SoASerializer.serialize(value, propStr, {
|
||||
isMap: self.serializeMapFields.has(propStr),
|
||||
isSet: self.serializeSetFields.has(propStr),
|
||||
isArray: self.serializeArrayFields.has(propStr)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// 复杂字段
|
||||
let complexFieldMap = self.complexFields.get(entityId);
|
||||
if (!complexFieldMap) {
|
||||
complexFieldMap = new Map();
|
||||
self.complexFields.set(entityId, complexFieldMap);
|
||||
}
|
||||
complexFieldMap.set(propStr, value);
|
||||
return true;
|
||||
},
|
||||
|
||||
has(_, prop) {
|
||||
const propStr = String(prop);
|
||||
return self.fields.has(propStr) ||
|
||||
self.stringFields.has(propStr) ||
|
||||
self.serializedFields.has(propStr) ||
|
||||
self.complexFields.get(entityId)?.has(propStr) || false;
|
||||
},
|
||||
|
||||
ownKeys() {
|
||||
const keys: string[] = [];
|
||||
for (const key of self.fields.keys()) keys.push(key);
|
||||
for (const key of self.stringFields.keys()) keys.push(key);
|
||||
for (const key of self.serializedFields.keys()) keys.push(key);
|
||||
const complexFieldMap = self.complexFields.get(entityId);
|
||||
if (complexFieldMap) {
|
||||
for (const key of complexFieldMap.keys()) keys.push(key);
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(_, prop) {
|
||||
const propStr = String(prop);
|
||||
if (self.fields.has(propStr) ||
|
||||
self.stringFields.has(propStr) ||
|
||||
self.serializedFields.has(propStr) ||
|
||||
self.complexFields.get(entityId)?.has(propStr)) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
// entityId 是只读的
|
||||
writable: propStr !== 'entityId',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return new Proxy({} as Record<string, unknown>, handler) as unknown as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件的快照副本(用于序列化等需要独立副本的场景)
|
||||
*/
|
||||
public getComponentSnapshot(entityId: number): T | null {
|
||||
const index = this.entityToIndex.get(entityId);
|
||||
if (index === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 需要 any 因为要动态写入泛型 T 的属性
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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()) {
|
||||
@@ -640,7 +559,11 @@ export class SoAStorage<T extends Component> {
|
||||
for (const [fieldName, serializedArray] of this.serializedFields.entries()) {
|
||||
const serialized = serializedArray[index];
|
||||
if (serialized) {
|
||||
component[fieldName] = this.deserializeValue(serialized, fieldName, serializeMapFields, serializeSetFields, serializeArrayFields);
|
||||
component[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
|
||||
isMap: this.serializeMapFields.has(fieldName),
|
||||
isSet: this.serializeSetFields.has(fieldName),
|
||||
isArray: this.serializeArrayFields.has(fieldName)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,10 +579,8 @@ export class SoAStorage<T extends Component> {
|
||||
}
|
||||
|
||||
private getFieldType(fieldName: string): string {
|
||||
// 通过创建临时实例检查字段类型
|
||||
const tempInstance = new this.type();
|
||||
const value = (tempInstance as any)[fieldName];
|
||||
return typeof value;
|
||||
// 使用缓存的字段类型
|
||||
return this.fieldTypes.get(fieldName) || 'unknown';
|
||||
}
|
||||
|
||||
public hasComponent(entityId: number): boolean {
|
||||
@@ -687,32 +608,7 @@ export class SoAStorage<T extends Component> {
|
||||
private resize(newCapacity: number): void {
|
||||
// 调整数值字段的TypedArray
|
||||
for (const [fieldName, oldArray] of this.fields.entries()) {
|
||||
let newArray: SupportedTypedArray;
|
||||
|
||||
if (oldArray instanceof Float32Array) {
|
||||
newArray = new Float32Array(newCapacity);
|
||||
} else if (oldArray instanceof Float64Array) {
|
||||
newArray = new Float64Array(newCapacity);
|
||||
} 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`);
|
||||
}
|
||||
|
||||
const newArray = SoATypeRegistry.createSameType(oldArray, newCapacity);
|
||||
newArray.set(oldArray);
|
||||
this.fields.set(fieldName, newArray);
|
||||
}
|
||||
@@ -849,42 +745,8 @@ export class SoAStorage<T extends Component> {
|
||||
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 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 typeName = SoATypeRegistry.getTypeName(array);
|
||||
const bytesPerElement = SoATypeRegistry.getBytesPerElement(typeName);
|
||||
const memory = array.length * bytesPerElement;
|
||||
totalMemory += memory;
|
||||
|
||||
@@ -914,4 +776,5 @@ export class SoAStorage<T extends Component> {
|
||||
const activeIndices = this.getActiveIndices();
|
||||
operation(this.fields, activeIndices);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
208
packages/core/src/ECS/Core/SoATypeRegistry.ts
Normal file
208
packages/core/src/ECS/Core/SoATypeRegistry.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* SoA存储器支持的TypedArray类型
|
||||
*/
|
||||
export type SupportedTypedArray =
|
||||
| Float32Array
|
||||
| Float64Array
|
||||
| Int32Array
|
||||
| Uint32Array
|
||||
| Int16Array
|
||||
| Uint16Array
|
||||
| Int8Array
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray;
|
||||
|
||||
export type TypedArrayConstructor =
|
||||
| typeof Float32Array
|
||||
| typeof Float64Array
|
||||
| typeof Int32Array
|
||||
| typeof Uint32Array
|
||||
| typeof Int16Array
|
||||
| typeof Uint16Array
|
||||
| typeof Int8Array
|
||||
| typeof Uint8Array
|
||||
| typeof Uint8ClampedArray;
|
||||
|
||||
/**
|
||||
* TypedArray 类型名称
|
||||
*/
|
||||
export type TypedArrayTypeName =
|
||||
| 'float32'
|
||||
| 'float64'
|
||||
| 'int32'
|
||||
| 'uint32'
|
||||
| 'int16'
|
||||
| 'uint16'
|
||||
| 'int8'
|
||||
| 'uint8'
|
||||
| 'uint8clamped';
|
||||
|
||||
/**
|
||||
* 字段元数据
|
||||
*/
|
||||
export interface FieldMetadata {
|
||||
name: string;
|
||||
type: 'number' | 'boolean' | 'string' | 'object';
|
||||
arrayType?: TypedArrayTypeName;
|
||||
isSerializedMap?: boolean;
|
||||
isSerializedSet?: boolean;
|
||||
isSerializedArray?: boolean;
|
||||
isDeepCopy?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SoA 类型注册器
|
||||
* 负责类型推断、TypedArray 创建和元数据管理
|
||||
*/
|
||||
export class SoATypeRegistry {
|
||||
private static readonly TYPE_CONSTRUCTORS: Record<TypedArrayTypeName, TypedArrayConstructor> = {
|
||||
float32: Float32Array,
|
||||
float64: Float64Array,
|
||||
int32: Int32Array,
|
||||
uint32: Uint32Array,
|
||||
int16: Int16Array,
|
||||
uint16: Uint16Array,
|
||||
int8: Int8Array,
|
||||
uint8: Uint8Array,
|
||||
uint8clamped: Uint8ClampedArray
|
||||
};
|
||||
|
||||
private static readonly TYPE_BYTES: Record<TypedArrayTypeName, number> = {
|
||||
float32: 4,
|
||||
float64: 8,
|
||||
int32: 4,
|
||||
uint32: 4,
|
||||
int16: 2,
|
||||
uint16: 2,
|
||||
int8: 1,
|
||||
uint8: 1,
|
||||
uint8clamped: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 TypedArray 构造函数
|
||||
*/
|
||||
public static getConstructor(typeName: TypedArrayTypeName): TypedArrayConstructor {
|
||||
return this.TYPE_CONSTRUCTORS[typeName] || Float32Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取每个元素的字节数
|
||||
*/
|
||||
public static getBytesPerElement(typeName: TypedArrayTypeName): number {
|
||||
return this.TYPE_BYTES[typeName] || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 TypedArray 实例获取类型名称
|
||||
*/
|
||||
public static getTypeName(array: SupportedTypedArray): TypedArrayTypeName {
|
||||
if (array instanceof Float32Array) return 'float32';
|
||||
if (array instanceof Float64Array) return 'float64';
|
||||
if (array instanceof Int32Array) return 'int32';
|
||||
if (array instanceof Uint32Array) return 'uint32';
|
||||
if (array instanceof Int16Array) return 'int16';
|
||||
if (array instanceof Uint16Array) return 'uint16';
|
||||
if (array instanceof Int8Array) return 'int8';
|
||||
if (array instanceof Uint8Array) return 'uint8';
|
||||
if (array instanceof Uint8ClampedArray) return 'uint8clamped';
|
||||
return 'float32';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的 TypedArray(与原数组同类型)
|
||||
*/
|
||||
public static createSameType(source: SupportedTypedArray, capacity: number): SupportedTypedArray {
|
||||
const typeName = this.getTypeName(source);
|
||||
const Constructor = this.getConstructor(typeName);
|
||||
return new Constructor(capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从组件类型提取字段元数据
|
||||
*/
|
||||
public static extractFieldMetadata<T>(
|
||||
componentType: new () => T
|
||||
): Map<string, FieldMetadata> {
|
||||
const instance = new componentType();
|
||||
const metadata = new Map<string, FieldMetadata>();
|
||||
|
||||
const typeWithMeta = componentType as typeof componentType & {
|
||||
__float64Fields?: Set<string>;
|
||||
__float32Fields?: Set<string>;
|
||||
__int32Fields?: Set<string>;
|
||||
__uint32Fields?: Set<string>;
|
||||
__int16Fields?: Set<string>;
|
||||
__uint16Fields?: Set<string>;
|
||||
__int8Fields?: Set<string>;
|
||||
__uint8Fields?: Set<string>;
|
||||
__uint8ClampedFields?: Set<string>;
|
||||
__serializeMapFields?: Set<string>;
|
||||
__serializeSetFields?: Set<string>;
|
||||
__serializeArrayFields?: Set<string>;
|
||||
__deepCopyFields?: Set<string>;
|
||||
};
|
||||
|
||||
// 收集装饰器标记
|
||||
const decoratorMap = new Map<string, TypedArrayTypeName>();
|
||||
|
||||
const addDecorators = (fields: Set<string> | undefined, type: TypedArrayTypeName) => {
|
||||
if (fields) {
|
||||
for (const key of fields) decoratorMap.set(key, type);
|
||||
}
|
||||
};
|
||||
|
||||
addDecorators(typeWithMeta.__float64Fields, 'float64');
|
||||
addDecorators(typeWithMeta.__float32Fields, 'float32');
|
||||
addDecorators(typeWithMeta.__int32Fields, 'int32');
|
||||
addDecorators(typeWithMeta.__uint32Fields, 'uint32');
|
||||
addDecorators(typeWithMeta.__int16Fields, 'int16');
|
||||
addDecorators(typeWithMeta.__uint16Fields, 'uint16');
|
||||
addDecorators(typeWithMeta.__int8Fields, 'int8');
|
||||
addDecorators(typeWithMeta.__uint8Fields, 'uint8');
|
||||
addDecorators(typeWithMeta.__uint8ClampedFields, 'uint8clamped');
|
||||
|
||||
// 遍历实例属性
|
||||
const instanceKeys = Object.keys(instance as object).filter((key) => key !== 'id');
|
||||
|
||||
for (const key of instanceKeys) {
|
||||
const value = (instance as Record<string, unknown>)[key];
|
||||
const type = typeof value;
|
||||
|
||||
if (type === 'function') continue;
|
||||
|
||||
const fieldMeta: FieldMetadata = {
|
||||
name: key,
|
||||
type: type as 'number' | 'boolean' | 'string' | 'object'
|
||||
};
|
||||
|
||||
const decoratorType = decoratorMap.get(key);
|
||||
|
||||
if (decoratorType) {
|
||||
fieldMeta.arrayType = decoratorType;
|
||||
} else if (type === 'number') {
|
||||
fieldMeta.arrayType = 'float32';
|
||||
} else if (type === 'boolean') {
|
||||
fieldMeta.arrayType = 'uint8';
|
||||
}
|
||||
|
||||
// 序列化标记
|
||||
if (typeWithMeta.__serializeMapFields?.has(key)) {
|
||||
fieldMeta.isSerializedMap = true;
|
||||
}
|
||||
if (typeWithMeta.__serializeSetFields?.has(key)) {
|
||||
fieldMeta.isSerializedSet = true;
|
||||
}
|
||||
if (typeWithMeta.__serializeArrayFields?.has(key)) {
|
||||
fieldMeta.isSerializedArray = true;
|
||||
}
|
||||
if (typeWithMeta.__deepCopyFields?.has(key)) {
|
||||
fieldMeta.isDeepCopy = true;
|
||||
}
|
||||
|
||||
metadata.set(key, fieldMeta);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export { ComponentPool, ComponentPoolManager } from '../ComponentPool';
|
||||
export { ComponentStorage, ComponentRegistry } from '../ComponentStorage';
|
||||
export { EnableSoA, HighPrecision, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';
|
||||
export { EnableSoA, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';
|
||||
|
||||
@@ -23,7 +23,6 @@ export {
|
||||
EnableSoA,
|
||||
|
||||
// 数值类型装饰器
|
||||
HighPrecision,
|
||||
Float64,
|
||||
Float32,
|
||||
Int32,
|
||||
@@ -34,10 +33,6 @@ export {
|
||||
Uint8,
|
||||
Uint8Clamped,
|
||||
|
||||
// 自动类型推断
|
||||
AutoTyped,
|
||||
TypeInference,
|
||||
|
||||
// 序列化装饰器
|
||||
SerializeMap,
|
||||
SerializeSet,
|
||||
|
||||
183
packages/core/tests/ECS/Core/SoASerializer.test.ts
Normal file
183
packages/core/tests/ECS/Core/SoASerializer.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { SoASerializer } from '../../../src/ECS/Core/SoASerializer';
|
||||
|
||||
describe('SoASerializer', () => {
|
||||
describe('serialize', () => {
|
||||
test('should serialize Map to JSON string', () => {
|
||||
const map = new Map([['key1', 'value1'], ['key2', 'value2']]);
|
||||
const result = SoASerializer.serialize(map, 'testMap', { isMap: true });
|
||||
expect(result).toBe('[["key1","value1"],["key2","value2"]]');
|
||||
});
|
||||
|
||||
test('should serialize Set to JSON string', () => {
|
||||
const set = new Set([1, 2, 3]);
|
||||
const result = SoASerializer.serialize(set, 'testSet', { isSet: true });
|
||||
expect(result).toBe('[1,2,3]');
|
||||
});
|
||||
|
||||
test('should serialize Array to JSON string', () => {
|
||||
const arr = [1, 2, 3];
|
||||
const result = SoASerializer.serialize(arr, 'testArray', { isArray: true });
|
||||
expect(result).toBe('[1,2,3]');
|
||||
});
|
||||
|
||||
test('should serialize plain object to JSON string', () => {
|
||||
const obj = { a: 1, b: 'test' };
|
||||
const result = SoASerializer.serialize(obj, 'testObj');
|
||||
expect(result).toBe('{"a":1,"b":"test"}');
|
||||
});
|
||||
|
||||
test('should serialize primitive values', () => {
|
||||
expect(SoASerializer.serialize(42, 'num')).toBe('42');
|
||||
expect(SoASerializer.serialize('hello', 'str')).toBe('"hello"');
|
||||
expect(SoASerializer.serialize(true, 'bool')).toBe('true');
|
||||
expect(SoASerializer.serialize(null, 'null')).toBe('null');
|
||||
});
|
||||
|
||||
test('should return empty object on serialization error', () => {
|
||||
const circular: Record<string, unknown> = {};
|
||||
circular.self = circular;
|
||||
const result = SoASerializer.serialize(circular, 'circular');
|
||||
expect(result).toBe('{}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deserialize', () => {
|
||||
test('should deserialize JSON string to Map', () => {
|
||||
const json = '[["key1","value1"],["key2","value2"]]';
|
||||
const result = SoASerializer.deserialize(json, 'testMap', { isMap: true });
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect((result as Map<string, string>).get('key1')).toBe('value1');
|
||||
expect((result as Map<string, string>).get('key2')).toBe('value2');
|
||||
});
|
||||
|
||||
test('should deserialize JSON string to Set', () => {
|
||||
const json = '[1,2,3]';
|
||||
const result = SoASerializer.deserialize(json, 'testSet', { isSet: true });
|
||||
expect(result).toBeInstanceOf(Set);
|
||||
expect((result as Set<number>).has(1)).toBe(true);
|
||||
expect((result as Set<number>).has(2)).toBe(true);
|
||||
expect((result as Set<number>).has(3)).toBe(true);
|
||||
});
|
||||
|
||||
test('should deserialize JSON string to Array', () => {
|
||||
const json = '[1,2,3]';
|
||||
const result = SoASerializer.deserialize(json, 'testArray', { isArray: true });
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('should deserialize JSON string to object', () => {
|
||||
const json = '{"a":1,"b":"test"}';
|
||||
const result = SoASerializer.deserialize(json, 'testObj');
|
||||
expect(result).toEqual({ a: 1, b: 'test' });
|
||||
});
|
||||
|
||||
test('should deserialize primitive values', () => {
|
||||
expect(SoASerializer.deserialize('42', 'num')).toBe(42);
|
||||
expect(SoASerializer.deserialize('"hello"', 'str')).toBe('hello');
|
||||
expect(SoASerializer.deserialize('true', 'bool')).toBe(true);
|
||||
expect(SoASerializer.deserialize('null', 'null')).toBe(null);
|
||||
});
|
||||
|
||||
test('should return null on deserialization error', () => {
|
||||
const result = SoASerializer.deserialize('invalid json', 'field');
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deepClone', () => {
|
||||
test('should return primitive values as-is', () => {
|
||||
expect(SoASerializer.deepClone(42)).toBe(42);
|
||||
expect(SoASerializer.deepClone('hello')).toBe('hello');
|
||||
expect(SoASerializer.deepClone(true)).toBe(true);
|
||||
expect(SoASerializer.deepClone(null)).toBe(null);
|
||||
expect(SoASerializer.deepClone(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
test('should clone Date objects', () => {
|
||||
const date = new Date('2023-01-01');
|
||||
const cloned = SoASerializer.deepClone(date);
|
||||
expect(cloned).toBeInstanceOf(Date);
|
||||
expect(cloned.getTime()).toBe(date.getTime());
|
||||
expect(cloned).not.toBe(date);
|
||||
});
|
||||
|
||||
test('should clone arrays deeply', () => {
|
||||
const arr = [1, [2, 3], { a: 4 }];
|
||||
const cloned = SoASerializer.deepClone(arr);
|
||||
expect(cloned).toEqual(arr);
|
||||
expect(cloned).not.toBe(arr);
|
||||
expect(cloned[1]).not.toBe(arr[1]);
|
||||
expect(cloned[2]).not.toBe(arr[2]);
|
||||
});
|
||||
|
||||
test('should clone Map objects deeply', () => {
|
||||
const map = new Map([
|
||||
['key1', { value: 1 }],
|
||||
['key2', { value: 2 }]
|
||||
]);
|
||||
const cloned = SoASerializer.deepClone(map);
|
||||
expect(cloned).toBeInstanceOf(Map);
|
||||
expect(cloned.size).toBe(2);
|
||||
expect(cloned.get('key1')).toEqual({ value: 1 });
|
||||
expect(cloned.get('key1')).not.toBe(map.get('key1'));
|
||||
});
|
||||
|
||||
test('should clone Set objects deeply', () => {
|
||||
const obj1 = { a: 1 };
|
||||
const obj2 = { b: 2 };
|
||||
const set = new Set([obj1, obj2]);
|
||||
const cloned = SoASerializer.deepClone(set);
|
||||
expect(cloned).toBeInstanceOf(Set);
|
||||
expect(cloned.size).toBe(2);
|
||||
|
||||
const clonedArray = Array.from(cloned);
|
||||
expect(clonedArray[0]).toEqual(obj1);
|
||||
expect(clonedArray[0]).not.toBe(obj1);
|
||||
});
|
||||
|
||||
test('should clone nested objects deeply', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: {
|
||||
c: 2,
|
||||
d: {
|
||||
e: 3
|
||||
}
|
||||
}
|
||||
};
|
||||
const cloned = SoASerializer.deepClone(obj);
|
||||
expect(cloned).toEqual(obj);
|
||||
expect(cloned).not.toBe(obj);
|
||||
expect(cloned.b).not.toBe(obj.b);
|
||||
expect(cloned.b.d).not.toBe(obj.b.d);
|
||||
});
|
||||
|
||||
test('should clone complex nested structures', () => {
|
||||
const complex = {
|
||||
array: [1, 2, 3],
|
||||
map: new Map([['a', 1]]),
|
||||
set: new Set([1, 2]),
|
||||
date: new Date('2023-01-01'),
|
||||
nested: {
|
||||
value: 'test'
|
||||
}
|
||||
};
|
||||
const cloned = SoASerializer.deepClone(complex);
|
||||
|
||||
expect(cloned.array).toEqual(complex.array);
|
||||
expect(cloned.array).not.toBe(complex.array);
|
||||
|
||||
expect(cloned.map).toBeInstanceOf(Map);
|
||||
expect(cloned.map.get('a')).toBe(1);
|
||||
|
||||
expect(cloned.set).toBeInstanceOf(Set);
|
||||
expect(cloned.set.has(1)).toBe(true);
|
||||
|
||||
expect(cloned.date).toBeInstanceOf(Date);
|
||||
expect(cloned.date.getTime()).toBe(complex.date.getTime());
|
||||
|
||||
expect(cloned.nested).toEqual(complex.nested);
|
||||
expect(cloned.nested).not.toBe(complex.nested);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { Component } from '../../../src/ECS/Component';
|
||||
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
|
||||
import {
|
||||
EnableSoA,
|
||||
HighPrecision,
|
||||
Float64,
|
||||
Int32,
|
||||
SerializeMap,
|
||||
@@ -50,7 +49,7 @@ class BasicTypesComponent extends Component {
|
||||
class DecoratedNumberComponent extends Component {
|
||||
public normalFloat: number;
|
||||
|
||||
@HighPrecision
|
||||
@Float64
|
||||
public highPrecisionNumber: number;
|
||||
|
||||
@Float64
|
||||
@@ -143,7 +142,7 @@ class ComplexObjectComponent extends Component {
|
||||
|
||||
@EnableSoA
|
||||
class MixedComponent extends Component {
|
||||
@HighPrecision
|
||||
@Float64
|
||||
public bigIntId: number;
|
||||
|
||||
@Float64
|
||||
@@ -288,7 +287,7 @@ describe('SoAStorage - SoA存储测试', () => {
|
||||
});
|
||||
|
||||
describe('数值类型装饰器', () => {
|
||||
test('@HighPrecision应该保持高精度数值', () => {
|
||||
test('@Float64应该保持高精度数值', () => {
|
||||
const component = new DecoratedNumberComponent(
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER
|
||||
@@ -336,7 +335,7 @@ describe('SoAStorage - SoA存储测试', () => {
|
||||
expect(storage.getFieldArray('normalFloat')).toBeInstanceOf(Float32Array);
|
||||
expect(storage.getFieldArray('preciseFloat')).toBeInstanceOf(Float64Array);
|
||||
expect(storage.getFieldArray('integerValue')).toBeInstanceOf(Int32Array);
|
||||
expect(storage.getFieldArray('highPrecisionNumber')).toBeNull();
|
||||
expect(storage.getFieldArray('highPrecisionNumber')).toBeInstanceOf(Float64Array);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
233
packages/core/tests/ECS/Core/SoATypeRegistry.test.ts
Normal file
233
packages/core/tests/ECS/Core/SoATypeRegistry.test.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import {
|
||||
SoATypeRegistry,
|
||||
TypedArrayTypeName
|
||||
} from '../../../src/ECS/Core/SoATypeRegistry';
|
||||
|
||||
// Test components
|
||||
class SimpleComponent extends Component {
|
||||
public value: number = 0;
|
||||
public flag: boolean = false;
|
||||
public name: string = '';
|
||||
}
|
||||
|
||||
describe('SoATypeRegistry', () => {
|
||||
describe('getConstructor', () => {
|
||||
test('should return Float32Array constructor for float32', () => {
|
||||
expect(SoATypeRegistry.getConstructor('float32')).toBe(Float32Array);
|
||||
});
|
||||
|
||||
test('should return Float64Array constructor for float64', () => {
|
||||
expect(SoATypeRegistry.getConstructor('float64')).toBe(Float64Array);
|
||||
});
|
||||
|
||||
test('should return Int32Array constructor for int32', () => {
|
||||
expect(SoATypeRegistry.getConstructor('int32')).toBe(Int32Array);
|
||||
});
|
||||
|
||||
test('should return Uint32Array constructor for uint32', () => {
|
||||
expect(SoATypeRegistry.getConstructor('uint32')).toBe(Uint32Array);
|
||||
});
|
||||
|
||||
test('should return Int16Array constructor for int16', () => {
|
||||
expect(SoATypeRegistry.getConstructor('int16')).toBe(Int16Array);
|
||||
});
|
||||
|
||||
test('should return Uint16Array constructor for uint16', () => {
|
||||
expect(SoATypeRegistry.getConstructor('uint16')).toBe(Uint16Array);
|
||||
});
|
||||
|
||||
test('should return Int8Array constructor for int8', () => {
|
||||
expect(SoATypeRegistry.getConstructor('int8')).toBe(Int8Array);
|
||||
});
|
||||
|
||||
test('should return Uint8Array constructor for uint8', () => {
|
||||
expect(SoATypeRegistry.getConstructor('uint8')).toBe(Uint8Array);
|
||||
});
|
||||
|
||||
test('should return Uint8ClampedArray constructor for uint8clamped', () => {
|
||||
expect(SoATypeRegistry.getConstructor('uint8clamped')).toBe(Uint8ClampedArray);
|
||||
});
|
||||
|
||||
test('should return Float32Array as default for unknown type', () => {
|
||||
expect(SoATypeRegistry.getConstructor('unknown' as TypedArrayTypeName)).toBe(Float32Array);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBytesPerElement', () => {
|
||||
test('should return 4 for float32', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('float32')).toBe(4);
|
||||
});
|
||||
|
||||
test('should return 8 for float64', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('float64')).toBe(8);
|
||||
});
|
||||
|
||||
test('should return 4 for int32', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('int32')).toBe(4);
|
||||
});
|
||||
|
||||
test('should return 4 for uint32', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('uint32')).toBe(4);
|
||||
});
|
||||
|
||||
test('should return 2 for int16', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('int16')).toBe(2);
|
||||
});
|
||||
|
||||
test('should return 2 for uint16', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('uint16')).toBe(2);
|
||||
});
|
||||
|
||||
test('should return 1 for int8', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('int8')).toBe(1);
|
||||
});
|
||||
|
||||
test('should return 1 for uint8', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('uint8')).toBe(1);
|
||||
});
|
||||
|
||||
test('should return 1 for uint8clamped', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('uint8clamped')).toBe(1);
|
||||
});
|
||||
|
||||
test('should return 4 as default for unknown type', () => {
|
||||
expect(SoATypeRegistry.getBytesPerElement('unknown' as TypedArrayTypeName)).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTypeName', () => {
|
||||
test('should return float32 for Float32Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Float32Array(1))).toBe('float32');
|
||||
});
|
||||
|
||||
test('should return float64 for Float64Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Float64Array(1))).toBe('float64');
|
||||
});
|
||||
|
||||
test('should return int32 for Int32Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Int32Array(1))).toBe('int32');
|
||||
});
|
||||
|
||||
test('should return uint32 for Uint32Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Uint32Array(1))).toBe('uint32');
|
||||
});
|
||||
|
||||
test('should return int16 for Int16Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Int16Array(1))).toBe('int16');
|
||||
});
|
||||
|
||||
test('should return uint16 for Uint16Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Uint16Array(1))).toBe('uint16');
|
||||
});
|
||||
|
||||
test('should return int8 for Int8Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Int8Array(1))).toBe('int8');
|
||||
});
|
||||
|
||||
test('should return uint8 for Uint8Array', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Uint8Array(1))).toBe('uint8');
|
||||
});
|
||||
|
||||
test('should return uint8clamped for Uint8ClampedArray', () => {
|
||||
expect(SoATypeRegistry.getTypeName(new Uint8ClampedArray(1))).toBe('uint8clamped');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSameType', () => {
|
||||
test('should create Float32Array from Float32Array source', () => {
|
||||
const source = new Float32Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 20);
|
||||
expect(result).toBeInstanceOf(Float32Array);
|
||||
expect(result.length).toBe(20);
|
||||
});
|
||||
|
||||
test('should create Float64Array from Float64Array source', () => {
|
||||
const source = new Float64Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 20);
|
||||
expect(result).toBeInstanceOf(Float64Array);
|
||||
expect(result.length).toBe(20);
|
||||
});
|
||||
|
||||
test('should create Int32Array from Int32Array source', () => {
|
||||
const source = new Int32Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 20);
|
||||
expect(result).toBeInstanceOf(Int32Array);
|
||||
expect(result.length).toBe(20);
|
||||
});
|
||||
|
||||
test('should create Uint32Array from Uint32Array source', () => {
|
||||
const source = new Uint32Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 20);
|
||||
expect(result).toBeInstanceOf(Uint32Array);
|
||||
expect(result.length).toBe(20);
|
||||
});
|
||||
|
||||
test('should create Int16Array from Int16Array source', () => {
|
||||
const source = new Int16Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 15);
|
||||
expect(result).toBeInstanceOf(Int16Array);
|
||||
expect(result.length).toBe(15);
|
||||
});
|
||||
|
||||
test('should create Uint16Array from Uint16Array source', () => {
|
||||
const source = new Uint16Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 15);
|
||||
expect(result).toBeInstanceOf(Uint16Array);
|
||||
expect(result.length).toBe(15);
|
||||
});
|
||||
|
||||
test('should create Int8Array from Int8Array source', () => {
|
||||
const source = new Int8Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 15);
|
||||
expect(result).toBeInstanceOf(Int8Array);
|
||||
expect(result.length).toBe(15);
|
||||
});
|
||||
|
||||
test('should create Uint8Array from Uint8Array source', () => {
|
||||
const source = new Uint8Array(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 15);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(15);
|
||||
});
|
||||
|
||||
test('should create Uint8ClampedArray from Uint8ClampedArray source', () => {
|
||||
const source = new Uint8ClampedArray(10);
|
||||
const result = SoATypeRegistry.createSameType(source, 15);
|
||||
expect(result).toBeInstanceOf(Uint8ClampedArray);
|
||||
expect(result.length).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFieldMetadata', () => {
|
||||
test('should extract metadata for simple component', () => {
|
||||
const metadata = SoATypeRegistry.extractFieldMetadata(SimpleComponent);
|
||||
|
||||
expect(metadata.has('value')).toBe(true);
|
||||
expect(metadata.get('value')?.type).toBe('number');
|
||||
expect(metadata.get('value')?.arrayType).toBe('float32');
|
||||
|
||||
expect(metadata.has('flag')).toBe(true);
|
||||
expect(metadata.get('flag')?.type).toBe('boolean');
|
||||
expect(metadata.get('flag')?.arrayType).toBe('uint8');
|
||||
|
||||
expect(metadata.has('name')).toBe(true);
|
||||
expect(metadata.get('name')?.type).toBe('string');
|
||||
});
|
||||
|
||||
test('should not include id field in metadata', () => {
|
||||
const metadata = SoATypeRegistry.extractFieldMetadata(SimpleComponent);
|
||||
expect(metadata.has('id')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle component with object fields', () => {
|
||||
class ObjectComponent extends Component {
|
||||
public data: object = {};
|
||||
}
|
||||
|
||||
const metadata = SoATypeRegistry.extractFieldMetadata(ObjectComponent);
|
||||
expect(metadata.has('data')).toBe(true);
|
||||
expect(metadata.get('data')?.type).toBe('object');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user