支持二进制序列化

This commit is contained in:
YHH
2025-10-08 20:42:55 +08:00
parent 06b3f92007
commit 69c46f32eb
9 changed files with 205 additions and 288 deletions

43
package-lock.json generated
View File

@@ -5220,6 +5220,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/msgpack-lite": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@types/msgpack-lite/-/msgpack-lite-0.1.11.tgz",
"integrity": "sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/multer": { "node_modules/@types/multer": {
"version": "1.4.13", "version": "1.4.13",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
@@ -7705,6 +7715,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/event-lite": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
"license": "MIT"
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -8656,7 +8672,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -8846,6 +8861,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/int64-buffer": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz",
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
"license": "MIT"
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@@ -9117,7 +9138,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/isexe": { "node_modules/isexe": {
@@ -11523,6 +11543,21 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true "dev": true
}, },
"node_modules/msgpack-lite": {
"version": "0.1.26",
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
"integrity": "sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==",
"license": "MIT",
"dependencies": {
"event-lite": "^0.1.1",
"ieee754": "^1.1.8",
"int64-buffer": "^0.1.9",
"isarray": "^1.0.0"
},
"bin": {
"msgpack": "bin/msgpack"
}
},
"node_modules/multimatch": { "node_modules/multimatch": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz",
@@ -15524,6 +15559,9 @@
"name": "@esengine/ecs-framework", "name": "@esengine/ecs-framework",
"version": "2.1.51", "version": "2.1.51",
"license": "MIT", "license": "MIT",
"dependencies": {
"msgpack-lite": "^0.1.26"
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.28.3", "@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
@@ -15534,6 +15572,7 @@
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17", "@types/node": "^20.19.17",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",

View File

@@ -58,6 +58,7 @@
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17", "@types/node": "^20.19.17",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
@@ -75,5 +76,8 @@
"type": "git", "type": "git",
"url": "https://github.com/esengine/ecs-framework.git", "url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/core" "directory": "packages/core"
},
"dependencies": {
"msgpack-lite": "^0.1.26"
} }
} }

View File

@@ -501,21 +501,26 @@ export class Scene implements IScene {
/** /**
* 序列化场景 * 序列化场景
* *
* 将场景及其所有实体、组件序列化为JSON字符串 * 将场景及其所有实体、组件序列化为JSON字符串或二进制Buffer
* *
* @param options 序列化选项 * @param options 序列化选项
* @returns 序列化后的JSON字符串 * @returns 序列化后的数据(JSON字符串或二进制Buffer
* *
* @example * @example
* ```typescript * ```typescript
* const saveData = scene.serialize({ * // JSON格式
* components: [PlayerComponent, PositionComponent], * const jsonData = scene.serialize({
* format: 'json', * format: 'json',
* pretty: true * pretty: true
* }); * });
*
* // 二进制格式(更小、更快)
* const binaryData = scene.serialize({
* format: 'binary'
* });
* ``` * ```
*/ */
public serialize(options?: SceneSerializationOptions): string { public serialize(options?: SceneSerializationOptions): string | Buffer {
return SceneSerializer.serialize(this, options); return SceneSerializer.serialize(this, options);
} }
@@ -524,19 +529,23 @@ export class Scene implements IScene {
* *
* 从序列化数据恢复场景状态 * 从序列化数据恢复场景状态
* *
* @param saveData 序列化的数据 * @param saveData 序列化的数据JSON字符串或二进制Buffer
* @param options 反序列化选项 * @param options 反序列化选项
* *
* @example * @example
* ```typescript * ```typescript
* scene.deserialize(saveData, { * // 从JSON恢复自动从ComponentRegistry获取组件类型
* strategy: 'replace', * scene.deserialize(jsonData, {
* preserveIds: false, * strategy: 'replace'
* componentRegistry: ComponentTypeRegistry.getRegistry() * });
*
* // 从二进制恢复
* scene.deserialize(binaryData, {
* strategy: 'replace'
* }); * });
* ``` * ```
*/ */
public deserialize(saveData: string, options?: SceneDeserializationOptions): void { public deserialize(saveData: string | Buffer, options?: SceneDeserializationOptions): void {
SceneSerializer.deserialize(this, saveData, options); SceneSerializer.deserialize(this, saveData, options);
} }
} }

View File

