重构network库(mvp版本)搭建基础设施和核心接口
定义ITransport/ISerializer/INetworkMessage接口 NetworkIdentity组件 基础事件定义
This commit is contained in:
@@ -1,101 +0,0 @@
|
||||
# ECS Framework 网络库 - 客户端
|
||||
|
||||
该包提供了完整的网络客户端功能,包括连接管理、预测、插值等现代网络游戏必需的特性。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- ✅ **传输层支持**: WebSocket 和 HTTP 两种传输方式
|
||||
- ✅ **网络客户端**: 完整的连接、认证、房间管理
|
||||
- ✅ **网络行为**: ClientNetworkBehaviour 基类和 NetworkIdentity 组件
|
||||
- ✅ **装饰器系统**: @SyncVar, @ClientRpc, @ServerRpc 装饰器
|
||||
- ✅ **客户端预测**: 减少网络延迟感知的预测系统
|
||||
- ✅ **插值系统**: 平滑的网络对象状态同步
|
||||
- ✅ **TypeScript**: 完整的类型支持
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework-network-client
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
import {
|
||||
NetworkClient,
|
||||
WebSocketClientTransport,
|
||||
ClientNetworkBehaviour,
|
||||
SyncVar,
|
||||
ServerRpc
|
||||
} from '@esengine/ecs-framework-network-client';
|
||||
|
||||
// 创建网络客户端
|
||||
const client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
secure: false
|
||||
}
|
||||
});
|
||||
|
||||
// 连接到服务器
|
||||
await client.connect();
|
||||
|
||||
// 认证
|
||||
const userInfo = await client.authenticate('username', 'password');
|
||||
|
||||
// 获取房间列表
|
||||
const rooms = await client.getRoomList();
|
||||
|
||||
// 加入房间
|
||||
const roomInfo = await client.joinRoom('room-id');
|
||||
```
|
||||
|
||||
## 网络行为示例
|
||||
|
||||
```typescript
|
||||
class PlayerController extends ClientNetworkBehaviour {
|
||||
@SyncVar({ clientCanModify: true })
|
||||
position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
@SyncVar()
|
||||
health: number = 100;
|
||||
|
||||
@ServerRpc({ requireLocalPlayer: true })
|
||||
async move(direction: string): Promise<void> {
|
||||
// 这个方法会被发送到服务器执行
|
||||
}
|
||||
|
||||
@ClientRpc()
|
||||
onDamaged(damage: number): void {
|
||||
// 这个方法会被服务器调用
|
||||
console.log(`Received damage: ${damage}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 预测和插值
|
||||
|
||||
```typescript
|
||||
import { PredictionSystem, InterpolationSystem } from '@esengine/ecs-framework-network-client';
|
||||
|
||||
// 启用预测系统
|
||||
const predictionSystem = new PredictionSystem(scene, 64, 500);
|
||||
scene.addSystem(predictionSystem);
|
||||
|
||||
// 启用插值系统
|
||||
const interpolationSystem = new InterpolationSystem(scene, {
|
||||
delay: 100,
|
||||
enableExtrapolation: false
|
||||
});
|
||||
scene.addSystem(interpolationSystem);
|
||||
```
|
||||
|
||||
## 编译状态
|
||||
|
||||
✅ **编译成功** - 所有 TypeScript 错误已修复,包生成完成
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -2,27 +2,32 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建 network-client 包...');
|
||||
console.log('🚀 使用 Rollup 构建 @esengine/network-client 包...');
|
||||
|
||||
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' });
|
||||
execSync('npx rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ network-client 构建完成!');
|
||||
console.log('✅ @esengine/network-client 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
@@ -53,7 +58,7 @@ function generatePackageJson() {
|
||||
files: [
|
||||
'index.mjs',
|
||||
'index.mjs.map',
|
||||
'index.cjs',
|
||||
'index.cjs',
|
||||
'index.cjs.map',
|
||||
'index.umd.js',
|
||||
'index.umd.js.map',
|
||||
@@ -63,18 +68,19 @@ function generatePackageJson() {
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'networking',
|
||||
'network',
|
||||
'client',
|
||||
'prediction',
|
||||
'interpolation',
|
||||
'game-engine',
|
||||
'multiplayer',
|
||||
'game',
|
||||
'browser',
|
||||
'cocos',
|
||||
'typescript'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
dependencies: sourcePackage.dependencies,
|
||||
peerDependencies: sourcePackage.peerDependencies,
|
||||
publishConfig: sourcePackage.publishConfig,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
@@ -87,7 +93,7 @@ function generatePackageJson() {
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: '../../LICENSE', dest: './dist/LICENSE' }
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom', // 客户端库使用 jsdom 环境
|
||||
testEnvironment: 'jsdom', // 客户端使用jsdom环境
|
||||
roots: ['<rootDir>/tests'],
|
||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
@@ -18,16 +18,10 @@ module.exports = {
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 60,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70
|
||||
},
|
||||
'./src/core/': {
|
||||
branches: 70,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
},
|
||||
verbose: true,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework-network-client",
|
||||
"version": "1.0.17",
|
||||
"description": "ECS Framework 网络库 - 客户端实现",
|
||||
"type": "module",
|
||||
"name": "@esengine/network-client",
|
||||
"version": "1.0.1",
|
||||
"description": "ECS Framework网络层 - 客户端实现",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
@@ -22,15 +21,16 @@
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"networking",
|
||||
"network",
|
||||
"client",
|
||||
"prediction",
|
||||
"interpolation",
|
||||
"game-engine",
|
||||
"multiplayer",
|
||||
"game",
|
||||
"browser",
|
||||
"cocos",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist",
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
@@ -45,32 +45,21 @@
|
||||
"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"
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.1.29",
|
||||
"@esengine/ecs-framework-network-shared": ">=1.0.0"
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"@esengine/network-shared": "file:../network-shared",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "*",
|
||||
"@esengine/ecs-framework-network-shared": "*",
|
||||
"@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",
|
||||
"@types/ws": "^8.5.13",
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -7,17 +7,18 @@ const { readFileSync } = require('fs');
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/ecs-framework-network-client v${pkg.version}
|
||||
* ECS Framework 网络库 - 客户端实现
|
||||
* @esengine/network-client v${pkg.version}
|
||||
* ECS网络层客户端实现
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
// 外部依赖,不打包进bundle
|
||||
const external = [
|
||||
'ws',
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/ecs-framework-network-shared'
|
||||
'@esengine/network-shared',
|
||||
'reflect-metadata'
|
||||
];
|
||||
|
||||
const commonPlugins = [
|
||||
@@ -81,7 +82,7 @@ module.exports = [
|
||||
}
|
||||
},
|
||||
|
||||
// UMD构建
|
||||
// UMD构建 - 用于浏览器直接使用
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
@@ -92,10 +93,9 @@ module.exports = [
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
globals: {
|
||||
'ws': 'WebSocket',
|
||||
'uuid': 'uuid',
|
||||
'@esengine/ecs-framework': 'ECS',
|
||||
'@esengine/ecs-framework-network-shared': 'ECSNetworkShared'
|
||||
'@esengine/network-shared': 'ECSNetworkShared',
|
||||
'reflect-metadata': 'Reflect'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@@ -119,7 +119,7 @@ module.exports = [
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/ecs-framework-network-client v${pkg.version}
|
||||
* @esengine/network-client v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/**
|
||||
* 客户端网络行为基类
|
||||
*
|
||||
* 类似Unity Mirror的NetworkBehaviour,提供网络功能
|
||||
*/
|
||||
|
||||
import { Component, Entity } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { NetworkClient } from './NetworkClient';
|
||||
import { NetworkIdentity } from './NetworkIdentity';
|
||||
|
||||
/**
|
||||
* 客户端网络行为基类
|
||||
*/
|
||||
export abstract class ClientNetworkBehaviour extends Component {
|
||||
/** 网络标识组件 */
|
||||
protected networkIdentity: NetworkIdentity | null = null;
|
||||
/** 网络客户端实例 */
|
||||
protected networkClient: NetworkClient | null = null;
|
||||
|
||||
/**
|
||||
* 组件初始化
|
||||
*/
|
||||
initialize(): void {
|
||||
|
||||
// 获取网络标识组件
|
||||
this.networkIdentity = this.entity.getComponent(NetworkIdentity);
|
||||
if (!this.networkIdentity) {
|
||||
throw new Error('NetworkBehaviour requires NetworkIdentity component');
|
||||
}
|
||||
|
||||
// 从全局获取网络客户端实例
|
||||
this.networkClient = this.getNetworkClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络客户端实例
|
||||
*/
|
||||
protected getNetworkClient(): NetworkClient | null {
|
||||
// 这里需要实现从全局管理器获取客户端实例的逻辑
|
||||
// 暂时返回null,在实际使用时需要通过单例模式或依赖注入获取
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为本地玩家
|
||||
*/
|
||||
get isLocalPlayer(): boolean {
|
||||
return this.networkIdentity?.isLocalPlayer ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为服务器权威
|
||||
*/
|
||||
get hasAuthority(): boolean {
|
||||
return this.networkIdentity?.hasAuthority ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络ID
|
||||
*/
|
||||
get networkId(): string {
|
||||
return this.networkIdentity?.networkId ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this.networkClient?.isInRoom() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送RPC到服务器
|
||||
*/
|
||||
protected async sendServerRpc(methodName: string, ...args: NetworkValue[]): Promise<NetworkValue> {
|
||||
if (!this.networkClient || !this.networkIdentity) {
|
||||
throw new Error('Network client or identity not available');
|
||||
}
|
||||
|
||||
return this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送不可靠RPC到服务器
|
||||
*/
|
||||
protected async sendServerRpcUnreliable(methodName: string, ...args: NetworkValue[]): Promise<void> {
|
||||
if (!this.networkClient || !this.networkIdentity) {
|
||||
throw new Error('Network client or identity not available');
|
||||
}
|
||||
|
||||
await this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SyncVar
|
||||
*/
|
||||
protected async updateSyncVar(fieldName: string, value: NetworkValue): Promise<void> {
|
||||
if (!this.networkClient || !this.networkIdentity) {
|
||||
throw new Error('Network client or identity not available');
|
||||
}
|
||||
|
||||
await this.networkClient.updateSyncVar(this.networkIdentity.networkId, fieldName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当收到RPC调用时
|
||||
*/
|
||||
onRpcReceived(methodName: string, args: NetworkValue[]): void {
|
||||
// 尝试调用对应的方法
|
||||
const method = (this as any)[methodName];
|
||||
if (typeof method === 'function') {
|
||||
try {
|
||||
method.apply(this, args);
|
||||
} catch (error) {
|
||||
console.error(`Error calling RPC method ${methodName}:`, error);
|
||||
}
|
||||
} else {
|
||||
console.warn(`RPC method ${methodName} not found on ${this.constructor.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当SyncVar更新时
|
||||
*/
|
||||
onSyncVarChanged(fieldName: string, oldValue: NetworkValue, newValue: NetworkValue): void {
|
||||
// 子类可以重写此方法来处理SyncVar变化
|
||||
}
|
||||
|
||||
/**
|
||||
* 当获得权威时
|
||||
*/
|
||||
onStartAuthority(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 当失去权威时
|
||||
*/
|
||||
onStopAuthority(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 当成为本地玩家时
|
||||
*/
|
||||
onStartLocalPlayer(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 当不再是本地玩家时
|
||||
*/
|
||||
onStopLocalPlayer(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络启动时调用
|
||||
*/
|
||||
onNetworkStart(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络停止时调用
|
||||
*/
|
||||
onNetworkStop(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.networkIdentity = null;
|
||||
this.networkClient = null;
|
||||
}
|
||||
}
|
||||
@@ -1,638 +0,0 @@
|
||||
/**
|
||||
* 网络客户端主类
|
||||
*
|
||||
* 管理连接、认证、房间加入等功能
|
||||
*/
|
||||
|
||||
import { Scene, EntityManager, Emitter, ITimer, Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
NetworkIdentity as SharedNetworkIdentity,
|
||||
NetworkValue,
|
||||
RpcMessage,
|
||||
SyncVarMessage
|
||||
} from '@esengine/ecs-framework-network-shared';
|
||||
import {
|
||||
ClientTransport,
|
||||
WebSocketClientTransport,
|
||||
HttpClientTransport,
|
||||
ConnectionState,
|
||||
ClientMessage,
|
||||
ClientTransportConfig,
|
||||
WebSocketClientConfig,
|
||||
HttpClientConfig
|
||||
} from '../transport';
|
||||
|
||||
/**
|
||||
* 网络客户端配置
|
||||
*/
|
||||
export interface NetworkClientConfig {
|
||||
/** 传输类型 */
|
||||
transport: 'websocket' | 'http';
|
||||
/** 传输配置 */
|
||||
transportConfig: WebSocketClientConfig | HttpClientConfig;
|
||||
/** 是否启用预测 */
|
||||
enablePrediction?: boolean;
|
||||
/** 预测缓冲区大小 */
|
||||
predictionBuffer?: number;
|
||||
/** 是否启用插值 */
|
||||
enableInterpolation?: boolean;
|
||||
/** 插值延迟(毫秒) */
|
||||
interpolationDelay?: number;
|
||||
/** 网络对象同步间隔(毫秒) */
|
||||
syncInterval?: number;
|
||||
/** 是否启用调试 */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 用户数据 */
|
||||
data?: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间信息
|
||||
*/
|
||||
export interface RoomInfo {
|
||||
/** 房间ID */
|
||||
roomId: string;
|
||||
/** 房间名称 */
|
||||
name: string;
|
||||
/** 当前人数 */
|
||||
playerCount: number;
|
||||
/** 最大人数 */
|
||||
maxPlayers: number;
|
||||
/** 房间元数据 */
|
||||
metadata?: NetworkValue;
|
||||
/** 是否私有房间 */
|
||||
isPrivate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证消息
|
||||
*/
|
||||
export interface AuthMessage {
|
||||
action: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
userData?: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间消息
|
||||
*/
|
||||
export interface RoomMessage {
|
||||
action: string;
|
||||
roomId?: string;
|
||||
name?: string;
|
||||
maxPlayers?: number;
|
||||
metadata?: NetworkValue;
|
||||
isPrivate?: boolean;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络客户端事件
|
||||
*/
|
||||
export interface NetworkClientEvents {
|
||||
/** 连接建立 */
|
||||
'connected': () => void;
|
||||
/** 连接断开 */
|
||||
'disconnected': (reason: string) => void;
|
||||
/** 认证成功 */
|
||||
'authenticated': (userInfo: UserInfo) => void;
|
||||
/** 加入房间成功 */
|
||||
'joined-room': (roomInfo: RoomInfo) => void;
|
||||
/** 离开房间 */
|
||||
'left-room': (roomId: string) => void;
|
||||
/** 房间列表更新 */
|
||||
'room-list-updated': (rooms: RoomInfo[]) => void;
|
||||
/** 玩家加入房间 */
|
||||
'player-joined': (userId: string, userInfo: UserInfo) => void;
|
||||
/** 玩家离开房间 */
|
||||
'player-left': (userId: string) => void;
|
||||
/** 网络对象创建 */
|
||||
'network-object-created': (networkId: string, data: NetworkValue) => void;
|
||||
/** 网络对象销毁 */
|
||||
'network-object-destroyed': (networkId: string) => void;
|
||||
/** SyncVar 更新 */
|
||||
'syncvar-updated': (networkId: string, fieldName: string, value: NetworkValue) => void;
|
||||
/** RPC 调用 */
|
||||
'rpc-received': (networkId: string, methodName: string, args: NetworkValue[]) => void;
|
||||
/** 错误发生 */
|
||||
'error': (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络客户端主类
|
||||
*/
|
||||
export class NetworkClient {
|
||||
private transport: ClientTransport;
|
||||
private config: NetworkClientConfig;
|
||||
private currentUser: UserInfo | null = null;
|
||||
private currentRoom: RoomInfo | null = null;
|
||||
private availableRooms: Map<string, RoomInfo> = new Map();
|
||||
private networkObjects: Map<string, SharedNetworkIdentity> = new Map();
|
||||
private pendingRpcs: Map<string, { resolve: Function; reject: Function; timeout: ITimer<any> }> = new Map();
|
||||
private scene: Scene | null = null;
|
||||
private eventEmitter: Emitter<keyof NetworkClientEvents, any>;
|
||||
|
||||
constructor(config: NetworkClientConfig) {
|
||||
this.eventEmitter = new Emitter();
|
||||
|
||||
this.config = {
|
||||
enablePrediction: true,
|
||||
predictionBuffer: 64,
|
||||
enableInterpolation: true,
|
||||
interpolationDelay: 100,
|
||||
syncInterval: 50,
|
||||
debug: false,
|
||||
...config
|
||||
};
|
||||
|
||||
this.transport = this.createTransport();
|
||||
this.setupTransportEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建传输层
|
||||
*/
|
||||
private createTransport(): ClientTransport {
|
||||
switch (this.config.transport) {
|
||||
case 'websocket':
|
||||
return new WebSocketClientTransport(this.config.transportConfig as WebSocketClientConfig);
|
||||
case 'http':
|
||||
return new HttpClientTransport(this.config.transportConfig as HttpClientConfig);
|
||||
default:
|
||||
throw new Error(`Unsupported transport type: ${this.config.transport}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置传输层事件监听
|
||||
*/
|
||||
private setupTransportEvents(): void {
|
||||
this.transport.on('connected', () => {
|
||||
this.eventEmitter.emit('connected');
|
||||
});
|
||||
|
||||
this.transport.on('disconnected', (reason) => {
|
||||
this.handleDisconnected(reason);
|
||||
});
|
||||
|
||||
this.transport.on('message', (message) => {
|
||||
this.handleMessage(message);
|
||||
});
|
||||
|
||||
this.transport.on('error', (error) => {
|
||||
this.eventEmitter.emit('error', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
return this.transport.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.transport.disconnect();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户认证
|
||||
*/
|
||||
async authenticate(username: string, password?: string, userData?: NetworkValue): Promise<UserInfo> {
|
||||
if (!this.transport.isConnected()) {
|
||||
throw new Error('Not connected to server');
|
||||
}
|
||||
|
||||
const authMessage: AuthMessage = {
|
||||
action: 'login',
|
||||
username,
|
||||
password,
|
||||
userData
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', authMessage as any);
|
||||
|
||||
if (response.success && response.userInfo) {
|
||||
this.currentUser = response.userInfo as UserInfo;
|
||||
this.eventEmitter.emit('authenticated', this.currentUser);
|
||||
return this.currentUser;
|
||||
} else {
|
||||
throw new Error(response.error || 'Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间列表
|
||||
*/
|
||||
async getRoomList(): Promise<RoomInfo[]> {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'list-rooms'
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
|
||||
if (response.success && response.rooms) {
|
||||
this.availableRooms.clear();
|
||||
response.rooms.forEach((room: RoomInfo) => {
|
||||
this.availableRooms.set(room.roomId, room);
|
||||
});
|
||||
|
||||
this.eventEmitter.emit('room-list-updated', response.rooms);
|
||||
return response.rooms;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to get room list');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*/
|
||||
async createRoom(name: string, maxPlayers: number = 8, metadata?: NetworkValue, isPrivate = false): Promise<RoomInfo> {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'create-room',
|
||||
name,
|
||||
maxPlayers,
|
||||
metadata,
|
||||
isPrivate
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
|
||||
if (response.success && response.room) {
|
||||
this.currentRoom = response.room as RoomInfo;
|
||||
this.eventEmitter.emit('joined-room', this.currentRoom);
|
||||
return this.currentRoom;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to create room');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间
|
||||
*/
|
||||
async joinRoom(roomId: string, password?: string): Promise<RoomInfo> {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'join-room',
|
||||
roomId,
|
||||
password
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
|
||||
if (response.success && response.room) {
|
||||
this.currentRoom = response.room as RoomInfo;
|
||||
this.eventEmitter.emit('joined-room', this.currentRoom);
|
||||
return this.currentRoom;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to join room');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间
|
||||
*/
|
||||
async leaveRoom(): Promise<void> {
|
||||
if (!this.currentRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'leave-room',
|
||||
roomId: this.currentRoom.roomId
|
||||
};
|
||||
|
||||
try {
|
||||
await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
} finally {
|
||||
const roomId = this.currentRoom.roomId;
|
||||
this.currentRoom = null;
|
||||
this.networkObjects.clear();
|
||||
this.eventEmitter.emit('left-room', roomId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送RPC调用
|
||||
*/
|
||||
async sendRpc(networkId: string, methodName: string, args: NetworkValue[] = [], reliable = true): Promise<NetworkValue> {
|
||||
if (!this.isInRoom()) {
|
||||
throw new Error('Not in a room');
|
||||
}
|
||||
|
||||
const rpcMessage: any = {
|
||||
networkId,
|
||||
methodName,
|
||||
args,
|
||||
isServer: false,
|
||||
messageId: this.generateMessageId()
|
||||
};
|
||||
|
||||
if (reliable) {
|
||||
return this.sendRequestWithResponse('rpc', rpcMessage);
|
||||
} else {
|
||||
await this.transport.sendMessage({
|
||||
type: 'rpc',
|
||||
data: rpcMessage as NetworkValue,
|
||||
reliable: false
|
||||
});
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SyncVar
|
||||
*/
|
||||
async updateSyncVar(networkId: string, fieldName: string, value: NetworkValue): Promise<void> {
|
||||
if (!this.isInRoom()) {
|
||||
throw new Error('Not in a room');
|
||||
}
|
||||
|
||||
const syncMessage: any = {
|
||||
networkId,
|
||||
propertyName: fieldName,
|
||||
value,
|
||||
isServer: false
|
||||
};
|
||||
|
||||
await this.transport.sendMessage({
|
||||
type: 'syncvar',
|
||||
data: syncMessage as NetworkValue,
|
||||
reliable: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置ECS场景
|
||||
*/
|
||||
setScene(scene: Scene): void {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
getCurrentUser(): UserInfo | null {
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前房间信息
|
||||
*/
|
||||
getCurrentRoom(): RoomInfo | null {
|
||||
return this.currentRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getConnectionState(): ConnectionState {
|
||||
return this.transport.getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.currentUser !== null && this.transport.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在房间中
|
||||
*/
|
||||
isInRoom(): boolean {
|
||||
return this.isAuthenticated() && this.currentRoom !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象
|
||||
*/
|
||||
getNetworkObject(networkId: string): SharedNetworkIdentity | null {
|
||||
return this.networkObjects.get(networkId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有网络对象
|
||||
*/
|
||||
getAllNetworkObjects(): SharedNetworkIdentity[] {
|
||||
return Array.from(this.networkObjects.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断开连接
|
||||
*/
|
||||
private handleDisconnected(reason: string): void {
|
||||
this.cleanup();
|
||||
this.eventEmitter.emit('disconnected', reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
private handleMessage(message: ClientMessage): void {
|
||||
try {
|
||||
switch (message.type) {
|
||||
case 'system':
|
||||
this.handleSystemMessage(message);
|
||||
break;
|
||||
case 'rpc':
|
||||
this.handleRpcMessage(message);
|
||||
break;
|
||||
case 'syncvar':
|
||||
this.handleSyncVarMessage(message);
|
||||
break;
|
||||
case 'custom':
|
||||
this.handleCustomMessage(message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling message:', error);
|
||||
this.eventEmitter.emit('error', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统消息
|
||||
*/
|
||||
private handleSystemMessage(message: ClientMessage): void {
|
||||
const data = message.data as any;
|
||||
|
||||
// 处理响应消息
|
||||
if (message.messageId && this.pendingRpcs.has(message.messageId)) {
|
||||
const pending = this.pendingRpcs.get(message.messageId)!;
|
||||
pending.timeout.stop();
|
||||
this.pendingRpcs.delete(message.messageId);
|
||||
|
||||
if (data.success) {
|
||||
pending.resolve(data);
|
||||
} else {
|
||||
pending.reject(new Error(data.error || 'Request failed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理广播消息
|
||||
switch (data.action) {
|
||||
case 'player-joined':
|
||||
this.eventEmitter.emit('player-joined', data.userId, data.userInfo);
|
||||
break;
|
||||
case 'player-left':
|
||||
this.eventEmitter.emit('player-left', data.userId);
|
||||
break;
|
||||
case 'network-object-created':
|
||||
this.handleNetworkObjectCreated(data);
|
||||
break;
|
||||
case 'network-object-destroyed':
|
||||
this.handleNetworkObjectDestroyed(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理RPC消息
|
||||
*/
|
||||
private handleRpcMessage(message: ClientMessage): void {
|
||||
const rpcData = message.data as any;
|
||||
this.eventEmitter.emit('rpc-received', rpcData.networkId, rpcData.methodName, rpcData.args || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SyncVar消息
|
||||
*/
|
||||
private handleSyncVarMessage(message: ClientMessage): void {
|
||||
const syncData = message.data as any;
|
||||
this.eventEmitter.emit('syncvar-updated', syncData.networkId, syncData.propertyName, syncData.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自定义消息
|
||||
*/
|
||||
private handleCustomMessage(message: ClientMessage): void {
|
||||
// 可扩展的自定义消息处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理网络对象创建
|
||||
*/
|
||||
private handleNetworkObjectCreated(data: any): void {
|
||||
const networkObject = new SharedNetworkIdentity();
|
||||
this.networkObjects.set(data.networkId, networkObject);
|
||||
this.eventEmitter.emit('network-object-created', data.networkId, data.data || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理网络对象销毁
|
||||
*/
|
||||
private handleNetworkObjectDestroyed(data: any): void {
|
||||
this.networkObjects.delete(data.networkId);
|
||||
this.eventEmitter.emit('network-object-destroyed', data.networkId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求并等待响应
|
||||
*/
|
||||
private sendRequestWithResponse(type: ClientMessage['type'], data: NetworkValue, timeout = 30000): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = this.generateMessageId();
|
||||
|
||||
const timeoutTimer = Core.schedule(timeout / 1000, false, this, () => {
|
||||
this.pendingRpcs.delete(messageId);
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
|
||||
this.pendingRpcs.set(messageId, {
|
||||
resolve,
|
||||
reject,
|
||||
timeout: timeoutTimer
|
||||
});
|
||||
|
||||
this.transport.sendMessage({
|
||||
type,
|
||||
data,
|
||||
messageId,
|
||||
reliable: true
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成消息ID
|
||||
*/
|
||||
private generateMessageId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private cleanup(): void {
|
||||
this.currentUser = null;
|
||||
this.currentRoom = null;
|
||||
this.availableRooms.clear();
|
||||
this.networkObjects.clear();
|
||||
|
||||
// 取消所有待处理的RPC
|
||||
this.pendingRpcs.forEach(pending => {
|
||||
pending.timeout.stop();
|
||||
pending.reject(new Error('Connection closed'));
|
||||
});
|
||||
this.pendingRpcs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁客户端
|
||||
*/
|
||||
destroy(): void {
|
||||
this.disconnect();
|
||||
this.transport.destroy();
|
||||
// 清理事件监听器,由于Emitter没有clear方法,我们重新创建一个
|
||||
this.eventEmitter = new Emitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
on<K extends keyof NetworkClientEvents>(event: K, listener: NetworkClientEvents[K]): void {
|
||||
this.eventEmitter.addObserver(event, listener, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听
|
||||
*/
|
||||
off<K extends keyof NetworkClientEvents>(event: K, listener: NetworkClientEvents[K]): void {
|
||||
this.eventEmitter.removeObserver(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
emit<K extends keyof NetworkClientEvents>(event: K, ...args: Parameters<NetworkClientEvents[K]>): void {
|
||||
this.eventEmitter.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
/**
|
||||
* 客户端网络标识组件
|
||||
*
|
||||
* 标识网络对象并管理其状态
|
||||
*/
|
||||
|
||||
import { Component, Entity } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientNetworkBehaviour } from './ClientNetworkBehaviour';
|
||||
|
||||
/**
|
||||
* 网络权威类型
|
||||
*/
|
||||
export enum NetworkAuthority {
|
||||
/** 服务器权威 */
|
||||
SERVER = 'server',
|
||||
/** 客户端权威 */
|
||||
CLIENT = 'client',
|
||||
/** 所有者权威 */
|
||||
OWNER = 'owner'
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar信息
|
||||
*/
|
||||
export interface SyncVarInfo {
|
||||
/** 字段名 */
|
||||
fieldName: string;
|
||||
/** 当前值 */
|
||||
currentValue: NetworkValue;
|
||||
/** 上一个值 */
|
||||
previousValue: NetworkValue;
|
||||
/** 最后更新时间 */
|
||||
lastUpdateTime: number;
|
||||
/** 是否已变更 */
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络标识组件
|
||||
*/
|
||||
export class NetworkIdentity extends Component {
|
||||
/** 网络ID */
|
||||
private _networkId: string = '';
|
||||
/** 所有者用户ID */
|
||||
private _ownerId: string = '';
|
||||
/** 是否为本地玩家 */
|
||||
private _isLocalPlayer: boolean = false;
|
||||
/** 权威类型 */
|
||||
private _authority: NetworkAuthority = NetworkAuthority.SERVER;
|
||||
/** 是否有权威 */
|
||||
private _hasAuthority: boolean = false;
|
||||
/** 网络行为组件列表 */
|
||||
private networkBehaviours: ClientNetworkBehaviour[] = [];
|
||||
/** SyncVar信息映射 */
|
||||
private syncVars: Map<string, SyncVarInfo> = new Map();
|
||||
/** 预测状态 */
|
||||
private predictionEnabled: boolean = false;
|
||||
/** 插值状态 */
|
||||
private interpolationEnabled: boolean = true;
|
||||
|
||||
/**
|
||||
* 网络ID
|
||||
*/
|
||||
get networkId(): string {
|
||||
return this._networkId;
|
||||
}
|
||||
|
||||
set networkId(value: string) {
|
||||
this._networkId = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有者用户ID
|
||||
*/
|
||||
get ownerId(): string {
|
||||
return this._ownerId;
|
||||
}
|
||||
|
||||
set ownerId(value: string) {
|
||||
this._ownerId = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为本地玩家
|
||||
*/
|
||||
get isLocalPlayer(): boolean {
|
||||
return this._isLocalPlayer;
|
||||
}
|
||||
|
||||
set isLocalPlayer(value: boolean) {
|
||||
if (this._isLocalPlayer !== value) {
|
||||
this._isLocalPlayer = value;
|
||||
this.notifyLocalPlayerChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权威类型
|
||||
*/
|
||||
get authority(): NetworkAuthority {
|
||||
return this._authority;
|
||||
}
|
||||
|
||||
set authority(value: NetworkAuthority) {
|
||||
if (this._authority !== value) {
|
||||
this._authority = value;
|
||||
this.updateAuthorityStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有权威
|
||||
*/
|
||||
get hasAuthority(): boolean {
|
||||
return this._hasAuthority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用预测
|
||||
*/
|
||||
get isPredictionEnabled(): boolean {
|
||||
return this.predictionEnabled;
|
||||
}
|
||||
|
||||
set isPredictionEnabled(value: boolean) {
|
||||
this.predictionEnabled = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用插值
|
||||
*/
|
||||
get isInterpolationEnabled(): boolean {
|
||||
return this.interpolationEnabled;
|
||||
}
|
||||
|
||||
set isInterpolationEnabled(value: boolean) {
|
||||
this.interpolationEnabled = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件初始化
|
||||
*/
|
||||
initialize(): void {
|
||||
this.collectNetworkBehaviours();
|
||||
this.notifyNetworkStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集网络行为组件
|
||||
*/
|
||||
private collectNetworkBehaviours(): void {
|
||||
// 暂时留空,等待实际集成时实现
|
||||
this.networkBehaviours = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新权威状态
|
||||
*/
|
||||
private updateAuthorityStatus(): void {
|
||||
const oldHasAuthority = this._hasAuthority;
|
||||
|
||||
// 根据权威类型计算是否有权威
|
||||
switch (this._authority) {
|
||||
case NetworkAuthority.SERVER:
|
||||
this._hasAuthority = false; // 客户端永远没有服务器权威
|
||||
break;
|
||||
case NetworkAuthority.CLIENT:
|
||||
this._hasAuthority = true; // 客户端权威
|
||||
break;
|
||||
case NetworkAuthority.OWNER:
|
||||
this._hasAuthority = this._isLocalPlayer; // 本地玩家才有权威
|
||||
break;
|
||||
}
|
||||
|
||||
// 通知权威变化
|
||||
if (oldHasAuthority !== this._hasAuthority) {
|
||||
this.notifyAuthorityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知权威变化
|
||||
*/
|
||||
private notifyAuthorityChanged(): void {
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
if (this._hasAuthority) {
|
||||
behaviour.onStartAuthority();
|
||||
} else {
|
||||
behaviour.onStopAuthority();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知本地玩家状态变化
|
||||
*/
|
||||
private notifyLocalPlayerChanged(): void {
|
||||
this.updateAuthorityStatus(); // 本地玩家状态影响权威
|
||||
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
if (this._isLocalPlayer) {
|
||||
behaviour.onStartLocalPlayer();
|
||||
} else {
|
||||
behaviour.onStopLocalPlayer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知网络启动
|
||||
*/
|
||||
private notifyNetworkStart(): void {
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onNetworkStart();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知网络停止
|
||||
*/
|
||||
private notifyNetworkStop(): void {
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onNetworkStop();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理RPC调用
|
||||
*/
|
||||
handleRpcCall(methodName: string, args: NetworkValue[]): void {
|
||||
// 将RPC调用分发给所有网络行为组件
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onRpcReceived(methodName, args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册SyncVar
|
||||
*/
|
||||
registerSyncVar(fieldName: string, initialValue: NetworkValue): void {
|
||||
this.syncVars.set(fieldName, {
|
||||
fieldName,
|
||||
currentValue: initialValue,
|
||||
previousValue: initialValue,
|
||||
lastUpdateTime: Date.now(),
|
||||
isDirty: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SyncVar
|
||||
*/
|
||||
updateSyncVar(fieldName: string, newValue: NetworkValue): void {
|
||||
const syncVar = this.syncVars.get(fieldName);
|
||||
if (!syncVar) {
|
||||
console.warn(`SyncVar ${fieldName} not registered on ${this._networkId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldValue = syncVar.currentValue;
|
||||
syncVar.previousValue = oldValue;
|
||||
syncVar.currentValue = newValue;
|
||||
syncVar.lastUpdateTime = Date.now();
|
||||
syncVar.isDirty = true;
|
||||
|
||||
// 通知所有网络行为组件
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onSyncVarChanged(fieldName, oldValue, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SyncVar值
|
||||
*/
|
||||
getSyncVar(fieldName: string): NetworkValue | undefined {
|
||||
return this.syncVars.get(fieldName)?.currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有SyncVar
|
||||
*/
|
||||
getAllSyncVars(): Map<string, SyncVarInfo> {
|
||||
return new Map(this.syncVars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取脏SyncVar
|
||||
*/
|
||||
getDirtySyncVars(): SyncVarInfo[] {
|
||||
return Array.from(this.syncVars.values()).filter(syncVar => syncVar.isDirty);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除脏标记
|
||||
*/
|
||||
clearDirtyFlags(): void {
|
||||
this.syncVars.forEach(syncVar => {
|
||||
syncVar.isDirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化网络状态
|
||||
*/
|
||||
serializeState(): NetworkValue {
|
||||
const state: any = {
|
||||
networkId: this._networkId,
|
||||
ownerId: this._ownerId,
|
||||
isLocalPlayer: this._isLocalPlayer,
|
||||
authority: this._authority,
|
||||
syncVars: {}
|
||||
};
|
||||
|
||||
// 序列化SyncVar
|
||||
this.syncVars.forEach((syncVar, fieldName) => {
|
||||
state.syncVars[fieldName] = syncVar.currentValue;
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化网络状态
|
||||
*/
|
||||
deserializeState(state: any): void {
|
||||
if (state.networkId) this._networkId = state.networkId;
|
||||
if (state.ownerId) this._ownerId = state.ownerId;
|
||||
if (typeof state.isLocalPlayer === 'boolean') this.isLocalPlayer = state.isLocalPlayer;
|
||||
if (state.authority) this.authority = state.authority;
|
||||
|
||||
// 反序列化SyncVar
|
||||
if (state.syncVars) {
|
||||
Object.entries(state.syncVars).forEach(([fieldName, value]) => {
|
||||
if (this.syncVars.has(fieldName)) {
|
||||
this.updateSyncVar(fieldName, value as NetworkValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置预测状态
|
||||
*/
|
||||
setPredictionState(enabled: boolean): void {
|
||||
this.predictionEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置插值状态
|
||||
*/
|
||||
setInterpolationState(enabled: boolean): void {
|
||||
this.interpolationEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以发送RPC
|
||||
*/
|
||||
canSendRpc(): boolean {
|
||||
return this._hasAuthority || this._isLocalPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以更新SyncVar
|
||||
*/
|
||||
canUpdateSyncVar(): boolean {
|
||||
return this._hasAuthority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.notifyNetworkStop();
|
||||
this.networkBehaviours = [];
|
||||
this.syncVars.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 核心模块导出
|
||||
*/
|
||||
|
||||
export * from './NetworkClient';
|
||||
export * from './ClientNetworkBehaviour';
|
||||
export * from './NetworkIdentity';
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* ClientRpc装饰器 - 客户端版本
|
||||
*
|
||||
* 用于标记可以从服务器调用的客户端方法
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* ClientRpc配置选项
|
||||
*/
|
||||
export interface ClientRpcOptions {
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 是否仅发送给所有者 */
|
||||
ownerOnly?: boolean;
|
||||
/** 是否包含发送者 */
|
||||
includeSender?: boolean;
|
||||
/** 权限要求 */
|
||||
requireAuthority?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ClientRpc元数据键
|
||||
*/
|
||||
export const CLIENT_RPC_METADATA_KEY = Symbol('client_rpc');
|
||||
|
||||
/**
|
||||
* ClientRpc元数据
|
||||
*/
|
||||
export interface ClientRpcMetadata {
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 配置选项 */
|
||||
options: ClientRpcOptions;
|
||||
/** 原始方法 */
|
||||
originalMethod: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* ClientRpc装饰器
|
||||
*/
|
||||
export function ClientRpc(options: ClientRpcOptions = {}): MethodDecorator {
|
||||
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||
const methodName = propertyKey as string;
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
// 获取已有的ClientRpc元数据
|
||||
const existingMetadata: ClientRpcMetadata[] = Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target.constructor) || [];
|
||||
|
||||
// 添加新的ClientRpc元数据
|
||||
existingMetadata.push({
|
||||
methodName,
|
||||
options: {
|
||||
reliable: true,
|
||||
timeout: 30000,
|
||||
ownerOnly: false,
|
||||
includeSender: false,
|
||||
requireAuthority: false,
|
||||
...options
|
||||
},
|
||||
originalMethod
|
||||
});
|
||||
|
||||
// 设置元数据
|
||||
Reflect.defineMetadata(CLIENT_RPC_METADATA_KEY, existingMetadata, target.constructor);
|
||||
|
||||
// 包装原方法(客户端接收RPC调用时执行)
|
||||
descriptor.value = function (this: any, ...args: NetworkValue[]) {
|
||||
try {
|
||||
// 直接调用原方法,客户端接收RPC调用
|
||||
return originalMethod.apply(this, args);
|
||||
} catch (error) {
|
||||
console.error(`Error executing ClientRpc ${methodName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的所有ClientRpc元数据
|
||||
*/
|
||||
export function getClientRpcMetadata(target: any): ClientRpcMetadata[] {
|
||||
return Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查方法是否为ClientRpc
|
||||
*/
|
||||
export function isClientRpc(target: any, methodName: string): boolean {
|
||||
const metadata = getClientRpcMetadata(target);
|
||||
return metadata.some(meta => meta.methodName === methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定方法的ClientRpc选项
|
||||
*/
|
||||
export function getClientRpcOptions(target: any, methodName: string): ClientRpcOptions | null {
|
||||
const metadata = getClientRpcMetadata(target);
|
||||
const rpc = metadata.find(meta => meta.methodName === methodName);
|
||||
return rpc ? rpc.options : null;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* ServerRpc装饰器 - 客户端版本
|
||||
*
|
||||
* 用于标记可以向服务器发送的RPC方法
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour';
|
||||
|
||||
/**
|
||||
* ServerRpc配置选项
|
||||
*/
|
||||
export interface ServerRpcOptions {
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 是否需要权威 */
|
||||
requireAuthority?: boolean;
|
||||
/** 是否需要是本地玩家 */
|
||||
requireLocalPlayer?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerRpc元数据键
|
||||
*/
|
||||
export const SERVER_RPC_METADATA_KEY = Symbol('server_rpc');
|
||||
|
||||
/**
|
||||
* ServerRpc元数据
|
||||
*/
|
||||
export interface ServerRpcMetadata {
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 配置选项 */
|
||||
options: ServerRpcOptions;
|
||||
/** 原始方法 */
|
||||
originalMethod: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerRpc装饰器
|
||||
*/
|
||||
export function ServerRpc(options: ServerRpcOptions = {}): MethodDecorator {
|
||||
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||
const methodName = propertyKey as string;
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
// 获取已有的ServerRpc元数据
|
||||
const existingMetadata: ServerRpcMetadata[] = Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target.constructor) || [];
|
||||
|
||||
// 添加新的ServerRpc元数据
|
||||
existingMetadata.push({
|
||||
methodName,
|
||||
options: {
|
||||
reliable: true,
|
||||
timeout: 30000,
|
||||
requireAuthority: false,
|
||||
requireLocalPlayer: false,
|
||||
...options
|
||||
},
|
||||
originalMethod
|
||||
});
|
||||
|
||||
// 设置元数据
|
||||
Reflect.defineMetadata(SERVER_RPC_METADATA_KEY, existingMetadata, target.constructor);
|
||||
|
||||
// 替换方法实现为发送RPC调用
|
||||
descriptor.value = async function (this: ClientNetworkBehaviour, ...args: NetworkValue[]) {
|
||||
try {
|
||||
// 获取NetworkIdentity
|
||||
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
|
||||
if (!networkIdentity) {
|
||||
throw new Error('NetworkIdentity component not found');
|
||||
}
|
||||
|
||||
// 检查权限要求
|
||||
if (options.requireAuthority && !(networkIdentity as any).hasAuthority) {
|
||||
throw new Error(`ServerRpc ${methodName} requires authority`);
|
||||
}
|
||||
|
||||
if (options.requireLocalPlayer && !(networkIdentity as any).isLocalPlayer) {
|
||||
throw new Error(`ServerRpc ${methodName} requires local player`);
|
||||
}
|
||||
|
||||
// 发送RPC到服务器
|
||||
if (options.reliable) {
|
||||
const result = await this.sendServerRpc(methodName, ...args);
|
||||
return result;
|
||||
} else {
|
||||
await this.sendServerRpcUnreliable(methodName, ...args);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error sending ServerRpc ${methodName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存原方法到特殊属性,用于本地预测或调试
|
||||
(descriptor.value as any).__originalMethod = originalMethod;
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的所有ServerRpc元数据
|
||||
*/
|
||||
export function getServerRpcMetadata(target: any): ServerRpcMetadata[] {
|
||||
return Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查方法是否为ServerRpc
|
||||
*/
|
||||
export function isServerRpc(target: any, methodName: string): boolean {
|
||||
const metadata = getServerRpcMetadata(target);
|
||||
return metadata.some(meta => meta.methodName === methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定方法的ServerRpc选项
|
||||
*/
|
||||
export function getServerRpcOptions(target: any, methodName: string): ServerRpcOptions | null {
|
||||
const metadata = getServerRpcMetadata(target);
|
||||
const rpc = metadata.find(meta => meta.methodName === methodName);
|
||||
return rpc ? rpc.options : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法的原始实现(未被装饰器修改的版本)
|
||||
*/
|
||||
export function getOriginalMethod(method: Function): Function | null {
|
||||
return (method as any).__originalMethod || null;
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/**
|
||||
* SyncVar装饰器 - 客户端版本
|
||||
*
|
||||
* 用于标记需要同步的变量
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour';
|
||||
|
||||
/**
|
||||
* SyncVar配置选项
|
||||
*/
|
||||
export interface SyncVarOptions {
|
||||
/** 是否可从客户端修改 */
|
||||
clientCanModify?: boolean;
|
||||
/** 同步间隔(毫秒),0表示立即同步 */
|
||||
syncInterval?: number;
|
||||
/** 是否仅同步给所有者 */
|
||||
ownerOnly?: boolean;
|
||||
/** 自定义序列化器 */
|
||||
serializer?: (value: any) => NetworkValue;
|
||||
/** 自定义反序列化器 */
|
||||
deserializer?: (value: NetworkValue) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar元数据键
|
||||
*/
|
||||
export const SYNCVAR_METADATA_KEY = Symbol('syncvar');
|
||||
|
||||
/**
|
||||
* SyncVar元数据
|
||||
*/
|
||||
export interface SyncVarMetadata {
|
||||
/** 属性名 */
|
||||
propertyKey: string;
|
||||
/** 配置选项 */
|
||||
options: SyncVarOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar装饰器
|
||||
*/
|
||||
export function SyncVar(options: SyncVarOptions = {}): PropertyDecorator {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
const key = propertyKey as string;
|
||||
|
||||
// 获取已有的SyncVar元数据
|
||||
const existingMetadata: SyncVarMetadata[] = Reflect.getMetadata(SYNCVAR_METADATA_KEY, target.constructor) || [];
|
||||
|
||||
// 添加新的SyncVar元数据
|
||||
existingMetadata.push({
|
||||
propertyKey: key,
|
||||
options: {
|
||||
clientCanModify: false,
|
||||
syncInterval: 0,
|
||||
ownerOnly: false,
|
||||
...options
|
||||
}
|
||||
});
|
||||
|
||||
// 设置元数据
|
||||
Reflect.defineMetadata(SYNCVAR_METADATA_KEY, existingMetadata, target.constructor);
|
||||
|
||||
// 存储原始属性名(用于内部存储)
|
||||
const privateKey = `_syncvar_${key}`;
|
||||
|
||||
// 创建属性访问器
|
||||
Object.defineProperty(target, key, {
|
||||
get: function (this: ClientNetworkBehaviour) {
|
||||
// 从NetworkIdentity获取SyncVar值
|
||||
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
|
||||
if (networkIdentity) {
|
||||
const syncVarValue = (networkIdentity as any).getSyncVar(key);
|
||||
if (syncVarValue !== undefined) {
|
||||
return options.deserializer ? options.deserializer(syncVarValue) : syncVarValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果网络值不存在,返回本地存储的值
|
||||
return (this as any)[privateKey];
|
||||
},
|
||||
|
||||
set: function (this: ClientNetworkBehaviour, value: any) {
|
||||
const oldValue = (this as any)[privateKey];
|
||||
const newValue = options.serializer ? options.serializer(value) : value;
|
||||
|
||||
// 存储到本地
|
||||
(this as any)[privateKey] = value;
|
||||
|
||||
// 获取NetworkIdentity
|
||||
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
|
||||
if (!networkIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否可以修改
|
||||
if (!options.clientCanModify && !(networkIdentity as any).hasAuthority) {
|
||||
console.warn(`Cannot modify SyncVar ${key} without authority`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册SyncVar(如果尚未注册)
|
||||
(networkIdentity as any).registerSyncVar(key, newValue);
|
||||
|
||||
// 更新NetworkIdentity中的值
|
||||
(networkIdentity as any).updateSyncVar(key, newValue);
|
||||
|
||||
// 如果有权威且值发生变化,发送到服务器
|
||||
if ((networkIdentity as any).hasAuthority && oldValue !== value) {
|
||||
this.updateSyncVar(key, newValue).catch(error => {
|
||||
console.error(`Failed to sync variable ${key}:`, error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的所有SyncVar元数据
|
||||
*/
|
||||
export function getSyncVarMetadata(target: any): SyncVarMetadata[] {
|
||||
return Reflect.getMetadata(SYNCVAR_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性是否为SyncVar
|
||||
*/
|
||||
export function isSyncVar(target: any, propertyKey: string): boolean {
|
||||
const metadata = getSyncVarMetadata(target);
|
||||
return metadata.some(meta => meta.propertyKey === propertyKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定属性的SyncVar选项
|
||||
*/
|
||||
export function getSyncVarOptions(target: any, propertyKey: string): SyncVarOptions | null {
|
||||
const metadata = getSyncVarMetadata(target);
|
||||
const syncVar = metadata.find(meta => meta.propertyKey === propertyKey);
|
||||
return syncVar ? syncVar.options : null;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 装饰器导出
|
||||
*/
|
||||
|
||||
export * from './SyncVar';
|
||||
export * from './ClientRpc';
|
||||
export * from './ServerRpc';
|
||||
@@ -1,23 +1,24 @@
|
||||
/**
|
||||
* ECS Framework 网络库 - 客户端
|
||||
*
|
||||
* 提供网络客户端功能,包括连接管理、预测、插值等
|
||||
* @esengine/network-client
|
||||
* ECS Framework网络层 - 客户端实现
|
||||
*/
|
||||
|
||||
// 核心模块
|
||||
export * from './core';
|
||||
// 核心客户端 (待实现)
|
||||
// export * from './core/NetworkClient';
|
||||
// export * from './core/ServerConnection';
|
||||
|
||||
// 传输层
|
||||
export * from './transport';
|
||||
// 传输层 (待实现)
|
||||
// export * from './transport/WebSocketClient';
|
||||
// export * from './transport/HttpClient';
|
||||
|
||||
// 装饰器
|
||||
export * from './decorators';
|
||||
// 系统层 (待实现)
|
||||
// export * from './systems/ClientSyncSystem';
|
||||
// export * from './systems/ClientRpcSystem';
|
||||
// export * from './systems/InterpolationSystem';
|
||||
|
||||
// 系统
|
||||
export * from './systems';
|
||||
// 平台适配器 (待实现)
|
||||
// export * from './adapters/BrowserAdapter';
|
||||
// export * from './adapters/CocosAdapter';
|
||||
|
||||
// 接口
|
||||
export * from './interfaces';
|
||||
|
||||
// 版本信息
|
||||
export const VERSION = '1.0.11';
|
||||
// 重新导出shared包的类型
|
||||
export * from '@esengine/network-shared';
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* 网络系统相关接口
|
||||
*/
|
||||
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 可预测组件接口
|
||||
*
|
||||
* 实现此接口的组件可以参与客户端预测系统
|
||||
*/
|
||||
export interface IPredictable {
|
||||
/**
|
||||
* 预测更新
|
||||
*
|
||||
* @param inputs 输入数据
|
||||
* @param timestamp 时间戳
|
||||
*/
|
||||
predictUpdate(inputs: NetworkValue, timestamp: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可插值组件接口
|
||||
*
|
||||
* 实现此接口的组件可以参与插值系统
|
||||
*/
|
||||
export interface IInterpolatable {
|
||||
/**
|
||||
* 应用插值状态
|
||||
*
|
||||
* @param state 插值后的状态数据
|
||||
*/
|
||||
applyInterpolatedState(state: NetworkValue): void;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 接口导出
|
||||
*/
|
||||
|
||||
export * from './NetworkInterfaces';
|
||||
@@ -1,520 +0,0 @@
|
||||
/**
|
||||
* 客户端插值系统
|
||||
*
|
||||
* 实现网络对象的平滑插值
|
||||
*/
|
||||
|
||||
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { NetworkIdentity } from '../core/NetworkIdentity';
|
||||
import { IInterpolatable } from '../interfaces/NetworkInterfaces';
|
||||
|
||||
/**
|
||||
* 插值状态快照
|
||||
*/
|
||||
export interface InterpolationSnapshot {
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 网络ID */
|
||||
networkId: string;
|
||||
/** 状态数据 */
|
||||
state: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值目标
|
||||
*/
|
||||
export interface InterpolationTarget {
|
||||
/** 网络ID */
|
||||
networkId: string;
|
||||
/** 起始状态 */
|
||||
fromState: NetworkValue;
|
||||
/** 目标状态 */
|
||||
toState: NetworkValue;
|
||||
/** 起始时间 */
|
||||
fromTime: number;
|
||||
/** 结束时间 */
|
||||
toTime: number;
|
||||
/** 当前插值进度 (0-1) */
|
||||
progress: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值配置
|
||||
*/
|
||||
export interface InterpolationConfig {
|
||||
/** 插值延迟(毫秒) */
|
||||
delay: number;
|
||||
/** 最大插值时间(毫秒) */
|
||||
maxTime: number;
|
||||
/** 插值缓冲区大小 */
|
||||
bufferSize: number;
|
||||
/** 外推是否启用 */
|
||||
enableExtrapolation: boolean;
|
||||
/** 最大外推时间(毫秒) */
|
||||
maxExtrapolationTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值算法类型
|
||||
*/
|
||||
export enum InterpolationType {
|
||||
/** 线性插值 */
|
||||
LINEAR = 'linear',
|
||||
/** 平滑插值 */
|
||||
SMOOTHSTEP = 'smoothstep',
|
||||
/** 三次贝塞尔插值 */
|
||||
CUBIC = 'cubic'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端插值系统
|
||||
*/
|
||||
export class InterpolationSystem extends EntitySystem {
|
||||
/** 插值状态缓冲区 */
|
||||
private stateBuffer: Map<string, InterpolationSnapshot[]> = new Map();
|
||||
/** 当前插值目标 */
|
||||
private interpolationTargets: Map<string, InterpolationTarget> = new Map();
|
||||
/** 插值配置 */
|
||||
private config: InterpolationConfig;
|
||||
/** 当前时间 */
|
||||
private currentTime: number = 0;
|
||||
|
||||
constructor(config?: Partial<InterpolationConfig>) {
|
||||
// 使用Matcher查询具有NetworkIdentity的实体
|
||||
super(Matcher.all(NetworkIdentity));
|
||||
|
||||
this.config = {
|
||||
delay: 100,
|
||||
maxTime: 500,
|
||||
bufferSize: 32,
|
||||
enableExtrapolation: false,
|
||||
maxExtrapolationTime: 50,
|
||||
...config
|
||||
};
|
||||
|
||||
this.currentTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统初始化
|
||||
*/
|
||||
override initialize(): void {
|
||||
super.initialize();
|
||||
this.currentTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统更新
|
||||
*/
|
||||
override update(): void {
|
||||
this.currentTime = Date.now();
|
||||
this.cleanupOldStates();
|
||||
|
||||
// 调用父类update,会自动调用process方法处理匹配的实体
|
||||
super.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理匹配的实体
|
||||
*/
|
||||
protected override process(entities: Entity[]): void {
|
||||
const interpolationTime = this.currentTime - this.config.delay;
|
||||
|
||||
for (const entity of entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
|
||||
if (networkIdentity && networkIdentity.isInterpolationEnabled) {
|
||||
const networkId = networkIdentity.networkId;
|
||||
const target = this.interpolationTargets.get(networkId);
|
||||
|
||||
if (target) {
|
||||
// 计算插值进度
|
||||
const duration = target.toTime - target.fromTime;
|
||||
if (duration > 0) {
|
||||
const elapsed = interpolationTime - target.fromTime;
|
||||
target.progress = Math.max(0, Math.min(1, elapsed / duration));
|
||||
|
||||
// 执行插值
|
||||
const interpolatedState = this.interpolateStates(
|
||||
target.fromState,
|
||||
target.toState,
|
||||
target.progress,
|
||||
InterpolationType.LINEAR
|
||||
);
|
||||
|
||||
// 应用插值状态
|
||||
this.applyInterpolatedState(entity, interpolatedState);
|
||||
|
||||
// 检查是否需要外推
|
||||
if (target.progress >= 1 && this.config.enableExtrapolation) {
|
||||
this.performExtrapolation(entity, target, interpolationTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加网络状态快照
|
||||
*/
|
||||
addStateSnapshot(networkId: string, state: NetworkValue, timestamp: number): void {
|
||||
// 获取或创建缓冲区
|
||||
if (!this.stateBuffer.has(networkId)) {
|
||||
this.stateBuffer.set(networkId, []);
|
||||
}
|
||||
|
||||
const buffer = this.stateBuffer.get(networkId)!;
|
||||
|
||||
const snapshot: InterpolationSnapshot = {
|
||||
timestamp,
|
||||
networkId,
|
||||
state
|
||||
};
|
||||
|
||||
// 插入到正确的位置(按时间戳排序)
|
||||
const insertIndex = this.findInsertIndex(buffer, timestamp);
|
||||
buffer.splice(insertIndex, 0, snapshot);
|
||||
|
||||
// 保持缓冲区大小
|
||||
if (buffer.length > this.config.bufferSize) {
|
||||
buffer.shift();
|
||||
}
|
||||
|
||||
// 更新插值目标
|
||||
this.updateInterpolationTarget(networkId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新插值目标
|
||||
*/
|
||||
private updateInterpolationTarget(networkId: string): void {
|
||||
const buffer = this.stateBuffer.get(networkId);
|
||||
if (!buffer || buffer.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interpolationTime = this.currentTime - this.config.delay;
|
||||
|
||||
// 查找插值区间
|
||||
const { from, to } = this.findInterpolationRange(buffer, interpolationTime);
|
||||
|
||||
if (!from || !to) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新或创建插值目标
|
||||
this.interpolationTargets.set(networkId, {
|
||||
networkId,
|
||||
fromState: from.state,
|
||||
toState: to.state,
|
||||
fromTime: from.timestamp,
|
||||
toTime: to.timestamp,
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找插值区间
|
||||
*/
|
||||
private findInterpolationRange(buffer: InterpolationSnapshot[], time: number): {
|
||||
from: InterpolationSnapshot | null;
|
||||
to: InterpolationSnapshot | null;
|
||||
} {
|
||||
let from: InterpolationSnapshot | null = null;
|
||||
let to: InterpolationSnapshot | null = null;
|
||||
|
||||
for (let i = 0; i < buffer.length - 1; i++) {
|
||||
const current = buffer[i];
|
||||
const next = buffer[i + 1];
|
||||
|
||||
if (time >= current.timestamp && time <= next.timestamp) {
|
||||
from = current;
|
||||
to = next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到区间,使用最近的两个状态
|
||||
if (!from && !to && buffer.length >= 2) {
|
||||
if (time < buffer[0].timestamp) {
|
||||
// 时间过早,使用前两个状态
|
||||
from = buffer[0];
|
||||
to = buffer[1];
|
||||
} else if (time > buffer[buffer.length - 1].timestamp) {
|
||||
// 时间过晚,使用后两个状态
|
||||
from = buffer[buffer.length - 2];
|
||||
to = buffer[buffer.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态插值
|
||||
*/
|
||||
private interpolateStates(
|
||||
fromState: NetworkValue,
|
||||
toState: NetworkValue,
|
||||
progress: number,
|
||||
type: InterpolationType
|
||||
): NetworkValue {
|
||||
// 调整插值进度曲线
|
||||
const adjustedProgress = this.adjustProgress(progress, type);
|
||||
|
||||
try {
|
||||
return this.interpolateValue(fromState, toState, adjustedProgress);
|
||||
} catch (error) {
|
||||
console.error('Error interpolating states:', error);
|
||||
return toState; // 出错时返回目标状态
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归插值值
|
||||
*/
|
||||
private interpolateValue(from: NetworkValue, to: NetworkValue, progress: number): NetworkValue {
|
||||
// 如果类型不同,直接返回目标值
|
||||
if (typeof from !== typeof to) {
|
||||
return to;
|
||||
}
|
||||
|
||||
// 数字插值
|
||||
if (typeof from === 'number' && typeof to === 'number') {
|
||||
return from + (to - from) * progress;
|
||||
}
|
||||
|
||||
// 字符串插值(直接切换)
|
||||
if (typeof from === 'string' && typeof to === 'string') {
|
||||
return progress < 0.5 ? from : to;
|
||||
}
|
||||
|
||||
// 布尔插值(直接切换)
|
||||
if (typeof from === 'boolean' && typeof to === 'boolean') {
|
||||
return progress < 0.5 ? from : to;
|
||||
}
|
||||
|
||||
// 数组插值
|
||||
if (Array.isArray(from) && Array.isArray(to)) {
|
||||
const result: NetworkValue[] = [];
|
||||
const maxLength = Math.max(from.length, to.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const fromValue = i < from.length ? from[i] : to[i];
|
||||
const toValue = i < to.length ? to[i] : from[i];
|
||||
result[i] = this.interpolateValue(fromValue, toValue, progress);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 对象插值
|
||||
if (from && to && typeof from === 'object' && typeof to === 'object') {
|
||||
const result: any = {};
|
||||
const allKeys = new Set([...Object.keys(from), ...Object.keys(to)]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const fromValue = (from as any)[key];
|
||||
const toValue = (to as any)[key];
|
||||
|
||||
if (fromValue !== undefined && toValue !== undefined) {
|
||||
result[key] = this.interpolateValue(fromValue, toValue, progress);
|
||||
} else {
|
||||
result[key] = toValue !== undefined ? toValue : fromValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 其他类型直接返回目标值
|
||||
return to;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整插值进度曲线
|
||||
*/
|
||||
private adjustProgress(progress: number, type: InterpolationType): number {
|
||||
switch (type) {
|
||||
case InterpolationType.LINEAR:
|
||||
return progress;
|
||||
|
||||
case InterpolationType.SMOOTHSTEP:
|
||||
return progress * progress * (3 - 2 * progress);
|
||||
|
||||
case InterpolationType.CUBIC:
|
||||
return progress < 0.5
|
||||
? 4 * progress * progress * progress
|
||||
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
||||
|
||||
default:
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用插值状态到实体
|
||||
*/
|
||||
private applyInterpolatedState(entity: Entity, state: NetworkValue): void {
|
||||
// 获取所有可插值的组件
|
||||
const components: any[] = [];
|
||||
for (const component of components) {
|
||||
if (this.isInterpolatable(component)) {
|
||||
try {
|
||||
(component as IInterpolatable).applyInterpolatedState(state);
|
||||
} catch (error) {
|
||||
console.error('Error applying interpolated state:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新NetworkIdentity中的状态
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (networkIdentity && typeof networkIdentity.deserializeState === 'function') {
|
||||
try {
|
||||
networkIdentity.deserializeState(state);
|
||||
} catch (error) {
|
||||
console.error('Error deserializing interpolated state:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否实现了IInterpolatable接口
|
||||
*/
|
||||
private isInterpolatable(component: any): component is IInterpolatable {
|
||||
return component && typeof component.applyInterpolatedState === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行外推
|
||||
*/
|
||||
private performExtrapolation(entity: Entity, target: InterpolationTarget, currentTime: number): void {
|
||||
if (!this.config.enableExtrapolation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extrapolationTime = currentTime - target.toTime;
|
||||
if (extrapolationTime > this.config.maxExtrapolationTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算外推状态
|
||||
const extrapolationProgress = extrapolationTime / (target.toTime - target.fromTime);
|
||||
const extrapolatedState = this.extrapolateState(
|
||||
target.fromState,
|
||||
target.toState,
|
||||
1 + extrapolationProgress
|
||||
);
|
||||
|
||||
// 应用外推状态
|
||||
this.applyInterpolatedState(entity, extrapolatedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态外推
|
||||
*/
|
||||
private extrapolateState(fromState: NetworkValue, toState: NetworkValue, progress: number): NetworkValue {
|
||||
// 简单的线性外推
|
||||
return this.interpolateValue(fromState, toState, progress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找插入位置
|
||||
*/
|
||||
private findInsertIndex(buffer: InterpolationSnapshot[], timestamp: number): number {
|
||||
let left = 0;
|
||||
let right = buffer.length;
|
||||
|
||||
while (left < right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
if (buffer[mid].timestamp < timestamp) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期状态
|
||||
*/
|
||||
private cleanupOldStates(): void {
|
||||
const cutoffTime = this.currentTime - this.config.maxTime;
|
||||
|
||||
this.stateBuffer.forEach((buffer, networkId) => {
|
||||
// 移除过期的状态
|
||||
const validStates = buffer.filter(snapshot => snapshot.timestamp > cutoffTime);
|
||||
|
||||
if (validStates.length !== buffer.length) {
|
||||
this.stateBuffer.set(networkId, validStates);
|
||||
}
|
||||
|
||||
// 如果缓冲区为空,移除它
|
||||
if (validStates.length === 0) {
|
||||
this.stateBuffer.delete(networkId);
|
||||
this.interpolationTargets.delete(networkId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据网络ID查找实体
|
||||
*/
|
||||
private findEntityByNetworkId(networkId: string): Entity | null {
|
||||
// 使用系统的entities属性来查找
|
||||
for (const entity of this.entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (networkIdentity && networkIdentity.networkId === networkId) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置插值配置
|
||||
*/
|
||||
setInterpolationConfig(config: Partial<InterpolationConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插值统计信息
|
||||
*/
|
||||
getInterpolationStats(): { [networkId: string]: { bufferSize: number; progress: number } } {
|
||||
const stats: { [networkId: string]: { bufferSize: number; progress: number } } = {};
|
||||
|
||||
this.stateBuffer.forEach((buffer, networkId) => {
|
||||
const target = this.interpolationTargets.get(networkId);
|
||||
stats[networkId] = {
|
||||
bufferSize: buffer.length,
|
||||
progress: target ? target.progress : 0
|
||||
};
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有插值数据
|
||||
*/
|
||||
clearInterpolationData(): void {
|
||||
this.stateBuffer.clear();
|
||||
this.interpolationTargets.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.clearInterpolationData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
/**
|
||||
* 客户端预测系统
|
||||
*
|
||||
* 实现客户端预测和服务器和解
|
||||
*/
|
||||
|
||||
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { NetworkIdentity } from '../core/NetworkIdentity';
|
||||
import { IPredictable } from '../interfaces/NetworkInterfaces';
|
||||
|
||||
/**
|
||||
* 预测状态快照
|
||||
*/
|
||||
export interface PredictionSnapshot {
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 网络ID */
|
||||
networkId: string;
|
||||
/** 状态数据 */
|
||||
state: NetworkValue;
|
||||
/** 输入数据 */
|
||||
inputs?: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预测输入
|
||||
*/
|
||||
export interface PredictionInput {
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 输入数据 */
|
||||
data: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端预测系统
|
||||
*/
|
||||
export class PredictionSystem extends EntitySystem {
|
||||
/** 预测状态缓冲区 */
|
||||
private predictionBuffer: Map<string, PredictionSnapshot[]> = new Map();
|
||||
/** 输入缓冲区 */
|
||||
private inputBuffer: PredictionInput[] = [];
|
||||
/** 最大缓冲区大小 */
|
||||
private maxBufferSize: number = 64;
|
||||
/** 预测时间窗口(毫秒) */
|
||||
private predictionWindow: number = 500;
|
||||
/** 当前预测时间戳 */
|
||||
private currentPredictionTime: number = 0;
|
||||
|
||||
constructor(maxBufferSize = 64, predictionWindow = 500) {
|
||||
// 使用Matcher查询具有NetworkIdentity的实体
|
||||
super(Matcher.all(NetworkIdentity));
|
||||
|
||||
this.maxBufferSize = maxBufferSize;
|
||||
this.predictionWindow = predictionWindow;
|
||||
this.currentPredictionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统初始化
|
||||
*/
|
||||
override initialize(): void {
|
||||
super.initialize();
|
||||
this.currentPredictionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统更新
|
||||
*/
|
||||
override update(): void {
|
||||
this.currentPredictionTime = Date.now();
|
||||
this.cleanupOldSnapshots();
|
||||
|
||||
// 调用父类update,会自动调用process方法处理匹配的实体
|
||||
super.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理匹配的实体
|
||||
*/
|
||||
protected override process(entities: Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
|
||||
if (networkIdentity &&
|
||||
networkIdentity.isPredictionEnabled &&
|
||||
networkIdentity.isLocalPlayer) {
|
||||
|
||||
// 保存当前状态快照
|
||||
this.saveSnapshot(entity);
|
||||
|
||||
// 应用当前输入进行预测
|
||||
const currentInputs = this.getCurrentInputs();
|
||||
if (currentInputs) {
|
||||
this.applyInputs(entity, currentInputs, this.currentPredictionTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加预测输入
|
||||
*/
|
||||
addInput(input: PredictionInput): void {
|
||||
this.inputBuffer.push(input);
|
||||
|
||||
// 保持输入缓冲区大小
|
||||
if (this.inputBuffer.length > this.maxBufferSize) {
|
||||
this.inputBuffer.shift();
|
||||
}
|
||||
|
||||
// 按时间戳排序
|
||||
this.inputBuffer.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存预测状态快照
|
||||
*/
|
||||
saveSnapshot(entity: Entity): void {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (!networkIdentity || !networkIdentity.isPredictionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const networkId = networkIdentity.networkId;
|
||||
const snapshot: PredictionSnapshot = {
|
||||
timestamp: this.currentPredictionTime,
|
||||
networkId,
|
||||
state: networkIdentity.serializeState(),
|
||||
inputs: this.getCurrentInputs() || undefined
|
||||
};
|
||||
|
||||
// 获取或创建缓冲区
|
||||
if (!this.predictionBuffer.has(networkId)) {
|
||||
this.predictionBuffer.set(networkId, []);
|
||||
}
|
||||
|
||||
const buffer = this.predictionBuffer.get(networkId)!;
|
||||
buffer.push(snapshot);
|
||||
|
||||
// 保持缓冲区大小
|
||||
if (buffer.length > this.maxBufferSize) {
|
||||
buffer.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器接收权威状态进行和解
|
||||
*/
|
||||
reconcileWithServer(networkId: string, serverState: NetworkValue, serverTimestamp: number): void {
|
||||
const buffer = this.predictionBuffer.get(networkId);
|
||||
if (!buffer || buffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找对应时间戳的预测状态
|
||||
const predictionSnapshot = this.findSnapshot(buffer, serverTimestamp);
|
||||
if (!predictionSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 比较预测状态和服务器状态
|
||||
if (this.statesMatch(predictionSnapshot.state, serverState)) {
|
||||
// 预测正确,移除已确认的快照
|
||||
this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预测错误,需要进行和解
|
||||
this.performReconciliation(networkId, serverState, serverTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行预测和解
|
||||
*/
|
||||
private performReconciliation(networkId: string, serverState: NetworkValue, serverTimestamp: number): void {
|
||||
const entity = this.findEntityByNetworkId(networkId);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (!networkIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 回滚到服务器状态
|
||||
if (typeof networkIdentity.deserializeState === 'function') {
|
||||
networkIdentity.deserializeState(serverState);
|
||||
}
|
||||
|
||||
// 重新应用服务器时间戳之后的输入
|
||||
const buffer = this.predictionBuffer.get(networkId)!;
|
||||
const snapshotsToReplay = buffer.filter(snapshot => snapshot.timestamp > serverTimestamp);
|
||||
|
||||
for (const snapshot of snapshotsToReplay) {
|
||||
if (snapshot.inputs) {
|
||||
this.applyInputs(entity, snapshot.inputs, snapshot.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已和解的快照
|
||||
this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 应用输入进行预测计算
|
||||
*/
|
||||
private applyInputs(entity: Entity, inputs: NetworkValue, timestamp: number): void {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (!networkIdentity) return;
|
||||
|
||||
// 获取实体的所有组件并检查是否实现了IPredictable接口
|
||||
const components: any[] = [];
|
||||
for (const component of components) {
|
||||
if (this.isPredictable(component)) {
|
||||
try {
|
||||
(component as IPredictable).predictUpdate(inputs, timestamp);
|
||||
} catch (error) {
|
||||
console.error('Error applying prediction:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否实现了IPredictable接口
|
||||
*/
|
||||
private isPredictable(component: any): component is IPredictable {
|
||||
return component && typeof component.predictUpdate === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前输入
|
||||
*/
|
||||
private getCurrentInputs(): NetworkValue | null {
|
||||
if (this.inputBuffer.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取最新的输入
|
||||
return this.inputBuffer[this.inputBuffer.length - 1].data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定时间戳的快照
|
||||
*/
|
||||
private findSnapshot(buffer: PredictionSnapshot[], timestamp: number): PredictionSnapshot | null {
|
||||
// 查找最接近的快照
|
||||
let closest: PredictionSnapshot | null = null;
|
||||
let minDiff = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
for (const snapshot of buffer) {
|
||||
const diff = Math.abs(snapshot.timestamp - timestamp);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closest = snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个状态是否匹配
|
||||
*/
|
||||
private statesMatch(predictedState: NetworkValue, serverState: NetworkValue): boolean {
|
||||
try {
|
||||
// 简单的JSON比较,实际应用中可能需要更精确的比较
|
||||
return JSON.stringify(predictedState) === JSON.stringify(serverState);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定时间戳之前的快照
|
||||
*/
|
||||
private removeSnapshotsBeforeTimestamp(buffer: PredictionSnapshot[], timestamp: number): void {
|
||||
for (let i = buffer.length - 1; i >= 0; i--) {
|
||||
if (buffer[i].timestamp < timestamp) {
|
||||
buffer.splice(0, i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的快照
|
||||
*/
|
||||
private cleanupOldSnapshots(): void {
|
||||
const cutoffTime = this.currentPredictionTime - this.predictionWindow;
|
||||
|
||||
this.predictionBuffer.forEach((buffer, networkId) => {
|
||||
this.removeSnapshotsBeforeTimestamp(buffer, cutoffTime);
|
||||
|
||||
// 如果缓冲区为空,移除它
|
||||
if (buffer.length === 0) {
|
||||
this.predictionBuffer.delete(networkId);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理过期的输入
|
||||
this.inputBuffer = this.inputBuffer.filter(input =>
|
||||
input.timestamp > cutoffTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据网络ID查找实体
|
||||
*/
|
||||
private findEntityByNetworkId(networkId: string): Entity | null {
|
||||
// 使用系统的entities属性来查找
|
||||
for (const entity of this.entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (networkIdentity && networkIdentity.networkId === networkId) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置预测配置
|
||||
*/
|
||||
setPredictionConfig(maxBufferSize: number, predictionWindow: number): void {
|
||||
this.maxBufferSize = maxBufferSize;
|
||||
this.predictionWindow = predictionWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预测统计信息
|
||||
*/
|
||||
getPredictionStats(): { [networkId: string]: number } {
|
||||
const stats: { [networkId: string]: number } = {};
|
||||
|
||||
this.predictionBuffer.forEach((buffer, networkId) => {
|
||||
stats[networkId] = buffer.length;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有预测数据
|
||||
*/
|
||||
clearPredictionData(): void {
|
||||
this.predictionBuffer.clear();
|
||||
this.inputBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.clearPredictionData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 系统导出
|
||||
*/
|
||||
|
||||
export * from './PredictionSystem';
|
||||
export * from './InterpolationSystem';
|
||||
@@ -1,445 +0,0 @@
|
||||
/**
|
||||
* 客户端传输层抽象接口
|
||||
*/
|
||||
|
||||
import { Emitter, ITimer, Core } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 客户端传输配置
|
||||
*/
|
||||
export interface ClientTransportConfig {
|
||||
/** 服务器地址 */
|
||||
host: string;
|
||||
/** 服务器端口 */
|
||||
port: number;
|
||||
/** 是否使用安全连接 */
|
||||
secure?: boolean;
|
||||
/** 连接超时时间(毫秒) */
|
||||
connectionTimeout?: number;
|
||||
/** 重连间隔(毫秒) */
|
||||
reconnectInterval?: number;
|
||||
/** 最大重连次数 */
|
||||
maxReconnectAttempts?: number;
|
||||
/** 心跳间隔(毫秒) */
|
||||
heartbeatInterval?: number;
|
||||
/** 消息队列最大大小 */
|
||||
maxQueueSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态
|
||||
*/
|
||||
export enum ConnectionState {
|
||||
/** 断开连接 */
|
||||
DISCONNECTED = 'disconnected',
|
||||
/** 连接中 */
|
||||
CONNECTING = 'connecting',
|
||||
/** 已连接 */
|
||||
CONNECTED = 'connected',
|
||||
/** 认证中 */
|
||||
AUTHENTICATING = 'authenticating',
|
||||
/** 已认证 */
|
||||
AUTHENTICATED = 'authenticated',
|
||||
/** 重连中 */
|
||||
RECONNECTING = 'reconnecting',
|
||||
/** 连接错误 */
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端消息
|
||||
*/
|
||||
export interface ClientMessage {
|
||||
/** 消息类型 */
|
||||
type: 'rpc' | 'syncvar' | 'system' | 'custom';
|
||||
/** 消息数据 */
|
||||
data: NetworkValue;
|
||||
/** 消息ID(用于响应匹配) */
|
||||
messageId?: string;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 时间戳 */
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接统计信息
|
||||
*/
|
||||
export interface ConnectionStats {
|
||||
/** 连接时间 */
|
||||
connectedAt: Date | null;
|
||||
/** 连接持续时间(毫秒) */
|
||||
connectionDuration: number;
|
||||
/** 发送消息数 */
|
||||
messagesSent: number;
|
||||
/** 接收消息数 */
|
||||
messagesReceived: number;
|
||||
/** 发送字节数 */
|
||||
bytesSent: number;
|
||||
/** 接收字节数 */
|
||||
bytesReceived: number;
|
||||
/** 重连次数 */
|
||||
reconnectCount: number;
|
||||
/** 丢失消息数 */
|
||||
messagesLost: number;
|
||||
/** 平均延迟(毫秒) */
|
||||
averageLatency: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端传输事件
|
||||
*/
|
||||
export interface ClientTransportEvents {
|
||||
/** 连接建立 */
|
||||
'connected': () => void;
|
||||
/** 连接断开 */
|
||||
'disconnected': (reason: string) => void;
|
||||
/** 连接状态变化 */
|
||||
'state-changed': (oldState: ConnectionState, newState: ConnectionState) => void;
|
||||
/** 收到消息 */
|
||||
'message': (message: ClientMessage) => void;
|
||||
/** 连接错误 */
|
||||
'error': (error: Error) => void;
|
||||
/** 重连开始 */
|
||||
'reconnecting': (attempt: number, maxAttempts: number) => void;
|
||||
/** 重连成功 */
|
||||
'reconnected': () => void;
|
||||
/** 重连失败 */
|
||||
'reconnect-failed': () => void;
|
||||
/** 延迟更新 */
|
||||
'latency-updated': (latency: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端传输层抽象类
|
||||
*/
|
||||
export abstract class ClientTransport {
|
||||
protected config: ClientTransportConfig;
|
||||
protected state: ConnectionState = ConnectionState.DISCONNECTED;
|
||||
protected stats: ConnectionStats;
|
||||
protected messageQueue: ClientMessage[] = [];
|
||||
protected reconnectAttempts = 0;
|
||||
protected reconnectTimer: ITimer<any> | null = null;
|
||||
protected heartbeatTimer: ITimer<any> | null = null;
|
||||
private latencyMeasurements: number[] = [];
|
||||
private eventEmitter: Emitter<keyof ClientTransportEvents, any>;
|
||||
|
||||
constructor(config: ClientTransportConfig) {
|
||||
this.eventEmitter = new Emitter<keyof ClientTransportEvents, any>();
|
||||
|
||||
this.config = {
|
||||
secure: false,
|
||||
connectionTimeout: 10000, // 10秒
|
||||
reconnectInterval: 3000, // 3秒
|
||||
maxReconnectAttempts: 10,
|
||||
heartbeatInterval: 30000, // 30秒
|
||||
maxQueueSize: 1000,
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
connectedAt: null,
|
||||
connectionDuration: 0,
|
||||
messagesSent: 0,
|
||||
messagesReceived: 0,
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
reconnectCount: 0,
|
||||
messagesLost: 0,
|
||||
averageLatency: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
abstract connect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
abstract disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
abstract sendMessage(message: ClientMessage): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 获取当前连接状态
|
||||
*/
|
||||
getState(): ConnectionState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已连接
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.state === ConnectionState.CONNECTED ||
|
||||
this.state === ConnectionState.AUTHENTICATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接统计信息
|
||||
*/
|
||||
getStats(): ConnectionStats {
|
||||
if (this.stats.connectedAt) {
|
||||
this.stats.connectionDuration = Date.now() - this.stats.connectedAt.getTime();
|
||||
}
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
getConfig(): Readonly<ClientTransportConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态
|
||||
*/
|
||||
protected setState(newState: ConnectionState): void {
|
||||
if (this.state !== newState) {
|
||||
const oldState = this.state;
|
||||
this.state = newState;
|
||||
this.eventEmitter.emit('state-changed', oldState, newState);
|
||||
|
||||
// 特殊状态处理
|
||||
if (newState === ConnectionState.CONNECTED) {
|
||||
this.stats.connectedAt = new Date();
|
||||
this.reconnectAttempts = 0;
|
||||
this.startHeartbeat();
|
||||
this.processMessageQueue();
|
||||
this.eventEmitter.emit('connected');
|
||||
|
||||
if (oldState === ConnectionState.RECONNECTING) {
|
||||
this.eventEmitter.emit('reconnected');
|
||||
}
|
||||
} else if (newState === ConnectionState.DISCONNECTED) {
|
||||
this.stats.connectedAt = null;
|
||||
this.stopHeartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
protected handleMessage(message: ClientMessage): void {
|
||||
this.stats.messagesReceived++;
|
||||
|
||||
if (message.data) {
|
||||
try {
|
||||
const messageSize = JSON.stringify(message.data).length;
|
||||
this.stats.bytesReceived += messageSize;
|
||||
} catch (error) {
|
||||
// 忽略序列化错误
|
||||
}
|
||||
}
|
||||
|
||||
// 处理系统消息
|
||||
if (message.type === 'system') {
|
||||
this.handleSystemMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventEmitter.emit('message', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统消息
|
||||
*/
|
||||
protected handleSystemMessage(message: ClientMessage): void {
|
||||
const data = message.data as any;
|
||||
|
||||
switch (data.action) {
|
||||
case 'ping':
|
||||
// 响应ping
|
||||
this.sendMessage({
|
||||
type: 'system',
|
||||
data: { action: 'pong', timestamp: data.timestamp }
|
||||
});
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
// 计算延迟
|
||||
if (data.timestamp) {
|
||||
const latency = Date.now() - data.timestamp;
|
||||
this.updateLatency(latency);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接错误
|
||||
*/
|
||||
protected handleError(error: Error): void {
|
||||
console.error('Transport error:', error.message);
|
||||
this.eventEmitter.emit('error', error);
|
||||
|
||||
if (this.isConnected()) {
|
||||
this.setState(ConnectionState.ERROR);
|
||||
this.startReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始重连
|
||||
*/
|
||||
protected startReconnect(): void {
|
||||
if (this.reconnectAttempts >= this.config.maxReconnectAttempts!) {
|
||||
this.eventEmitter.emit('reconnect-failed');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.RECONNECTING);
|
||||
this.reconnectAttempts++;
|
||||
this.stats.reconnectCount++;
|
||||
|
||||
this.eventEmitter.emit('reconnecting', this.reconnectAttempts, this.config.maxReconnectAttempts!);
|
||||
|
||||
this.reconnectTimer = Core.schedule(this.config.reconnectInterval! / 1000, false, this, async () => {
|
||||
try {
|
||||
await this.connect();
|
||||
} catch (error) {
|
||||
this.startReconnect(); // 继续重连
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止重连
|
||||
*/
|
||||
protected stopReconnect(): void {
|
||||
if (this.reconnectTimer) {
|
||||
this.reconnectTimer.stop();
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将消息加入队列
|
||||
*/
|
||||
protected queueMessage(message: ClientMessage): boolean {
|
||||
if (this.messageQueue.length >= this.config.maxQueueSize!) {
|
||||
this.stats.messagesLost++;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.messageQueue.push(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息队列
|
||||
*/
|
||||
protected async processMessageQueue(): Promise<void> {
|
||||
while (this.messageQueue.length > 0 && this.isConnected()) {
|
||||
const message = this.messageQueue.shift()!;
|
||||
await this.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
protected startHeartbeat(): void {
|
||||
if (this.config.heartbeatInterval && this.config.heartbeatInterval > 0) {
|
||||
this.heartbeatTimer = Core.schedule(this.config.heartbeatInterval / 1000, true, this, () => {
|
||||
this.sendHeartbeat();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
protected stopHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
this.heartbeatTimer.stop();
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳
|
||||
*/
|
||||
protected sendHeartbeat(): void {
|
||||
this.sendMessage({
|
||||
type: 'system',
|
||||
data: { action: 'ping', timestamp: Date.now() }
|
||||
}).catch(() => {
|
||||
// 心跳发送失败,可能连接有问题
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新延迟统计
|
||||
*/
|
||||
protected updateLatency(latency: number): void {
|
||||
this.latencyMeasurements.push(latency);
|
||||
|
||||
// 只保留最近的10个测量值
|
||||
if (this.latencyMeasurements.length > 10) {
|
||||
this.latencyMeasurements.shift();
|
||||
}
|
||||
|
||||
// 计算平均延迟
|
||||
const sum = this.latencyMeasurements.reduce((a, b) => a + b, 0);
|
||||
this.stats.averageLatency = sum / this.latencyMeasurements.length;
|
||||
|
||||
this.eventEmitter.emit('latency-updated', latency);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新发送统计
|
||||
*/
|
||||
protected updateSendStats(message: ClientMessage): void {
|
||||
this.stats.messagesSent++;
|
||||
|
||||
if (message.data) {
|
||||
try {
|
||||
const messageSize = JSON.stringify(message.data).length;
|
||||
this.stats.bytesSent += messageSize;
|
||||
} catch (error) {
|
||||
// 忽略序列化错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁传输层
|
||||
*/
|
||||
destroy(): void {
|
||||
this.stopReconnect();
|
||||
this.stopHeartbeat();
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
on<K extends keyof ClientTransportEvents>(event: K, listener: ClientTransportEvents[K]): this {
|
||||
this.eventEmitter.addObserver(event, listener, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听
|
||||
*/
|
||||
off<K extends keyof ClientTransportEvents>(event: K, listener: ClientTransportEvents[K]): this {
|
||||
this.eventEmitter.removeObserver(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
emit<K extends keyof ClientTransportEvents>(event: K, ...args: Parameters<ClientTransportEvents[K]>): void {
|
||||
this.eventEmitter.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
/**
|
||||
* HTTP 客户端传输实现
|
||||
*
|
||||
* 支持 REST API 和长轮询
|
||||
*/
|
||||
|
||||
import { Core, ITimer } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ClientTransport,
|
||||
ClientTransportConfig,
|
||||
ConnectionState,
|
||||
ClientMessage
|
||||
} from './ClientTransport';
|
||||
|
||||
/**
|
||||
* HTTP 客户端配置
|
||||
*/
|
||||
export interface HttpClientConfig extends ClientTransportConfig {
|
||||
/** API 路径前缀 */
|
||||
apiPrefix?: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
requestTimeout?: number;
|
||||
/** 长轮询超时时间(毫秒) */
|
||||
longPollTimeout?: number;
|
||||
/** 是否启用长轮询 */
|
||||
enableLongPolling?: boolean;
|
||||
/** 额外的请求头 */
|
||||
headers?: Record<string, string>;
|
||||
/** 认证令牌 */
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 响应接口
|
||||
*/
|
||||
interface HttpResponse {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
messages?: ClientMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 客户端传输
|
||||
*/
|
||||
export class HttpClientTransport extends ClientTransport {
|
||||
private connectionId: string | null = null;
|
||||
private longPollController: AbortController | null = null;
|
||||
private longPollRunning = false;
|
||||
private connectPromise: Promise<void> | null = null;
|
||||
private requestTimers: Set<ITimer<any>> = new Set();
|
||||
|
||||
protected override config: HttpClientConfig;
|
||||
|
||||
constructor(config: HttpClientConfig) {
|
||||
super(config);
|
||||
|
||||
this.config = {
|
||||
apiPrefix: '/api',
|
||||
requestTimeout: 30000, // 30秒
|
||||
longPollTimeout: 25000, // 25秒
|
||||
enableLongPolling: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.CONNECTED) {
|
||||
return this.connectPromise || Promise.resolve();
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.CONNECTING);
|
||||
this.stopReconnect();
|
||||
|
||||
this.connectPromise = this.performConnect();
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行连接
|
||||
*/
|
||||
private async performConnect(): Promise<void> {
|
||||
try {
|
||||
// 发送连接请求
|
||||
const response = await this.makeRequest('/connect', 'POST', {});
|
||||
|
||||
if (response.success && response.data.connectionId) {
|
||||
this.connectionId = response.data.connectionId;
|
||||
this.setState(ConnectionState.CONNECTED);
|
||||
|
||||
// 启动长轮询
|
||||
if (this.config.enableLongPolling) {
|
||||
this.startLongPolling();
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.error || 'Connection failed');
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState(ConnectionState.ERROR);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
this.stopReconnect();
|
||||
this.stopLongPolling();
|
||||
|
||||
if (this.connectionId) {
|
||||
try {
|
||||
await this.makeRequest('/disconnect', 'POST', {
|
||||
connectionId: this.connectionId
|
||||
});
|
||||
} catch (error) {
|
||||
// 忽略断开连接时的错误
|
||||
}
|
||||
|
||||
this.connectionId = null;
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
this.connectPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(message: ClientMessage): Promise<boolean> {
|
||||
if (!this.connectionId) {
|
||||
// 如果未连接,将消息加入队列
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.RECONNECTING) {
|
||||
return this.queueMessage(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('/send', 'POST', {
|
||||
connectionId: this.connectionId,
|
||||
message: {
|
||||
...message,
|
||||
timestamp: message.timestamp || Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.updateSendStats(message);
|
||||
return true;
|
||||
} else {
|
||||
console.error('Send message failed:', response.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动长轮询
|
||||
*/
|
||||
private startLongPolling(): void {
|
||||
if (this.longPollRunning || !this.connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.longPollRunning = true;
|
||||
this.performLongPoll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止长轮询
|
||||
*/
|
||||
private stopLongPolling(): void {
|
||||
this.longPollRunning = false;
|
||||
|
||||
if (this.longPollController) {
|
||||
this.longPollController.abort();
|
||||
this.longPollController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行长轮询
|
||||
*/
|
||||
private async performLongPoll(): Promise<void> {
|
||||
while (this.longPollRunning && this.connectionId) {
|
||||
try {
|
||||
this.longPollController = new AbortController();
|
||||
|
||||
const response = await this.makeRequest('/poll', 'GET', {
|
||||
connectionId: this.connectionId
|
||||
}, {
|
||||
signal: this.longPollController.signal,
|
||||
timeout: this.config.longPollTimeout
|
||||
});
|
||||
|
||||
if (response.success && response.messages && response.messages.length > 0) {
|
||||
// 处理接收到的消息
|
||||
for (const message of response.messages) {
|
||||
this.handleMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果服务器指示断开连接
|
||||
if (response.data && response.data.disconnected) {
|
||||
this.handleServerDisconnect();
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if ((error as any).name === 'AbortError') {
|
||||
// 被主动取消,正常情况
|
||||
break;
|
||||
}
|
||||
|
||||
console.warn('Long polling error:', (error as Error).message);
|
||||
|
||||
// 如果是网络错误,尝试重连
|
||||
if (this.isNetworkError(error as Error)) {
|
||||
this.handleError(error as Error);
|
||||
break;
|
||||
}
|
||||
|
||||
// 短暂等待后重试
|
||||
await this.delay(1000);
|
||||
}
|
||||
|
||||
this.longPollController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理服务器主动断开连接
|
||||
*/
|
||||
private handleServerDisconnect(): void {
|
||||
this.connectionId = null;
|
||||
this.stopLongPolling();
|
||||
this.emit('disconnected', 'Server disconnect');
|
||||
|
||||
if (this.reconnectAttempts < this.config.maxReconnectAttempts!) {
|
||||
this.startReconnect();
|
||||
} else {
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*/
|
||||
private async makeRequest(
|
||||
path: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any,
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
timeout?: number;
|
||||
} = {}
|
||||
): Promise<HttpResponse> {
|
||||
const url = this.buildUrl(path);
|
||||
const headers = this.buildHeaders();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
signal: options.signal
|
||||
};
|
||||
|
||||
// 添加请求体
|
||||
if (method !== 'GET' && data) {
|
||||
requestOptions.body = JSON.stringify(data);
|
||||
} else if (method === 'GET' && data) {
|
||||
// GET 请求将数据作为查询参数
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
params.append(key, String(value));
|
||||
});
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return this.fetchWithTimeout(`${url}${separator}${params}`, requestOptions, options.timeout);
|
||||
}
|
||||
|
||||
return this.fetchWithTimeout(url, requestOptions, options.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带超时的 fetch 请求
|
||||
*/
|
||||
private async fetchWithTimeout(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
timeout?: number
|
||||
): Promise<HttpResponse> {
|
||||
const actualTimeout = timeout || this.config.requestTimeout!;
|
||||
|
||||
const controller = new AbortController();
|
||||
let timeoutTimer: ITimer<any> | null = null;
|
||||
|
||||
// 创建超时定时器
|
||||
timeoutTimer = Core.schedule(actualTimeout / 1000, false, this, () => {
|
||||
controller.abort();
|
||||
if (timeoutTimer) {
|
||||
this.requestTimers.delete(timeoutTimer);
|
||||
}
|
||||
});
|
||||
|
||||
this.requestTimers.add(timeoutTimer);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: options.signal || controller.signal
|
||||
});
|
||||
|
||||
// 清理定时器
|
||||
if (timeoutTimer) {
|
||||
timeoutTimer.stop();
|
||||
this.requestTimers.delete(timeoutTimer);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result as HttpResponse;
|
||||
|
||||
} catch (error) {
|
||||
// 清理定时器
|
||||
if (timeoutTimer) {
|
||||
timeoutTimer.stop();
|
||||
this.requestTimers.delete(timeoutTimer);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求URL
|
||||
*/
|
||||
private buildUrl(path: string): string {
|
||||
const protocol = this.config.secure ? 'https' : 'http';
|
||||
const basePath = this.config.apiPrefix || '';
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
||||
|
||||
return `${protocol}://${this.config.host}:${this.config.port}${basePath}${cleanPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求头
|
||||
*/
|
||||
private buildHeaders(): Record<string, string> {
|
||||
const headers = { ...this.config.headers };
|
||||
|
||||
if (this.config.authToken) {
|
||||
headers['Authorization'] = `Bearer ${this.config.authToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为网络错误
|
||||
*/
|
||||
private isNetworkError(error: Error): boolean {
|
||||
return error.message.includes('fetch') ||
|
||||
error.message.includes('network') ||
|
||||
error.message.includes('timeout') ||
|
||||
error.name === 'TypeError';
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const timer = Core.schedule(ms / 1000, false, this, () => {
|
||||
this.requestTimers.delete(timer);
|
||||
resolve();
|
||||
});
|
||||
this.requestTimers.add(timer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置认证令牌
|
||||
*/
|
||||
setAuthToken(token: string): void {
|
||||
this.config.authToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接ID
|
||||
*/
|
||||
getConnectionId(): string | null {
|
||||
return this.connectionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持 Fetch API
|
||||
*/
|
||||
static isSupported(): boolean {
|
||||
return typeof fetch !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁传输层
|
||||
*/
|
||||
override destroy(): void {
|
||||
// 清理所有请求定时器
|
||||
this.requestTimers.forEach(timer => timer.stop());
|
||||
this.requestTimers.clear();
|
||||
|
||||
this.disconnect();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
/**
|
||||
* WebSocket 客户端传输实现
|
||||
*/
|
||||
|
||||
import { Core, ITimer } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ClientTransport,
|
||||
ClientTransportConfig,
|
||||
ConnectionState,
|
||||
ClientMessage
|
||||
} from './ClientTransport';
|
||||
|
||||
/**
|
||||
* WebSocket 客户端配置
|
||||
*/
|
||||
export interface WebSocketClientConfig extends ClientTransportConfig {
|
||||
/** WebSocket 路径 */
|
||||
path?: string;
|
||||
/** 协议列表 */
|
||||
protocols?: string | string[];
|
||||
/** 额外的请求头 */
|
||||
headers?: Record<string, string>;
|
||||
/** 是否启用二进制消息 */
|
||||
binaryType?: 'blob' | 'arraybuffer';
|
||||
/** WebSocket 扩展 */
|
||||
extensions?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 客户端传输
|
||||
*/
|
||||
export class WebSocketClientTransport extends ClientTransport {
|
||||
private websocket: WebSocket | null = null;
|
||||
private connectionPromise: Promise<void> | null = null;
|
||||
private connectionTimeoutTimer: ITimer<any> | null = null;
|
||||
|
||||
protected override config: WebSocketClientConfig;
|
||||
|
||||
constructor(config: WebSocketClientConfig) {
|
||||
super(config);
|
||||
|
||||
this.config = {
|
||||
path: '/ws',
|
||||
protocols: [],
|
||||
headers: {},
|
||||
binaryType: 'arraybuffer',
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.CONNECTED) {
|
||||
return this.connectionPromise || Promise.resolve();
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.CONNECTING);
|
||||
this.stopReconnect(); // 停止任何正在进行的重连
|
||||
|
||||
this.connectionPromise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 构建WebSocket URL
|
||||
const protocol = this.config.secure ? 'wss' : 'ws';
|
||||
const url = `${protocol}://${this.config.host}:${this.config.port}${this.config.path}`;
|
||||
|
||||
// 创建WebSocket连接
|
||||
this.websocket = new WebSocket(url, this.config.protocols);
|
||||
|
||||
if (this.config.binaryType) {
|
||||
this.websocket.binaryType = this.config.binaryType;
|
||||
}
|
||||
|
||||
// 设置连接超时
|
||||
this.connectionTimeoutTimer = Core.schedule(this.config.connectionTimeout! / 1000, false, this, () => {
|
||||
if (this.websocket && this.websocket.readyState === WebSocket.CONNECTING) {
|
||||
this.websocket.close();
|
||||
reject(new Error('Connection timeout'));
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket 事件处理
|
||||
this.websocket.onopen = () => {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
this.setState(ConnectionState.CONNECTED);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.websocket.onclose = (event) => {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
this.handleClose(event.code, event.reason);
|
||||
|
||||
if (this.state === ConnectionState.CONNECTING) {
|
||||
reject(new Error(`Connection failed: ${event.reason || 'Unknown error'}`));
|
||||
}
|
||||
};
|
||||
|
||||
this.websocket.onerror = (event) => {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
const error = new Error('WebSocket error');
|
||||
this.handleError(error);
|
||||
|
||||
if (this.state === ConnectionState.CONNECTING) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
this.handleWebSocketMessage(event);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.setState(ConnectionState.ERROR);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
this.stopReconnect();
|
||||
|
||||
if (this.websocket) {
|
||||
// 设置状态为断开连接,避免触发重连
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
|
||||
if (this.websocket.readyState === WebSocket.OPEN ||
|
||||
this.websocket.readyState === WebSocket.CONNECTING) {
|
||||
this.websocket.close(1000, 'Client disconnect');
|
||||
}
|
||||
|
||||
this.websocket = null;
|
||||
}
|
||||
|
||||
this.connectionPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(message: ClientMessage): Promise<boolean> {
|
||||
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
||||
// 如果未连接,将消息加入队列
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.RECONNECTING) {
|
||||
return this.queueMessage(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 序列化消息
|
||||
const serialized = JSON.stringify({
|
||||
...message,
|
||||
timestamp: message.timestamp || Date.now()
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
this.websocket.send(serialized);
|
||||
this.updateSendStats(message);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WebSocket 消息
|
||||
*/
|
||||
private handleWebSocketMessage(event: MessageEvent): void {
|
||||
try {
|
||||
let data: string;
|
||||
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
// 处理二进制数据
|
||||
data = new TextDecoder().decode(event.data);
|
||||
} else if (event.data instanceof Blob) {
|
||||
// Blob 需要异步处理
|
||||
event.data.text().then(text => {
|
||||
this.processMessage(text);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
// 字符串数据
|
||||
data = event.data;
|
||||
}
|
||||
|
||||
this.processMessage(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息内容
|
||||
*/
|
||||
private processMessage(data: string): void {
|
||||
try {
|
||||
const message: ClientMessage = JSON.parse(data);
|
||||
this.handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Error parsing message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接关闭
|
||||
*/
|
||||
private handleClose(code: number, reason: string): void {
|
||||
this.websocket = null;
|
||||
this.connectionPromise = null;
|
||||
|
||||
const wasConnected = this.isConnected();
|
||||
|
||||
// 根据关闭代码决定是否重连
|
||||
if (code === 1000) {
|
||||
// 正常关闭,不重连
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
this.emit('disconnected', reason || 'Normal closure');
|
||||
} else if (wasConnected && this.reconnectAttempts < this.config.maxReconnectAttempts!) {
|
||||
// 异常关闭,尝试重连
|
||||
this.emit('disconnected', reason || `Abnormal closure (${code})`);
|
||||
this.startReconnect();
|
||||
} else {
|
||||
// 达到最大重连次数或其他情况
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
this.emit('disconnected', reason || `Connection lost (${code})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 就绪状态
|
||||
*/
|
||||
getReadyState(): number {
|
||||
return this.websocket?.readyState ?? WebSocket.CLOSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 实例
|
||||
*/
|
||||
getWebSocket(): WebSocket | null {
|
||||
return this.websocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持 WebSocket
|
||||
*/
|
||||
static isSupported(): boolean {
|
||||
return typeof WebSocket !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁传输层
|
||||
*/
|
||||
override destroy(): void {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
this.disconnect();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 传输层导出
|
||||
*/
|
||||
|
||||
export * from './ClientTransport';
|
||||
export * from './WebSocketClientTransport';
|
||||
export * from './HttpClientTransport';
|
||||
@@ -1,384 +0,0 @@
|
||||
/**
|
||||
* NetworkClient 集成测试
|
||||
* 测试网络客户端的完整功能,包括依赖注入和错误处理
|
||||
*/
|
||||
|
||||
import { NetworkClient } from '../src/core/NetworkClient';
|
||||
|
||||
// Mock 所有外部依赖
|
||||
jest.mock('@esengine/ecs-framework', () => ({
|
||||
Core: {
|
||||
scene: null,
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
},
|
||||
Emitter: jest.fn().mockImplementation(() => ({
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
jest.mock('@esengine/ecs-framework-network-shared', () => ({
|
||||
NetworkValue: {},
|
||||
generateMessageId: jest.fn(() => 'test-message-id-123'),
|
||||
generateNetworkId: jest.fn(() => 12345),
|
||||
NetworkUtils: {
|
||||
generateMessageId: jest.fn(() => 'test-message-id-456'),
|
||||
calculateDistance: jest.fn(() => 100),
|
||||
isNodeEnvironment: jest.fn(() => false),
|
||||
isBrowserEnvironment: jest.fn(() => true)
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock WebSocket
|
||||
class MockWebSocket {
|
||||
public readyState: number = WebSocket.CONNECTING;
|
||||
public onopen: ((event: Event) => void) | null = null;
|
||||
public onclose: ((event: CloseEvent) => void) | null = null;
|
||||
public onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
public onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(public url: string, public protocols?: string | string[]) {}
|
||||
|
||||
send(data: string | ArrayBuffer | Blob): void {}
|
||||
close(code?: number, reason?: string): void {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(global as any).WebSocket = MockWebSocket;
|
||||
(global as any).WebSocket.CONNECTING = 0;
|
||||
(global as any).WebSocket.OPEN = 1;
|
||||
(global as any).WebSocket.CLOSING = 2;
|
||||
(global as any).WebSocket.CLOSED = 3;
|
||||
|
||||
describe('NetworkClient 集成测试', () => {
|
||||
let client: NetworkClient;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (client) {
|
||||
client.disconnect().catch(() => {});
|
||||
client = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理所有依赖模块', () => {
|
||||
expect(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(client).toBeInstanceOf(NetworkClient);
|
||||
});
|
||||
|
||||
it('应该正确使用network-shared中的工具函数', () => {
|
||||
const { generateMessageId, NetworkUtils } = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
// 验证network-shared模块被正确导入
|
||||
expect(generateMessageId).toBeDefined();
|
||||
expect(NetworkUtils).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确使用ecs-framework中的Core模块', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
expect(Core).toBeDefined();
|
||||
expect(Core.schedule).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('构造函数错误处理', () => {
|
||||
it('应该处理network-shared模块导入失败', () => {
|
||||
// 重置模块并模拟导入失败
|
||||
jest.resetModules();
|
||||
jest.doMock('@esengine/ecs-framework-network-shared', () => {
|
||||
throw new Error('network-shared模块导入失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||||
new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理ecs-framework模块导入失败', () => {
|
||||
// 重置模块并模拟导入失败
|
||||
jest.resetModules();
|
||||
jest.doMock('@esengine/ecs-framework', () => {
|
||||
throw new Error('ecs-framework模块导入失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||||
new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理传输层构造失败', () => {
|
||||
// Mock传输层构造函数抛出异常
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
(global as any).WebSocket = jest.fn(() => {
|
||||
throw new Error('WebSocket不可用');
|
||||
});
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
});
|
||||
|
||||
describe('功能测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够成功连接', async () => {
|
||||
const connectPromise = client.connect();
|
||||
|
||||
// 模拟连接成功
|
||||
setTimeout(() => {
|
||||
const transport = (client as any).transport;
|
||||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||||
transport.websocket.readyState = WebSocket.OPEN;
|
||||
transport.websocket.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该能够发送消息', async () => {
|
||||
// 先连接
|
||||
const connectPromise = client.connect();
|
||||
setTimeout(() => {
|
||||
const transport = (client as any).transport;
|
||||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||||
transport.websocket.readyState = WebSocket.OPEN;
|
||||
transport.websocket.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
await connectPromise;
|
||||
|
||||
// 发送消息
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'message' },
|
||||
reliable: true
|
||||
};
|
||||
|
||||
// NetworkClient没有直接的sendMessage方法,它通过RPC调用
|
||||
});
|
||||
|
||||
it('应该能够正确断开连接', async () => {
|
||||
await expect(client.disconnect()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该返回正确的认证状态', () => {
|
||||
expect(client.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能够获取网络对象列表', () => {
|
||||
const networkObjects = client.getAllNetworkObjects();
|
||||
expect(Array.isArray(networkObjects)).toBe(true);
|
||||
expect(networkObjects.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息ID生成测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够生成唯一的消息ID', () => {
|
||||
const messageId1 = (client as any).generateMessageId();
|
||||
const messageId2 = (client as any).generateMessageId();
|
||||
|
||||
expect(typeof messageId1).toBe('string');
|
||||
expect(typeof messageId2).toBe('string');
|
||||
expect(messageId1).not.toBe(messageId2);
|
||||
});
|
||||
|
||||
it('生成的消息ID应该符合预期格式', () => {
|
||||
const messageId = (client as any).generateMessageId();
|
||||
|
||||
// 检查消息ID格式(时间戳 + 随机字符串)
|
||||
expect(messageId).toMatch(/^[a-z0-9]+$/);
|
||||
expect(messageId.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误恢复测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
maxReconnectAttempts: 2,
|
||||
reconnectInterval: 100
|
||||
});
|
||||
});
|
||||
|
||||
it('连接失败后应该尝试重连', async () => {
|
||||
let connectAttempts = 0;
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
|
||||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||||
connectAttempts++;
|
||||
const ws = new originalWebSocket('ws://localhost:8080');
|
||||
// 模拟连接失败
|
||||
setTimeout(() => {
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
}, 0);
|
||||
return ws;
|
||||
});
|
||||
|
||||
await expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 等待重连尝试
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
expect(connectAttempts).toBeGreaterThan(1);
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
|
||||
it('达到最大重连次数后应该停止重连', async () => {
|
||||
const maxAttempts = 2;
|
||||
client = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
maxReconnectAttempts: maxAttempts,
|
||||
reconnectInterval: 50
|
||||
});
|
||||
|
||||
let connectAttempts = 0;
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
|
||||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||||
connectAttempts++;
|
||||
const ws = new originalWebSocket('ws://localhost:8080');
|
||||
setTimeout(() => {
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
}, 0);
|
||||
return ws;
|
||||
});
|
||||
|
||||
await expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 等待所有重连尝试完成
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(connectAttempts).toBeLessThanOrEqual(maxAttempts + 1);
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存泄漏防护测试', () => {
|
||||
it('断开连接时应该清理所有资源', async () => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[Emitter.mock.results.length - 1].value;
|
||||
|
||||
await client.disconnect();
|
||||
|
||||
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('多次创建和销毁客户端不应该造成内存泄漏', () => {
|
||||
const initialEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||||
|
||||
// 创建和销毁多个客户端实例
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const tempClient = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
tempClient.disconnect().catch(() => {});
|
||||
}
|
||||
|
||||
const finalEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||||
|
||||
// 验证Emitter实例数量符合预期
|
||||
expect(finalEmitterCallCount - initialEmitterCallCount).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,65 @@
|
||||
/**
|
||||
* Jest测试环境设置 - 客户端
|
||||
*/
|
||||
|
||||
// 导入reflect-metadata以支持装饰器
|
||||
import 'reflect-metadata';
|
||||
|
||||
// Mock WebSocket for testing
|
||||
(global as any).WebSocket = class MockWebSocket {
|
||||
onopen: ((event: Event) => void) | null = null;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onclose: ((event: CloseEvent) => void) | null = null;
|
||||
onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(public url: string) {}
|
||||
|
||||
send(data: string | ArrayBuffer | Blob) {
|
||||
// Mock implementation
|
||||
}
|
||||
|
||||
close() {
|
||||
// Mock implementation
|
||||
}
|
||||
};
|
||||
// 模拟浏览器环境的WebSocket
|
||||
Object.defineProperty(global, 'WebSocket', {
|
||||
value: class MockWebSocket {
|
||||
static CONNECTING = 0;
|
||||
static OPEN = 1;
|
||||
static CLOSING = 2;
|
||||
static CLOSED = 3;
|
||||
|
||||
global.beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
readyState = MockWebSocket.CONNECTING;
|
||||
url: string;
|
||||
onopen: ((event: Event) => void) | null = null;
|
||||
onclose: ((event: CloseEvent) => void) | null = null;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
// 模拟异步连接
|
||||
setTimeout(() => {
|
||||
this.readyState = MockWebSocket.OPEN;
|
||||
if (this.onopen) {
|
||||
this.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
send(data: string | ArrayBuffer) {
|
||||
// 模拟发送
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = MockWebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose(new CloseEvent('close'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
global.afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
// 全局测试配置
|
||||
beforeAll(() => {
|
||||
// 设置测试环境
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.NETWORK_ENV = 'client';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// 清理测试环境
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// 每个测试前的准备工作
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 每个测试后的清理工作
|
||||
// 清理可能的网络连接、定时器等
|
||||
});
|
||||
@@ -1,374 +0,0 @@
|
||||
/**
|
||||
* ClientTransport 基类测试
|
||||
* 测试客户端传输层基类的构造函数和依赖问题
|
||||
*/
|
||||
|
||||
import { ClientTransport, ClientTransportConfig, ConnectionState } from '../../src/transport/ClientTransport';
|
||||
|
||||
// Mock Emitter 和 Core
|
||||
jest.mock('@esengine/ecs-framework', () => ({
|
||||
Emitter: jest.fn().mockImplementation(() => ({
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
})),
|
||||
Core: {
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock network-shared
|
||||
jest.mock('@esengine/ecs-framework-network-shared', () => ({
|
||||
NetworkValue: {}
|
||||
}));
|
||||
|
||||
// 创建测试用的具体实现类
|
||||
class TestClientTransport extends ClientTransport {
|
||||
async connect(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async sendMessage(message: any): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('ClientTransport', () => {
|
||||
let transport: TestClientTransport;
|
||||
const defaultConfig: ClientTransportConfig = {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (transport) {
|
||||
transport = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('构造函数测试', () => {
|
||||
it('应该能够成功创建ClientTransport实例', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(transport).toBeInstanceOf(ClientTransport);
|
||||
});
|
||||
|
||||
it('应该正确设置默认配置', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.host).toBe('localhost');
|
||||
expect(config.port).toBe(8080);
|
||||
expect(config.secure).toBe(false);
|
||||
expect(config.connectionTimeout).toBe(10000);
|
||||
expect(config.reconnectInterval).toBe(3000);
|
||||
expect(config.maxReconnectAttempts).toBe(10);
|
||||
expect(config.heartbeatInterval).toBe(30000);
|
||||
expect(config.maxQueueSize).toBe(1000);
|
||||
});
|
||||
|
||||
it('应该允许自定义配置覆盖默认值', () => {
|
||||
const customConfig: ClientTransportConfig = {
|
||||
host: 'example.com',
|
||||
port: 9090,
|
||||
secure: true,
|
||||
connectionTimeout: 15000,
|
||||
reconnectInterval: 5000,
|
||||
maxReconnectAttempts: 5,
|
||||
heartbeatInterval: 60000,
|
||||
maxQueueSize: 500
|
||||
};
|
||||
|
||||
transport = new TestClientTransport(customConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.host).toBe('example.com');
|
||||
expect(config.port).toBe(9090);
|
||||
expect(config.secure).toBe(true);
|
||||
expect(config.connectionTimeout).toBe(15000);
|
||||
expect(config.reconnectInterval).toBe(5000);
|
||||
expect(config.maxReconnectAttempts).toBe(5);
|
||||
expect(config.heartbeatInterval).toBe(60000);
|
||||
expect(config.maxQueueSize).toBe(500);
|
||||
});
|
||||
|
||||
it('应该正确初始化内部状态', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
expect((transport as any).messageQueue).toEqual([]);
|
||||
expect((transport as any).reconnectAttempts).toBe(0);
|
||||
expect((transport as any).reconnectTimer).toBeNull();
|
||||
expect((transport as any).heartbeatTimer).toBeNull();
|
||||
expect((transport as any).latencyMeasurements).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确初始化统计信息', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.connectedAt).toBeNull();
|
||||
expect(stats.connectionDuration).toBe(0);
|
||||
expect(stats.messagesSent).toBe(0);
|
||||
expect(stats.messagesReceived).toBe(0);
|
||||
expect(stats.bytesSent).toBe(0);
|
||||
expect(stats.bytesReceived).toBe(0);
|
||||
expect(stats.averageLatency).toBe(0);
|
||||
expect(stats.averageLatency).toBe(0);
|
||||
expect(stats.reconnectCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理@esengine/ecs-framework中的Emitter', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(Emitter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('构造函数中Emitter初始化失败应该抛出异常', () => {
|
||||
// Mock Emitter构造函数抛出异常
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
Emitter.mockImplementation(() => {
|
||||
throw new Error('Emitter初始化失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).toThrow('Emitter初始化失败');
|
||||
});
|
||||
|
||||
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
|
||||
const networkShared = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(networkShared).toBeDefined();
|
||||
expect(networkShared.NetworkValue).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('事件系统测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够注册事件监听器', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
transport.on('connected', mockCallback);
|
||||
|
||||
expect(emitterInstance.on).toHaveBeenCalledWith('connected', mockCallback);
|
||||
});
|
||||
|
||||
it('应该能够移除事件监听器', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
transport.off('connected', mockCallback);
|
||||
|
||||
expect(emitterInstance.off).toHaveBeenCalledWith('connected', mockCallback);
|
||||
});
|
||||
|
||||
it('应该能够发出事件', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).emit('connected');
|
||||
|
||||
expect(emitterInstance.emit).toHaveBeenCalledWith('connected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息队列测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够将消息加入队列', async () => {
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'data' },
|
||||
reliable: true,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
await transport.sendMessage(message);
|
||||
|
||||
const messageQueue = (transport as any).messageQueue;
|
||||
expect(messageQueue).toHaveLength(1);
|
||||
expect(messageQueue[0]).toEqual(message);
|
||||
});
|
||||
|
||||
it('消息队列达到最大大小时应该移除旧消息', async () => {
|
||||
// 设置较小的队列大小
|
||||
const smallQueueConfig = { ...defaultConfig, maxQueueSize: 2 };
|
||||
transport = new TestClientTransport(smallQueueConfig);
|
||||
|
||||
const message1 = { type: 'custom' as const, data: { id: 1 }, reliable: true, timestamp: Date.now() };
|
||||
const message2 = { type: 'custom' as const, data: { id: 2 }, reliable: true, timestamp: Date.now() };
|
||||
const message3 = { type: 'custom' as const, data: { id: 3 }, reliable: true, timestamp: Date.now() };
|
||||
|
||||
await transport.sendMessage(message1);
|
||||
await transport.sendMessage(message2);
|
||||
await transport.sendMessage(message3);
|
||||
|
||||
const messageQueue = (transport as any).messageQueue;
|
||||
expect(messageQueue).toHaveLength(2);
|
||||
expect(messageQueue[0]).toEqual(message2);
|
||||
expect(messageQueue[1]).toEqual(message3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('连接状态测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该正确获取连接状态', () => {
|
||||
expect(transport.getState()).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
|
||||
it('应该正确检查连接状态', () => {
|
||||
expect(transport.isConnected()).toBe(false);
|
||||
|
||||
(transport as any).state = ConnectionState.CONNECTED;
|
||||
expect(transport.isConnected()).toBe(true);
|
||||
|
||||
(transport as any).state = ConnectionState.AUTHENTICATED;
|
||||
expect(transport.isConnected()).toBe(true);
|
||||
});
|
||||
|
||||
it('状态变化时应该发出事件', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).setState(ConnectionState.CONNECTING);
|
||||
|
||||
expect(emitterInstance.emit).toHaveBeenCalledWith(
|
||||
'state-changed',
|
||||
ConnectionState.DISCONNECTED,
|
||||
ConnectionState.CONNECTING
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('延迟测量测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够更新延迟测量', () => {
|
||||
(transport as any).updateLatency(100);
|
||||
(transport as any).updateLatency(200);
|
||||
(transport as any).updateLatency(150);
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.averageLatency).toBe(150);
|
||||
});
|
||||
|
||||
it('应该限制延迟测量样本数量', () => {
|
||||
// 添加超过最大样本数的测量
|
||||
for (let i = 0; i < 150; i++) {
|
||||
(transport as any).updateLatency(i * 10);
|
||||
}
|
||||
|
||||
const latencyMeasurements = (transport as any).latencyMeasurements;
|
||||
expect(latencyMeasurements.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('配置验证测试', () => {
|
||||
it('应该拒绝无效的主机名', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: '', port: 8080 });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该拒绝无效的端口号', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: 'localhost', port: 0 });
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: 'localhost', port: 65536 });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该拒绝负数的超时配置', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
connectionTimeout: -1000
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('资源清理测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够清理所有定时器', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
const mockTimer = { stop: jest.fn() };
|
||||
Core.schedule.scheduleRepeating.mockReturnValue(mockTimer);
|
||||
|
||||
// 设置一些定时器
|
||||
(transport as any).reconnectTimer = mockTimer;
|
||||
(transport as any).heartbeatTimer = mockTimer;
|
||||
|
||||
// 调用清理方法
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect(mockTimer.stop).toHaveBeenCalledTimes(2);
|
||||
expect((transport as any).reconnectTimer).toBeNull();
|
||||
expect((transport as any).heartbeatTimer).toBeNull();
|
||||
});
|
||||
|
||||
it('应该能够清理消息队列', () => {
|
||||
(transport as any).messageQueue = [
|
||||
{ type: 'custom', data: {}, reliable: true, timestamp: Date.now() }
|
||||
];
|
||||
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect((transport as any).messageQueue).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该能够移除所有事件监听器', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,348 +0,0 @@
|
||||
/**
|
||||
* WebSocketClientTransport 测试
|
||||
* 测试WebSocket客户端传输层的构造函数和依赖问题
|
||||
*/
|
||||
|
||||
import { WebSocketClientTransport, WebSocketClientConfig } from '../../src/transport/WebSocketClientTransport';
|
||||
import { ConnectionState } from '../../src/transport/ClientTransport';
|
||||
|
||||
// Mock WebSocket
|
||||
class MockWebSocket {
|
||||
public readyState: number = WebSocket.CONNECTING;
|
||||
public onopen: ((event: Event) => void) | null = null;
|
||||
public onclose: ((event: CloseEvent) => void) | null = null;
|
||||
public onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
public onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(public url: string, public protocols?: string | string[]) {}
|
||||
|
||||
send(data: string | ArrayBuffer | Blob): void {}
|
||||
close(code?: number, reason?: string): void {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock依赖 - 直接创建mock对象而不依赖外部模块
|
||||
const mockCore = {
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const mockEmitter = {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
};
|
||||
|
||||
const mockNetworkShared = {
|
||||
NetworkValue: {},
|
||||
generateMessageId: jest.fn(() => 'mock-message-id-123')
|
||||
};
|
||||
|
||||
// 设置模块mock
|
||||
jest.doMock('@esengine/ecs-framework', () => ({
|
||||
Core: mockCore,
|
||||
Emitter: jest.fn(() => mockEmitter)
|
||||
}));
|
||||
|
||||
jest.doMock('@esengine/ecs-framework-network-shared', () => mockNetworkShared);
|
||||
|
||||
// 设置全局WebSocket mock
|
||||
(global as any).WebSocket = MockWebSocket;
|
||||
(global as any).WebSocket.CONNECTING = 0;
|
||||
(global as any).WebSocket.OPEN = 1;
|
||||
(global as any).WebSocket.CLOSING = 2;
|
||||
(global as any).WebSocket.CLOSED = 3;
|
||||
|
||||
describe('WebSocketClientTransport', () => {
|
||||
let transport: WebSocketClientTransport;
|
||||
const defaultConfig: WebSocketClientConfig = {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
reconnectInterval: 1000,
|
||||
maxReconnectAttempts: 3,
|
||||
heartbeatInterval: 30000
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (transport) {
|
||||
transport.disconnect().catch(() => {});
|
||||
transport = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('构造函数测试', () => {
|
||||
it('应该能够成功创建WebSocketClientTransport实例', () => {
|
||||
expect(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(transport).toBeInstanceOf(WebSocketClientTransport);
|
||||
});
|
||||
|
||||
it('应该正确合并默认配置', () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.path).toBe('/ws');
|
||||
expect(config.protocols).toEqual([]);
|
||||
expect(config.headers).toEqual({});
|
||||
expect(config.binaryType).toBe('arraybuffer');
|
||||
expect(config.host).toBe('localhost');
|
||||
expect(config.port).toBe(8080);
|
||||
});
|
||||
|
||||
it('应该允许自定义配置覆盖默认值', () => {
|
||||
const customConfig: WebSocketClientConfig = {
|
||||
...defaultConfig,
|
||||
path: '/custom-ws',
|
||||
protocols: ['custom-protocol'],
|
||||
headers: { 'X-Custom': 'value' },
|
||||
binaryType: 'blob'
|
||||
};
|
||||
|
||||
transport = new WebSocketClientTransport(customConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.path).toBe('/custom-ws');
|
||||
expect(config.protocols).toEqual(['custom-protocol']);
|
||||
expect(config.headers).toEqual({ 'X-Custom': 'value' });
|
||||
expect(config.binaryType).toBe('blob');
|
||||
});
|
||||
|
||||
it('应该正确初始化内部状态', () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
expect((transport as any).websocket).toBeNull();
|
||||
expect((transport as any).connectionPromise).toBeNull();
|
||||
expect((transport as any).connectionTimeoutTimer).toBeNull();
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理@esengine/ecs-framework依赖', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
|
||||
expect(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(Core).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
|
||||
const { generateMessageId } = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
expect(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(generateMessageId).toBeDefined();
|
||||
expect(typeof generateMessageId).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('连接功能测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够发起连接', async () => {
|
||||
const connectPromise = transport.connect();
|
||||
|
||||
expect((transport as any).websocket).toBeInstanceOf(MockWebSocket);
|
||||
expect((transport as any).state).toBe(ConnectionState.CONNECTING);
|
||||
|
||||
// 模拟连接成功
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该构造正确的WebSocket URL', async () => {
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
expect(ws.url).toBe('ws://localhost:8080/ws');
|
||||
});
|
||||
|
||||
it('使用安全连接时应该构造HTTPS URL', async () => {
|
||||
const secureConfig = { ...defaultConfig, secure: true };
|
||||
transport = new WebSocketClientTransport(secureConfig);
|
||||
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
expect(ws.url).toBe('wss://localhost:8080/ws');
|
||||
});
|
||||
|
||||
it('应该设置WebSocket事件处理器', async () => {
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
expect(ws.onopen).toBeDefined();
|
||||
expect(ws.onclose).toBeDefined();
|
||||
expect(ws.onmessage).toBeDefined();
|
||||
expect(ws.onerror).toBeDefined();
|
||||
});
|
||||
|
||||
it('连接超时应该被正确处理', async () => {
|
||||
const shortTimeoutConfig = { ...defaultConfig, connectionTimeout: 100 };
|
||||
transport = new WebSocketClientTransport(shortTimeoutConfig);
|
||||
|
||||
const connectPromise = transport.connect();
|
||||
|
||||
// 不触发onopen事件,让连接超时
|
||||
await expect(connectPromise).rejects.toThrow('连接超时');
|
||||
});
|
||||
|
||||
it('应该能够正确断开连接', async () => {
|
||||
transport.connect();
|
||||
|
||||
// 模拟连接成功
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
await transport.disconnect();
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息发送测试', () => {
|
||||
beforeEach(async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('未连接时发送消息应该加入队列', async () => {
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'data' },
|
||||
reliable: true,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
await transport.sendMessage(message);
|
||||
|
||||
const messageQueue = (transport as any).messageQueue;
|
||||
expect(messageQueue).toHaveLength(1);
|
||||
expect(messageQueue[0]).toEqual(message);
|
||||
});
|
||||
|
||||
it('连接后应该发送队列中的消息', async () => {
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'data' },
|
||||
reliable: true,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 先发送消息到队列
|
||||
await transport.sendMessage(message);
|
||||
|
||||
// 然后连接
|
||||
transport.connect();
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
const sendSpy = jest.spyOn(ws, 'send');
|
||||
|
||||
// 模拟连接成功
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
expect(sendSpy).toHaveBeenCalled();
|
||||
expect((transport as any).messageQueue).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理测试', () => {
|
||||
it('应该处理WebSocket构造函数异常', () => {
|
||||
// Mock WebSocket构造函数抛出异常
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
(global as any).WebSocket = jest.fn(() => {
|
||||
throw new Error('WebSocket构造失败');
|
||||
});
|
||||
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
expect(transport.connect()).rejects.toThrow('WebSocket构造失败');
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
|
||||
it('应该处理网络连接错误', async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
const connectPromise = transport.connect();
|
||||
|
||||
// 模拟连接错误
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
|
||||
await expect(connectPromise).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理意外的连接关闭', () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
|
||||
// 模拟连接意外关闭
|
||||
if (ws.onclose) {
|
||||
ws.onclose(new CloseEvent('close', { code: 1006, reason: '意外关闭' }));
|
||||
}
|
||||
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息测试', () => {
|
||||
it('应该正确计算连接统计信息', async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
const initialStats = transport.getStats();
|
||||
expect(initialStats.connectedAt).toBeNull();
|
||||
expect(initialStats.messagesSent).toBe(0);
|
||||
expect(initialStats.messagesReceived).toBe(0);
|
||||
});
|
||||
|
||||
it('连接后应该更新统计信息', async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
transport.connect();
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.connectedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,11 @@
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"lib": ["ES2020", "DOM", "WebWorker"],
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -41,5 +42,13 @@
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../network-shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user