使用Lerna 和 monorepo管理项目结构
This commit is contained in:
124
packages/network/build-rollup.cjs
Normal file
124
packages/network/build-rollup.cjs
Normal file
@@ -0,0 +1,124 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建ECS网络插件...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 构建失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePackageJson() {
|
||||
const sourcePackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const distPackage = {
|
||||
name: sourcePackage.name,
|
||||
version: sourcePackage.version,
|
||||
description: sourcePackage.description,
|
||||
main: 'index.cjs',
|
||||
module: 'index.mjs',
|
||||
types: 'index.d.ts',
|
||||
exports: {
|
||||
'.': {
|
||||
import: './index.mjs',
|
||||
require: './index.cjs',
|
||||
types: './index.d.ts'
|
||||
}
|
||||
},
|
||||
files: [
|
||||
'index.mjs',
|
||||
'index.mjs.map',
|
||||
'index.cjs',
|
||||
'index.cjs.map',
|
||||
'index.d.ts',
|
||||
'README.md',
|
||||
'LICENSE'
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'networking',
|
||||
'frame-sync',
|
||||
'protobuf',
|
||||
'serialization',
|
||||
'multiplayer',
|
||||
'game-engine',
|
||||
'typescript',
|
||||
'rollup'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
dependencies: {
|
||||
'@esengine/ecs-framework': sourcePackage.peerDependencies['@esengine/ecs-framework'],
|
||||
'protobufjs': sourcePackage.dependencies.protobufjs,
|
||||
'reflect-metadata': sourcePackage.dependencies['reflect-metadata']
|
||||
},
|
||||
sideEffects: false
|
||||
};
|
||||
|
||||
fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2));
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
if (fs.existsSync(src)) {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` ✓ 复制: ${path.basename(dest)}`);
|
||||
} else {
|
||||
console.log(` ⚠️ 文件不存在: ${src}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showBuildResults() {
|
||||
const distDir = './dist';
|
||||
const files = ['index.mjs', 'index.cjs', 'index.d.ts'];
|
||||
|
||||
console.log('\n📊 构建结果:');
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(distDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const size = fs.statSync(filePath).size;
|
||||
console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
63
packages/network/jest.config.cjs
Normal file
63
packages/network/jest.config.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/tests', '<rootDir>/src'],
|
||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '\\.performance\\.test\\.ts$'],
|
||||
collectCoverage: false,
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/index.ts',
|
||||
'!src/**/index.ts',
|
||||
'!**/*.d.ts',
|
||||
'!src/**/*.test.ts',
|
||||
'!src/**/*.spec.ts'
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
// 设置覆盖度阈值
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 6,
|
||||
functions: 17,
|
||||
lines: 16,
|
||||
statements: 15
|
||||
},
|
||||
// 核心模块要求更高覆盖率
|
||||
'./src/ECS/Core/': {
|
||||
branches: 8,
|
||||
functions: 20,
|
||||
lines: 18,
|
||||
statements: 18
|
||||
},
|
||||
// ECS基础模块
|
||||
'./src/ECS/': {
|
||||
branches: 7,
|
||||
functions: 18,
|
||||
lines: 17,
|
||||
statements: 16
|
||||
}
|
||||
},
|
||||
verbose: true,
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', {
|
||||
tsconfig: 'tsconfig.test.json',
|
||||
}],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||
// 测试超时设置
|
||||
testTimeout: 10000,
|
||||
// 清除模块缓存
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
// 忽略某些模块
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/bin/',
|
||||
'<rootDir>/dist/',
|
||||
'<rootDir>/node_modules/'
|
||||
]
|
||||
};
|
||||
70
packages/network/package.json
Normal file
70
packages/network/package.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework-network",
|
||||
"version": "1.0.0",
|
||||
"description": "ECS框架网络插件 - 提供protobuf序列化、帧同步和快照功能",
|
||||
"type": "module",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"files": [
|
||||
"bin/**/*",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"networking",
|
||||
"frame-sync",
|
||||
"protobuf",
|
||||
"serialization",
|
||||
"multiplayer",
|
||||
"game-engine",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
"build:watch": "tsc --watch",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build:npm": "npm run build && node build-rollup.cjs",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs",
|
||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
||||
"test:clear": "jest --clearCache"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protobufjs": "^7.5.3",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.1.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/network"
|
||||
}
|
||||
}
|
||||
103
packages/network/rollup.config.cjs
Normal file
103
packages/network/rollup.config.cjs
Normal file
@@ -0,0 +1,103 @@
|
||||
const resolve = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const terser = require('@rollup/plugin-terser');
|
||||
const dts = require('rollup-plugin-dts').default;
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/ecs-framework-network v${pkg.version}
|
||||
* ECS框架网络插件 - protobuf序列化、帧同步和快照功能
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
// 外部依赖 - 不打包到bundle中
|
||||
const external = [
|
||||
'@esengine/ecs-framework',
|
||||
'protobufjs',
|
||||
'reflect-metadata'
|
||||
];
|
||||
|
||||
const commonPlugins = [
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs({
|
||||
include: /node_modules/
|
||||
})
|
||||
];
|
||||
|
||||
module.exports = [
|
||||
// ES模块构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.mjs',
|
||||
format: 'es',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
unknownGlobalSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// CommonJS构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// 类型定义构建
|
||||
{
|
||||
input: 'bin/index.d.ts',
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/ecs-framework-network v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
respectExternal: true
|
||||
})
|
||||
],
|
||||
external: ['@esengine/ecs-framework']
|
||||
}
|
||||
];
|
||||
47
packages/network/src/INetworkSyncable.ts
Normal file
47
packages/network/src/INetworkSyncable.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 网络同步接口
|
||||
*
|
||||
* 为帧同步框架提供网络状态管理
|
||||
*/
|
||||
export interface INetworkSyncable {
|
||||
/**
|
||||
* 获取网络同步状态
|
||||
*
|
||||
* @returns 序列化的网络状态数据
|
||||
*/
|
||||
getNetworkState(): Uint8Array;
|
||||
|
||||
/**
|
||||
* 应用网络状态
|
||||
*
|
||||
* @param data - 网络状态数据
|
||||
*/
|
||||
applyNetworkState(data: Uint8Array): void;
|
||||
|
||||
/**
|
||||
* 获取变化的字段编号列表
|
||||
*
|
||||
* @returns 变化字段的编号数组
|
||||
*/
|
||||
getDirtyFields(): number[];
|
||||
|
||||
/**
|
||||
* 标记所有字段为干净状态
|
||||
*/
|
||||
markClean(): void;
|
||||
|
||||
/**
|
||||
* 标记字段为脏状态
|
||||
*
|
||||
* @param fieldNumber - 字段编号
|
||||
*/
|
||||
markFieldDirty(fieldNumber: number): void;
|
||||
|
||||
/**
|
||||
* 检查字段是否为脏状态
|
||||
*
|
||||
* @param fieldNumber - 字段编号
|
||||
* @returns 是否为脏状态
|
||||
*/
|
||||
isFieldDirty(fieldNumber: number): boolean;
|
||||
}
|
||||
161
packages/network/src/NetworkComponent.ts
Normal file
161
packages/network/src/NetworkComponent.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { INetworkSyncable } from './INetworkSyncable';
|
||||
|
||||
/**
|
||||
* 网络组件基类
|
||||
*
|
||||
* 继承核心ECS的Component类,添加网络同步功能。
|
||||
* 用于需要网络同步的组件,提供帧同步框架所需的网络状态管理。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { NetworkComponent } from '@esengine/ecs-framework-network';
|
||||
* import { ProtoSerializable, ProtoFloat } from '@esengine/ecs-framework-network';
|
||||
*
|
||||
* @ProtoSerializable('Position')
|
||||
* class PositionComponent extends NetworkComponent {
|
||||
* @ProtoFloat(1)
|
||||
* public x: number = 0;
|
||||
*
|
||||
* @ProtoFloat(2)
|
||||
* public y: number = 0;
|
||||
*
|
||||
* constructor(x: number = 0, y: number = 0) {
|
||||
* super();
|
||||
* this.x = x;
|
||||
* this.y = y;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class NetworkComponent extends Component implements INetworkSyncable {
|
||||
/**
|
||||
* 脏字段标记集合
|
||||
*
|
||||
* 记录已修改但尚未同步的字段编号
|
||||
*/
|
||||
private _dirtyFields: Set<number> = new Set();
|
||||
|
||||
/**
|
||||
* 字段变化时间戳
|
||||
*
|
||||
* 记录每个字段最后修改的时间
|
||||
*/
|
||||
private _fieldTimestamps: Map<number, number> = new Map();
|
||||
|
||||
/**
|
||||
* 获取网络同步状态
|
||||
*
|
||||
* 序列化当前组件状态为网络传输格式
|
||||
* @returns 序列化的网络状态数据
|
||||
*/
|
||||
public getNetworkState(): Uint8Array {
|
||||
const { isProtoSerializable } = require('./Serialization/ProtobufDecorators');
|
||||
const { ProtobufSerializer } = require('./Serialization/ProtobufSerializer');
|
||||
|
||||
if (!isProtoSerializable(this)) {
|
||||
throw new Error(`组件 ${this.constructor.name} 不支持网络同步,请添加@ProtoSerializable装饰器`);
|
||||
}
|
||||
|
||||
try {
|
||||
const serializer = ProtobufSerializer.getInstance();
|
||||
const serializedData = serializer.serialize(this);
|
||||
return serializedData.data;
|
||||
} catch (error) {
|
||||
throw new Error(`获取网络状态失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用网络状态
|
||||
*
|
||||
* 从网络数据恢复组件状态
|
||||
* @param data - 网络状态数据
|
||||
*/
|
||||
public applyNetworkState(data: Uint8Array): void {
|
||||
const { isProtoSerializable } = require('./Serialization/ProtobufDecorators');
|
||||
const { ProtobufSerializer } = require('./Serialization/ProtobufSerializer');
|
||||
|
||||
if (!isProtoSerializable(this)) {
|
||||
throw new Error(`组件 ${this.constructor.name} 不支持网络同步,请添加@ProtoSerializable装饰器`);
|
||||
}
|
||||
|
||||
try {
|
||||
const serializer = ProtobufSerializer.getInstance();
|
||||
const serializedData = {
|
||||
type: 'protobuf' as const,
|
||||
componentType: this.constructor.name,
|
||||
data: data,
|
||||
size: data.length
|
||||
};
|
||||
serializer.deserialize(this, serializedData);
|
||||
|
||||
// 应用后清理脏字段标记
|
||||
this.markClean();
|
||||
} catch (error) {
|
||||
throw new Error(`应用网络状态失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变化的字段编号列表
|
||||
*
|
||||
* @returns 变化字段的编号数组
|
||||
*/
|
||||
public getDirtyFields(): number[] {
|
||||
return Array.from(this._dirtyFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记所有字段为干净状态
|
||||
*
|
||||
* 清除所有脏字段标记
|
||||
*/
|
||||
public markClean(): void {
|
||||
this._dirtyFields.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记字段为脏状态
|
||||
*
|
||||
* 用于标记字段已修改,需要网络同步
|
||||
* @param fieldNumber - 字段编号
|
||||
*/
|
||||
public markFieldDirty(fieldNumber: number): void {
|
||||
this._dirtyFields.add(fieldNumber);
|
||||
this._fieldTimestamps.set(fieldNumber, Date.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字段是否为脏状态
|
||||
*
|
||||
* @param fieldNumber - 字段编号
|
||||
* @returns 是否为脏状态
|
||||
*/
|
||||
public isFieldDirty(fieldNumber: number): boolean {
|
||||
return this._dirtyFields.has(fieldNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段最后修改时间
|
||||
*
|
||||
* @param fieldNumber - 字段编号
|
||||
* @returns 最后修改时间戳,如果字段从未修改则返回0
|
||||
*/
|
||||
public getFieldTimestamp(fieldNumber: number): number {
|
||||
return this._fieldTimestamps.get(fieldNumber) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有脏字段及其时间戳
|
||||
*
|
||||
* @returns 脏字段和时间戳的映射
|
||||
*/
|
||||
public getDirtyFieldsWithTimestamps(): Map<number, number> {
|
||||
const result = new Map<number, number>();
|
||||
for (const fieldNumber of this._dirtyFields) {
|
||||
result.set(fieldNumber, this._fieldTimestamps.get(fieldNumber) || 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
459
packages/network/src/Serialization/ProtobufDecorators.ts
Normal file
459
packages/network/src/Serialization/ProtobufDecorators.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* Protobuf序列化装饰器
|
||||
*
|
||||
* 提供装饰器语法来标记组件和字段进行protobuf序列化
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Protobuf字段类型枚举
|
||||
*/
|
||||
export enum ProtoFieldType {
|
||||
DOUBLE = 'double',
|
||||
FLOAT = 'float',
|
||||
INT32 = 'int32',
|
||||
INT64 = 'int64',
|
||||
UINT32 = 'uint32',
|
||||
UINT64 = 'uint64',
|
||||
SINT32 = 'sint32',
|
||||
SINT64 = 'sint64',
|
||||
FIXED32 = 'fixed32',
|
||||
FIXED64 = 'fixed64',
|
||||
SFIXED32 = 'sfixed32',
|
||||
SFIXED64 = 'sfixed64',
|
||||
BOOL = 'bool',
|
||||
STRING = 'string',
|
||||
BYTES = 'bytes',
|
||||
// 扩展类型
|
||||
MESSAGE = 'message',
|
||||
ENUM = 'enum',
|
||||
ANY = 'google.protobuf.Any',
|
||||
TIMESTAMP = 'google.protobuf.Timestamp',
|
||||
DURATION = 'google.protobuf.Duration',
|
||||
STRUCT = 'google.protobuf.Struct',
|
||||
VALUE = 'google.protobuf.Value'
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段同步优先级
|
||||
*/
|
||||
export enum FieldSyncPriority {
|
||||
/** 关键字段 - 每帧必须同步 */
|
||||
CRITICAL = 'critical',
|
||||
/** 高优先级 - 高频同步 */
|
||||
HIGH = 'high',
|
||||
/** 中等优先级 - 中频同步 */
|
||||
MEDIUM = 'medium',
|
||||
/** 低优先级 - 低频同步 */
|
||||
LOW = 'low'
|
||||
}
|
||||
|
||||
/**
|
||||
* Protobuf字段定义接口
|
||||
*/
|
||||
export interface ProtoFieldDefinition {
|
||||
/** 字段编号 */
|
||||
fieldNumber: number;
|
||||
/** 字段类型 */
|
||||
type: ProtoFieldType;
|
||||
/** 是否为数组 */
|
||||
repeated?: boolean;
|
||||
/** 是否可选 */
|
||||
optional?: boolean;
|
||||
/** 字段名称 */
|
||||
name: string;
|
||||
/** 自定义类型名称 */
|
||||
customTypeName?: string;
|
||||
/** 枚举值映射 */
|
||||
enumValues?: Record<string, number>;
|
||||
/** 默认值 */
|
||||
defaultValue?: any;
|
||||
|
||||
// 帧同步特定选项
|
||||
/** 同步优先级 */
|
||||
syncPriority?: FieldSyncPriority;
|
||||
/** 数值精度(用于量化压缩) */
|
||||
precision?: number;
|
||||
/** 是否支持插值 */
|
||||
interpolation?: boolean;
|
||||
/** 量化位数 */
|
||||
quantizationBits?: number;
|
||||
/** 变化阈值(小于此值不同步) */
|
||||
changeThreshold?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件同步模式
|
||||
*/
|
||||
export enum ComponentSyncMode {
|
||||
/** 完整同步 - 传输所有字段 */
|
||||
FULL = 'full',
|
||||
/** 增量同步 - 只传输变化字段 */
|
||||
DELTA = 'delta',
|
||||
/** 自适应 - 根据变化量自动选择 */
|
||||
ADAPTIVE = 'adaptive'
|
||||
}
|
||||
|
||||
/**
|
||||
* Protobuf组件定义接口
|
||||
*/
|
||||
export interface ProtoComponentDefinition {
|
||||
/** 组件名称 */
|
||||
name: string;
|
||||
/** 字段定义列表 */
|
||||
fields: Map<string, ProtoFieldDefinition>;
|
||||
/** 构造函数 */
|
||||
constructor: any;
|
||||
|
||||
// 帧同步特定选项
|
||||
/** 同步模式 */
|
||||
syncMode?: ComponentSyncMode;
|
||||
/** 同步频率(每秒同步次数) */
|
||||
syncFrequency?: number;
|
||||
/** 是否启用压缩 */
|
||||
enableCompression?: boolean;
|
||||
/** 网络优先级 */
|
||||
networkPriority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protobuf注册表
|
||||
*/
|
||||
export class ProtobufRegistry {
|
||||
private static instance: ProtobufRegistry;
|
||||
private components = new Map<string, ProtoComponentDefinition>();
|
||||
|
||||
public static getInstance(): ProtobufRegistry {
|
||||
if (!ProtobufRegistry.instance) {
|
||||
ProtobufRegistry.instance = new ProtobufRegistry();
|
||||
}
|
||||
return ProtobufRegistry.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册组件定义
|
||||
*/
|
||||
public registerComponent(componentName: string, definition: ProtoComponentDefinition): void {
|
||||
this.components.set(componentName, definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件定义
|
||||
*/
|
||||
public getComponentDefinition(componentName: string): ProtoComponentDefinition | undefined {
|
||||
return this.components.get(componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否支持protobuf
|
||||
*/
|
||||
public hasProtoDefinition(componentName: string): boolean {
|
||||
return this.components.has(componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册的组件
|
||||
*/
|
||||
public getAllComponents(): Map<string, ProtoComponentDefinition> {
|
||||
return new Map(this.components);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成proto文件定义
|
||||
*/
|
||||
public generateProtoDefinition(): string {
|
||||
let protoContent = 'syntax = "proto3";\n\n';
|
||||
protoContent += 'package ecs;\n\n';
|
||||
|
||||
// 生成消息定义
|
||||
for (const [name, definition] of this.components) {
|
||||
protoContent += `message ${name} {\n`;
|
||||
|
||||
// 按字段编号排序
|
||||
const sortedFields = Array.from(definition.fields.values())
|
||||
.sort((a, b) => a.fieldNumber - b.fieldNumber);
|
||||
|
||||
for (const field of sortedFields) {
|
||||
let fieldDef = ' ';
|
||||
|
||||
if (field.repeated) {
|
||||
fieldDef += 'repeated ';
|
||||
} else if (field.optional) {
|
||||
fieldDef += 'optional ';
|
||||
}
|
||||
|
||||
fieldDef += `${field.type} ${field.name} = ${field.fieldNumber};\n`;
|
||||
protoContent += fieldDef;
|
||||
}
|
||||
|
||||
protoContent += '}\n\n';
|
||||
}
|
||||
|
||||
return protoContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件同步选项接口
|
||||
*/
|
||||
export interface ComponentSyncOptions {
|
||||
/** 同步模式 */
|
||||
syncMode?: ComponentSyncMode;
|
||||
/** 同步频率(每秒同步次数) */
|
||||
syncFrequency?: number;
|
||||
/** 是否启用压缩 */
|
||||
enableCompression?: boolean;
|
||||
/** 网络优先级(1-10,数字越大优先级越高) */
|
||||
networkPriority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProtoSerializable 组件装饰器
|
||||
*
|
||||
* 标记组件支持protobuf序列化,专为帧同步框架优化
|
||||
* @param protoName protobuf消息名称,默认使用类名
|
||||
* @param options 同步选项
|
||||
* @example
|
||||
* ```typescript
|
||||
* @ProtoSerializable('Position', {
|
||||
* syncMode: ComponentSyncMode.DELTA,
|
||||
* syncFrequency: 30,
|
||||
* networkPriority: 8
|
||||
* })
|
||||
* class PositionComponent extends Component {
|
||||
* // 组件实现
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ProtoSerializable(protoName?: string, options?: ComponentSyncOptions) {
|
||||
return function <T extends { new(...args: any[]): Component }>(constructor: T) {
|
||||
const componentName = protoName || constructor.name;
|
||||
const registry = ProtobufRegistry.getInstance();
|
||||
|
||||
// 获取字段定义
|
||||
const fields = (constructor.prototype._protoFields as Map<string, ProtoFieldDefinition>)
|
||||
|| new Map<string, ProtoFieldDefinition>();
|
||||
|
||||
// 注册组件定义
|
||||
registry.registerComponent(componentName, {
|
||||
name: componentName,
|
||||
fields: fields,
|
||||
constructor: constructor,
|
||||
syncMode: options?.syncMode || ComponentSyncMode.FULL,
|
||||
syncFrequency: options?.syncFrequency || 30,
|
||||
enableCompression: options?.enableCompression || true,
|
||||
networkPriority: options?.networkPriority || 5
|
||||
});
|
||||
|
||||
// 标记组件支持protobuf
|
||||
(constructor.prototype._isProtoSerializable = true);
|
||||
(constructor.prototype._protoName = componentName);
|
||||
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段同步选项接口
|
||||
*/
|
||||
export interface FieldSyncOptions {
|
||||
/** 是否为数组 */
|
||||
repeated?: boolean;
|
||||
/** 是否可选 */
|
||||
optional?: boolean;
|
||||
/** 自定义类型名称 */
|
||||
customTypeName?: string;
|
||||
/** 枚举值映射 */
|
||||
enumValues?: Record<string, number>;
|
||||
/** 默认值 */
|
||||
defaultValue?: any;
|
||||
|
||||
// 帧同步特定选项
|
||||
/** 同步优先级 */
|
||||
syncPriority?: FieldSyncPriority;
|
||||
/** 数值精度(用于量化压缩) */
|
||||
precision?: number;
|
||||
/** 是否支持插值 */
|
||||
interpolation?: boolean;
|
||||
/** 量化位数 */
|
||||
quantizationBits?: number;
|
||||
/** 变化阈值(小于此值不同步) */
|
||||
changeThreshold?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProtoField 字段装饰器
|
||||
*
|
||||
* 标记字段参与protobuf序列化,针对帧同步进行优化
|
||||
* @param fieldNumber protobuf字段编号,必须唯一且大于0
|
||||
* @param type 字段类型,默认自动推断
|
||||
* @param options 字段选项
|
||||
* @example
|
||||
* ```typescript
|
||||
* class PositionComponent extends Component {
|
||||
* @ProtoField(1, ProtoFieldType.FLOAT, {
|
||||
* syncPriority: FieldSyncPriority.CRITICAL,
|
||||
* precision: 0.01,
|
||||
* interpolation: true
|
||||
* })
|
||||
* public x: number = 0;
|
||||
*
|
||||
* @ProtoField(2, ProtoFieldType.FLOAT, {
|
||||
* syncPriority: FieldSyncPriority.CRITICAL,
|
||||
* precision: 0.01,
|
||||
* interpolation: true
|
||||
* })
|
||||
* public y: number = 0;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ProtoField(
|
||||
fieldNumber: number,
|
||||
type?: ProtoFieldType,
|
||||
options?: FieldSyncOptions
|
||||
) {
|
||||
return function (target: any, propertyKey: string) {
|
||||
// 验证字段编号
|
||||
if (fieldNumber <= 0) {
|
||||
throw new Error(`ProtoField: 字段编号必须大于0,当前值: ${fieldNumber}`);
|
||||
}
|
||||
|
||||
// 初始化字段集合
|
||||
if (!target._protoFields) {
|
||||
target._protoFields = new Map<string, ProtoFieldDefinition>();
|
||||
}
|
||||
|
||||
// 自动推断类型
|
||||
let inferredType = type;
|
||||
if (!inferredType) {
|
||||
const designType = Reflect.getMetadata?.('design:type', target, propertyKey);
|
||||
inferredType = inferProtoType(designType);
|
||||
}
|
||||
|
||||
// 检查字段编号冲突
|
||||
for (const [key, field] of target._protoFields) {
|
||||
if (field.fieldNumber === fieldNumber && key !== propertyKey) {
|
||||
throw new Error(`ProtoField: 字段编号 ${fieldNumber} 已被字段 ${key} 使用`);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加字段定义
|
||||
target._protoFields.set(propertyKey, {
|
||||
fieldNumber,
|
||||
type: inferredType || ProtoFieldType.STRING,
|
||||
repeated: options?.repeated || false,
|
||||
optional: options?.optional || false,
|
||||
name: propertyKey,
|
||||
customTypeName: options?.customTypeName,
|
||||
enumValues: options?.enumValues,
|
||||
defaultValue: options?.defaultValue,
|
||||
// 帧同步特定选项
|
||||
syncPriority: options?.syncPriority || FieldSyncPriority.MEDIUM,
|
||||
precision: options?.precision,
|
||||
interpolation: options?.interpolation || false,
|
||||
quantizationBits: options?.quantizationBits,
|
||||
changeThreshold: options?.changeThreshold || 0
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动推断protobuf类型
|
||||
*/
|
||||
function inferProtoType(jsType: any): ProtoFieldType {
|
||||
if (!jsType) return ProtoFieldType.STRING;
|
||||
|
||||
switch (jsType) {
|
||||
case Number:
|
||||
return ProtoFieldType.DOUBLE;
|
||||
case Boolean:
|
||||
return ProtoFieldType.BOOL;
|
||||
case String:
|
||||
return ProtoFieldType.STRING;
|
||||
case Date:
|
||||
return ProtoFieldType.TIMESTAMP;
|
||||
case Array:
|
||||
return ProtoFieldType.STRING;
|
||||
case Uint8Array:
|
||||
case ArrayBuffer:
|
||||
return ProtoFieldType.BYTES;
|
||||
case Object:
|
||||
return ProtoFieldType.STRUCT;
|
||||
default:
|
||||
return ProtoFieldType.STRING;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷装饰器 - 常用类型
|
||||
*/
|
||||
export const ProtoInt32 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.INT32, options);
|
||||
|
||||
export const ProtoFloat = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.FLOAT, options);
|
||||
|
||||
export const ProtoString = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.STRING, options);
|
||||
|
||||
export const ProtoBool = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.BOOL, options);
|
||||
|
||||
// 扩展的便捷装饰器
|
||||
export const ProtoDouble = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.DOUBLE, options);
|
||||
|
||||
export const ProtoInt64 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.INT64, options);
|
||||
|
||||
export const ProtoUint32 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.UINT32, options);
|
||||
|
||||
export const ProtoUint64 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.UINT64, options);
|
||||
|
||||
export const ProtoBytes = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.BYTES, options);
|
||||
|
||||
export const ProtoTimestamp = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.TIMESTAMP, options);
|
||||
|
||||
export const ProtoDuration = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.DURATION, options);
|
||||
|
||||
export const ProtoStruct = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.STRUCT, options);
|
||||
|
||||
/**
|
||||
* 自定义消息类型装饰器
|
||||
* @param fieldNumber 字段编号
|
||||
* @param customTypeName 自定义类型名称
|
||||
* @param options 额外选项
|
||||
*/
|
||||
export const ProtoMessage = (fieldNumber: number, customTypeName: string, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.MESSAGE, { ...options, customTypeName });
|
||||
|
||||
/**
|
||||
* 枚举类型装饰器
|
||||
* @param fieldNumber 字段编号
|
||||
* @param enumValues 枚举值映射
|
||||
* @param options 额外选项
|
||||
*/
|
||||
export const ProtoEnum = (fieldNumber: number, enumValues: Record<string, number>, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.ENUM, { ...options, enumValues });
|
||||
|
||||
/**
|
||||
* 检查组件是否支持protobuf序列化
|
||||
*/
|
||||
export function isProtoSerializable(component: Component): boolean {
|
||||
return !!(component as any)._isProtoSerializable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件的protobuf名称
|
||||
*/
|
||||
export function getProtoName(component: Component): string | undefined {
|
||||
return (component as any)._protoName;
|
||||
}
|
||||
693
packages/network/src/Serialization/ProtobufSerializer.ts
Normal file
693
packages/network/src/Serialization/ProtobufSerializer.ts
Normal file
@@ -0,0 +1,693 @@
|
||||
/**
|
||||
* Protobuf序列化器
|
||||
*
|
||||
* 处理组件的protobuf序列化和反序列化
|
||||
*/
|
||||
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { BigIntFactory } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ProtobufRegistry,
|
||||
ProtoComponentDefinition,
|
||||
ProtoFieldDefinition,
|
||||
ProtoFieldType,
|
||||
isProtoSerializable,
|
||||
getProtoName
|
||||
} from './ProtobufDecorators';
|
||||
import { SerializedData } from './SerializationTypes';
|
||||
|
||||
|
||||
/**
|
||||
* Protobuf序列化器
|
||||
*/
|
||||
export class ProtobufSerializer {
|
||||
private registry: ProtobufRegistry;
|
||||
private static instance: ProtobufSerializer;
|
||||
|
||||
/** protobuf.js库实例 */
|
||||
private protobuf: any = null;
|
||||
private root: any = null;
|
||||
|
||||
/** MessageType缓存映射表 */
|
||||
private messageTypeCache: Map<string, any> = new Map();
|
||||
|
||||
/** 组件序列化数据缓存 */
|
||||
private componentDataCache: Map<string, any> = new Map();
|
||||
|
||||
/** 缓存访问计数器 */
|
||||
private cacheAccessCount: Map<string, number> = new Map();
|
||||
|
||||
/** 最大缓存大小 */
|
||||
private maxCacheSize: number = 1000;
|
||||
|
||||
/** 是否启用数据验证 */
|
||||
private enableValidation: boolean = process.env.NODE_ENV === 'development';
|
||||
|
||||
/** 是否启用组件数据缓存 */
|
||||
private enableComponentDataCache: boolean = true;
|
||||
|
||||
private constructor() {
|
||||
this.registry = ProtobufRegistry.getInstance();
|
||||
this.initializeProtobuf();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置性能选项
|
||||
* @param options 性能配置选项
|
||||
* @param options.enableValidation 是否启用数据验证
|
||||
* @param options.enableComponentDataCache 是否启用组件数据缓存
|
||||
* @param options.maxCacheSize 最大缓存大小
|
||||
* @param options.clearCache 是否清空消息类型缓存
|
||||
* @param options.clearAllCaches 是否清空所有缓存
|
||||
*/
|
||||
public setPerformanceOptions(options: {
|
||||
enableValidation?: boolean;
|
||||
enableComponentDataCache?: boolean;
|
||||
maxCacheSize?: number;
|
||||
clearCache?: boolean;
|
||||
clearAllCaches?: boolean;
|
||||
}): void {
|
||||
if (options.enableValidation !== undefined) {
|
||||
this.enableValidation = options.enableValidation;
|
||||
}
|
||||
if (options.enableComponentDataCache !== undefined) {
|
||||
this.enableComponentDataCache = options.enableComponentDataCache;
|
||||
}
|
||||
if (options.maxCacheSize !== undefined && options.maxCacheSize > 0) {
|
||||
this.maxCacheSize = options.maxCacheSize;
|
||||
}
|
||||
if (options.clearCache) {
|
||||
this.messageTypeCache.clear();
|
||||
}
|
||||
if (options.clearAllCaches) {
|
||||
this.clearAllCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
public clearAllCaches(): void {
|
||||
this.messageTypeCache.clear();
|
||||
this.componentDataCache.clear();
|
||||
this.cacheAccessCount.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动初始化protobuf支持
|
||||
*/
|
||||
private async initializeProtobuf(): Promise<void> {
|
||||
try {
|
||||
// 动态导入protobufjs
|
||||
this.protobuf = await import('protobufjs');
|
||||
this.buildProtoDefinitions();
|
||||
console.log('[ProtobufSerializer] Protobuf支持已启用');
|
||||
} catch (error) {
|
||||
throw new Error('[ProtobufSerializer] 无法加载protobufjs: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): ProtobufSerializer {
|
||||
if (!ProtobufSerializer.instance) {
|
||||
ProtobufSerializer.instance = new ProtobufSerializer();
|
||||
}
|
||||
return ProtobufSerializer.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动初始化protobuf.js库
|
||||
* @param protobufJs protobuf.js库实例
|
||||
*/
|
||||
public initialize(protobufJs: any): void {
|
||||
this.protobuf = protobufJs;
|
||||
this.buildProtoDefinitions();
|
||||
console.log('[ProtobufSerializer] Protobuf支持已手动启用');
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化组件
|
||||
* @param component 要序列化的组件
|
||||
* @returns 序列化数据
|
||||
*/
|
||||
public serialize(component: Component): SerializedData {
|
||||
const componentType = component.constructor.name;
|
||||
|
||||
// 检查是否支持protobuf序列化
|
||||
if (!isProtoSerializable(component)) {
|
||||
throw new Error(`组件 ${componentType} 不支持protobuf序列化,请添加@ProtoSerializable装饰器`);
|
||||
}
|
||||
|
||||
const protoName = getProtoName(component);
|
||||
if (!protoName) {
|
||||
throw new Error(`组件 ${componentType} 未设置protobuf名称`);
|
||||
}
|
||||
|
||||
const definition = this.registry.getComponentDefinition(protoName);
|
||||
if (!definition) {
|
||||
throw new Error(`未找到组件定义: ${protoName}`);
|
||||
}
|
||||
|
||||
// 获取protobuf消息类型
|
||||
const MessageType = this.getMessageType(protoName);
|
||||
if (!MessageType) {
|
||||
throw new Error(`未找到消息类型: ${protoName}`);
|
||||
}
|
||||
|
||||
// 构建protobuf数据对象
|
||||
const protoData = this.buildProtoData(component, definition);
|
||||
|
||||
// 数据验证
|
||||
if (this.enableValidation) {
|
||||
const error = MessageType.verify(protoData);
|
||||
if (error) {
|
||||
throw new Error(`数据验证失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建消息并编码
|
||||
const message = MessageType.create(protoData);
|
||||
const buffer = MessageType.encode(message).finish();
|
||||
|
||||
return {
|
||||
type: 'protobuf',
|
||||
componentType: componentType,
|
||||
data: buffer,
|
||||
size: buffer.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化组件
|
||||
* @param component 目标组件实例
|
||||
* @param serializedData 序列化数据
|
||||
*/
|
||||
public deserialize(component: Component, serializedData: SerializedData): void {
|
||||
if (serializedData.type !== 'protobuf') {
|
||||
throw new Error(`不支持的序列化类型: ${serializedData.type}`);
|
||||
}
|
||||
|
||||
const protoName = getProtoName(component);
|
||||
if (!protoName) {
|
||||
throw new Error(`组件 ${component.constructor.name} 未设置protobuf名称`);
|
||||
}
|
||||
|
||||
const MessageType = this.getMessageType(protoName);
|
||||
if (!MessageType) {
|
||||
throw new Error(`未找到消息类型: ${protoName}`);
|
||||
}
|
||||
|
||||
// 解码消息
|
||||
const message = MessageType.decode(serializedData.data);
|
||||
const data = MessageType.toObject(message);
|
||||
|
||||
// 应用数据到组件
|
||||
this.applyDataToComponent(component, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否支持protobuf序列化
|
||||
*/
|
||||
public canSerialize(component: Component): boolean {
|
||||
if (!this.protobuf) return false;
|
||||
return isProtoSerializable(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量序列化组件
|
||||
* @param components 要序列化的组件数组
|
||||
* @param options 批量序列化选项
|
||||
* @param options.continueOnError 遇到错误时是否继续处理
|
||||
* @param options.maxBatchSize 最大批次大小
|
||||
* @returns 序列化结果数组
|
||||
*/
|
||||
public serializeBatch(
|
||||
components: Component[],
|
||||
options?: {
|
||||
continueOnError?: boolean;
|
||||
maxBatchSize?: number;
|
||||
}
|
||||
): SerializedData[] {
|
||||
const results: SerializedData[] = [];
|
||||
const errors: Error[] = [];
|
||||
|
||||
const continueOnError = options?.continueOnError ?? false;
|
||||
const maxBatchSize = options?.maxBatchSize ?? 1000;
|
||||
|
||||
// 分批处理大量组件
|
||||
const batches = this.splitIntoBatches(components, maxBatchSize);
|
||||
|
||||
for (const batch of batches) {
|
||||
const batchResults = this.serializeBatchSerial(batch, continueOnError);
|
||||
results.push(...batchResults.results);
|
||||
errors.push(...batchResults.errors);
|
||||
}
|
||||
|
||||
// 如果有错误且不继续执行,抛出第一个错误
|
||||
if (errors.length > 0 && !continueOnError) {
|
||||
throw errors[0];
|
||||
}
|
||||
|
||||
// 记录错误统计
|
||||
if (errors.length > 0) {
|
||||
console.warn(`[ProtobufSerializer] 批量序列化完成,${results.length} 成功,${errors.length} 失败`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行批量序列化
|
||||
*/
|
||||
private serializeBatchSerial(
|
||||
components: Component[],
|
||||
continueOnError: boolean
|
||||
): { results: SerializedData[], errors: Error[] } {
|
||||
const results: SerializedData[] = [];
|
||||
const errors: Error[] = [];
|
||||
|
||||
// 按组件类型分组,减少重复查找
|
||||
const componentGroups = this.groupComponentsByType(components, continueOnError, errors);
|
||||
|
||||
// 按组分别序列化
|
||||
for (const [protoName, groupComponents] of componentGroups) {
|
||||
const definition = this.registry.getComponentDefinition(protoName);
|
||||
const MessageType = this.getMessageType(protoName);
|
||||
|
||||
if (!definition || !MessageType) {
|
||||
const error = new Error(`[ProtobufSerializer] 组件类型 ${protoName} 未正确注册`);
|
||||
if (continueOnError) {
|
||||
errors.push(error);
|
||||
continue;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 预编译消息类型和字段定义
|
||||
const compiledType = this.getCompiledMessageType(protoName, definition, MessageType);
|
||||
|
||||
for (const component of groupComponents) {
|
||||
try {
|
||||
const result = this.serializeSingleComponent(component, definition, compiledType);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
if (continueOnError) {
|
||||
errors.push(error instanceof Error ? error : new Error(String(error)));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { results, errors };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 序列化单个组件
|
||||
*/
|
||||
private serializeSingleComponent(
|
||||
component: Component,
|
||||
definition: ProtoComponentDefinition,
|
||||
compiledType: any
|
||||
): SerializedData {
|
||||
const protoData = this.buildProtoData(component, definition);
|
||||
|
||||
// 数据验证
|
||||
if (this.enableValidation && compiledType.verify) {
|
||||
const error = compiledType.verify(protoData);
|
||||
if (error) {
|
||||
throw new Error(`[ProtobufSerializer] 数据验证失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const message = compiledType.create(protoData);
|
||||
const buffer = compiledType.encode(message).finish();
|
||||
|
||||
return {
|
||||
type: 'protobuf',
|
||||
componentType: component.constructor.name,
|
||||
data: buffer,
|
||||
size: buffer.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型分组组件
|
||||
*/
|
||||
private groupComponentsByType(
|
||||
components: Component[],
|
||||
continueOnError: boolean,
|
||||
errors: Error[]
|
||||
): Map<string, Component[]> {
|
||||
const componentGroups = new Map<string, Component[]>();
|
||||
|
||||
for (const component of components) {
|
||||
try {
|
||||
if (!isProtoSerializable(component)) {
|
||||
throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 不支持protobuf序列化`);
|
||||
}
|
||||
|
||||
const protoName = getProtoName(component);
|
||||
if (!protoName) {
|
||||
throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 未设置protobuf名称`);
|
||||
}
|
||||
|
||||
if (!componentGroups.has(protoName)) {
|
||||
componentGroups.set(protoName, []);
|
||||
}
|
||||
componentGroups.get(protoName)!.push(component);
|
||||
} catch (error) {
|
||||
if (continueOnError) {
|
||||
errors.push(error instanceof Error ? error : new Error(String(error)));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return componentGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取编译后的消息类型
|
||||
*/
|
||||
private getCompiledMessageType(_protoName: string, _definition: ProtoComponentDefinition, MessageType: any): any {
|
||||
// TODO: 实现消息类型编译和缓存优化
|
||||
return MessageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组分割成批次
|
||||
*/
|
||||
private splitIntoBatches<T>(items: T[], batchSize: number): T[][] {
|
||||
const batches: T[][] = [];
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
batches.push(items.slice(i, i + batchSize));
|
||||
}
|
||||
return batches;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取序列化统计信息
|
||||
*/
|
||||
public getStats(): {
|
||||
registeredComponents: number;
|
||||
protobufAvailable: boolean;
|
||||
messageTypeCacheSize: number;
|
||||
componentDataCacheSize: number;
|
||||
enableComponentDataCache: boolean;
|
||||
maxCacheSize: number;
|
||||
} {
|
||||
return {
|
||||
registeredComponents: this.registry.getAllComponents().size,
|
||||
protobufAvailable: !!this.protobuf,
|
||||
messageTypeCacheSize: this.messageTypeCache.size,
|
||||
componentDataCacheSize: this.componentDataCache.size,
|
||||
enableComponentDataCache: this.enableComponentDataCache,
|
||||
maxCacheSize: this.maxCacheSize
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建protobuf数据对象
|
||||
*/
|
||||
private buildProtoData(component: Component, definition: ProtoComponentDefinition): any {
|
||||
const componentType = component.constructor.name;
|
||||
|
||||
// 生成缓存键
|
||||
const cacheKey = this.generateComponentCacheKey(component, componentType);
|
||||
|
||||
// 检查缓存
|
||||
if (this.enableComponentDataCache && this.componentDataCache.has(cacheKey)) {
|
||||
this.updateCacheAccess(cacheKey);
|
||||
return this.componentDataCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const data: any = {};
|
||||
|
||||
for (const [propertyName, fieldDef] of definition.fields) {
|
||||
const value = (component as any)[propertyName];
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
data[fieldDef.name] = this.convertValueToProtoType(value, fieldDef);
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存结果,仅在启用且数据较小时缓存
|
||||
if (this.enableComponentDataCache && JSON.stringify(data).length < 1000) {
|
||||
this.setCacheWithLRU(cacheKey, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换值到protobuf类型
|
||||
*/
|
||||
private convertValueToProtoType(value: any, fieldDef: ProtoFieldDefinition): any {
|
||||
if (fieldDef.repeated && Array.isArray(value)) {
|
||||
return value.map(v => this.convertSingleValue(v, fieldDef.type));
|
||||
}
|
||||
|
||||
return this.convertSingleValue(value, fieldDef.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个值为protobuf类型
|
||||
*/
|
||||
private convertSingleValue(value: any, type: ProtoFieldType): any {
|
||||
switch (type) {
|
||||
case ProtoFieldType.INT32:
|
||||
case ProtoFieldType.UINT32:
|
||||
case ProtoFieldType.SINT32:
|
||||
case ProtoFieldType.FIXED32:
|
||||
case ProtoFieldType.SFIXED32:
|
||||
return typeof value === 'number' ? (value | 0) : (parseInt(value) || 0);
|
||||
|
||||
case ProtoFieldType.INT64:
|
||||
case ProtoFieldType.UINT64:
|
||||
case ProtoFieldType.SINT64:
|
||||
case ProtoFieldType.FIXED64:
|
||||
case ProtoFieldType.SFIXED64:
|
||||
// 使用BigIntFactory处理64位整数以确保兼容性
|
||||
const bigIntValue = BigIntFactory.create(value || 0);
|
||||
return bigIntValue.valueOf(); // 转换为数值用于protobuf
|
||||
|
||||
case ProtoFieldType.FLOAT:
|
||||
case ProtoFieldType.DOUBLE:
|
||||
return typeof value === 'number' ? value : (parseFloat(value) || 0);
|
||||
|
||||
case ProtoFieldType.BOOL:
|
||||
return typeof value === 'boolean' ? value : Boolean(value);
|
||||
|
||||
case ProtoFieldType.STRING:
|
||||
return typeof value === 'string' ? value : String(value);
|
||||
|
||||
case ProtoFieldType.BYTES:
|
||||
if (value instanceof Uint8Array) return value;
|
||||
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
||||
if (typeof value === 'string') return new TextEncoder().encode(value);
|
||||
return new Uint8Array();
|
||||
|
||||
case ProtoFieldType.TIMESTAMP:
|
||||
if (value instanceof Date) {
|
||||
return {
|
||||
seconds: Math.floor(value.getTime() / 1000),
|
||||
nanos: (value.getTime() % 1000) * 1000000
|
||||
};
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const date = new Date(value);
|
||||
return {
|
||||
seconds: Math.floor(date.getTime() / 1000),
|
||||
nanos: (date.getTime() % 1000) * 1000000
|
||||
};
|
||||
}
|
||||
return { seconds: 0, nanos: 0 };
|
||||
|
||||
case ProtoFieldType.DURATION:
|
||||
if (typeof value === 'number') {
|
||||
return {
|
||||
seconds: Math.floor(value / 1000),
|
||||
nanos: (value % 1000) * 1000000
|
||||
};
|
||||
}
|
||||
return { seconds: 0, nanos: 0 };
|
||||
|
||||
case ProtoFieldType.STRUCT:
|
||||
if (value && typeof value === 'object') {
|
||||
return this.convertObjectToStruct(value);
|
||||
}
|
||||
return {};
|
||||
|
||||
case ProtoFieldType.MESSAGE:
|
||||
case ProtoFieldType.ENUM:
|
||||
// 对于自定义消息和枚举,直接返回值,让protobuf.js处理
|
||||
return value;
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换对象为Protobuf Struct格式
|
||||
*/
|
||||
private convertObjectToStruct(obj: any): any {
|
||||
const result: any = { fields: {} };
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
result.fields[key] = this.convertValueToStructValue(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换值为Protobuf Value格式
|
||||
*/
|
||||
private convertValueToStructValue(value: any): any {
|
||||
if (value === null) return { nullValue: 0 };
|
||||
if (typeof value === 'number') return { numberValue: value };
|
||||
if (typeof value === 'string') return { stringValue: value };
|
||||
if (typeof value === 'boolean') return { boolValue: value };
|
||||
if (Array.isArray(value)) {
|
||||
return {
|
||||
listValue: {
|
||||
values: value.map(v => this.convertValueToStructValue(v))
|
||||
}
|
||||
};
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return { structValue: this.convertObjectToStruct(value) };
|
||||
}
|
||||
return { stringValue: String(value) };
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据到组件
|
||||
*/
|
||||
private applyDataToComponent(component: Component, data: any): void {
|
||||
const protoName = getProtoName(component);
|
||||
if (!protoName) return;
|
||||
|
||||
const definition = this.registry.getComponentDefinition(protoName);
|
||||
if (!definition) return;
|
||||
|
||||
for (const [propertyName, fieldDef] of definition.fields) {
|
||||
const value = data[fieldDef.name];
|
||||
if (value !== undefined) {
|
||||
(component as any)[propertyName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建protobuf定义
|
||||
*/
|
||||
private buildProtoDefinitions(): void {
|
||||
if (!this.protobuf) return;
|
||||
|
||||
try {
|
||||
const protoDefinition = this.registry.generateProtoDefinition();
|
||||
this.root = this.protobuf.parse(protoDefinition).root;
|
||||
// 清空缓存,schema已更新
|
||||
this.messageTypeCache.clear();
|
||||
} catch (error) {
|
||||
console.error('[ProtobufSerializer] 构建protobuf定义失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息类型并缓存结果
|
||||
*/
|
||||
private getMessageType(typeName: string): any {
|
||||
if (!this.root) return null;
|
||||
|
||||
// 检查缓存
|
||||
const fullTypeName = `ecs.${typeName}`;
|
||||
if (this.messageTypeCache.has(fullTypeName)) {
|
||||
return this.messageTypeCache.get(fullTypeName);
|
||||
}
|
||||
|
||||
try {
|
||||
const messageType = this.root.lookupType(fullTypeName);
|
||||
// 缓存结果
|
||||
this.messageTypeCache.set(fullTypeName, messageType);
|
||||
return messageType;
|
||||
} catch (error) {
|
||||
console.warn(`[ProtobufSerializer] 未找到消息类型: ${fullTypeName}`);
|
||||
// 缓存null结果以避免重复查找
|
||||
this.messageTypeCache.set(fullTypeName, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成组件缓存键
|
||||
*/
|
||||
private generateComponentCacheKey(component: Component, componentType: string): string {
|
||||
// TODO: 考虑更高效的缓存键生成策略
|
||||
const properties = Object.keys(component).sort();
|
||||
const values = properties.map(key => String((component as any)[key])).join('|');
|
||||
return `${componentType}:${this.simpleHash(values)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单哈希函数
|
||||
*/
|
||||
private simpleHash(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存访问计数
|
||||
*/
|
||||
private updateCacheAccess(cacheKey: string): void {
|
||||
const currentCount = this.cacheAccessCount.get(cacheKey) || 0;
|
||||
this.cacheAccessCount.set(cacheKey, currentCount + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用LRU策略设置缓存
|
||||
*/
|
||||
private setCacheWithLRU(cacheKey: string, data: any): void {
|
||||
// 检查是否需要淘汰缓存
|
||||
if (this.componentDataCache.size >= this.maxCacheSize) {
|
||||
this.evictLRUCache();
|
||||
}
|
||||
|
||||
this.componentDataCache.set(cacheKey, data);
|
||||
this.cacheAccessCount.set(cacheKey, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 淘汰LRU缓存项
|
||||
*/
|
||||
private evictLRUCache(): void {
|
||||
let lruKey = '';
|
||||
let minAccessCount = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// 找到访问次数最少的缓存项
|
||||
for (const [key, count] of this.cacheAccessCount) {
|
||||
if (count < minAccessCount) {
|
||||
minAccessCount = count;
|
||||
lruKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (lruKey) {
|
||||
this.componentDataCache.delete(lruKey);
|
||||
this.cacheAccessCount.delete(lruKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/network/src/Serialization/SerializationTypes.ts
Normal file
17
packages/network/src/Serialization/SerializationTypes.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 序列化类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 序列化数据接口
|
||||
*/
|
||||
export interface SerializedData {
|
||||
/** 序列化类型 */
|
||||
type: 'protobuf' | 'json';
|
||||
/** 组件类型名称 */
|
||||
componentType: string;
|
||||
/** 序列化后的数据 */
|
||||
data: Uint8Array | any;
|
||||
/** 数据大小(字节) */
|
||||
size: number;
|
||||
}
|
||||
41
packages/network/src/Serialization/index.ts
Normal file
41
packages/network/src/Serialization/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 网络插件序列化系统导出
|
||||
*
|
||||
* 统一导出所有protobuf序列化相关的类型、装饰器和工具函数
|
||||
*/
|
||||
|
||||
// Protobuf序列化系统(用于帧同步框架)
|
||||
export {
|
||||
ProtobufSerializer
|
||||
} from './ProtobufSerializer';
|
||||
|
||||
export {
|
||||
ProtoSerializable,
|
||||
ProtoField,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtoBytes,
|
||||
ProtoTimestamp,
|
||||
ProtoDouble,
|
||||
ProtoInt64,
|
||||
ProtoStruct,
|
||||
ProtoMessage,
|
||||
ProtoEnum,
|
||||
isProtoSerializable,
|
||||
getProtoName,
|
||||
ProtobufRegistry,
|
||||
ProtoComponentDefinition,
|
||||
ProtoFieldDefinition,
|
||||
ProtoFieldType,
|
||||
FieldSyncPriority,
|
||||
ComponentSyncMode,
|
||||
ComponentSyncOptions,
|
||||
FieldSyncOptions
|
||||
} from './ProtobufDecorators';
|
||||
|
||||
export {
|
||||
SerializedData
|
||||
} from './SerializationTypes';
|
||||
|
||||
92
packages/network/src/Snapshot/ISnapshotable.ts
Normal file
92
packages/network/src/Snapshot/ISnapshotable.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 可序列化接口
|
||||
*
|
||||
* 实现此接口的类可以被快照系统序列化和反序列化
|
||||
*/
|
||||
export interface ISnapshotable {
|
||||
/**
|
||||
* 序列化对象到可传输的数据格式
|
||||
*
|
||||
* @returns 序列化后的数据
|
||||
*/
|
||||
serialize(): any;
|
||||
|
||||
/**
|
||||
* 从序列化数据恢复对象状态
|
||||
*
|
||||
* @param data - 序列化数据
|
||||
*/
|
||||
deserialize(data: any): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快照配置接口
|
||||
*/
|
||||
export interface SnapshotConfig {
|
||||
/** 是否包含在快照中 */
|
||||
includeInSnapshot: boolean;
|
||||
/** 压缩级别 (0-9) */
|
||||
compressionLevel: number;
|
||||
/** 同步优先级 (数字越大优先级越高) */
|
||||
syncPriority: number;
|
||||
/** 是否启用增量快照 */
|
||||
enableIncremental: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件快照数据
|
||||
*/
|
||||
export interface ComponentSnapshot {
|
||||
/** 组件类型名称 */
|
||||
type: string;
|
||||
/** 组件ID */
|
||||
id: number;
|
||||
/** 序列化数据 */
|
||||
data: any;
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 快照配置 */
|
||||
config?: SnapshotConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体快照数据
|
||||
*/
|
||||
export interface EntitySnapshot {
|
||||
/** 实体ID */
|
||||
id: number;
|
||||
/** 实体名称 */
|
||||
name: string;
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 是否激活 */
|
||||
active: boolean;
|
||||
/** 标签 */
|
||||
tag: number;
|
||||
/** 更新顺序 */
|
||||
updateOrder: number;
|
||||
/** 组件快照列表 */
|
||||
components: ComponentSnapshot[];
|
||||
/** 子实体ID列表 */
|
||||
children: number[];
|
||||
/** 父实体ID */
|
||||
parent?: number;
|
||||
/** 快照时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景快照数据
|
||||
*/
|
||||
export interface SceneSnapshot {
|
||||
/** 实体快照列表 */
|
||||
entities: EntitySnapshot[];
|
||||
/** 快照时间戳 */
|
||||
timestamp: number;
|
||||
/** 框架版本 */
|
||||
version: string;
|
||||
/** 快照类型 */
|
||||
type: 'full' | 'incremental';
|
||||
/** 基础快照ID(增量快照使用) */
|
||||
baseSnapshotId?: string;
|
||||
}
|
||||
255
packages/network/src/Snapshot/SnapshotExtension.ts
Normal file
255
packages/network/src/Snapshot/SnapshotExtension.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { ISnapshotable, SnapshotConfig } from './ISnapshotable';
|
||||
|
||||
/**
|
||||
* 快照扩展接口
|
||||
*
|
||||
* 为Component基类提供快照功能的扩展接口
|
||||
*/
|
||||
export interface ISnapshotExtension {
|
||||
/** 快照配置 */
|
||||
snapshotConfig?: SnapshotConfig;
|
||||
|
||||
/** 序列化方法 */
|
||||
serialize?(): any;
|
||||
|
||||
/** 反序列化方法 */
|
||||
deserialize?(data: any): void;
|
||||
|
||||
/** 变化检测方法 */
|
||||
hasChanged?(baseData: any): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快照装饰器
|
||||
*
|
||||
* 用于标记组件属性为可序列化
|
||||
*/
|
||||
export function Serializable(config?: Partial<SnapshotConfig>) {
|
||||
return function (target: any, propertyKey: string) {
|
||||
// 确保组件有快照配置
|
||||
if (!target.snapshotConfig) {
|
||||
target.snapshotConfig = {
|
||||
includeInSnapshot: true,
|
||||
compressionLevel: 0,
|
||||
syncPriority: 5,
|
||||
enableIncremental: true
|
||||
};
|
||||
}
|
||||
|
||||
// 标记属性为可序列化
|
||||
if (!target._serializableProperties) {
|
||||
target._serializableProperties = new Set<string>();
|
||||
}
|
||||
target._serializableProperties.add(propertyKey);
|
||||
|
||||
// 应用配置
|
||||
if (config) {
|
||||
Object.assign(target.snapshotConfig, config);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 快照配置装饰器
|
||||
*
|
||||
* 用于配置组件的快照行为
|
||||
*/
|
||||
export function SnapshotConfigDecorator(config: SnapshotConfig) {
|
||||
return function (target: any) {
|
||||
target.prototype.snapshotConfig = config;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 快照扩展工具类
|
||||
*/
|
||||
export class SnapshotExtension {
|
||||
/**
|
||||
* 为组件添加快照支持
|
||||
*
|
||||
* @param component - 目标组件
|
||||
* @param config - 快照配置
|
||||
*/
|
||||
public static enableSnapshot(component: Component, config?: Partial<SnapshotConfig>): void {
|
||||
const defaultConfig: SnapshotConfig = {
|
||||
includeInSnapshot: true,
|
||||
compressionLevel: 0,
|
||||
syncPriority: 5,
|
||||
enableIncremental: true
|
||||
};
|
||||
|
||||
(component as any).snapshotConfig = { ...defaultConfig, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用组件的快照功能
|
||||
*
|
||||
* @param component - 目标组件
|
||||
*/
|
||||
public static disableSnapshot(component: Component): void {
|
||||
if ((component as any).snapshotConfig) {
|
||||
(component as any).snapshotConfig.includeInSnapshot = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否支持快照
|
||||
*
|
||||
* @param component - 目标组件
|
||||
* @returns 是否支持快照
|
||||
*/
|
||||
public static isSnapshotable(component: Component): boolean {
|
||||
const config = (component as any).snapshotConfig;
|
||||
return config && config.includeInSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件的可序列化属性
|
||||
*
|
||||
* @param component - 目标组件
|
||||
* @returns 可序列化属性列表
|
||||
*/
|
||||
public static getSerializableProperties(component: Component): string[] {
|
||||
const properties = (component as any)._serializableProperties;
|
||||
if (properties) {
|
||||
return Array.from(properties);
|
||||
}
|
||||
|
||||
// 如果没有标记,返回所有公共属性
|
||||
const publicProperties: string[] = [];
|
||||
for (const key in component) {
|
||||
if (component.hasOwnProperty(key) &&
|
||||
typeof (component as any)[key] !== 'function' &&
|
||||
key !== 'id' &&
|
||||
key !== 'entity' &&
|
||||
key !== '_enabled' &&
|
||||
key !== '_updateOrder') {
|
||||
publicProperties.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return publicProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组件的默认序列化方法
|
||||
*
|
||||
* @param component - 目标组件
|
||||
* @returns 序列化数据
|
||||
*/
|
||||
public static createDefaultSerializer(component: Component): () => any {
|
||||
return function() {
|
||||
const data: any = {};
|
||||
const properties = SnapshotExtension.getSerializableProperties(component);
|
||||
|
||||
for (const prop of properties) {
|
||||
const value = (component as any)[prop];
|
||||
if (value !== undefined && value !== null) {
|
||||
data[prop] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组件的默认反序列化方法
|
||||
*
|
||||
* @param component - 目标组件
|
||||
* @returns 反序列化函数
|
||||
*/
|
||||
public static createDefaultDeserializer(component: Component): (data: any) => void {
|
||||
return function(data: any) {
|
||||
const properties = SnapshotExtension.getSerializableProperties(component);
|
||||
|
||||
for (const prop of properties) {
|
||||
if (data.hasOwnProperty(prop)) {
|
||||
(component as any)[prop] = data[prop];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建简单的变化检测方法
|
||||
*
|
||||
* @param component - 目标组件
|
||||
* @returns 变化检测函数
|
||||
*/
|
||||
public static createSimpleChangeDetector(component: Component): (baseData: any) => boolean {
|
||||
return function(baseData: any) {
|
||||
const properties = SnapshotExtension.getSerializableProperties(component);
|
||||
|
||||
for (const prop of properties) {
|
||||
const currentValue = (component as any)[prop];
|
||||
const baseValue = baseData[prop];
|
||||
|
||||
if (currentValue !== baseValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建深度变化检测方法
|
||||
*
|
||||
* @param component - 目标组件
|
||||
* @returns 变化检测函数
|
||||
*/
|
||||
public static createDeepChangeDetector(component: Component): (baseData: any) => boolean {
|
||||
return function(baseData: any) {
|
||||
const properties = SnapshotExtension.getSerializableProperties(component);
|
||||
|
||||
for (const prop of properties) {
|
||||
const currentValue = (component as any)[prop];
|
||||
const baseValue = baseData[prop];
|
||||
|
||||
if (SnapshotExtension.deepCompare(currentValue, baseValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度比较两个值
|
||||
*/
|
||||
private static deepCompare(value1: any, value2: any): boolean {
|
||||
if (value1 === value2) return false;
|
||||
|
||||
if (typeof value1 !== typeof value2) return true;
|
||||
|
||||
if (value1 === null || value2 === null) return value1 !== value2;
|
||||
|
||||
if (typeof value1 !== 'object') return value1 !== value2;
|
||||
|
||||
if (Array.isArray(value1) !== Array.isArray(value2)) return true;
|
||||
|
||||
if (Array.isArray(value1)) {
|
||||
if (value1.length !== value2.length) return true;
|
||||
for (let i = 0; i < value1.length; i++) {
|
||||
if (this.deepCompare(value1[i], value2[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(value1);
|
||||
const keys2 = Object.keys(value2);
|
||||
|
||||
if (keys1.length !== keys2.length) return true;
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!keys2.includes(key)) return true;
|
||||
if (this.deepCompare(value1[key], value2[key])) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
608
packages/network/src/Snapshot/SnapshotManager.ts
Normal file
608
packages/network/src/Snapshot/SnapshotManager.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import { ISnapshotable, SceneSnapshot, EntitySnapshot, ComponentSnapshot, SnapshotConfig } from './ISnapshotable';
|
||||
import { ProtobufSerializer } from '../Serialization/ProtobufSerializer';
|
||||
import { SerializedData } from '../Serialization/SerializationTypes';
|
||||
import { isProtoSerializable } from '../Serialization/ProtobufDecorators';
|
||||
|
||||
/**
|
||||
* 快照管理器
|
||||
*
|
||||
* 负责创建和管理ECS系统的快照,支持完整快照和增量快照
|
||||
* 使用protobuf序列化
|
||||
*/
|
||||
export class SnapshotManager {
|
||||
/** 默认快照配置 */
|
||||
private static readonly DEFAULT_CONFIG: SnapshotConfig = {
|
||||
includeInSnapshot: true,
|
||||
compressionLevel: 0,
|
||||
syncPriority: 5,
|
||||
enableIncremental: true
|
||||
};
|
||||
|
||||
/** 框架版本 */
|
||||
private readonly version: string = '1.0.0';
|
||||
|
||||
/** 最后快照时间戳 */
|
||||
private lastSnapshotTime: number = 0;
|
||||
|
||||
/** 快照缓存 */
|
||||
private snapshotCache = new Map<string, SceneSnapshot>();
|
||||
|
||||
/** 最大缓存数量 */
|
||||
private maxCacheSize: number = 10;
|
||||
|
||||
/** Protobuf序列化器 */
|
||||
private protobufSerializer: ProtobufSerializer;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor() {
|
||||
this.protobufSerializer = ProtobufSerializer.getInstance();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建场景快照
|
||||
*
|
||||
* @param entities - 实体列表
|
||||
* @param type - 快照类型
|
||||
* @returns 场景快照
|
||||
*/
|
||||
public createSceneSnapshot(entities: Entity[], type: 'full' | 'incremental' = 'full'): SceneSnapshot {
|
||||
const entitySnapshots: EntitySnapshot[] = [];
|
||||
|
||||
const sortedEntities = entities.sort((a, b) => a.id - b.id);
|
||||
|
||||
for (const entity of sortedEntities) {
|
||||
if (entity.isDestroyed) continue;
|
||||
|
||||
const entitySnapshot = this.createEntitySnapshot(entity);
|
||||
if (entitySnapshot) {
|
||||
entitySnapshots.push(entitySnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
const snapshot: SceneSnapshot = {
|
||||
entities: entitySnapshots,
|
||||
timestamp: Date.now(),
|
||||
version: this.version,
|
||||
type: type
|
||||
};
|
||||
|
||||
this.lastSnapshotTime = snapshot.timestamp;
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照恢复场景
|
||||
*
|
||||
* @param snapshot - 场景快照
|
||||
* @param targetEntities - 目标实体列表(用于增量恢复)
|
||||
* @param createMissingEntities - 是否创建缺失的实体
|
||||
*/
|
||||
public restoreFromSnapshot(snapshot: SceneSnapshot, targetEntities?: Entity[], createMissingEntities: boolean = false): Entity[] {
|
||||
if (snapshot.type === 'incremental' && targetEntities) {
|
||||
return this.restoreIncrementalSnapshot(snapshot, targetEntities);
|
||||
} else {
|
||||
return this.restoreFullSnapshot(snapshot, targetEntities, createMissingEntities);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照恢复实体列表
|
||||
*
|
||||
* @param snapshot - 场景快照
|
||||
* @param targetEntities - 目标实体列表
|
||||
* @param createMissingEntities - 是否创建缺失的实体
|
||||
*/
|
||||
public restoreEntitiesFromSnapshot(snapshot: SceneSnapshot, targetEntities: Entity[], createMissingEntities: boolean = false): Entity[] {
|
||||
const restoredEntities: Entity[] = [];
|
||||
const targetEntityMap = new Map<number, Entity>();
|
||||
|
||||
for (const entity of targetEntities) {
|
||||
targetEntityMap.set(entity.id, entity);
|
||||
}
|
||||
|
||||
for (const entitySnapshot of snapshot.entities) {
|
||||
let targetEntity = targetEntityMap.get(entitySnapshot.id);
|
||||
|
||||
if (!targetEntity && createMissingEntities) {
|
||||
// 创建缺失的实体
|
||||
const newEntity = this.createEntityFromSnapshot(entitySnapshot);
|
||||
if (newEntity) {
|
||||
restoredEntities.push(newEntity);
|
||||
}
|
||||
} else if (targetEntity) {
|
||||
// 恢复现有实体
|
||||
this.restoreEntityFromSnapshot(targetEntity, entitySnapshot);
|
||||
restoredEntities.push(targetEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return restoredEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照创建实体
|
||||
*/
|
||||
private createEntityFromSnapshot(entitySnapshot: EntitySnapshot): Entity | null {
|
||||
try {
|
||||
const entity = new Entity(entitySnapshot.name, entitySnapshot.id);
|
||||
|
||||
// 设置基本属性
|
||||
entity.enabled = entitySnapshot.enabled;
|
||||
entity.active = entitySnapshot.active;
|
||||
entity.tag = entitySnapshot.tag;
|
||||
entity.updateOrder = entitySnapshot.updateOrder;
|
||||
|
||||
// 创建组件
|
||||
for (const componentSnapshot of entitySnapshot.components) {
|
||||
this.createComponentFromSnapshot(entity, componentSnapshot);
|
||||
}
|
||||
|
||||
return entity;
|
||||
} catch (error) {
|
||||
console.error(`[SnapshotManager] 创建实体失败: ${entitySnapshot.name}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照创建组件
|
||||
*/
|
||||
private createComponentFromSnapshot(entity: Entity, componentSnapshot: ComponentSnapshot): void {
|
||||
try {
|
||||
// 尝试获取组件构造函数
|
||||
const componentType = this.getComponentType(componentSnapshot.type);
|
||||
if (!componentType) {
|
||||
console.warn(`[SnapshotManager] 未知组件类型: ${componentSnapshot.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建组件实例
|
||||
const component = entity.createComponent(componentType);
|
||||
|
||||
// 恢复组件启用状态
|
||||
component.enabled = componentSnapshot.enabled;
|
||||
|
||||
// 恢复组件数据
|
||||
const serializedData = componentSnapshot.data as SerializedData;
|
||||
|
||||
if (!isProtoSerializable(component)) {
|
||||
throw new Error(`[SnapshotManager] 组件 ${component.constructor.name} 不支持protobuf反序列化`);
|
||||
}
|
||||
|
||||
this.protobufSerializer.deserialize(component, serializedData);
|
||||
} catch (error) {
|
||||
console.error(`[SnapshotManager] 创建组件失败: ${componentSnapshot.type}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件类型
|
||||
*/
|
||||
private getComponentType(typeName: string): any {
|
||||
// TODO: 实现组件类型注册表或者使用其他方式获取组件类型
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建快速快照(跳过变化检测)
|
||||
*
|
||||
* @param entities - 实体列表
|
||||
* @returns 场景快照
|
||||
*/
|
||||
public createQuickSnapshot(entities: Entity[]): SceneSnapshot {
|
||||
return this.createSceneSnapshot(entities, 'full');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建增量快照
|
||||
*
|
||||
* @param entities - 实体列表
|
||||
* @param baseSnapshot - 基础快照
|
||||
* @param enableChangeDetection - 是否启用变化检测
|
||||
* @returns 增量快照
|
||||
*/
|
||||
public createIncrementalSnapshot(entities: Entity[], baseSnapshot: SceneSnapshot, enableChangeDetection: boolean = true): SceneSnapshot {
|
||||
const incrementalEntities: EntitySnapshot[] = [];
|
||||
|
||||
const baseEntityMap = new Map<number, EntitySnapshot>();
|
||||
for (const entity of baseSnapshot.entities) {
|
||||
baseEntityMap.set(entity.id, entity);
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
if (entity.isDestroyed) continue;
|
||||
|
||||
const baseEntity = baseEntityMap.get(entity.id);
|
||||
if (!baseEntity) {
|
||||
const entitySnapshot = this.createEntitySnapshot(entity);
|
||||
if (entitySnapshot) {
|
||||
incrementalEntities.push(entitySnapshot);
|
||||
}
|
||||
} else if (enableChangeDetection) {
|
||||
const changedComponents = this.getChangedComponents(entity, baseEntity);
|
||||
if (this.hasEntityStructureChanged(entity, baseEntity) || changedComponents.length > 0) {
|
||||
const incrementalEntitySnapshot = this.createIncrementalEntitySnapshot(entity, baseEntity, changedComponents);
|
||||
if (incrementalEntitySnapshot) {
|
||||
incrementalEntities.push(incrementalEntitySnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entities: incrementalEntities,
|
||||
timestamp: Date.now(),
|
||||
version: this.version,
|
||||
type: 'incremental',
|
||||
baseSnapshotId: this.generateSnapshotId(baseSnapshot)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存快照
|
||||
*
|
||||
* @param id - 快照ID
|
||||
* @param snapshot - 快照数据
|
||||
*/
|
||||
public cacheSnapshot(id: string, snapshot: SceneSnapshot): void {
|
||||
// 清理过期缓存
|
||||
if (this.snapshotCache.size >= this.maxCacheSize) {
|
||||
const oldestKey = this.snapshotCache.keys().next().value;
|
||||
if (oldestKey) {
|
||||
this.snapshotCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
this.snapshotCache.set(id, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的快照
|
||||
*
|
||||
* @param id - 快照ID
|
||||
* @returns 快照数据或undefined
|
||||
*/
|
||||
public getCachedSnapshot(id: string): SceneSnapshot | undefined {
|
||||
return this.snapshotCache.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空快照缓存
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.snapshotCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
public clearAllCaches(): void {
|
||||
this.snapshotCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
public getCacheStats(): {
|
||||
snapshotCacheSize: number;
|
||||
protobufStats?: {
|
||||
registeredComponents: number;
|
||||
protobufAvailable: boolean;
|
||||
};
|
||||
} {
|
||||
const stats: any = {
|
||||
snapshotCacheSize: this.snapshotCache.size
|
||||
};
|
||||
|
||||
if (this.protobufSerializer) {
|
||||
stats.protobufStats = this.protobufSerializer.getStats();
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动初始化protobuf支持(可选,通常会自动初始化)
|
||||
*
|
||||
* @param protobufJs - protobuf.js库实例
|
||||
*/
|
||||
public initializeProtobuf(protobufJs: any): void {
|
||||
if (this.protobufSerializer) {
|
||||
this.protobufSerializer.initialize(protobufJs);
|
||||
console.log('[SnapshotManager] Protobuf支持已手动启用');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建实体快照
|
||||
*/
|
||||
private createEntitySnapshot(entity: Entity): EntitySnapshot | null {
|
||||
const componentSnapshots: ComponentSnapshot[] = [];
|
||||
|
||||
for (const component of entity.components) {
|
||||
const componentSnapshot = this.createComponentSnapshot(component);
|
||||
if (componentSnapshot) {
|
||||
componentSnapshots.push(componentSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
enabled: entity.enabled,
|
||||
active: entity.active,
|
||||
tag: entity.tag,
|
||||
updateOrder: entity.updateOrder,
|
||||
components: componentSnapshots,
|
||||
children: entity.children.map(child => child.id),
|
||||
parent: entity.parent?.id || undefined,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组件快照
|
||||
*
|
||||
* 使用protobuf序列化
|
||||
*/
|
||||
private createComponentSnapshot(component: Component): ComponentSnapshot | null {
|
||||
if (!this.isComponentSnapshotable(component)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isProtoSerializable(component)) {
|
||||
throw new Error(`[SnapshotManager] 组件 ${component.constructor.name} 不支持protobuf序列化,请添加@ProtoSerializable装饰器`);
|
||||
}
|
||||
|
||||
const serializedData = this.protobufSerializer.serialize(component);
|
||||
|
||||
return {
|
||||
type: component.constructor.name,
|
||||
id: component.id,
|
||||
data: serializedData,
|
||||
enabled: component.enabled,
|
||||
config: this.getComponentSnapshotConfig(component)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否支持快照
|
||||
*/
|
||||
private isComponentSnapshotable(component: Component): boolean {
|
||||
// 检查是否有快照配置
|
||||
const config = this.getComponentSnapshotConfig(component);
|
||||
return config.includeInSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件快照配置
|
||||
*/
|
||||
private getComponentSnapshotConfig(component: Component): SnapshotConfig {
|
||||
// 检查组件是否有自定义配置
|
||||
if ((component as any).snapshotConfig) {
|
||||
return { ...SnapshotManager.DEFAULT_CONFIG, ...(component as any).snapshotConfig };
|
||||
}
|
||||
|
||||
return SnapshotManager.DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 恢复完整快照
|
||||
*/
|
||||
private restoreFullSnapshot(snapshot: SceneSnapshot, targetEntities?: Entity[], createMissingEntities: boolean = false): Entity[] {
|
||||
if (targetEntities && createMissingEntities) {
|
||||
return this.restoreEntitiesFromSnapshot(snapshot, targetEntities, true);
|
||||
} else if (targetEntities) {
|
||||
return this.restoreEntitiesFromSnapshot(snapshot, targetEntities, false);
|
||||
} else {
|
||||
const restoredEntities: Entity[] = [];
|
||||
for (const entitySnapshot of snapshot.entities) {
|
||||
const entity = this.createEntityFromSnapshot(entitySnapshot);
|
||||
if (entity) {
|
||||
restoredEntities.push(entity);
|
||||
}
|
||||
}
|
||||
return restoredEntities;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复增量快照
|
||||
*/
|
||||
private restoreIncrementalSnapshot(snapshot: SceneSnapshot, targetEntities: Entity[]): Entity[] {
|
||||
const restoredEntities: Entity[] = [];
|
||||
const targetEntityMap = new Map<number, Entity>();
|
||||
|
||||
for (const entity of targetEntities) {
|
||||
targetEntityMap.set(entity.id, entity);
|
||||
}
|
||||
|
||||
for (const entitySnapshot of snapshot.entities) {
|
||||
const targetEntity = targetEntityMap.get(entitySnapshot.id);
|
||||
if (targetEntity) {
|
||||
this.restoreEntityFromSnapshot(targetEntity, entitySnapshot);
|
||||
restoredEntities.push(targetEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return restoredEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照恢复实体
|
||||
*/
|
||||
private restoreEntityFromSnapshot(entity: Entity, entitySnapshot: EntitySnapshot): void {
|
||||
// 恢复实体基本属性
|
||||
entity.enabled = entitySnapshot.enabled;
|
||||
entity.active = entitySnapshot.active;
|
||||
entity.tag = entitySnapshot.tag;
|
||||
entity.updateOrder = entitySnapshot.updateOrder;
|
||||
|
||||
// 恢复组件
|
||||
for (const componentSnapshot of entitySnapshot.components) {
|
||||
this.restoreComponentFromSnapshot(entity, componentSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照恢复组件
|
||||
*
|
||||
* 使用protobuf反序列化
|
||||
*/
|
||||
private restoreComponentFromSnapshot(entity: Entity, componentSnapshot: ComponentSnapshot): void {
|
||||
// 查找现有组件
|
||||
let component = entity.getComponent(componentSnapshot.type as any);
|
||||
|
||||
if (!component) {
|
||||
// 组件不存在,需要创建
|
||||
console.warn(`[SnapshotManager] 组件 ${componentSnapshot.type} 不存在于实体 ${entity.name},无法恢复`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复组件启用状态
|
||||
component.enabled = componentSnapshot.enabled;
|
||||
|
||||
// 恢复组件数据
|
||||
const serializedData = componentSnapshot.data as SerializedData;
|
||||
|
||||
if (!isProtoSerializable(component)) {
|
||||
throw new Error(`[SnapshotManager] 组件 ${component.constructor.name} 不支持protobuf反序列化`);
|
||||
}
|
||||
|
||||
this.protobufSerializer.deserialize(component, serializedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体结构是否发生变化(组件数量、类型等)
|
||||
*/
|
||||
private hasEntityStructureChanged(entity: Entity, baseSnapshot: EntitySnapshot): boolean {
|
||||
// 检查基本属性变化
|
||||
if (entity.enabled !== baseSnapshot.enabled ||
|
||||
entity.active !== baseSnapshot.active ||
|
||||
entity.tag !== baseSnapshot.tag ||
|
||||
entity.updateOrder !== baseSnapshot.updateOrder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查组件数量变化
|
||||
if (entity.components.length !== baseSnapshot.components.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查组件类型变化
|
||||
const currentComponentTypes = new Set(entity.components.map(c => c.constructor.name));
|
||||
const baseComponentTypes = new Set(baseSnapshot.components.map(c => c.type));
|
||||
|
||||
if (currentComponentTypes.size !== baseComponentTypes.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const type of currentComponentTypes) {
|
||||
if (!baseComponentTypes.has(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取发生变化的组件列表
|
||||
*/
|
||||
private getChangedComponents(entity: Entity, baseSnapshot: EntitySnapshot): ComponentSnapshot[] {
|
||||
const changedComponents: ComponentSnapshot[] = [];
|
||||
|
||||
const baseComponentMap = new Map<string, ComponentSnapshot>();
|
||||
for (const comp of baseSnapshot.components) {
|
||||
baseComponentMap.set(comp.type, comp);
|
||||
}
|
||||
|
||||
for (const component of entity.components) {
|
||||
const baseComponent = baseComponentMap.get(component.constructor.name);
|
||||
|
||||
if (!baseComponent) {
|
||||
const componentSnapshot = this.createComponentSnapshot(component);
|
||||
if (componentSnapshot) {
|
||||
changedComponents.push(componentSnapshot);
|
||||
}
|
||||
} else {
|
||||
if (this.hasComponentDataChanged(component, baseComponent)) {
|
||||
const componentSnapshot = this.createComponentSnapshot(component);
|
||||
if (componentSnapshot) {
|
||||
changedComponents.push(componentSnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changedComponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件数据是否发生变化
|
||||
*/
|
||||
private hasComponentDataChanged(component: Component, baseComponent: ComponentSnapshot): boolean {
|
||||
if (component.enabled !== baseComponent.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.hasChangeDetectionMethod(component)) {
|
||||
try {
|
||||
return (component as any).hasChanged(baseComponent.data);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否有变化检测方法
|
||||
*/
|
||||
private hasChangeDetectionMethod(component: Component): boolean {
|
||||
return typeof (component as any).hasChanged === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建增量实体快照(只包含变化的组件)
|
||||
*/
|
||||
private createIncrementalEntitySnapshot(entity: Entity, baseSnapshot: EntitySnapshot, changedComponents: ComponentSnapshot[]): EntitySnapshot | null {
|
||||
// 检查实体基本属性是否变化
|
||||
const hasBasicChanges = entity.enabled !== baseSnapshot.enabled ||
|
||||
entity.active !== baseSnapshot.active ||
|
||||
entity.tag !== baseSnapshot.tag ||
|
||||
entity.updateOrder !== baseSnapshot.updateOrder;
|
||||
|
||||
// 如果没有基本变化且没有组件变化,返回null
|
||||
if (!hasBasicChanges && changedComponents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
enabled: entity.enabled,
|
||||
active: entity.active,
|
||||
tag: entity.tag,
|
||||
updateOrder: entity.updateOrder,
|
||||
components: changedComponents, // 只包含变化的组件
|
||||
children: entity.children.map(child => child.id),
|
||||
parent: entity.parent?.id || undefined,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成快照ID
|
||||
*/
|
||||
private generateSnapshotId(snapshot: SceneSnapshot): string {
|
||||
return `${snapshot.timestamp}_${snapshot.entities.length}`;
|
||||
}
|
||||
}
|
||||
19
packages/network/src/Snapshot/index.ts
Normal file
19
packages/network/src/Snapshot/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 快照系统模块
|
||||
*
|
||||
* 提供ECS系统的快照功能,支持实体和组件的序列化与反序列化
|
||||
*/
|
||||
|
||||
// 核心接口
|
||||
export * from './ISnapshotable';
|
||||
|
||||
// 快照管理器
|
||||
export { SnapshotManager } from './SnapshotManager';
|
||||
|
||||
// 快照扩展
|
||||
export {
|
||||
ISnapshotExtension,
|
||||
Serializable,
|
||||
SnapshotConfigDecorator,
|
||||
SnapshotExtension
|
||||
} from './SnapshotExtension';
|
||||
15
packages/network/src/index.ts
Normal file
15
packages/network/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* ECS Framework Network Plugin - 网络插件
|
||||
*
|
||||
* 为ECS框架提供网络同步和帧同步功能
|
||||
*/
|
||||
|
||||
// 网络组件基类
|
||||
export { NetworkComponent } from './NetworkComponent';
|
||||
export { INetworkSyncable } from './INetworkSyncable';
|
||||
|
||||
// Protobuf序列化系统
|
||||
export * from './Serialization';
|
||||
|
||||
// 快照系统(帧同步)
|
||||
export * from './Snapshot';
|
||||
441
packages/network/tests/Serialization/Performance.test.ts
Normal file
441
packages/network/tests/Serialization/Performance.test.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* Protobuf序列化性能测试
|
||||
*/
|
||||
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Scene } from '../../../src/ECS/Scene';
|
||||
import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager';
|
||||
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 性能测试组件
|
||||
@ProtoSerializable('PerfPosition')
|
||||
class PerfPositionComponent extends Component {
|
||||
@ProtoFloat(1) public x: number = 0;
|
||||
@ProtoFloat(2) public y: number = 0;
|
||||
@ProtoFloat(3) public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfVelocity')
|
||||
class PerfVelocityComponent extends Component {
|
||||
@ProtoFloat(1) public vx: number = 0;
|
||||
@ProtoFloat(2) public vy: number = 0;
|
||||
@ProtoFloat(3) public vz: number = 0;
|
||||
|
||||
constructor(vx: number = 0, vy: number = 0, vz: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.vz = vz;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfHealth')
|
||||
class PerfHealthComponent extends Component {
|
||||
@ProtoInt32(1) public maxHealth: number = 100;
|
||||
@ProtoInt32(2) public currentHealth: number = 100;
|
||||
@ProtoBool(3) public isDead: boolean = false;
|
||||
@ProtoFloat(4) public regenerationRate: number = 0.5;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfPlayer')
|
||||
class PerfPlayerComponent extends Component {
|
||||
@ProtoString(1) public name: string = '';
|
||||
@ProtoInt32(2) public level: number = 1;
|
||||
@ProtoInt32(3) public experience: number = 0;
|
||||
@ProtoInt32(4) public score: number = 0;
|
||||
@ProtoBool(5) public isOnline: boolean = true;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// 传统JSON序列化组件(用于对比)
|
||||
class JsonPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPlayerComponent extends Component {
|
||||
public name: string = '';
|
||||
public level: number = 1;
|
||||
public experience: number = 0;
|
||||
public score: number = 0;
|
||||
public isOnline: boolean = true;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js for performance testing
|
||||
const createMockProtobuf = () => {
|
||||
const mockEncodedData = new Uint8Array(32); // 模拟32字节的编码数据
|
||||
mockEncodedData.fill(1);
|
||||
|
||||
return {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => ({
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(mockEncodedData)
|
||||
}),
|
||||
decode: jest.fn().mockReturnValue({
|
||||
x: 10, y: 20, z: 30,
|
||||
vx: 1, vy: 2, vz: 3,
|
||||
maxHealth: 100, currentHealth: 80, isDead: false, regenerationRate: 0.5,
|
||||
name: 'TestPlayer', level: 5, experience: 1000, score: 5000, isOnline: true
|
||||
}),
|
||||
toObject: jest.fn().mockImplementation((message) => message)
|
||||
}))
|
||||
}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
describe('Protobuf序列化性能测试', () => {
|
||||
let protobufSerializer: ProtobufSerializer;
|
||||
let snapshotManager: SnapshotManager;
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
protobufSerializer = ProtobufSerializer.getInstance();
|
||||
protobufSerializer.initialize(createMockProtobuf());
|
||||
|
||||
snapshotManager = new SnapshotManager();
|
||||
snapshotManager.initializeProtobuf(createMockProtobuf());
|
||||
|
||||
scene = new Scene();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('单组件序列化性能', () => {
|
||||
const iterations = 1000;
|
||||
|
||||
it('应该比较protobuf和JSON序列化速度', () => {
|
||||
const protobufComponents: PerfPositionComponent[] = [];
|
||||
const jsonComponents: JsonPositionComponent[] = [];
|
||||
|
||||
// 准备测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
protobufComponents.push(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
|
||||
jsonComponents.push(new JsonPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
}
|
||||
|
||||
// 测试Protobuf序列化
|
||||
const protobufStartTime = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
|
||||
for (const component of protobufComponents) {
|
||||
const result = protobufSerializer.serialize(component);
|
||||
protobufTotalSize += result.size;
|
||||
}
|
||||
|
||||
const protobufEndTime = performance.now();
|
||||
const protobufTime = protobufEndTime - protobufStartTime;
|
||||
|
||||
// 测试JSON序列化
|
||||
const jsonStartTime = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
|
||||
for (const component of jsonComponents) {
|
||||
const jsonString = JSON.stringify({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
jsonTotalSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonEndTime = performance.now();
|
||||
const jsonTime = jsonEndTime - jsonStartTime;
|
||||
|
||||
// 性能断言
|
||||
console.log(`\\n=== 单组件序列化性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`Protobuf总大小: ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON总大小: ${jsonTotalSize} bytes`);
|
||||
|
||||
if (jsonTime > 0) {
|
||||
const speedImprovement = ((jsonTime - protobufTime) / jsonTime * 100);
|
||||
console.log(`速度提升: ${speedImprovement.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (jsonTotalSize > 0) {
|
||||
const sizeReduction = ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100);
|
||||
console.log(`大小减少: ${sizeReduction.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
// 基本性能验证
|
||||
expect(protobufTime).toBeLessThan(1000); // 不应该超过1秒
|
||||
expect(jsonTime).toBeLessThan(1000);
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该测试复杂组件的序列化性能', () => {
|
||||
const protobufPlayers: PerfPlayerComponent[] = [];
|
||||
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||
|
||||
// 创建测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
protobufPlayers.push(new PerfPlayerComponent(
|
||||
`Player${i}`,
|
||||
Math.floor(Math.random() * 100) + 1
|
||||
));
|
||||
|
||||
jsonPlayers.push(new JsonPlayerComponent(
|
||||
`Player${i}`,
|
||||
Math.floor(Math.random() * 100) + 1
|
||||
));
|
||||
}
|
||||
|
||||
// Protobuf序列化测试
|
||||
const protobufStart = performance.now();
|
||||
for (const player of protobufPlayers) {
|
||||
protobufSerializer.serialize(player);
|
||||
}
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON序列化测试
|
||||
const jsonStart = performance.now();
|
||||
for (const player of jsonPlayers) {
|
||||
JSON.stringify({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline
|
||||
});
|
||||
}
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`\\n=== 复杂组件序列化性能 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||
|
||||
expect(protobufTime).toBeLessThan(1000);
|
||||
expect(jsonTime).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量实体序列化性能', () => {
|
||||
it('应该测试大量实体的快照创建性能', () => {
|
||||
const entityCount = 100;
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 创建测试实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Entity${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
entity.addComponent(new PerfVelocityComponent(
|
||||
Math.random() * 10 - 5,
|
||||
Math.random() * 10 - 5,
|
||||
Math.random() * 2 - 1
|
||||
));
|
||||
entity.addComponent(new PerfHealthComponent(100 + Math.floor(Math.random() * 50)));
|
||||
entity.addComponent(new PerfPlayerComponent(`Player${i}`, Math.floor(Math.random() * 50) + 1));
|
||||
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// 测试快照创建性能
|
||||
const snapshotStart = performance.now();
|
||||
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||
const snapshotTime = performance.now() - snapshotStart;
|
||||
|
||||
console.log(`\\n=== 批量实体序列化性能 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`每个实体组件数: 4`);
|
||||
console.log(`总组件数: ${entityCount * 4}`);
|
||||
console.log(`快照创建时间: ${snapshotTime.toFixed(2)}ms`);
|
||||
console.log(`平均每组件时间: ${(snapshotTime / (entityCount * 4)).toFixed(3)}ms`);
|
||||
|
||||
expect(snapshot.entities).toHaveLength(entityCount);
|
||||
expect(snapshotTime).toBeLessThan(5000); // 不应该超过5秒
|
||||
|
||||
// 计算快照大小
|
||||
let totalSnapshotSize = 0;
|
||||
for (const entitySnapshot of snapshot.entities) {
|
||||
for (const componentSnapshot of entitySnapshot.components) {
|
||||
if (componentSnapshot.data && typeof componentSnapshot.data === 'object' && 'size' in componentSnapshot.data) {
|
||||
totalSnapshotSize += (componentSnapshot.data as any).size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`快照总大小: ${totalSnapshotSize} bytes`);
|
||||
console.log(`平均每实体大小: ${(totalSnapshotSize / entityCount).toFixed(1)} bytes`);
|
||||
|
||||
expect(totalSnapshotSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化性能', () => {
|
||||
it('应该测试快照恢复性能', () => {
|
||||
const entityCount = 50;
|
||||
const originalEntities: Entity[] = [];
|
||||
|
||||
// 创建原始实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Original${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(i * 10, i * 20, i));
|
||||
entity.addComponent(new PerfHealthComponent(100 + i));
|
||||
originalEntities.push(entity);
|
||||
}
|
||||
|
||||
// 创建快照
|
||||
const snapshotStart = performance.now();
|
||||
const snapshot = snapshotManager.createSceneSnapshot(originalEntities);
|
||||
const snapshotTime = performance.now() - snapshotStart;
|
||||
|
||||
// 创建目标实体
|
||||
const targetEntities: Entity[] = [];
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Target${i}`);
|
||||
entity.addComponent(new PerfPositionComponent());
|
||||
entity.addComponent(new PerfHealthComponent());
|
||||
targetEntities.push(entity);
|
||||
}
|
||||
|
||||
// 测试恢复性能
|
||||
const restoreStart = performance.now();
|
||||
snapshotManager.restoreFromSnapshot(snapshot, targetEntities);
|
||||
const restoreTime = performance.now() - restoreStart;
|
||||
|
||||
console.log(`\\n=== 反序列化性能测试 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`序列化时间: ${snapshotTime.toFixed(2)}ms`);
|
||||
console.log(`反序列化时间: ${restoreTime.toFixed(2)}ms`);
|
||||
console.log(`总往返时间: ${(snapshotTime + restoreTime).toFixed(2)}ms`);
|
||||
console.log(`平均每实体往返时间: ${((snapshotTime + restoreTime) / entityCount).toFixed(3)}ms`);
|
||||
|
||||
expect(restoreTime).toBeLessThan(2000); // 不应该超过2秒
|
||||
expect(snapshotTime + restoreTime).toBeLessThan(3000); // 总时间不超过3秒
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存使用', () => {
|
||||
it('应该监控序列化过程中的内存使用', () => {
|
||||
const entityCount = 200;
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 创建大量实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`MemoryTest${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
entity.addComponent(new PerfVelocityComponent(
|
||||
Math.random() * 10,
|
||||
Math.random() * 10,
|
||||
Math.random() * 2
|
||||
));
|
||||
entity.addComponent(new PerfHealthComponent(Math.floor(Math.random() * 200) + 50));
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// 记录初始内存(如果可用)
|
||||
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
|
||||
// 执行序列化
|
||||
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||
|
||||
// 记录序列化后内存
|
||||
const afterMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
const memoryIncrease = afterMemory - initialMemory;
|
||||
|
||||
if (initialMemory > 0) {
|
||||
console.log(`\\n=== 内存使用测试 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`初始内存: ${(initialMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`序列化后内存: ${(afterMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`内存增加: ${(memoryIncrease / 1024).toFixed(2)} KB`);
|
||||
console.log(`平均每实体内存: ${(memoryIncrease / entityCount).toFixed(1)} bytes`);
|
||||
}
|
||||
|
||||
expect(snapshot.entities).toHaveLength(entityCount);
|
||||
|
||||
// 清理
|
||||
entities.length = 0;
|
||||
});
|
||||
});
|
||||
|
||||
describe('极端情况性能', () => {
|
||||
it('应该处理大量小组件的性能', () => {
|
||||
const componentCount = 5000;
|
||||
const components: PerfPositionComponent[] = [];
|
||||
|
||||
// 创建大量小组件
|
||||
for (let i = 0; i < componentCount; i++) {
|
||||
components.push(new PerfPositionComponent(i, i * 2, i * 3));
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
for (const component of components) {
|
||||
protobufSerializer.serialize(component);
|
||||
}
|
||||
const time = performance.now() - start;
|
||||
|
||||
console.log(`\\n=== 大量小组件性能测试 ===`);
|
||||
console.log(`组件数量: ${componentCount}`);
|
||||
console.log(`总时间: ${time.toFixed(2)}ms`);
|
||||
console.log(`平均每组件: ${(time / componentCount).toFixed(4)}ms`);
|
||||
console.log(`每秒处理: ${Math.floor(componentCount / (time / 1000))} 个组件`);
|
||||
|
||||
expect(time).toBeLessThan(10000); // 不超过10秒
|
||||
expect(time / componentCount).toBeLessThan(2); // 每个组件不超过2ms
|
||||
});
|
||||
});
|
||||
});
|
||||
294
packages/network/tests/Serialization/ProtobufDecorators.test.ts
Normal file
294
packages/network/tests/Serialization/ProtobufDecorators.test.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Protobuf装饰器测试
|
||||
*/
|
||||
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoField,
|
||||
ProtoFieldType,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry,
|
||||
isProtoSerializable,
|
||||
getProtoName
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('TestPosition')
|
||||
class TestPositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
@ProtoFloat(3)
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('TestPlayer')
|
||||
class TestPlayerComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public name: string = '';
|
||||
|
||||
@ProtoInt32(2)
|
||||
public level: number = 1;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public health: number = 100;
|
||||
|
||||
@ProtoBool(4)
|
||||
public isAlive: boolean = true;
|
||||
|
||||
constructor(name: string = '', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有装饰器的组件
|
||||
class PlainComponent extends Component {
|
||||
public data: string = 'test';
|
||||
}
|
||||
|
||||
// 测试字段编号冲突的组件
|
||||
const createConflictingComponent = () => {
|
||||
try {
|
||||
@ProtoSerializable('Conflict')
|
||||
class ConflictComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(1) // 故意使用相同的字段编号
|
||||
public y: number = 0;
|
||||
}
|
||||
return ConflictComponent;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
describe('ProtobufDecorators', () => {
|
||||
let registry: ProtobufRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
// 获取注册表实例
|
||||
registry = ProtobufRegistry.getInstance();
|
||||
});
|
||||
|
||||
describe('@ProtoSerializable装饰器', () => {
|
||||
it('应该正确标记组件为可序列化', () => {
|
||||
const component = new TestPositionComponent(10, 20, 30);
|
||||
|
||||
expect(isProtoSerializable(component)).toBe(true);
|
||||
expect(getProtoName(component)).toBe('TestPosition');
|
||||
});
|
||||
|
||||
it('应该在注册表中注册组件定义', () => {
|
||||
expect(registry.hasProtoDefinition('TestPosition')).toBe(true);
|
||||
expect(registry.hasProtoDefinition('TestPlayer')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确处理没有装饰器的组件', () => {
|
||||
const component = new PlainComponent();
|
||||
|
||||
expect(isProtoSerializable(component)).toBe(false);
|
||||
expect(getProtoName(component)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('@ProtoField装饰器', () => {
|
||||
it('应该正确定义字段', () => {
|
||||
const definition = registry.getComponentDefinition('TestPosition');
|
||||
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition!.fields.size).toBe(3);
|
||||
|
||||
const xField = definition!.fields.get('x');
|
||||
expect(xField).toEqual({
|
||||
fieldNumber: 1,
|
||||
type: ProtoFieldType.FLOAT,
|
||||
repeated: false,
|
||||
optional: false,
|
||||
name: 'x',
|
||||
customTypeName: undefined,
|
||||
enumValues: undefined,
|
||||
defaultValue: undefined,
|
||||
syncPriority: 'medium',
|
||||
precision: undefined,
|
||||
interpolation: false,
|
||||
quantizationBits: undefined,
|
||||
changeThreshold: 0
|
||||
});
|
||||
|
||||
const yField = definition!.fields.get('y');
|
||||
expect(yField).toEqual({
|
||||
fieldNumber: 2,
|
||||
type: ProtoFieldType.FLOAT,
|
||||
repeated: false,
|
||||
optional: false,
|
||||
name: 'y',
|
||||
customTypeName: undefined,
|
||||
enumValues: undefined,
|
||||
defaultValue: undefined,
|
||||
syncPriority: 'medium',
|
||||
precision: undefined,
|
||||
interpolation: false,
|
||||
quantizationBits: undefined,
|
||||
changeThreshold: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持不同的字段类型', () => {
|
||||
const definition = registry.getComponentDefinition('TestPlayer');
|
||||
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition!.fields.size).toBe(4);
|
||||
|
||||
const nameField = definition!.fields.get('name');
|
||||
expect(nameField!.type).toBe(ProtoFieldType.STRING);
|
||||
|
||||
const levelField = definition!.fields.get('level');
|
||||
expect(levelField!.type).toBe(ProtoFieldType.INT32);
|
||||
|
||||
const healthField = definition!.fields.get('health');
|
||||
expect(healthField!.type).toBe(ProtoFieldType.INT32);
|
||||
|
||||
const isAliveField = definition!.fields.get('isAlive');
|
||||
expect(isAliveField!.type).toBe(ProtoFieldType.BOOL);
|
||||
});
|
||||
|
||||
it('应该检测字段编号冲突', () => {
|
||||
const result = createConflictingComponent();
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
expect((result as Error).message).toContain('字段编号 1 已被字段');
|
||||
});
|
||||
|
||||
it('应该验证字段编号有效性', () => {
|
||||
expect(() => {
|
||||
class InvalidFieldComponent extends Component {
|
||||
@ProtoField(0) // 无效的字段编号
|
||||
public invalid: number = 0;
|
||||
}
|
||||
}).toThrow('字段编号必须大于0');
|
||||
|
||||
expect(() => {
|
||||
class InvalidFieldComponent extends Component {
|
||||
@ProtoField(-1) // 无效的字段编号
|
||||
public invalid: number = 0;
|
||||
}
|
||||
}).toThrow('字段编号必须大于0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('便捷装饰器', () => {
|
||||
it('ProtoFloat应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('FloatTest')
|
||||
class FloatTestComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public value: number = 0;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('FloatTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.FLOAT);
|
||||
});
|
||||
|
||||
it('ProtoInt32应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('Int32Test')
|
||||
class Int32TestComponent extends Component {
|
||||
@ProtoInt32(1)
|
||||
public value: number = 0;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('Int32Test');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.INT32);
|
||||
});
|
||||
|
||||
it('ProtoString应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('StringTest')
|
||||
class StringTestComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public value: string = '';
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('StringTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.STRING);
|
||||
});
|
||||
|
||||
it('ProtoBool应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('BoolTest')
|
||||
class BoolTestComponent extends Component {
|
||||
@ProtoBool(1)
|
||||
public value: boolean = false;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('BoolTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.BOOL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProtobufRegistry', () => {
|
||||
it('应该正确生成proto定义', () => {
|
||||
const protoDefinition = registry.generateProtoDefinition();
|
||||
|
||||
expect(protoDefinition).toContain('syntax = "proto3";');
|
||||
expect(protoDefinition).toContain('package ecs;');
|
||||
expect(protoDefinition).toContain('message TestPosition');
|
||||
expect(protoDefinition).toContain('message TestPlayer');
|
||||
expect(protoDefinition).toContain('float x = 1;');
|
||||
expect(protoDefinition).toContain('float y = 2;');
|
||||
expect(protoDefinition).toContain('string name = 1;');
|
||||
expect(protoDefinition).toContain('int32 level = 2;');
|
||||
expect(protoDefinition).toContain('bool isAlive = 4;');
|
||||
});
|
||||
|
||||
it('应该正确管理组件注册', () => {
|
||||
const allComponents = registry.getAllComponents();
|
||||
|
||||
expect(allComponents.size).toBeGreaterThanOrEqual(2);
|
||||
expect(allComponents.has('TestPosition')).toBe(true);
|
||||
expect(allComponents.has('TestPlayer')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('字段选项', () => {
|
||||
it('应该支持repeated字段', () => {
|
||||
@ProtoSerializable('RepeatedTest')
|
||||
class RepeatedTestComponent extends Component {
|
||||
@ProtoField(1, ProtoFieldType.INT32, { repeated: true })
|
||||
public values: number[] = [];
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('RepeatedTest');
|
||||
const field = definition!.fields.get('values');
|
||||
expect(field!.repeated).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持optional字段', () => {
|
||||
@ProtoSerializable('OptionalTest')
|
||||
class OptionalTestComponent extends Component {
|
||||
@ProtoField(1, ProtoFieldType.STRING, { optional: true })
|
||||
public optionalValue?: string;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('OptionalTest');
|
||||
const field = definition!.fields.get('optionalValue');
|
||||
expect(field!.optional).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
315
packages/network/tests/Serialization/ProtobufSerializer.test.ts
Normal file
315
packages/network/tests/Serialization/ProtobufSerializer.test.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Protobuf序列化器测试
|
||||
*/
|
||||
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
|
||||
import { SerializedData } from '../../../src/Utils/Serialization/SerializationTypes';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('Position')
|
||||
class PositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
@ProtoFloat(3)
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Health')
|
||||
class HealthComponent extends Component {
|
||||
@ProtoInt32(1)
|
||||
public maxHealth: number = 100;
|
||||
|
||||
@ProtoInt32(2)
|
||||
public currentHealth: number = 100;
|
||||
|
||||
@ProtoBool(3)
|
||||
public isDead: boolean = false;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
this.isDead = this.currentHealth <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Player')
|
||||
class PlayerComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public playerName: string = '';
|
||||
|
||||
@ProtoInt32(2)
|
||||
public playerId: number = 0;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public level: number = 1;
|
||||
|
||||
constructor(playerId: number = 0, playerName: string = '') {
|
||||
super();
|
||||
this.playerId = playerId;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有protobuf装饰器的组件
|
||||
class CustomComponent extends Component {
|
||||
public customData = {
|
||||
settings: { volume: 0.8 },
|
||||
achievements: ['first_kill', 'level_up'],
|
||||
inventory: new Map([['sword', 1], ['potion', 3]])
|
||||
};
|
||||
|
||||
// 自定义序列化方法
|
||||
serialize(): any {
|
||||
return {
|
||||
customData: {
|
||||
settings: this.customData.settings,
|
||||
achievements: this.customData.achievements,
|
||||
inventory: Array.from(this.customData.inventory.entries())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: any): void {
|
||||
if (data.customData) {
|
||||
this.customData.settings = data.customData.settings || this.customData.settings;
|
||||
this.customData.achievements = data.customData.achievements || this.customData.achievements;
|
||||
if (data.customData.inventory) {
|
||||
this.customData.inventory = new Map(data.customData.inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js
|
||||
const mockProtobuf = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
// 模拟protobuf消息类型
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null), // 验证通过
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) // 模拟编码结果
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => ({
|
||||
x: 10, y: 20, z: 30,
|
||||
maxHealth: 100, currentHealth: 80, isDead: false,
|
||||
playerName: 'TestPlayer', playerId: 1001, level: 5
|
||||
})),
|
||||
toObject: jest.fn().mockImplementation((message) => message)
|
||||
};
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
describe('ProtobufSerializer', () => {
|
||||
let serializer: ProtobufSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = ProtobufSerializer.getInstance();
|
||||
// 重置mock
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('初始化', () => {
|
||||
it('应该正确初始化protobuf支持', () => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
|
||||
expect(mockProtobuf.parse).toHaveBeenCalled();
|
||||
expect(serializer.canSerialize(new PositionComponent())).toBe(true);
|
||||
});
|
||||
|
||||
it('没有初始化时应该无法序列化protobuf组件', () => {
|
||||
const newSerializer = new (ProtobufSerializer as any)();
|
||||
expect(newSerializer.canSerialize(new PositionComponent())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('序列化', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
});
|
||||
|
||||
it('应该正确序列化protobuf组件', () => {
|
||||
const position = new PositionComponent(10, 20, 30);
|
||||
const result = serializer.serialize(position);
|
||||
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('PositionComponent');
|
||||
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||
expect(result.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该正确序列化复杂protobuf组件', () => {
|
||||
const health = new HealthComponent(150);
|
||||
health.takeDamage(50);
|
||||
|
||||
const result = serializer.serialize(health);
|
||||
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('HealthComponent');
|
||||
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('应该回退到JSON序列化非protobuf组件', () => {
|
||||
const custom = new CustomComponent();
|
||||
const result = serializer.serialize(custom);
|
||||
|
||||
expect(result.type).toBe('json');
|
||||
expect(result.componentType).toBe('CustomComponent');
|
||||
expect(result.data).toEqual(custom.serialize());
|
||||
});
|
||||
|
||||
it('protobuf序列化失败时应该回退到JSON', () => {
|
||||
// 模拟protobuf验证失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||
mockType.verify.mockReturnValue('验证失败');
|
||||
|
||||
const position = new PositionComponent(10, 20, 30);
|
||||
const result = serializer.serialize(position);
|
||||
|
||||
expect(result.type).toBe('json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
});
|
||||
|
||||
it('应该正确反序列化protobuf数据', () => {
|
||||
const position = new PositionComponent();
|
||||
const serializedData: SerializedData = {
|
||||
type: 'protobuf',
|
||||
componentType: 'PositionComponent',
|
||||
data: new Uint8Array([1, 2, 3, 4]),
|
||||
size: 4
|
||||
};
|
||||
|
||||
serializer.deserialize(position, serializedData);
|
||||
|
||||
// 验证decode和toObject被调用
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||
expect(mockType.decode).toHaveBeenCalled();
|
||||
expect(mockType.toObject).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确反序列化JSON数据', () => {
|
||||
const custom = new CustomComponent();
|
||||
const originalData = custom.serialize();
|
||||
|
||||
const serializedData: SerializedData = {
|
||||
type: 'json',
|
||||
componentType: 'CustomComponent',
|
||||
data: originalData,
|
||||
size: 100
|
||||
};
|
||||
|
||||
// 修改组件数据
|
||||
custom.customData.settings.volume = 0.5;
|
||||
|
||||
// 反序列化
|
||||
serializer.deserialize(custom, serializedData);
|
||||
|
||||
// 验证数据被恢复
|
||||
expect(custom.customData.settings.volume).toBe(0.8);
|
||||
});
|
||||
|
||||
it('应该处理反序列化错误', () => {
|
||||
const position = new PositionComponent();
|
||||
const invalidData: SerializedData = {
|
||||
type: 'protobuf',
|
||||
componentType: 'PositionComponent',
|
||||
data: new Uint8Array([255, 255, 255, 255]), // 无效数据
|
||||
size: 4
|
||||
};
|
||||
|
||||
// 模拟解码失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||
mockType.decode.mockImplementation(() => {
|
||||
throw new Error('解码失败');
|
||||
});
|
||||
|
||||
// 应该不抛出异常
|
||||
expect(() => {
|
||||
serializer.deserialize(position, invalidData);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息', () => {
|
||||
it('应该返回正确的统计信息', () => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
const stats = serializer.getStats();
|
||||
|
||||
expect(stats.protobufAvailable).toBe(true);
|
||||
expect(stats.registeredComponents).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('未初始化时应该返回正确的状态', () => {
|
||||
const newSerializer = new (ProtobufSerializer as any)();
|
||||
const stats = newSerializer.getStats();
|
||||
|
||||
expect(stats.protobufAvailable).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
});
|
||||
|
||||
it('应该处理空值和undefined', () => {
|
||||
const position = new PositionComponent();
|
||||
// 设置一些undefined值
|
||||
(position as any).undefinedProp = undefined;
|
||||
(position as any).nullProp = null;
|
||||
|
||||
const result = serializer.serialize(position);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该处理循环引用', () => {
|
||||
const custom = new CustomComponent();
|
||||
// 创建循环引用
|
||||
(custom as any).circular = custom;
|
||||
|
||||
const result = serializer.serialize(custom);
|
||||
expect(result.type).toBe('json');
|
||||
});
|
||||
|
||||
it('应该处理非常大的数值', () => {
|
||||
const position = new PositionComponent(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, 0);
|
||||
|
||||
const result = serializer.serialize(position);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Protobuf序列化器边界情况测试
|
||||
*/
|
||||
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { BigIntFactory } from '../../../src/ECS/Utils/BigIntCompatibility';
|
||||
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtoBytes,
|
||||
ProtoTimestamp,
|
||||
ProtoDouble,
|
||||
ProtoInt64,
|
||||
ProtoStruct
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 边界测试组件
|
||||
@ProtoSerializable('EdgeCaseComponent')
|
||||
class EdgeCaseComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public floatValue: number = 0;
|
||||
|
||||
@ProtoDouble(2)
|
||||
public doubleValue: number = 0;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public intValue: number = 0;
|
||||
|
||||
@ProtoInt64(4)
|
||||
public bigIntValue: any = BigIntFactory.zero();
|
||||
|
||||
@ProtoString(5)
|
||||
public stringValue: string = '';
|
||||
|
||||
@ProtoBool(6)
|
||||
public boolValue: boolean = false;
|
||||
|
||||
@ProtoBytes(7)
|
||||
public bytesValue: Uint8Array = new Uint8Array();
|
||||
|
||||
@ProtoTimestamp(8)
|
||||
public timestampValue: Date = new Date();
|
||||
|
||||
@ProtoStruct(9)
|
||||
public structValue: any = {};
|
||||
|
||||
@ProtoFloat(10, { repeated: true })
|
||||
public arrayValue: number[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// 不完整的组件(缺少字段)
|
||||
@ProtoSerializable('IncompleteComponent')
|
||||
class IncompleteComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public name: string = '';
|
||||
|
||||
// 故意添加没有装饰器的字段
|
||||
public undecoratedField: number = 42;
|
||||
|
||||
constructor(name: string = '') {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// 有循环引用的组件
|
||||
@ProtoSerializable('CircularComponent')
|
||||
class CircularComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public name: string = '';
|
||||
|
||||
@ProtoStruct(2)
|
||||
public circular: any = null;
|
||||
|
||||
constructor(name: string = '') {
|
||||
super();
|
||||
this.name = name;
|
||||
// 创建循环引用
|
||||
this.circular = this;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有protobuf装饰器的组件
|
||||
class NonSerializableComponent extends Component {
|
||||
public data: string = 'test';
|
||||
|
||||
serialize(): any {
|
||||
return { data: this.data };
|
||||
}
|
||||
|
||||
deserialize(data: any): void {
|
||||
this.data = data.data || this.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js
|
||||
const mockProtobuf = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => ({
|
||||
floatValue: 3.14,
|
||||
doubleValue: 2.718,
|
||||
intValue: 42,
|
||||
bigIntValue: BigIntFactory.create(999),
|
||||
stringValue: 'test',
|
||||
boolValue: true,
|
||||
bytesValue: new Uint8Array([65, 66, 67]),
|
||||
timestampValue: { seconds: 1609459200, nanos: 0 },
|
||||
structValue: { fields: { key: { stringValue: 'value' } } },
|
||||
arrayValue: [1.1, 2.2, 3.3],
|
||||
name: 'TestComponent'
|
||||
})),
|
||||
toObject: jest.fn().mockImplementation((message) => message)
|
||||
};
|
||||
}),
|
||||
lookupTypeOrEnum: jest.fn().mockImplementation((typeName: string) => {
|
||||
if (typeName === 'google.protobuf.Timestamp') {
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => ({
|
||||
seconds: 1609459200,
|
||||
nanos: 0
|
||||
}))
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
describe('ProtobufSerializer边界情况测试', () => {
|
||||
let serializer: ProtobufSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = ProtobufSerializer.getInstance();
|
||||
serializer.initialize(mockProtobuf);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('极值测试', () => {
|
||||
it('应该处理极大值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.floatValue = Number.MAX_VALUE;
|
||||
component.doubleValue = Number.MAX_VALUE;
|
||||
component.intValue = Number.MAX_SAFE_INTEGER;
|
||||
component.bigIntValue = BigIntFactory.create(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该处理极小值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.floatValue = Number.MIN_VALUE;
|
||||
component.doubleValue = Number.MIN_VALUE;
|
||||
component.intValue = Number.MIN_SAFE_INTEGER;
|
||||
component.bigIntValue = BigIntFactory.create(Number.MIN_SAFE_INTEGER);
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理特殊数值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.floatValue = NaN;
|
||||
component.doubleValue = Infinity;
|
||||
component.intValue = 0;
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('空值和undefined测试', () => {
|
||||
it('应该处理null值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
(component as any).stringValue = null;
|
||||
(component as any).structValue = null;
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理undefined值', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
(component as any).stringValue = undefined;
|
||||
(component as any).floatValue = undefined;
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理空数组', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.arrayValue = [];
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('复杂数据类型测试', () => {
|
||||
it('应该处理复杂对象结构', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.structValue = {
|
||||
nested: {
|
||||
array: [1, 2, 3],
|
||||
object: { key: 'value' },
|
||||
date: new Date(),
|
||||
null: null,
|
||||
undefined: undefined
|
||||
}
|
||||
};
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理Date对象', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.timestampValue = new Date('2021-01-01T00:00:00Z');
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该处理Uint8Array', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
component.bytesValue = new Uint8Array([0, 255, 128, 64]);
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('循环引用测试', () => {
|
||||
it('应该拒绝循环引用对象并抛出错误', () => {
|
||||
const component = new CircularComponent('circular');
|
||||
|
||||
// 循环引用应该抛出错误,不再回退到JSON序列化
|
||||
expect(() => {
|
||||
serializer.serialize(component);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('不完整组件测试', () => {
|
||||
it('应该处理缺少装饰器的字段', () => {
|
||||
const component = new IncompleteComponent('test');
|
||||
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('IncompleteComponent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('非序列化组件测试', () => {
|
||||
it('应该拒绝非protobuf组件并抛出错误', () => {
|
||||
const component = new NonSerializableComponent();
|
||||
|
||||
// 没有protobuf装饰器的组件应该抛出错误,不再回退到JSON序列化
|
||||
expect(() => {
|
||||
serializer.serialize(component);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量序列化边界测试', () => {
|
||||
it('应该处理空数组', () => {
|
||||
const results = serializer.serializeBatch([]);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该处理混合组件类型', () => {
|
||||
const components = [
|
||||
new EdgeCaseComponent(),
|
||||
new NonSerializableComponent(),
|
||||
new IncompleteComponent('mixed'),
|
||||
];
|
||||
|
||||
// continueOnError: true 时,只有可序列化的组件能成功,其他会被跳过
|
||||
const results = serializer.serializeBatch(components, { continueOnError: true });
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
expect(results.every(r => r.type === 'protobuf')).toBe(true);
|
||||
|
||||
// continueOnError: false 时应该抛出错误
|
||||
expect(() => {
|
||||
serializer.serializeBatch(components, { continueOnError: false });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理批量数据', () => {
|
||||
const components = Array.from({ length: 50 }, () => new EdgeCaseComponent());
|
||||
|
||||
const results = serializer.serializeBatch(components);
|
||||
expect(results).toHaveLength(50);
|
||||
expect(results.every(r => r.type === 'protobuf')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理序列化错误', () => {
|
||||
// 创建会导致序列化失败的组件
|
||||
const components = [new NonSerializableComponent()];
|
||||
|
||||
// continueOnError = false 应该抛出异常
|
||||
expect(() => {
|
||||
serializer.serializeBatch(components, { continueOnError: false });
|
||||
}).toThrow();
|
||||
|
||||
// continueOnError = true 应该返回空数组(跳过失败的组件)
|
||||
const results = serializer.serializeBatch(components, { continueOnError: true });
|
||||
expect(results).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化边界测试', () => {
|
||||
it('应该拒绝JSON类型的反序列化并抛出错误', () => {
|
||||
const component = new NonSerializableComponent();
|
||||
const serializedData = {
|
||||
type: 'json' as const,
|
||||
componentType: 'NonSerializableComponent',
|
||||
data: { data: 'deserialized' },
|
||||
size: 100
|
||||
};
|
||||
|
||||
// JSON类型的数据应该被拒绝,抛出错误
|
||||
expect(() => {
|
||||
serializer.deserialize(component, serializedData);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该优雅处理反序列化错误', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
const invalidData = {
|
||||
type: 'protobuf' as const,
|
||||
componentType: 'EdgeCaseComponent',
|
||||
data: new Uint8Array([255, 255, 255, 255]),
|
||||
size: 4
|
||||
};
|
||||
|
||||
// 模拟解码失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.EdgeCaseComponent');
|
||||
mockType.decode.mockImplementation(() => {
|
||||
throw new Error('Decode failed');
|
||||
});
|
||||
|
||||
// 不应该抛出异常
|
||||
expect(() => {
|
||||
serializer.deserialize(component, invalidData);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理缺失的proto定义', () => {
|
||||
const component = new EdgeCaseComponent();
|
||||
// 清除proto名称以模拟缺失情况
|
||||
(component as any)._protoName = undefined;
|
||||
|
||||
const serializedData = {
|
||||
type: 'protobuf' as const,
|
||||
componentType: 'EdgeCaseComponent',
|
||||
data: new Uint8Array([1, 2, 3, 4]),
|
||||
size: 4
|
||||
};
|
||||
|
||||
// 不应该抛出异常
|
||||
expect(() => {
|
||||
serializer.deserialize(component, serializedData);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('缓存测试', () => {
|
||||
it('应该能清空所有缓存', () => {
|
||||
serializer.clearAllCaches();
|
||||
const stats = serializer.getStats();
|
||||
expect(stats.messageTypeCacheSize).toBe(0);
|
||||
expect(stats.componentDataCacheSize).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能选项测试', () => {
|
||||
it('应该能禁用数据验证', () => {
|
||||
serializer.setPerformanceOptions({ enableValidation: false });
|
||||
|
||||
const component = new EdgeCaseComponent();
|
||||
const result = serializer.serialize(component);
|
||||
expect(result.type).toBe('protobuf');
|
||||
});
|
||||
|
||||
it('应该能禁用组件数据缓存', () => {
|
||||
serializer.setPerformanceOptions({ enableComponentDataCache: false });
|
||||
|
||||
const component = new EdgeCaseComponent();
|
||||
serializer.serialize(component);
|
||||
|
||||
const stats = serializer.getStats();
|
||||
expect(stats.componentDataCacheSize).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息测试', () => {
|
||||
it('应该返回正确的统计信息', () => {
|
||||
const stats = serializer.getStats();
|
||||
|
||||
expect(typeof stats.registeredComponents).toBe('number');
|
||||
expect(typeof stats.protobufAvailable).toBe('boolean');
|
||||
expect(typeof stats.messageTypeCacheSize).toBe('number');
|
||||
expect(typeof stats.componentDataCacheSize).toBe('number');
|
||||
expect(typeof stats.enableComponentDataCache).toBe('boolean');
|
||||
expect(typeof stats.maxCacheSize).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
393
packages/network/tests/Serialization/RealPerformance.test.ts
Normal file
393
packages/network/tests/Serialization/RealPerformance.test.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* 真实 Protobuf 序列化性能测试
|
||||
* 使用实际的 protobufjs 库进行性能对比
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('Position')
|
||||
class PositionComponent extends Component {
|
||||
@ProtoFloat(1) public x: number = 0;
|
||||
@ProtoFloat(2) public y: number = 0;
|
||||
@ProtoFloat(3) public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Player')
|
||||
class PlayerComponent extends Component {
|
||||
@ProtoString(1) public name: string = '';
|
||||
@ProtoInt32(2) public level: number = 1;
|
||||
@ProtoInt32(3) public experience: number = 0;
|
||||
@ProtoInt32(4) public score: number = 0;
|
||||
@ProtoBool(5) public isOnline: boolean = true;
|
||||
@ProtoFloat(6) public health: number = 100.0;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.experience = level * 1000;
|
||||
this.score = level * 500;
|
||||
this.health = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
// JSON 对比组件
|
||||
class JsonPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPlayerComponent extends Component {
|
||||
public name: string = '';
|
||||
public level: number = 1;
|
||||
public experience: number = 0;
|
||||
public score: number = 0;
|
||||
public isOnline: boolean = true;
|
||||
public health: number = 100.0;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.experience = level * 1000;
|
||||
this.score = level * 500;
|
||||
this.health = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
describe('真实 Protobuf 性能测试', () => {
|
||||
let protobuf: any;
|
||||
let root: any;
|
||||
let PositionType: any;
|
||||
let PlayerType: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
// 尝试加载真实的 protobufjs
|
||||
protobuf = require('protobufjs');
|
||||
|
||||
// 生成 proto 定义
|
||||
const registry = ProtobufRegistry.getInstance();
|
||||
const protoDefinition = registry.generateProtoDefinition();
|
||||
|
||||
console.log('Generated proto definition:');
|
||||
console.log(protoDefinition);
|
||||
|
||||
// 解析 proto 定义
|
||||
root = protobuf.parse(protoDefinition).root;
|
||||
PositionType = root.lookupType('ecs.Position');
|
||||
PlayerType = root.lookupType('ecs.Player');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Protobuf not available, skipping real performance tests:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const skipIfNoProtobuf = () => {
|
||||
if (!protobuf || !root) {
|
||||
console.log('Skipping test: protobufjs not available');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
describe('简单组件性能对比', () => {
|
||||
it('Position 组件序列化性能', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const iterations = 1000;
|
||||
const protobufComponents: PositionComponent[] = [];
|
||||
const jsonComponents: JsonPositionComponent[] = [];
|
||||
|
||||
// 准备测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const x = Math.random() * 1000;
|
||||
const y = Math.random() * 1000;
|
||||
const z = Math.random() * 100;
|
||||
|
||||
protobufComponents.push(new PositionComponent(x, y, z));
|
||||
jsonComponents.push(new JsonPositionComponent(x, y, z));
|
||||
}
|
||||
|
||||
// Protobuf 序列化测试
|
||||
const protobufStartTime = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
const protobufResults: Uint8Array[] = [];
|
||||
|
||||
for (const component of protobufComponents) {
|
||||
const message = PositionType.create({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
const buffer = PositionType.encode(message).finish();
|
||||
protobufResults.push(buffer);
|
||||
protobufTotalSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufEndTime = performance.now();
|
||||
const protobufTime = protobufEndTime - protobufStartTime;
|
||||
|
||||
// JSON 序列化测试
|
||||
const jsonStartTime = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
const jsonResults: string[] = [];
|
||||
|
||||
for (const component of jsonComponents) {
|
||||
const jsonString = JSON.stringify({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
jsonResults.push(jsonString);
|
||||
jsonTotalSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonEndTime = performance.now();
|
||||
const jsonTime = jsonEndTime - jsonStartTime;
|
||||
|
||||
// 计算性能指标
|
||||
const speedImprovement = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||
const sizeReduction = jsonTotalSize > 0 ? ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100) : 0;
|
||||
|
||||
console.log(`\\n=== Position 组件性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`速度变化: ${speedImprovement > 0 ? '+' : ''}${speedImprovement.toFixed(1)}%`);
|
||||
console.log('');
|
||||
console.log(`Protobuf 总大小: ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON 总大小: ${jsonTotalSize} bytes`);
|
||||
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||
console.log(`平均 Protobuf 大小: ${(protobufTotalSize / iterations).toFixed(1)} bytes`);
|
||||
console.log(`平均 JSON 大小: ${(jsonTotalSize / iterations).toFixed(1)} bytes`);
|
||||
|
||||
// 验证反序列化
|
||||
let deserializeTime = performance.now();
|
||||
for (const buffer of protobufResults.slice(0, 10)) { // 只测试前10个
|
||||
const decoded = PositionType.decode(buffer);
|
||||
expect(typeof decoded.x).toBe('number');
|
||||
expect(typeof decoded.y).toBe('number');
|
||||
expect(typeof decoded.z).toBe('number');
|
||||
}
|
||||
deserializeTime = performance.now() - deserializeTime;
|
||||
console.log(`Protobuf 反序列化 10 个: ${deserializeTime.toFixed(2)}ms`);
|
||||
|
||||
// 基本验证
|
||||
expect(protobufTime).toBeGreaterThan(0);
|
||||
expect(jsonTime).toBeGreaterThan(0);
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('复杂 Player 组件序列化性能', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const iterations = 500;
|
||||
const protobufPlayers: PlayerComponent[] = [];
|
||||
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||
|
||||
// 创建测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const name = `Player_${i}_${'x'.repeat(10 + Math.floor(Math.random() * 20))}`;
|
||||
const level = Math.floor(Math.random() * 100) + 1;
|
||||
|
||||
protobufPlayers.push(new PlayerComponent(name, level));
|
||||
jsonPlayers.push(new JsonPlayerComponent(name, level));
|
||||
}
|
||||
|
||||
// Protobuf 序列化测试
|
||||
const protobufStart = performance.now();
|
||||
let protobufSize = 0;
|
||||
|
||||
for (const player of protobufPlayers) {
|
||||
const message = PlayerType.create({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline,
|
||||
health: player.health
|
||||
});
|
||||
const buffer = PlayerType.encode(message).finish();
|
||||
protobufSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 序列化测试
|
||||
const jsonStart = performance.now();
|
||||
let jsonSize = 0;
|
||||
|
||||
for (const player of jsonPlayers) {
|
||||
const jsonString = JSON.stringify({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline,
|
||||
health: player.health
|
||||
});
|
||||
jsonSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
const speedChange = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||
const sizeReduction = jsonSize > 0 ? ((jsonSize - protobufSize) / jsonSize * 100) : 0;
|
||||
|
||||
console.log(`\\n=== Player 组件性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`速度变化: ${speedChange > 0 ? '+' : ''}${speedChange.toFixed(1)}%`);
|
||||
console.log('');
|
||||
console.log(`Protobuf 总大小: ${protobufSize} bytes`);
|
||||
console.log(`JSON 总大小: ${jsonSize} bytes`);
|
||||
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||
console.log(`平均 Protobuf 大小: ${(protobufSize / iterations).toFixed(1)} bytes`);
|
||||
console.log(`平均 JSON 大小: ${(jsonSize / iterations).toFixed(1)} bytes`);
|
||||
|
||||
expect(protobufTime).toBeGreaterThan(0);
|
||||
expect(jsonTime).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量数据性能测试', () => {
|
||||
it('大量小对象序列化', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const count = 5000;
|
||||
console.log(`\\n=== 大量小对象测试 (${count} 个 Position) ===`);
|
||||
|
||||
// 准备数据
|
||||
const positions = Array.from({ length: count }, (_, i) => ({
|
||||
x: i * 0.1,
|
||||
y: i * 0.2,
|
||||
z: i * 0.05
|
||||
}));
|
||||
|
||||
// Protobuf 批量序列化
|
||||
const protobufStart = performance.now();
|
||||
let protobufSize = 0;
|
||||
|
||||
for (const pos of positions) {
|
||||
const message = PositionType.create(pos);
|
||||
const buffer = PositionType.encode(message).finish();
|
||||
protobufSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 批量序列化
|
||||
const jsonStart = performance.now();
|
||||
let jsonSize = 0;
|
||||
|
||||
for (const pos of positions) {
|
||||
const jsonString = JSON.stringify(pos);
|
||||
jsonSize += jsonString.length;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufSize} bytes`);
|
||||
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonSize} bytes`);
|
||||
console.log(`速度: ${protobufTime < jsonTime ? 'Protobuf 更快' : 'JSON 更快'} (${Math.abs(protobufTime - jsonTime).toFixed(2)}ms 差异)`);
|
||||
console.log(`大小: Protobuf ${protobufSize < jsonSize ? '更小' : '更大'} (${Math.abs(protobufSize - jsonSize)} bytes 差异)`);
|
||||
console.log(`处理速度: Protobuf ${Math.floor(count / (protobufTime / 1000))} ops/s, JSON ${Math.floor(count / (jsonTime / 1000))} ops/s`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('真实网络场景模拟', () => {
|
||||
it('游戏状态同步场景', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
console.log(`\\n=== 游戏状态同步场景 ===`);
|
||||
|
||||
// 模拟 100 个玩家的位置更新
|
||||
const playerCount = 100;
|
||||
const updateData = Array.from({ length: playerCount }, (_, i) => ({
|
||||
playerId: i,
|
||||
x: Math.random() * 1000,
|
||||
y: Math.random() * 1000,
|
||||
z: Math.random() * 100,
|
||||
health: Math.floor(Math.random() * 100),
|
||||
isMoving: Math.random() > 0.5
|
||||
}));
|
||||
|
||||
// 创建组合消息类型(模拟)
|
||||
const GameUpdateType = root.lookupType('ecs.Position'); // 简化使用 Position
|
||||
|
||||
// Protobuf 序列化所有更新
|
||||
const protobufStart = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
|
||||
for (const update of updateData) {
|
||||
const message = GameUpdateType.create({
|
||||
x: update.x,
|
||||
y: update.y,
|
||||
z: update.z
|
||||
});
|
||||
const buffer = GameUpdateType.encode(message).finish();
|
||||
protobufTotalSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 序列化所有更新
|
||||
const jsonStart = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
|
||||
for (const update of updateData) {
|
||||
const jsonString = JSON.stringify({
|
||||
playerId: update.playerId,
|
||||
x: update.x,
|
||||
y: update.y,
|
||||
z: update.z,
|
||||
health: update.health,
|
||||
isMoving: update.isMoving
|
||||
});
|
||||
jsonTotalSize += jsonString.length;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`${playerCount} 个玩家位置更新:`);
|
||||
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonTotalSize} bytes`);
|
||||
|
||||
// 计算网络传输节省
|
||||
const sizeSaving = jsonTotalSize - protobufTotalSize;
|
||||
const percentSaving = (sizeSaving / jsonTotalSize * 100);
|
||||
|
||||
console.log(`数据大小节省: ${sizeSaving} bytes (${percentSaving.toFixed(1)}%)`);
|
||||
console.log(`每秒 60 次更新的带宽节省: ${(sizeSaving * 60 / 1024).toFixed(2)} KB/s`);
|
||||
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* SnapshotManager与Protobuf序列化集成测试
|
||||
*/
|
||||
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Scene } from '../../../src/ECS/Scene';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('TestPosition')
|
||||
class TestPositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('TestVelocity')
|
||||
class TestVelocityComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public vx: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public vy: number = 0;
|
||||
|
||||
constructor(vx: number = 0, vy: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('TestHealth')
|
||||
class TestHealthComponent extends Component {
|
||||
@ProtoInt32(1)
|
||||
public maxHealth: number = 100;
|
||||
|
||||
@ProtoInt32(2)
|
||||
public currentHealth: number = 100;
|
||||
|
||||
@ProtoBool(3)
|
||||
public isDead: boolean = false;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
}
|
||||
|
||||
// 传统JSON序列化组件
|
||||
class TraditionalComponent extends Component {
|
||||
public customData = {
|
||||
name: 'traditional',
|
||||
values: [1, 2, 3],
|
||||
settings: { enabled: true }
|
||||
};
|
||||
|
||||
serialize(): any {
|
||||
return {
|
||||
customData: this.customData
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: any): void {
|
||||
if (data.customData) {
|
||||
this.customData = data.customData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 简单组件(使用默认序列化)
|
||||
class SimpleComponent extends Component {
|
||||
public value: number = 42;
|
||||
public text: string = 'simple';
|
||||
public flag: boolean = true;
|
||||
}
|
||||
|
||||
// Mock protobuf.js
|
||||
const mockProtobuf = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
const mockData: Record<string, any> = {
|
||||
'ecs.TestPosition': { x: 10, y: 20 },
|
||||
'ecs.TestVelocity': { vx: 5, vy: 3 },
|
||||
'ecs.TestHealth': { maxHealth: 100, currentHealth: 80, isDead: false }
|
||||
};
|
||||
|
||||
return {
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data: any) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockReturnValue(mockData[typeName] || {}),
|
||||
toObject: jest.fn().mockImplementation((message: any) => message)
|
||||
};
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
describe('SnapshotManager Protobuf集成', () => {
|
||||
let snapshotManager: SnapshotManager;
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
snapshotManager = new SnapshotManager();
|
||||
snapshotManager.initializeProtobuf(mockProtobuf);
|
||||
scene = new Scene();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('混合序列化快照', () => {
|
||||
it('应该正确创建包含protobuf和JSON组件的快照', () => {
|
||||
// 创建实体
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new TestPositionComponent(100, 200));
|
||||
player.addComponent(new TestVelocityComponent(5, 3));
|
||||
player.addComponent(new TestHealthComponent(120));
|
||||
player.addComponent(new TraditionalComponent());
|
||||
player.addComponent(new SimpleComponent());
|
||||
|
||||
// 创建快照
|
||||
const snapshot = snapshotManager.createSceneSnapshot([player]);
|
||||
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot.entities).toHaveLength(1);
|
||||
expect(snapshot.entities[0].components).toHaveLength(5);
|
||||
|
||||
// 验证快照包含所有组件
|
||||
const componentTypes = snapshot.entities[0].components.map(c => c.type);
|
||||
expect(componentTypes).toContain('TestPositionComponent');
|
||||
expect(componentTypes).toContain('TestVelocityComponent');
|
||||
expect(componentTypes).toContain('TestHealthComponent');
|
||||
expect(componentTypes).toContain('TraditionalComponent');
|
||||
expect(componentTypes).toContain('SimpleComponent');
|
||||
});
|
||||
|
||||
it('应该根据组件类型使用相应的序列化方式', () => {
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
const position = new TestPositionComponent(50, 75);
|
||||
const traditional = new TraditionalComponent();
|
||||
|
||||
entity.addComponent(position);
|
||||
entity.addComponent(traditional);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([entity]);
|
||||
const components = snapshot.entities[0].components;
|
||||
|
||||
// 检查序列化数据格式
|
||||
const positionSnapshot = components.find(c => c.type === 'TestPositionComponent');
|
||||
const traditionalSnapshot = components.find(c => c.type === 'TraditionalComponent');
|
||||
|
||||
expect(positionSnapshot).toBeDefined();
|
||||
expect(traditionalSnapshot).toBeDefined();
|
||||
|
||||
// Protobuf组件应该有SerializedData格式
|
||||
expect(positionSnapshot!.data).toHaveProperty('type');
|
||||
expect(positionSnapshot!.data).toHaveProperty('componentType');
|
||||
expect(positionSnapshot!.data).toHaveProperty('data');
|
||||
expect(positionSnapshot!.data).toHaveProperty('size');
|
||||
});
|
||||
});
|
||||
|
||||
describe('快照恢复', () => {
|
||||
it('应该正确恢复protobuf序列化的组件', () => {
|
||||
// 创建原始实体
|
||||
const originalEntity = scene.createEntity('Original');
|
||||
const originalPosition = new TestPositionComponent(100, 200);
|
||||
const originalHealth = new TestHealthComponent(150);
|
||||
originalHealth.currentHealth = 120;
|
||||
|
||||
originalEntity.addComponent(originalPosition);
|
||||
originalEntity.addComponent(originalHealth);
|
||||
|
||||
// 创建快照
|
||||
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||
|
||||
// 创建新实体进行恢复
|
||||
const newEntity = scene.createEntity('New');
|
||||
newEntity.addComponent(new TestPositionComponent());
|
||||
newEntity.addComponent(new TestHealthComponent());
|
||||
|
||||
// 恢复快照
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
|
||||
// 验证数据被正确恢复(注意:由于使用mock,实际值来自mock数据)
|
||||
const restoredPosition = newEntity.getComponent(TestPositionComponent as any);
|
||||
const restoredHealth = newEntity.getComponent(TestHealthComponent as any);
|
||||
|
||||
expect(restoredPosition).toBeDefined();
|
||||
expect(restoredHealth).toBeDefined();
|
||||
|
||||
// 验证protobuf的decode方法被调用
|
||||
expect(mockProtobuf.parse().root.lookupType).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确恢复传统JSON序列化的组件', () => {
|
||||
const originalEntity = scene.createEntity('Original');
|
||||
const originalTraditional = new TraditionalComponent();
|
||||
originalTraditional.customData.name = 'modified';
|
||||
originalTraditional.customData.values = [4, 5, 6];
|
||||
|
||||
originalEntity.addComponent(originalTraditional);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||
|
||||
const newEntity = scene.createEntity('New');
|
||||
const newTraditional = new TraditionalComponent();
|
||||
newEntity.addComponent(newTraditional);
|
||||
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
|
||||
// 验证JSON数据被正确恢复(由于使用mock,验证组件被恢复即可)
|
||||
expect(newTraditional.customData).toBeDefined();
|
||||
expect(newTraditional.customData.name).toBe('traditional');
|
||||
expect(newTraditional.customData.values).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('应该处理混合序列化的实体恢复', () => {
|
||||
const originalEntity = scene.createEntity('Mixed');
|
||||
const position = new TestPositionComponent(30, 40);
|
||||
const traditional = new TraditionalComponent();
|
||||
const simple = new SimpleComponent();
|
||||
|
||||
traditional.customData.name = 'mixed_test';
|
||||
simple.value = 99;
|
||||
simple.text = 'updated';
|
||||
|
||||
originalEntity.addComponent(position);
|
||||
originalEntity.addComponent(traditional);
|
||||
originalEntity.addComponent(simple);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||
|
||||
const newEntity = scene.createEntity('NewMixed');
|
||||
newEntity.addComponent(new TestPositionComponent());
|
||||
newEntity.addComponent(new TraditionalComponent());
|
||||
newEntity.addComponent(new SimpleComponent());
|
||||
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
|
||||
// 验证所有组件都被正确恢复
|
||||
const restoredTraditional = newEntity.getComponent(TraditionalComponent);
|
||||
const restoredSimple = newEntity.getComponent(SimpleComponent);
|
||||
|
||||
expect(restoredTraditional!.customData.name).toBe('traditional');
|
||||
expect(restoredSimple!.value).toBe(42);
|
||||
expect(restoredSimple!.text).toBe('simple');
|
||||
});
|
||||
});
|
||||
|
||||
describe('向后兼容性', () => {
|
||||
it('应该能够处理旧格式的快照数据', () => {
|
||||
// 模拟旧格式的快照数据
|
||||
const legacySnapshot = {
|
||||
entities: [{
|
||||
id: 1,
|
||||
name: 'LegacyEntity',
|
||||
enabled: true,
|
||||
active: true,
|
||||
tag: 0,
|
||||
updateOrder: 0,
|
||||
components: [{
|
||||
type: 'SimpleComponent',
|
||||
id: 1,
|
||||
data: { value: 123, text: 'legacy', flag: false }, // 直接的JSON数据
|
||||
enabled: true,
|
||||
config: { includeInSnapshot: true, compressionLevel: 0, syncPriority: 5, enableIncremental: true }
|
||||
}],
|
||||
children: [],
|
||||
timestamp: Date.now()
|
||||
}],
|
||||
timestamp: Date.now(),
|
||||
version: '1.0.0',
|
||||
type: 'full' as const
|
||||
};
|
||||
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
entity.addComponent(new SimpleComponent());
|
||||
|
||||
snapshotManager.restoreFromSnapshot(legacySnapshot, [entity]);
|
||||
|
||||
const component = entity.getComponent(SimpleComponent);
|
||||
expect(component!.value).toBe(42);
|
||||
expect(component!.text).toBe('simple');
|
||||
expect(component!.flag).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该优雅地处理protobuf序列化失败', () => {
|
||||
// 模拟protobuf验证失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType;
|
||||
mockType.mockImplementation(() => ({
|
||||
verify: jest.fn().mockReturnValue('验证失败'),
|
||||
create: jest.fn(),
|
||||
encode: jest.fn(),
|
||||
decode: jest.fn(),
|
||||
toObject: jest.fn()
|
||||
}));
|
||||
|
||||
const entity = scene.createEntity('ErrorTest');
|
||||
entity.addComponent(new TestPositionComponent(10, 20));
|
||||
|
||||
// 应该不抛出异常,而是回退到JSON序列化
|
||||
expect(() => {
|
||||
snapshotManager.createSceneSnapshot([entity]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该优雅地处理protobuf反序列化失败', () => {
|
||||
const entity = scene.createEntity('Test');
|
||||
const position = new TestPositionComponent(10, 20);
|
||||
entity.addComponent(position);
|
||||
|
||||
const snapshot = snapshotManager.createSceneSnapshot([entity]);
|
||||
|
||||
// 模拟反序列化失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType;
|
||||
mockType.mockImplementation(() => ({
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn(),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||
}),
|
||||
decode: jest.fn().mockImplementation(() => {
|
||||
throw new Error('解码失败');
|
||||
}),
|
||||
toObject: jest.fn()
|
||||
}));
|
||||
|
||||
const newEntity = scene.createEntity('NewTest');
|
||||
newEntity.addComponent(new TestPositionComponent());
|
||||
|
||||
// 应该不抛出异常
|
||||
expect(() => {
|
||||
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息', () => {
|
||||
it('应该包含protobuf统计信息', () => {
|
||||
const stats = snapshotManager.getCacheStats();
|
||||
|
||||
expect(stats).toHaveProperty('snapshotCacheSize');
|
||||
expect(stats).toHaveProperty('protobufStats');
|
||||
expect(stats.protobufStats).toHaveProperty('registeredComponents');
|
||||
expect(stats.protobufStats).toHaveProperty('protobufAvailable');
|
||||
expect(stats.protobufStats!.protobufAvailable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
17
packages/network/tests/Serialization/index.test.ts
Normal file
17
packages/network/tests/Serialization/index.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 序列化模块集成测试
|
||||
*/
|
||||
|
||||
// 导入所有测试
|
||||
import './ProtobufDecorators.test';
|
||||
import './ProtobufSerializer.test';
|
||||
import './SnapshotManagerIntegration.test';
|
||||
import './Performance.test';
|
||||
|
||||
// 这个文件确保所有序列化相关的测试都被包含在测试套件中
|
||||
describe('序列化模块集成测试', () => {
|
||||
it('应该包含所有序列化测试', () => {
|
||||
// 这个测试确保模块正确加载
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
72
packages/network/tests/setup.ts
Normal file
72
packages/network/tests/setup.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Jest 测试全局设置文件
|
||||
*
|
||||
* 此文件在每个测试文件执行前运行,用于设置全局测试环境
|
||||
*/
|
||||
|
||||
// 设置测试超时时间(毫秒)
|
||||
jest.setTimeout(10000);
|
||||
|
||||
// 模拟控制台方法以减少测试输出噪音
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
// 在测试环境中可以选择性地静默某些日志
|
||||
beforeAll(() => {
|
||||
// 可以在这里设置全局的模拟或配置
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// 清理全局资源
|
||||
});
|
||||
|
||||
// 每个测试前的清理
|
||||
beforeEach(() => {
|
||||
// 清理定时器
|
||||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 恢复所有模拟
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// 导出测试工具函数
|
||||
export const TestUtils = {
|
||||
/**
|
||||
* 创建测试用的延迟
|
||||
* @param ms 延迟毫秒数
|
||||
*/
|
||||
delay: (ms: number): Promise<void> => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
},
|
||||
|
||||
/**
|
||||
* 等待条件满足
|
||||
* @param condition 条件函数
|
||||
* @param timeout 超时时间(毫秒)
|
||||
* @param interval 检查间隔(毫秒)
|
||||
*/
|
||||
waitFor: async (
|
||||
condition: () => boolean,
|
||||
timeout: number = 5000,
|
||||
interval: number = 10
|
||||
): Promise<void> => {
|
||||
const start = Date.now();
|
||||
while (!condition() && Date.now() - start < timeout) {
|
||||
await TestUtils.delay(interval);
|
||||
}
|
||||
if (!condition()) {
|
||||
throw new Error(`等待条件超时 (${timeout}ms)`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 模拟时间前进
|
||||
* @param ms 前进的毫秒数
|
||||
*/
|
||||
advanceTime: (ms: number): void => {
|
||||
jest.advanceTimersByTime(ms);
|
||||
}
|
||||
};
|
||||
44
packages/network/tsconfig.json
Normal file
44
packages/network/tsconfig.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"importHelpers": false,
|
||||
"downlevelIteration": true,
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user