@@ -1,194 +0,0 @@
/**
* 全局组件类型注册表
*
* 用于序列化系统的组件类型查找和管理
*/
import { Component } from '../Component';
import { ComponentType } from '../Core/ComponentStorage';
import { getComponentTypeName } from '../Decorators';
/**
* 全局组件类型注册表
*
* 维护组件类型名称到构造函数的映射,用于序列化/反序列化
*/
export class ComponentTypeRegistry {
/**
* 组件类型映射表
* Map<类型名称, 构造函数>
*/
private static registry = new Map<string, ComponentType>();
/**
* 注册组件类型
*
* @param componentClass 组件构造函数
* @param typeName 组件类型名称(可选,默认使用类名或@ECSComponent装饰器指定的名称
*
* @example
* ```typescript
* @ECSComponent('Player')
* @Serializable({ version: 1 })
* class PlayerComponent extends Component {
* @Serialize() name: string = '';
* }
*
* // 注册组件
* ComponentTypeRegistry.register(PlayerComponent);
* ```
*/
public static register(componentClass: ComponentType, typeName?: string): void {
const name = typeName || getComponentTypeName(componentClass);
if (this.registry.has(name)) {
console.warn(`Component type "${name}" is already registered, overwriting...`);
}
this.registry.set(name, componentClass);
}
/**
* 批量注册组件类型
*
* @param componentClasses 组件构造函数数组
*
* @example
* ```typescript
* ComponentTypeRegistry.registerMany([
* PlayerComponent,
* PositionComponent,
* VelocityComponent
* ]);
* ```
*/
public static registerMany(componentClasses: ComponentType[]): void {
for (const componentClass of componentClasses) {
this.register(componentClass);
}
}
/**
* 获取组件类型
*
* @param typeName 组件类型名称
* @returns 组件构造函数如果未找到则返回undefined
*/
public static get(typeName: string): ComponentType | undefined {
return this.registry.get(typeName);
}
/**
* 检查组件类型是否已注册
*
* @param typeName 组件类型名称
* @returns 如果已注册返回true
*/
public static has(typeName: string): boolean {
return this.registry.has(typeName);
}
/**
* 取消注册组件类型
*
* @param typeName 组件类型名称
* @returns 如果成功取消注册返回true
*/
public static unregister(typeName: string): boolean {
return this.registry.delete(typeName);
}
/**
* 清空注册表
*/
public static clear(): void {
this.registry.clear();
}
/**
* 获取所有已注册的组件类型名称
*
* @returns 组件类型名称数组
*/
public static getAllTypeNames(): string[] {
return Array.from(this.registry.keys());
}
/**
* 获取所有已注册的组件类型
*
* @returns 组件构造函数数组
*/
public static getAllTypes(): ComponentType[] {
return Array.from(this.registry.values());
}
/**
* 获取注册表的Map副本
*
* @returns 组件类型注册表的副本
*/
public static getRegistry(): Map<string, ComponentType> {
return new Map(this.registry);
}
/**
* 获取注册的组件数量
*
* @returns 已注册的组件类型数量
*/
public static get size(): number {
return this.registry.size;
}
/**
* 从组件实例获取类型名称
*
* @param component 组件实例
* @returns 组件类型名称
*/
public static getTypeName(component: Component): string {
return getComponentTypeName(component.constructor as ComponentType);
}
/**
* 根据组件类查找已注册的类型名称
*
* @param componentClass 组件构造函数
* @returns 类型名称如果未注册则返回undefined
*/
public static findTypeName(componentClass: ComponentType): string | undefined {
const typeName = getComponentTypeName(componentClass);
// 检查是否已注册
if (this.registry.get(typeName) === componentClass) {
return typeName;
}
// 遍历查找
for (const [name, cls] of this.registry) {
if (cls === componentClass) {
return name;
}
}
return undefined;
}
/**
* 自动发现并注册所有装饰的组件
*
* 注意:此方法需要组件类已经被加载到内存中
*
* @param components 组件类数组
*/
public static autoRegister(components: ComponentType[]): void {
for (const component of components) {
try {
this.register(component);
} catch (error) {
console.error(`Failed to auto-register component ${component.name}:`, error);
}
}
}
}

View File

@@ -7,11 +7,11 @@
import type { IScene } from '../IScene'; import type { IScene } from '../IScene';
import { Entity } from '../Entity'; import { Entity } from '../Entity';
import { Component } from '../Component'; import { Component } from '../Component';
import { ComponentType } from '../Core/ComponentStorage'; import { ComponentType, ComponentRegistry } from '../Core/ComponentStorage';
import { EntitySerializer, SerializedEntity } from './EntitySerializer'; import { EntitySerializer, SerializedEntity } from './EntitySerializer';
import { getComponentTypeName } from '../Decorators'; import { getComponentTypeName } from '../Decorators';
import { getSerializationMetadata } from './SerializationDecorators'; import { getSerializationMetadata } from './SerializationDecorators';
import { ComponentTypeRegistry } from './ComponentTypeRegistry'; import * as msgpack from 'msgpack-lite';
/** /**
* 场景序列化格式 * 场景序列化格式
@@ -154,9 +154,9 @@ export class SceneSerializer {
* *
* @param scene 要序列化的场景 * @param scene 要序列化的场景
* @param options 序列化选项 * @param options 序列化选项
* @returns 序列化后的数据JSON字符串或二进制数据 * @returns 序列化后的数据JSON字符串或二进制Buffer
*/ */
public static serialize(scene: IScene, options?: SceneSerializationOptions): string { public static serialize(scene: IScene, options?: SceneSerializationOptions): string | Buffer {
const opts: SceneSerializationOptions = { const opts: SceneSerializationOptions = {
systems: false, systems: false,
format: 'json', format: 'json',
@@ -206,8 +206,8 @@ export class SceneSerializer {
? JSON.stringify(serializedScene, null, 2) ? JSON.stringify(serializedScene, null, 2)
: JSON.stringify(serializedScene); : JSON.stringify(serializedScene);
} else { } else {
// 二进制格式(未来实现 // 二进制格式(使用 MessagePack
throw new Error('Binary serialization format is not yet implemented'); return msgpack.encode(serializedScene);
} }
} }
@@ -215,12 +215,12 @@ export class SceneSerializer {
* 反序列化场景 * 反序列化场景
* *
* @param scene 目标场景 * @param scene 目标场景
* @param saveData 序列化的数据 * @param saveData 序列化的数据JSON字符串或二进制Buffer
* @param options 反序列化选项 * @param options 反序列化选项
*/ */
public static deserialize( public static deserialize(
scene: IScene, scene: IScene,
saveData: string, saveData: string | Buffer,
options?: SceneDeserializationOptions options?: SceneDeserializationOptions
): void { ): void {
const opts: SceneDeserializationOptions = { const opts: SceneDeserializationOptions = {
@@ -232,7 +232,13 @@ export class SceneSerializer {
// 解析数据 // 解析数据
let serializedScene: SerializedScene; let serializedScene: SerializedScene;
try { try {
serializedScene = JSON.parse(saveData); if (typeof saveData === 'string') {
// JSON格式
serializedScene = JSON.parse(saveData);
} else {
// 二进制格式MessagePack
serializedScene = msgpack.decode(saveData);
}
} catch (error) { } catch (error) {
throw new Error(`Failed to parse save data: ${error}`); throw new Error(`Failed to parse save data: ${error}`);
} }
@@ -455,7 +461,7 @@ export class SceneSerializer {
* 从所有已注册的组件类型构建注册表 * 从所有已注册的组件类型构建注册表
*/ */
private static getGlobalComponentRegistry(): Map<string, ComponentType> { private static getGlobalComponentRegistry(): Map<string, ComponentType> {
return ComponentTypeRegistry.getRegistry(); return ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>;
} }
/** /**

View File

@@ -49,6 +49,3 @@ export type {
ComponentMigrationFunction, ComponentMigrationFunction,
SceneMigrationFunction SceneMigrationFunction
} from './VersionMigration'; } from './VersionMigration';
// 组件类型注册表
export { ComponentTypeRegistry } from './ComponentTypeRegistry';

View File

@@ -198,6 +198,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
protected sharedBuffer: SharedArrayBuffer | null = null; protected sharedBuffer: SharedArrayBuffer | null = null;
protected sharedFloatArray: Float32Array | null = null; protected sharedFloatArray: Float32Array | null = null;
private platformAdapter: IPlatformAdapter; private platformAdapter: IPlatformAdapter;
private hasLoggedSyncMode = false;
constructor(matcher?: Matcher, config: WorkerSystemConfig = {}) { constructor(matcher?: Matcher, config: WorkerSystemConfig = {}) {
super(matcher); super(matcher);
@@ -449,7 +450,10 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
} }
} else { } else {
// 同步处理最后的fallback // 同步处理最后的fallback
this.logger.info(`${this.systemName}: Worker不可用使用同步处理`); if (!this.hasLoggedSyncMode) {
this.logger.info(`${this.systemName}: Worker不可用使用同步处理`);
this.hasLoggedSyncMode = true;
}
this.processSynchronously(entities); this.processSynchronously(entities);
this.isProcessing = false; this.isProcessing = false;
} }

View File

@@ -29,7 +29,6 @@ export * from './ECS';
// TypeScript类型增强API // TypeScript类型增强API
export * from './ECS/TypedEntity'; export * from './ECS/TypedEntity';
export * from './ECS/Systems/TypedEntitySystem';
export * from './ECS/Core/Query/TypedQuery'; export * from './ECS/Core/Query/TypedQuery';
// 事件系统 // 事件系统
@@ -39,5 +38,8 @@ export { ECSEventType, EventPriority, EVENT_TYPES, EventTypeValidator } from './
export * from './Utils'; export * from './Utils';
export * from './Types'; export * from './Types';
// 显式导出ComponentPool类解决与Types中ComponentPool接口的命名冲突
export { ComponentPool, ComponentPoolManager } from './ECS/Core/Storage';
// 平台适配 // 平台适配
export * from './Platform'; export * from './Platform';

View File

@@ -14,11 +14,11 @@ import {
ComponentSerializer, ComponentSerializer,
EntitySerializer, EntitySerializer,
SceneSerializer, SceneSerializer,
ComponentTypeRegistry,
VersionMigrationManager, VersionMigrationManager,
MigrationBuilder MigrationBuilder
} from '../../../src/ECS/Serialization'; } from '../../../src/ECS/Serialization';
import { ECSComponent } from '../../../src/ECS/Decorators'; import { ECSComponent } from '../../../src/ECS/Decorators';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
// 测试组件定义 // 测试组件定义
@ECSComponent('Position') @ECSComponent('Position')
@@ -83,14 +83,8 @@ class NonSerializableComponent extends Component {
describe('ECS Serialization System', () => { describe('ECS Serialization System', () => {
beforeEach(() => { beforeEach(() => {
// 注册组件类型 // @ECSComponent装饰器会自动注册组件到ComponentRegistry无需手动注册
ComponentTypeRegistry.clear(); ComponentRegistry.reset(); // 清空测试环境
ComponentTypeRegistry.registerMany([
PositionComponent,
VelocityComponent,
PlayerComponent,
HealthComponent
]);
}); });
describe('Component Serialization', () => { describe('Component Serialization', () => {
@@ -112,7 +106,7 @@ describe('ECS Serialization System', () => {
data: { x: 150, y: 250 } data: { x: 150, y: 250 }
}; };
const registry = ComponentTypeRegistry.getRegistry(); const registry = ComponentRegistry.getAllComponentNames() as Map<string, any>;
const component = ComponentSerializer.deserialize(serializedData, registry); const component = ComponentSerializer.deserialize(serializedData, registry);
expect(component).not.toBeNull(); expect(component).not.toBeNull();
@@ -152,7 +146,7 @@ describe('ECS Serialization System', () => {
} }
}; };
const registry = ComponentTypeRegistry.getRegistry(); const registry = ComponentRegistry.getAllComponentNames() as Map<string, any>;
const component = ComponentSerializer.deserialize( const component = ComponentSerializer.deserialize(
serializedData, serializedData,
registry registry
@@ -231,7 +225,7 @@ describe('ECS Serialization System', () => {
children: [] children: []
}; };
const registry = ComponentTypeRegistry.getRegistry(); const registry = ComponentRegistry.getAllComponentNames() as Map<string, any>;
let idCounter = 10; let idCounter = 10;
const entity = EntitySerializer.deserialize( const entity = EntitySerializer.deserialize(
serializedEntity, serializedEntity,
@@ -267,8 +261,9 @@ describe('ECS Serialization System', () => {
const saveData = scene.serialize({ format: 'json', pretty: true }); const saveData = scene.serialize({ format: 'json', pretty: true });
expect(saveData).toBeTruthy(); expect(saveData).toBeTruthy();
expect(typeof saveData).toBe('string');
const parsed = JSON.parse(saveData); const parsed = JSON.parse(saveData as string);
expect(parsed.name).toBe('TestScene'); expect(parsed.name).toBe('TestScene');
expect(parsed.version).toBe(1); expect(parsed.version).toBe(1);
expect(parsed.entities.length).toBe(2); expect(parsed.entities.length).toBe(2);
@@ -287,7 +282,7 @@ describe('ECS Serialization System', () => {
// 清空并重新加载 // 清空并重新加载
scene.deserialize(saveData, { scene.deserialize(saveData, {
strategy: 'replace', strategy: 'replace',
componentRegistry: ComponentTypeRegistry.getRegistry() // componentRegistry会自动从ComponentRegistry获取
}); });
expect(scene.entities.count).toBeGreaterThan(0); expect(scene.entities.count).toBeGreaterThan(0);
@@ -300,10 +295,11 @@ describe('ECS Serialization System', () => {
entity.addComponent(new HealthComponent()); entity.addComponent(new HealthComponent());
const saveData = scene.serialize({ const saveData = scene.serialize({
components: [PositionComponent, PlayerComponent] components: [PositionComponent, PlayerComponent],
format: 'json'
}); });
const parsed = JSON.parse(saveData); const parsed = JSON.parse(saveData as string);
expect(parsed.entities.length).toBeGreaterThan(0); expect(parsed.entities.length).toBeGreaterThan(0);
}); });
@@ -315,8 +311,8 @@ describe('ECS Serialization System', () => {
parent.addComponent(new PositionComponent(0, 0)); parent.addComponent(new PositionComponent(0, 0));
child.addComponent(new PositionComponent(10, 10)); child.addComponent(new PositionComponent(10, 10));
const saveData = scene.serialize(); const saveData = scene.serialize({ format: 'json' });
const parsed = JSON.parse(saveData); const parsed = JSON.parse(saveData as string);
// 只有父实体在顶层 // 只有父实体在顶层
expect(parsed.entities.length).toBe(1); expect(parsed.entities.length).toBe(1);
@@ -327,8 +323,8 @@ describe('ECS Serialization System', () => {
const entity = scene.createEntity('Test'); const entity = scene.createEntity('Test');
entity.addComponent(new PositionComponent(5, 5)); entity.addComponent(new PositionComponent(5, 5));
const saveData = scene.serialize(); const saveData = scene.serialize({ format: 'json' });
const validation = SceneSerializer.validate(saveData); const validation = SceneSerializer.validate(saveData as string);
expect(validation.valid).toBe(true); expect(validation.valid).toBe(true);
expect(validation.version).toBe(1); expect(validation.version).toBe(1);
@@ -338,8 +334,8 @@ describe('ECS Serialization System', () => {
const entity = scene.createEntity('InfoTest'); const entity = scene.createEntity('InfoTest');
entity.addComponent(new PositionComponent(1, 1)); entity.addComponent(new PositionComponent(1, 1));
const saveData = scene.serialize(); const saveData = scene.serialize({ format: 'json' });
const info = SceneSerializer.getInfo(saveData); const info = SceneSerializer.getInfo(saveData as string);
expect(info).not.toBeNull(); expect(info).not.toBeNull();
expect(info!.name).toBe('TestScene'); expect(info!.name).toBe('TestScene');
@@ -434,48 +430,7 @@ describe('ECS Serialization System', () => {
}); });
}); });
describe('ComponentTypeRegistry', () => { // ComponentTypeRegistry已被移除现在使用ComponentRegistry自动管理组件类型
it('should register and retrieve component types', () => {
ComponentTypeRegistry.clear();
ComponentTypeRegistry.register(PositionComponent);
expect(ComponentTypeRegistry.has('Position')).toBe(true);
expect(ComponentTypeRegistry.get('Position')).toBe(PositionComponent);
});
it('should register multiple component types', () => {
ComponentTypeRegistry.clear();
ComponentTypeRegistry.registerMany([
PositionComponent,
VelocityComponent,
PlayerComponent
]);
expect(ComponentTypeRegistry.size).toBe(3);
});
it('should get all type names', () => {
ComponentTypeRegistry.clear();
ComponentTypeRegistry.register(PositionComponent);
ComponentTypeRegistry.register(VelocityComponent);
const typeNames = ComponentTypeRegistry.getAllTypeNames();
expect(typeNames).toContain('Position');
expect(typeNames).toContain('Velocity');
});
it('should unregister component types', () => {
ComponentTypeRegistry.clear();
ComponentTypeRegistry.register(PositionComponent);
expect(ComponentTypeRegistry.has('Position')).toBe(true);
ComponentTypeRegistry.unregister('Position');
expect(ComponentTypeRegistry.has('Position')).toBe(false);
});
});
describe('Integration Tests', () => { describe('Integration Tests', () => {
it('should perform full save/load cycle', () => { it('should perform full save/load cycle', () => {
@@ -508,7 +463,7 @@ describe('ECS Serialization System', () => {
// 反序列化 // 反序列化
scene2.deserialize(saveData, { scene2.deserialize(saveData, {
strategy: 'replace', strategy: 'replace',
componentRegistry: ComponentTypeRegistry.getRegistry() // componentRegistry会自动从ComponentRegistry获取
}); });
// 验证 // 验证
@@ -549,7 +504,7 @@ describe('ECS Serialization System', () => {
// 反序列化 // 反序列化
scene2.deserialize(saveData, { scene2.deserialize(saveData, {
strategy: 'replace', strategy: 'replace',
componentRegistry: ComponentTypeRegistry.getRegistry() // componentRegistry会自动从ComponentRegistry获取
}); });
// 验证场景数据 // 验证场景数据
@@ -571,5 +526,100 @@ describe('ECS Serialization System', () => {
scene1.end(); scene1.end();
scene2.end(); scene2.end();
}); });
it('should serialize and deserialize using binary format', () => {
const scene1 = new Scene({ name: 'BinaryTest' });
// 创建测试数据
const player = scene1.createEntity('Player');
const playerComp = new PlayerComponent();
playerComp.name = 'BinaryHero';
playerComp.level = 5;
playerComp.inventory.set('sword', 1);
player.addComponent(playerComp);
player.addComponent(new PositionComponent(100, 200));
scene1.sceneData.set('weather', 'sunny');
scene1.sceneData.set('score', 9999);
// 二进制序列化
const binaryData = scene1.serialize({ format: 'binary' });
// 验证是Buffer类型
expect(Buffer.isBuffer(binaryData)).toBe(true);
// JSON序列化对比
const jsonData = scene1.serialize({ format: 'json', pretty: false });
// 二进制应该更小
const binarySize = (binaryData as Buffer).length;
const jsonSize = (jsonData as string).length;
console.log(`Binary size: ${binarySize} bytes, JSON size: ${jsonSize} bytes`);
expect(binarySize).toBeLessThan(jsonSize);
// 新场景反序列化二进制数据
const scene2 = new Scene({ name: 'LoadTest' });
scene2.deserialize(binaryData, {
strategy: 'replace',
// componentRegistry会自动从ComponentRegistry获取
});
// 验证数据完整性
const loadedPlayer = scene2.findEntity('Player');
expect(loadedPlayer).not.toBeNull();
const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent;
expect(loadedPlayerComp.name).toBe('BinaryHero');
expect(loadedPlayerComp.level).toBe(5);
expect(loadedPlayerComp.inventory.get('sword')).toBe(1);
const loadedPos = loadedPlayer!.getComponent(PositionComponent as any) as PositionComponent;
expect(loadedPos.x).toBe(100);
expect(loadedPos.y).toBe(200);
expect(scene2.sceneData.get('weather')).toBe('sunny');
expect(scene2.sceneData.get('score')).toBe(9999);
scene1.end();
scene2.end();
});
it('should handle complex nested data in binary format', () => {
const scene1 = new Scene({ name: 'NestedBinaryTest' });
// 复杂嵌套数据
scene1.sceneData.set('config', {
graphics: {
quality: 'high',
resolution: { width: 1920, height: 1080 }
},
audio: {
masterVolume: 0.8,
effects: new Map([['music', 0.7], ['sfx', 0.9]])
},
tags: new Set(['multiplayer', 'ranked']),
timestamp: new Date('2024-01-01')
});
// 二进制序列化
const binaryData = scene1.serialize({ format: 'binary' });
// 反序列化
const scene2 = new Scene({ name: 'LoadTest' });
scene2.deserialize(binaryData, {
// componentRegistry会自动从ComponentRegistry获取
});
const config = scene2.sceneData.get('config');
expect(config.graphics.quality).toBe('high');
expect(config.graphics.resolution.width).toBe(1920);
expect(config.audio.masterVolume).toBe(0.8);
expect(config.audio.effects.get('music')).toBe(0.7);
expect(config.tags.has('multiplayer')).toBe(true);
expect(config.timestamp).toBeInstanceOf(Date);
scene1.end();
scene2.end();
});
}); });
}); });