feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364)
* feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
This commit is contained in:
@@ -1,153 +1,199 @@
|
||||
import { type IPlugin, Core, type ServiceContainer, type Scene } from '@esengine/ecs-framework';
|
||||
import { NetworkService } from './services/NetworkService';
|
||||
import { NetworkSyncSystem } from './systems/NetworkSyncSystem';
|
||||
import { NetworkSpawnSystem, type PrefabFactory } from './systems/NetworkSpawnSystem';
|
||||
import { NetworkInputSystem } from './systems/NetworkInputSystem';
|
||||
/**
|
||||
* @zh 网络插件
|
||||
* @en Network Plugin
|
||||
*/
|
||||
|
||||
import { type IPlugin, Core, type ServiceContainer, type Scene } from '@esengine/ecs-framework'
|
||||
import { GameNetworkService, type NetworkServiceOptions } from './services/NetworkService'
|
||||
import { NetworkSyncSystem } from './systems/NetworkSyncSystem'
|
||||
import { NetworkSpawnSystem, type PrefabFactory } from './systems/NetworkSpawnSystem'
|
||||
import { NetworkInputSystem } from './systems/NetworkInputSystem'
|
||||
|
||||
/**
|
||||
* 网络插件
|
||||
* Network plugin
|
||||
* @zh 网络插件
|
||||
* @en Network plugin
|
||||
*
|
||||
* 提供基于 TSRPC 的网络同步功能。
|
||||
* Provides TSRPC-based network synchronization.
|
||||
* @zh 提供基于 @esengine/rpc 的网络同步功能
|
||||
* @en Provides @esengine/rpc based network synchronization
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Core } from '@esengine/ecs-framework';
|
||||
* import { NetworkPlugin } from '@esengine/network';
|
||||
* import { Core } from '@esengine/ecs-framework'
|
||||
* import { NetworkPlugin } from '@esengine/network'
|
||||
*
|
||||
* const networkPlugin = new NetworkPlugin();
|
||||
* await Core.installPlugin(networkPlugin);
|
||||
* const networkPlugin = new NetworkPlugin()
|
||||
* await Core.installPlugin(networkPlugin)
|
||||
*
|
||||
* // 连接到服务器 | Connect to server
|
||||
* await networkPlugin.connect('ws://localhost:3000', 'Player1');
|
||||
* // 连接到服务器
|
||||
* await networkPlugin.connect({ url: 'ws://localhost:3000', playerName: 'Player1' })
|
||||
*
|
||||
* // 注册预制体 | Register prefab
|
||||
* // 注册预制体
|
||||
* networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||
* const entity = scene.createEntity('Player');
|
||||
* return entity;
|
||||
* });
|
||||
* const entity = scene.createEntity('Player')
|
||||
* return entity
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class NetworkPlugin implements IPlugin {
|
||||
public readonly name = '@esengine/network';
|
||||
public readonly version = '1.0.0';
|
||||
public readonly name = '@esengine/network'
|
||||
public readonly version = '2.0.0'
|
||||
|
||||
private _networkService!: NetworkService;
|
||||
private _syncSystem!: NetworkSyncSystem;
|
||||
private _spawnSystem!: NetworkSpawnSystem;
|
||||
private _inputSystem!: NetworkInputSystem;
|
||||
private _networkService!: GameNetworkService
|
||||
private _syncSystem!: NetworkSyncSystem
|
||||
private _spawnSystem!: NetworkSpawnSystem
|
||||
private _inputSystem!: NetworkInputSystem
|
||||
private _localPlayerId: number = 0
|
||||
|
||||
/**
|
||||
* 网络服务
|
||||
* Network service
|
||||
* @zh 网络服务
|
||||
* @en Network service
|
||||
*/
|
||||
get networkService(): NetworkService {
|
||||
return this._networkService;
|
||||
get networkService(): GameNetworkService {
|
||||
return this._networkService
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步系统
|
||||
* Sync system
|
||||
* @zh 同步系统
|
||||
* @en Sync system
|
||||
*/
|
||||
get syncSystem(): NetworkSyncSystem {
|
||||
return this._syncSystem;
|
||||
return this._syncSystem
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成系统
|
||||
* Spawn system
|
||||
* @zh 生成系统
|
||||
* @en Spawn system
|
||||
*/
|
||||
get spawnSystem(): NetworkSpawnSystem {
|
||||
return this._spawnSystem;
|
||||
return this._spawnSystem
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入系统
|
||||
* Input system
|
||||
* @zh 输入系统
|
||||
* @en Input system
|
||||
*/
|
||||
get inputSystem(): NetworkInputSystem {
|
||||
return this._inputSystem;
|
||||
return this._inputSystem
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
* Is connected
|
||||
* @zh 本地玩家 ID
|
||||
* @en Local player ID
|
||||
*/
|
||||
get localPlayerId(): number {
|
||||
return this._localPlayerId
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否已连接
|
||||
* @en Is connected
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this._networkService?.isConnected ?? false;
|
||||
return this._networkService?.isConnected ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
* Install plugin
|
||||
* @zh 安装插件
|
||||
* @en Install plugin
|
||||
*/
|
||||
install(_core: Core, _services: ServiceContainer): void {
|
||||
this._networkService = new NetworkService();
|
||||
this._networkService = new GameNetworkService()
|
||||
|
||||
// 当场景加载时添加系统
|
||||
// Add systems when scene loads
|
||||
const scene = Core.scene;
|
||||
const scene = Core.scene
|
||||
if (scene) {
|
||||
this._setupSystems(scene as Scene);
|
||||
this._setupSystems(scene as Scene)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
* Uninstall plugin
|
||||
* @zh 卸载插件
|
||||
* @en Uninstall plugin
|
||||
*/
|
||||
uninstall(): void {
|
||||
this._networkService?.disconnect();
|
||||
this._networkService?.disconnect()
|
||||
}
|
||||
|
||||
private _setupSystems(scene: Scene): void {
|
||||
this._syncSystem = new NetworkSyncSystem(this._networkService);
|
||||
this._spawnSystem = new NetworkSpawnSystem(this._networkService, this._syncSystem);
|
||||
this._inputSystem = new NetworkInputSystem(this._networkService);
|
||||
this._syncSystem = new NetworkSyncSystem()
|
||||
this._spawnSystem = new NetworkSpawnSystem(this._syncSystem)
|
||||
this._inputSystem = new NetworkInputSystem(this._networkService)
|
||||
|
||||
scene.addSystem(this._syncSystem);
|
||||
scene.addSystem(this._spawnSystem);
|
||||
scene.addSystem(this._inputSystem);
|
||||
scene.addSystem(this._syncSystem)
|
||||
scene.addSystem(this._spawnSystem)
|
||||
scene.addSystem(this._inputSystem)
|
||||
|
||||
this._setupMessageHandlers()
|
||||
}
|
||||
|
||||
private _setupMessageHandlers(): void {
|
||||
this._networkService
|
||||
.onSync((data) => {
|
||||
this._syncSystem.handleSync({ entities: data.entities })
|
||||
})
|
||||
.onSpawn((data) => {
|
||||
this._spawnSystem.handleSpawn(data)
|
||||
})
|
||||
.onDespawn((data) => {
|
||||
this._spawnSystem.handleDespawn(data)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
* Connect to server
|
||||
* @zh 连接到服务器
|
||||
* @en Connect to server
|
||||
*/
|
||||
public async connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean> {
|
||||
return this._networkService.connect(serverUrl, playerName, roomId);
|
||||
public async connect(options: NetworkServiceOptions & { playerName: string; roomId?: string }): Promise<boolean> {
|
||||
try {
|
||||
await this._networkService.connect(options)
|
||||
|
||||
const result = await this._networkService.call('join', {
|
||||
playerName: options.playerName,
|
||||
roomId: options.roomId,
|
||||
})
|
||||
|
||||
this._localPlayerId = result.playerId
|
||||
this._spawnSystem.setLocalPlayerId(this._localPlayerId)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
* Disconnect
|
||||
* @zh 断开连接
|
||||
* @en Disconnect
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
await this._networkService.disconnect();
|
||||
try {
|
||||
await this._networkService.call('leave', undefined)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this._networkService.disconnect()
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册预制体工厂
|
||||
* Register prefab factory
|
||||
* @zh 注册预制体工厂
|
||||
* @en Register prefab factory
|
||||
*/
|
||||
public registerPrefab(prefabType: string, factory: PrefabFactory): void {
|
||||
this._spawnSystem?.registerPrefab(prefabType, factory);
|
||||
this._spawnSystem?.registerPrefab(prefabType, factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送移动输入
|
||||
* Send move input
|
||||
* @zh 发送移动输入
|
||||
* @en Send move input
|
||||
*/
|
||||
public sendMoveInput(x: number, y: number): void {
|
||||
this._inputSystem?.addMoveInput(x, y);
|
||||
this._inputSystem?.addMoveInput(x, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送动作输入
|
||||
* Send action input
|
||||
* @zh 发送动作输入
|
||||
* @en Send action input
|
||||
*/
|
||||
public sendActionInput(action: string): void {
|
||||
this._inputSystem?.addActionInput(action);
|
||||
this._inputSystem?.addActionInput(action)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
/**
|
||||
* @esengine/network
|
||||
* @zh @esengine/network 网络同步模块
|
||||
* @en @esengine/network Network synchronization module
|
||||
*
|
||||
* 基于 TSRPC 的网络同步模块(客户端)
|
||||
* TSRPC-based network synchronization module (client)
|
||||
* @zh 基于 @esengine/rpc 的网络同步模块,提供类型安全的多人游戏网络通信
|
||||
* @en Network synchronization module based on @esengine/rpc for type-safe multiplayer game communication
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Re-export from protocols | 从协议包重新导出
|
||||
// Re-export from RPC | 从 RPC 包重新导出
|
||||
// ============================================================================
|
||||
|
||||
export { rpc } from '@esengine/rpc'
|
||||
export type {
|
||||
ServiceType,
|
||||
Vec2,
|
||||
IEntityState,
|
||||
IPlayerInput,
|
||||
MsgSync,
|
||||
MsgInput,
|
||||
MsgSpawn,
|
||||
MsgDespawn,
|
||||
ReqJoin,
|
||||
ResJoin
|
||||
} from '@esengine/network-protocols';
|
||||
ProtocolDef,
|
||||
ApiDef,
|
||||
MsgDef,
|
||||
ApiInput,
|
||||
ApiOutput,
|
||||
MsgData,
|
||||
ApiNames,
|
||||
MsgNames,
|
||||
RpcError,
|
||||
} from '@esengine/rpc'
|
||||
|
||||
export { serviceProto } from '@esengine/network-protocols';
|
||||
// ============================================================================
|
||||
// Protocol | 协议
|
||||
// ============================================================================
|
||||
|
||||
export {
|
||||
gameProtocol,
|
||||
type GameProtocol,
|
||||
type PlayerInput,
|
||||
type EntitySyncState,
|
||||
type SyncData,
|
||||
type SpawnData,
|
||||
type DespawnData,
|
||||
type JoinRequest,
|
||||
type JoinResponse,
|
||||
} from './protocol'
|
||||
|
||||
// ============================================================================
|
||||
// Tokens | 服务令牌
|
||||
@@ -32,37 +47,44 @@ export {
|
||||
NetworkServiceToken,
|
||||
NetworkSyncSystemToken,
|
||||
NetworkSpawnSystemToken,
|
||||
NetworkInputSystemToken
|
||||
} from './tokens';
|
||||
NetworkInputSystemToken,
|
||||
} from './tokens'
|
||||
|
||||
// ============================================================================
|
||||
// Plugin | 插件
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkPlugin } from './NetworkPlugin';
|
||||
export { NetworkPlugin } from './NetworkPlugin'
|
||||
|
||||
// ============================================================================
|
||||
// Services | 服务
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkService, ENetworkState } from './services/NetworkService';
|
||||
export type { INetworkCallbacks } from './services/NetworkService';
|
||||
export {
|
||||
RpcService,
|
||||
GameNetworkService,
|
||||
NetworkService,
|
||||
NetworkState,
|
||||
createNetworkService,
|
||||
} from './services/NetworkService'
|
||||
export type { NetworkServiceOptions } from './services/NetworkService'
|
||||
|
||||
// ============================================================================
|
||||
// Components | 组件
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkIdentity } from './components/NetworkIdentity';
|
||||
export { NetworkTransform } from './components/NetworkTransform';
|
||||
export { NetworkIdentity } from './components/NetworkIdentity'
|
||||
export { NetworkTransform } from './components/NetworkTransform'
|
||||
|
||||
// ============================================================================
|
||||
// Systems | 系统
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkSyncSystem } from './systems/NetworkSyncSystem';
|
||||
export { NetworkSpawnSystem } from './systems/NetworkSpawnSystem';
|
||||
export type { PrefabFactory } from './systems/NetworkSpawnSystem';
|
||||
export { NetworkInputSystem } from './systems/NetworkInputSystem';
|
||||
export { NetworkSyncSystem } from './systems/NetworkSyncSystem'
|
||||
export type { SyncMessage } from './systems/NetworkSyncSystem'
|
||||
export { NetworkSpawnSystem } from './systems/NetworkSpawnSystem'
|
||||
export type { PrefabFactory, SpawnMessage, DespawnMessage } from './systems/NetworkSpawnSystem'
|
||||
export { NetworkInputSystem } from './systems/NetworkInputSystem'
|
||||
|
||||
// ============================================================================
|
||||
// State Sync | 状态同步
|
||||
@@ -73,8 +95,8 @@ export type {
|
||||
ITransformState,
|
||||
ITransformStateWithVelocity,
|
||||
ISnapshotBufferConfig,
|
||||
ISnapshotBuffer
|
||||
} from './sync';
|
||||
ISnapshotBuffer,
|
||||
} from './sync'
|
||||
|
||||
export type {
|
||||
IInterpolator,
|
||||
@@ -82,8 +104,8 @@ export type {
|
||||
IInputSnapshot,
|
||||
IPredictedState,
|
||||
IPredictor,
|
||||
ClientPredictionConfig
|
||||
} from './sync';
|
||||
ClientPredictionConfig,
|
||||
} from './sync'
|
||||
|
||||
export {
|
||||
lerp,
|
||||
@@ -96,8 +118,8 @@ export {
|
||||
createTransformInterpolator,
|
||||
createHermiteTransformInterpolator,
|
||||
ClientPrediction,
|
||||
createClientPrediction
|
||||
} from './sync';
|
||||
createClientPrediction,
|
||||
} from './sync'
|
||||
|
||||
// ============================================================================
|
||||
// Blueprint Nodes | 蓝图节点
|
||||
@@ -114,5 +136,5 @@ export {
|
||||
HasAuthorityExecutor,
|
||||
GetNetworkIdExecutor,
|
||||
GetLocalPlayerIdExecutor,
|
||||
NetworkNodeDefinitions
|
||||
} from './nodes';
|
||||
NetworkNodeDefinitions,
|
||||
} from './nodes'
|
||||
|
||||
180
packages/framework/network/src/protocol.ts
Normal file
180
packages/framework/network/src/protocol.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @zh 游戏网络协议定义
|
||||
* @en Game Network Protocol Definition
|
||||
*
|
||||
* @zh 定义客户端与服务器之间的通信协议
|
||||
* @en Defines the communication protocol between client and server
|
||||
*/
|
||||
|
||||
import { rpc } from '@esengine/rpc'
|
||||
|
||||
// ============================================================================
|
||||
// Message Types | 消息类型
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 玩家输入
|
||||
* @en Player input
|
||||
*/
|
||||
export interface PlayerInput {
|
||||
/**
|
||||
* @zh 帧序号
|
||||
* @en Frame number
|
||||
*/
|
||||
frame: number
|
||||
|
||||
/**
|
||||
* @zh 移动方向
|
||||
* @en Move direction
|
||||
*/
|
||||
moveDir?: { x: number; y: number }
|
||||
|
||||
/**
|
||||
* @zh 动作列表
|
||||
* @en Action list
|
||||
*/
|
||||
actions?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 实体同步状态
|
||||
* @en Entity sync state
|
||||
*/
|
||||
export interface EntitySyncState {
|
||||
netId: number
|
||||
pos?: { x: number; y: number }
|
||||
rot?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 同步消息
|
||||
* @en Sync message
|
||||
*/
|
||||
export interface SyncData {
|
||||
/**
|
||||
* @zh 服务器帧号
|
||||
* @en Server frame number
|
||||
*/
|
||||
frame: number
|
||||
|
||||
/**
|
||||
* @zh 实体状态列表
|
||||
* @en Entity state list
|
||||
*/
|
||||
entities: EntitySyncState[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成消息
|
||||
* @en Spawn message
|
||||
*/
|
||||
export interface SpawnData {
|
||||
netId: number
|
||||
ownerId: number
|
||||
prefab: string
|
||||
pos: { x: number; y: number }
|
||||
rot?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 销毁消息
|
||||
* @en Despawn message
|
||||
*/
|
||||
export interface DespawnData {
|
||||
netId: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Types | API 类型
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 加入房间请求
|
||||
* @en Join room request
|
||||
*/
|
||||
export interface JoinRequest {
|
||||
playerName: string
|
||||
roomId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加入房间响应
|
||||
* @en Join room response
|
||||
*/
|
||||
export interface JoinResponse {
|
||||
playerId: number
|
||||
roomId: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Protocol Definition | 协议定义
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 默认游戏网络协议
|
||||
* @en Default game network protocol
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用默认协议
|
||||
* const service = new NetworkService(gameProtocol)
|
||||
*
|
||||
* // 或者扩展协议
|
||||
* const customProtocol = rpc.define({
|
||||
* api: {
|
||||
* ...gameProtocol.api,
|
||||
* customApi: rpc.api<CustomInput, CustomOutput>(),
|
||||
* },
|
||||
* msg: {
|
||||
* ...gameProtocol.msg,
|
||||
* customMsg: rpc.msg<CustomData>(),
|
||||
* },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const gameProtocol = rpc.define({
|
||||
api: {
|
||||
/**
|
||||
* @zh 加入房间
|
||||
* @en Join room
|
||||
*/
|
||||
join: rpc.api<JoinRequest, JoinResponse>(),
|
||||
|
||||
/**
|
||||
* @zh 离开房间
|
||||
* @en Leave room
|
||||
*/
|
||||
leave: rpc.api<void, void>(),
|
||||
},
|
||||
msg: {
|
||||
/**
|
||||
* @zh 玩家输入
|
||||
* @en Player input
|
||||
*/
|
||||
input: rpc.msg<PlayerInput>(),
|
||||
|
||||
/**
|
||||
* @zh 状态同步
|
||||
* @en State sync
|
||||
*/
|
||||
sync: rpc.msg<SyncData>(),
|
||||
|
||||
/**
|
||||
* @zh 实体生成
|
||||
* @en Entity spawn
|
||||
*/
|
||||
spawn: rpc.msg<SpawnData>(),
|
||||
|
||||
/**
|
||||
* @zh 实体销毁
|
||||
* @en Entity despawn
|
||||
*/
|
||||
despawn: rpc.msg<DespawnData>(),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* @zh 游戏协议类型
|
||||
* @en Game protocol type
|
||||
*/
|
||||
export type GameProtocol = typeof gameProtocol
|
||||
@@ -1,172 +1,274 @@
|
||||
import { WsClient } from 'tsrpc-browser';
|
||||
/**
|
||||
* @zh 网络服务模块
|
||||
* @en Network Service Module
|
||||
*/
|
||||
|
||||
import {
|
||||
serviceProto,
|
||||
type ServiceType,
|
||||
type MsgSync,
|
||||
type MsgSpawn,
|
||||
type MsgDespawn,
|
||||
type IPlayerInput
|
||||
} from '@esengine/network-protocols';
|
||||
RpcClient,
|
||||
type ProtocolDef,
|
||||
type ApiNames,
|
||||
type MsgNames,
|
||||
type ApiInput,
|
||||
type ApiOutput,
|
||||
type MsgData,
|
||||
type RpcClientOptions,
|
||||
} from '@esengine/rpc/client'
|
||||
import { gameProtocol, type GameProtocol, type PlayerInput } from '../protocol'
|
||||
|
||||
// ============================================================================
|
||||
// Types | 类型定义
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 连接状态
|
||||
* Connection state
|
||||
* @zh 连接状态
|
||||
* @en Connection state
|
||||
*/
|
||||
export const enum ENetworkState {
|
||||
export const enum NetworkState {
|
||||
Disconnected = 0,
|
||||
Connecting = 1,
|
||||
Connected = 2
|
||||
Connected = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络事件回调
|
||||
* Network event callbacks
|
||||
* @zh 网络服务配置
|
||||
* @en Network service options
|
||||
*/
|
||||
export interface INetworkCallbacks {
|
||||
onConnected?: (clientId: number, roomId: string) => void;
|
||||
onDisconnected?: () => void;
|
||||
onSync?: (msg: MsgSync) => void;
|
||||
onSpawn?: (msg: MsgSpawn) => void;
|
||||
onDespawn?: (msg: MsgDespawn) => void;
|
||||
onError?: (error: Error) => void;
|
||||
export interface NetworkServiceOptions extends RpcClientOptions {
|
||||
/**
|
||||
* @zh 服务器地址
|
||||
* @en Server URL
|
||||
*/
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 TSRPC 客户端
|
||||
* Create TSRPC client
|
||||
*/
|
||||
function createClient(serverUrl: string): WsClient<ServiceType> {
|
||||
return new WsClient(serviceProto, {
|
||||
server: serverUrl,
|
||||
json: true,
|
||||
logLevel: 'warn'
|
||||
});
|
||||
}
|
||||
// ============================================================================
|
||||
// RpcService - Base Class | RPC 服务基类
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 网络服务
|
||||
* Network service
|
||||
* @zh RPC 服务基类
|
||||
* @en RPC Service base class
|
||||
*
|
||||
* 基于 TSRPC 的网络服务封装,提供类型安全的网络通信。
|
||||
* TSRPC-based network service wrapper with type-safe communication.
|
||||
* @zh 纯粹的 RPC 客户端封装,不包含任何游戏特定逻辑
|
||||
* @en Pure RPC client wrapper without any game-specific logic
|
||||
*
|
||||
* @typeParam P - @zh 协议定义类型 @en Protocol definition type
|
||||
*/
|
||||
export class NetworkService {
|
||||
private _client: WsClient<ServiceType> | null = null;
|
||||
private _state: ENetworkState = ENetworkState.Disconnected;
|
||||
private _clientId: number = 0;
|
||||
private _roomId: string = '';
|
||||
private _callbacks: INetworkCallbacks = {};
|
||||
export class RpcService<P extends ProtocolDef> {
|
||||
protected _client: RpcClient<P> | null = null
|
||||
protected _state: NetworkState = NetworkState.Disconnected
|
||||
|
||||
get state(): ENetworkState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
get clientId(): number {
|
||||
return this._clientId;
|
||||
}
|
||||
|
||||
get roomId(): string {
|
||||
return this._roomId;
|
||||
constructor(protected readonly _protocol: P) {}
|
||||
|
||||
/**
|
||||
* @zh 获取连接状态
|
||||
* @en Get connection state
|
||||
*/
|
||||
get state(): NetworkState {
|
||||
return this._state
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否已连接
|
||||
* @en Whether connected
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this._state === ENetworkState.Connected;
|
||||
return this._state === NetworkState.Connected
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置回调
|
||||
* Set callbacks
|
||||
* @zh 获取底层 RPC 客户端
|
||||
* @en Get underlying RPC client
|
||||
*/
|
||||
setCallbacks(callbacks: INetworkCallbacks): void {
|
||||
this._callbacks = { ...this._callbacks, ...callbacks };
|
||||
get client(): RpcClient<P> | null {
|
||||
return this._client
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
* Connect to server
|
||||
* @zh 连接到服务器
|
||||
* @en Connect to server
|
||||
*/
|
||||
async connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean> {
|
||||
if (this._state !== ENetworkState.Disconnected) {
|
||||
return false;
|
||||
async connect(options: NetworkServiceOptions): Promise<void> {
|
||||
if (this._state !== NetworkState.Disconnected) {
|
||||
throw new Error('Already connected or connecting')
|
||||
}
|
||||
|
||||
this._state = ENetworkState.Connecting;
|
||||
this._client = createClient(serverUrl);
|
||||
this._setupListeners();
|
||||
this._state = NetworkState.Connecting
|
||||
|
||||
// 连接
|
||||
// Connect
|
||||
const connectResult = await this._client.connect();
|
||||
if (!connectResult.isSucc) {
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._callbacks.onError?.(new Error(connectResult.errMsg));
|
||||
return false;
|
||||
try {
|
||||
this._client = new RpcClient(this._protocol, options.url, {
|
||||
...options,
|
||||
onConnect: () => {
|
||||
this._state = NetworkState.Connected
|
||||
options.onConnect?.()
|
||||
},
|
||||
onDisconnect: (reason) => {
|
||||
this._state = NetworkState.Disconnected
|
||||
options.onDisconnect?.(reason)
|
||||
},
|
||||
onError: options.onError,
|
||||
})
|
||||
await this._client.connect()
|
||||
this._state = NetworkState.Connected
|
||||
} catch (err) {
|
||||
this._state = NetworkState.Disconnected
|
||||
this._client = null
|
||||
throw err
|
||||
}
|
||||
|
||||
// 加入房间
|
||||
// Join room
|
||||
const joinResult = await this._client.callApi('Join', {
|
||||
playerName,
|
||||
roomId
|
||||
});
|
||||
|
||||
if (!joinResult.isSucc) {
|
||||
await this._client.disconnect();
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._callbacks.onError?.(new Error(joinResult.err.message));
|
||||
return false;
|
||||
}
|
||||
|
||||
this._clientId = joinResult.res.clientId;
|
||||
this._roomId = joinResult.res.roomId;
|
||||
this._state = ENetworkState.Connected;
|
||||
this._callbacks.onConnected?.(this._clientId, this._roomId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
* Disconnect
|
||||
* @zh 断开连接
|
||||
* @en Disconnect
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this._client) {
|
||||
await this._client.disconnect();
|
||||
}
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._clientId = 0;
|
||||
this._roomId = '';
|
||||
this._client = null;
|
||||
disconnect(): void {
|
||||
this._client?.disconnect()
|
||||
this._client = null
|
||||
this._state = NetworkState.Disconnected
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送输入
|
||||
* Send input
|
||||
* @zh 调用 API
|
||||
* @en Call API
|
||||
*/
|
||||
sendInput(input: IPlayerInput): void {
|
||||
if (!this.isConnected || !this._client) return;
|
||||
this._client.sendMsg('Input', { input });
|
||||
call<K extends ApiNames<P>>(
|
||||
name: K,
|
||||
input: ApiInput<P['api'][K]>
|
||||
): Promise<ApiOutput<P['api'][K]>> {
|
||||
if (!this._client) {
|
||||
return Promise.reject(new Error('Not connected'))
|
||||
}
|
||||
return this._client.call(name, input)
|
||||
}
|
||||
|
||||
private _setupListeners(): void {
|
||||
if (!this._client) return;
|
||||
/**
|
||||
* @zh 发送消息
|
||||
* @en Send message
|
||||
*/
|
||||
send<K extends MsgNames<P>>(name: K, data: MsgData<P['msg'][K]>): void {
|
||||
this._client?.send(name, data)
|
||||
}
|
||||
|
||||
this._client.listenMsg('Sync', (msg) => {
|
||||
this._callbacks.onSync?.(msg);
|
||||
});
|
||||
/**
|
||||
* @zh 监听消息
|
||||
* @en Listen to message
|
||||
*/
|
||||
on<K extends MsgNames<P>>(
|
||||
name: K,
|
||||
handler: (data: MsgData<P['msg'][K]>) => void
|
||||
): this {
|
||||
this._client?.on(name, handler)
|
||||
return this
|
||||
}
|
||||
|
||||
this._client.listenMsg('Spawn', (msg) => {
|
||||
this._callbacks.onSpawn?.(msg);
|
||||
});
|
||||
/**
|
||||
* @zh 取消监听消息
|
||||
* @en Remove message listener
|
||||
*/
|
||||
off<K extends MsgNames<P>>(
|
||||
name: K,
|
||||
handler?: (data: MsgData<P['msg'][K]>) => void
|
||||
): this {
|
||||
this._client?.off(name, handler)
|
||||
return this
|
||||
}
|
||||
|
||||
this._client.listenMsg('Despawn', (msg) => {
|
||||
this._callbacks.onDespawn?.(msg);
|
||||
});
|
||||
|
||||
this._client.flows.postDisconnectFlow.push((v) => {
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._callbacks.onDisconnected?.();
|
||||
return v;
|
||||
});
|
||||
/**
|
||||
* @zh 监听消息(只触发一次)
|
||||
* @en Listen to message (once)
|
||||
*/
|
||||
once<K extends MsgNames<P>>(
|
||||
name: K,
|
||||
handler: (data: MsgData<P['msg'][K]>) => void
|
||||
): this {
|
||||
this._client?.once(name, handler)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GameNetworkService - Game-specific Class | 游戏网络服务
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 游戏网络服务
|
||||
* @en Game network service
|
||||
*
|
||||
* @zh 基于默认游戏协议的网络服务,提供游戏特定的便捷方法
|
||||
* @en Network service based on default game protocol with game-specific convenience methods
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const network = new GameNetworkService()
|
||||
* await network.connect({ url: 'ws://localhost:3000' })
|
||||
*
|
||||
* // 游戏特定的便捷方法
|
||||
* network.sendInput({ frame: 1, moveDir: { x: 1, y: 0 } })
|
||||
*
|
||||
* network.onSync((data) => {
|
||||
* for (const entity of data.entities) {
|
||||
* // 更新实体状态
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class GameNetworkService extends RpcService<GameProtocol> {
|
||||
constructor() {
|
||||
super(gameProtocol)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 发送玩家输入
|
||||
* @en Send player input
|
||||
*/
|
||||
sendInput(input: PlayerInput): void {
|
||||
this.send('input', input)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 监听状态同步
|
||||
* @en Listen to state sync
|
||||
*/
|
||||
onSync(handler: (data: MsgData<GameProtocol['msg']['sync']>) => void): this {
|
||||
return this.on('sync', handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 监听实体生成
|
||||
* @en Listen to entity spawn
|
||||
*/
|
||||
onSpawn(handler: (data: MsgData<GameProtocol['msg']['spawn']>) => void): this {
|
||||
return this.on('spawn', handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 监听实体销毁
|
||||
* @en Listen to entity despawn
|
||||
*/
|
||||
onDespawn(handler: (data: MsgData<GameProtocol['msg']['despawn']>) => void): this {
|
||||
return this.on('despawn', handler)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Exports & Factories | 导出与工厂函数
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 网络服务(GameNetworkService 的别名)
|
||||
* @en Network service (alias for GameNetworkService)
|
||||
*/
|
||||
export { GameNetworkService as NetworkService }
|
||||
|
||||
/**
|
||||
* @zh 创建网络服务
|
||||
* @en Create network service
|
||||
*/
|
||||
export function createNetworkService(): GameNetworkService
|
||||
export function createNetworkService<P extends ProtocolDef>(protocol: P): RpcService<P>
|
||||
export function createNetworkService<P extends ProtocolDef>(protocol?: P): RpcService<P> | GameNetworkService {
|
||||
if (protocol) {
|
||||
return new RpcService(protocol)
|
||||
}
|
||||
return new GameNetworkService()
|
||||
}
|
||||
|
||||
@@ -1,73 +1,74 @@
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import type { IPlayerInput } from '@esengine/network-protocols';
|
||||
import type { NetworkService } from '../services/NetworkService';
|
||||
/**
|
||||
* @zh 网络输入系统
|
||||
* @en Network Input System
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework'
|
||||
import type { PlayerInput } from '../protocol'
|
||||
import type { NetworkService } from '../services/NetworkService'
|
||||
|
||||
/**
|
||||
* 网络输入系统
|
||||
* Network input system
|
||||
* @zh 网络输入系统
|
||||
* @en Network input system
|
||||
*
|
||||
* 收集本地玩家输入并发送到服务器。
|
||||
* Collects local player input and sends to server.
|
||||
* @zh 收集本地玩家输入并发送到服务器
|
||||
* @en Collects local player input and sends to server
|
||||
*/
|
||||
export class NetworkInputSystem extends EntitySystem {
|
||||
private _networkService: NetworkService;
|
||||
private _frame: number = 0;
|
||||
private _inputQueue: IPlayerInput[] = [];
|
||||
private _networkService: NetworkService
|
||||
private _frame: number = 0
|
||||
private _inputQueue: PlayerInput[] = []
|
||||
|
||||
constructor(networkService: NetworkService) {
|
||||
// 不查询任何实体,此系统只处理输入
|
||||
// Don't query any entities, this system only handles input
|
||||
super(Matcher.nothing());
|
||||
this._networkService = networkService;
|
||||
super(Matcher.nothing())
|
||||
this._networkService = networkService
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理输入队列
|
||||
* Process input queue
|
||||
* @zh 处理输入队列
|
||||
* @en Process input queue
|
||||
*/
|
||||
protected override process(): void {
|
||||
if (!this._networkService.isConnected) return;
|
||||
if (!this._networkService.isConnected) return
|
||||
|
||||
this._frame++;
|
||||
this._frame++
|
||||
|
||||
// 发送队列中的输入
|
||||
// Send queued inputs
|
||||
while (this._inputQueue.length > 0) {
|
||||
const input = this._inputQueue.shift()!;
|
||||
input.frame = this._frame;
|
||||
this._networkService.sendInput(input);
|
||||
const input = this._inputQueue.shift()!
|
||||
input.frame = this._frame
|
||||
this._networkService.sendInput(input)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加移动输入
|
||||
* Add move input
|
||||
* @zh 添加移动输入
|
||||
* @en Add move input
|
||||
*/
|
||||
public addMoveInput(x: number, y: number): void {
|
||||
this._inputQueue.push({
|
||||
frame: 0,
|
||||
moveDir: { x, y }
|
||||
});
|
||||
moveDir: { x, y },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加动作输入
|
||||
* Add action input
|
||||
* @zh 添加动作输入
|
||||
* @en Add action input
|
||||
*/
|
||||
public addActionInput(action: string): void {
|
||||
const lastInput = this._inputQueue[this._inputQueue.length - 1];
|
||||
const lastInput = this._inputQueue[this._inputQueue.length - 1]
|
||||
if (lastInput) {
|
||||
lastInput.actions = lastInput.actions || [];
|
||||
lastInput.actions.push(action);
|
||||
lastInput.actions = lastInput.actions || []
|
||||
lastInput.actions.push(action)
|
||||
} else {
|
||||
this._inputQueue.push({
|
||||
frame: 0,
|
||||
actions: [action]
|
||||
});
|
||||
actions: [action],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._inputQueue.length = 0;
|
||||
this._inputQueue.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,123 @@
|
||||
import { EntitySystem, Entity, type Scene, Matcher } from '@esengine/ecs-framework';
|
||||
import type { MsgSpawn, MsgDespawn } from '@esengine/network-protocols';
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity';
|
||||
import { NetworkTransform } from '../components/NetworkTransform';
|
||||
import type { NetworkService } from '../services/NetworkService';
|
||||
import type { NetworkSyncSystem } from './NetworkSyncSystem';
|
||||
import { EntitySystem, Entity, type Scene, Matcher } from '@esengine/ecs-framework'
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity'
|
||||
import { NetworkTransform } from '../components/NetworkTransform'
|
||||
import type { NetworkSyncSystem } from './NetworkSyncSystem'
|
||||
|
||||
/**
|
||||
* 预制体工厂函数类型
|
||||
* Prefab factory function type
|
||||
* @zh 生成消息接口
|
||||
* @en Spawn message interface
|
||||
*/
|
||||
export type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||
export interface SpawnMessage {
|
||||
netId: number
|
||||
ownerId: number
|
||||
prefab: string
|
||||
pos: { x: number; y: number }
|
||||
rot?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络生成系统
|
||||
* Network spawn system
|
||||
* @zh 销毁消息接口
|
||||
* @en Despawn message interface
|
||||
*/
|
||||
export interface DespawnMessage {
|
||||
netId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 预制体工厂函数类型
|
||||
* @en Prefab factory function type
|
||||
*/
|
||||
export type PrefabFactory = (scene: Scene, spawn: SpawnMessage) => Entity
|
||||
|
||||
/**
|
||||
* @zh 网络生成系统
|
||||
* @en Network spawn system
|
||||
*
|
||||
* 处理网络实体的生成和销毁。
|
||||
* Handles spawning and despawning of networked entities.
|
||||
* @zh 处理网络实体的生成和销毁
|
||||
* @en Handles spawning and despawning of networked entities
|
||||
*/
|
||||
export class NetworkSpawnSystem extends EntitySystem {
|
||||
private _networkService: NetworkService;
|
||||
private _syncSystem: NetworkSyncSystem;
|
||||
private _prefabFactories: Map<string, PrefabFactory> = new Map();
|
||||
private _syncSystem: NetworkSyncSystem
|
||||
private _prefabFactories: Map<string, PrefabFactory> = new Map()
|
||||
private _localPlayerId: number = 0
|
||||
|
||||
constructor(networkService: NetworkService, syncSystem: NetworkSyncSystem) {
|
||||
// 不查询任何实体,此系统只响应网络消息
|
||||
// Don't query any entities, this system only responds to network messages
|
||||
super(Matcher.nothing());
|
||||
this._networkService = networkService;
|
||||
this._syncSystem = syncSystem;
|
||||
}
|
||||
|
||||
protected override onInitialize(): void {
|
||||
this._networkService.setCallbacks({
|
||||
onSpawn: this._handleSpawn.bind(this),
|
||||
onDespawn: this._handleDespawn.bind(this)
|
||||
});
|
||||
constructor(syncSystem: NetworkSyncSystem) {
|
||||
super(Matcher.nothing())
|
||||
this._syncSystem = syncSystem
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册预制体工厂
|
||||
* Register prefab factory
|
||||
* @zh 设置本地玩家 ID
|
||||
* @en Set local player ID
|
||||
*/
|
||||
public registerPrefab(prefabType: string, factory: PrefabFactory): void {
|
||||
this._prefabFactories.set(prefabType, factory);
|
||||
setLocalPlayerId(id: number): void {
|
||||
this._localPlayerId = id
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销预制体工厂
|
||||
* Unregister prefab factory
|
||||
* @zh 处理生成消息
|
||||
* @en Handle spawn message
|
||||
*/
|
||||
public unregisterPrefab(prefabType: string): void {
|
||||
this._prefabFactories.delete(prefabType);
|
||||
}
|
||||
handleSpawn(msg: SpawnMessage): Entity | null {
|
||||
if (!this.scene) return null
|
||||
|
||||
private _handleSpawn(msg: MsgSpawn): void {
|
||||
if (!this.scene) return;
|
||||
|
||||
const factory = this._prefabFactories.get(msg.prefab);
|
||||
const factory = this._prefabFactories.get(msg.prefab)
|
||||
if (!factory) {
|
||||
this.logger.warn(`Unknown prefab: ${msg.prefab}`);
|
||||
return;
|
||||
this.logger.warn(`Unknown prefab: ${msg.prefab}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const entity = factory(this.scene, msg);
|
||||
const entity = factory(this.scene, msg)
|
||||
|
||||
// 添加网络组件
|
||||
// Add network components
|
||||
const identity = entity.addComponent(new NetworkIdentity());
|
||||
identity.netId = msg.netId;
|
||||
identity.ownerId = msg.ownerId;
|
||||
identity.prefabType = msg.prefab;
|
||||
identity.bHasAuthority = msg.ownerId === this._networkService.clientId;
|
||||
identity.bIsLocalPlayer = identity.bHasAuthority;
|
||||
const identity = entity.addComponent(new NetworkIdentity())
|
||||
identity.netId = msg.netId
|
||||
identity.ownerId = msg.ownerId
|
||||
identity.prefabType = msg.prefab
|
||||
identity.bHasAuthority = msg.ownerId === this._localPlayerId
|
||||
identity.bIsLocalPlayer = identity.bHasAuthority
|
||||
|
||||
const transform = entity.addComponent(new NetworkTransform());
|
||||
transform.setTarget(msg.pos.x, msg.pos.y, msg.rot);
|
||||
transform.snap();
|
||||
const transform = entity.addComponent(new NetworkTransform())
|
||||
transform.setTarget(msg.pos.x, msg.pos.y, msg.rot ?? 0)
|
||||
transform.snap()
|
||||
|
||||
// 注册到同步系统
|
||||
// Register to sync system
|
||||
this._syncSystem.registerEntity(msg.netId, entity.id);
|
||||
this._syncSystem.registerEntity(msg.netId, entity.id)
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
private _handleDespawn(msg: MsgDespawn): void {
|
||||
const entityId = this._syncSystem.getEntityId(msg.netId);
|
||||
if (entityId === undefined) return;
|
||||
/**
|
||||
* @zh 处理销毁消息
|
||||
* @en Handle despawn message
|
||||
*/
|
||||
handleDespawn(msg: DespawnMessage): void {
|
||||
const entityId = this._syncSystem.getEntityId(msg.netId)
|
||||
if (entityId === undefined) return
|
||||
|
||||
const entity = this.scene?.findEntityById(entityId);
|
||||
const entity = this.scene?.findEntityById(entityId)
|
||||
if (entity) {
|
||||
entity.destroy();
|
||||
entity.destroy()
|
||||
}
|
||||
|
||||
this._syncSystem.unregisterEntity(msg.netId);
|
||||
this._syncSystem.unregisterEntity(msg.netId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注册预制体工厂
|
||||
* @en Register prefab factory
|
||||
*/
|
||||
registerPrefab(prefabType: string, factory: PrefabFactory): void {
|
||||
this._prefabFactories.set(prefabType, factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注销预制体工厂
|
||||
* @en Unregister prefab factory
|
||||
*/
|
||||
unregisterPrefab(prefabType: string): void {
|
||||
this._prefabFactories.delete(prefabType)
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._prefabFactories.clear();
|
||||
this._prefabFactories.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +1,102 @@
|
||||
import { EntitySystem, Matcher, Time, type Entity } from '@esengine/ecs-framework';
|
||||
import type { MsgSync } from '@esengine/network-protocols';
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity';
|
||||
import { NetworkTransform } from '../components/NetworkTransform';
|
||||
import type { NetworkService } from '../services/NetworkService';
|
||||
import { EntitySystem, Matcher, Time, type Entity } from '@esengine/ecs-framework'
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity'
|
||||
import { NetworkTransform } from '../components/NetworkTransform'
|
||||
|
||||
/**
|
||||
* 网络同步系统
|
||||
* Network sync system
|
||||
* @zh 同步消息接口
|
||||
* @en Sync message interface
|
||||
*/
|
||||
export interface SyncMessage {
|
||||
entities: Array<{
|
||||
netId: number
|
||||
pos?: { x: number; y: number }
|
||||
rot?: number
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 网络同步系统
|
||||
* @en Network sync system
|
||||
*
|
||||
* 处理网络实体的状态同步和插值。
|
||||
* Handles state synchronization and interpolation for networked entities.
|
||||
* @zh 处理网络实体的状态同步和插值
|
||||
* @en Handles state synchronization and interpolation for networked entities
|
||||
*/
|
||||
export class NetworkSyncSystem extends EntitySystem {
|
||||
private _networkService: NetworkService;
|
||||
private _netIdToEntity: Map<number, number> = new Map();
|
||||
private _netIdToEntity: Map<number, number> = new Map()
|
||||
|
||||
constructor(networkService: NetworkService) {
|
||||
super(Matcher.all(NetworkIdentity, NetworkTransform));
|
||||
this._networkService = networkService;
|
||||
}
|
||||
|
||||
protected override onInitialize(): void {
|
||||
this._networkService.setCallbacks({
|
||||
onSync: this._handleSync.bind(this)
|
||||
});
|
||||
constructor() {
|
||||
super(Matcher.all(NetworkIdentity, NetworkTransform))
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理实体列表
|
||||
* Process entities
|
||||
* @zh 处理同步消息
|
||||
* @en Handle sync message
|
||||
*/
|
||||
handleSync(msg: SyncMessage): void {
|
||||
for (const state of msg.entities) {
|
||||
const entityId = this._netIdToEntity.get(state.netId)
|
||||
if (entityId === undefined) continue
|
||||
|
||||
const entity = this.scene?.findEntityById(entityId)
|
||||
if (!entity) continue
|
||||
|
||||
const transform = entity.getComponent(NetworkTransform)
|
||||
if (transform && state.pos) {
|
||||
transform.setTarget(state.pos.x, state.pos.y, state.rot ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
const deltaTime = Time.deltaTime
|
||||
|
||||
for (const entity of entities) {
|
||||
const transform = this.requireComponent(entity, NetworkTransform);
|
||||
const identity = this.requireComponent(entity, NetworkIdentity);
|
||||
const transform = this.requireComponent(entity, NetworkTransform)
|
||||
const identity = this.requireComponent(entity, NetworkIdentity)
|
||||
|
||||
// 只有非本地玩家需要插值
|
||||
// Only non-local players need interpolation
|
||||
if (!identity.bHasAuthority && transform.bInterpolate) {
|
||||
this._interpolate(transform, deltaTime);
|
||||
this._interpolate(transform, deltaTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册网络实体
|
||||
* Register network entity
|
||||
* @zh 注册网络实体
|
||||
* @en Register network entity
|
||||
*/
|
||||
public registerEntity(netId: number, entityId: number): void {
|
||||
this._netIdToEntity.set(netId, entityId);
|
||||
registerEntity(netId: number, entityId: number): void {
|
||||
this._netIdToEntity.set(netId, entityId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销网络实体
|
||||
* Unregister network entity
|
||||
* @zh 注销网络实体
|
||||
* @en Unregister network entity
|
||||
*/
|
||||
public unregisterEntity(netId: number): void {
|
||||
this._netIdToEntity.delete(netId);
|
||||
unregisterEntity(netId: number): void {
|
||||
this._netIdToEntity.delete(netId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据网络 ID 获取实体 ID
|
||||
* Get entity ID by network ID
|
||||
* @zh 根据网络 ID 获取实体 ID
|
||||
* @en Get entity ID by network ID
|
||||
*/
|
||||
public getEntityId(netId: number): number | undefined {
|
||||
return this._netIdToEntity.get(netId);
|
||||
}
|
||||
|
||||
private _handleSync(msg: MsgSync): void {
|
||||
for (const state of msg.entities) {
|
||||
const entityId = this._netIdToEntity.get(state.netId);
|
||||
if (entityId === undefined) continue;
|
||||
|
||||
const entity = this.scene?.findEntityById(entityId);
|
||||
if (!entity) continue;
|
||||
|
||||
const transform = entity.getComponent(NetworkTransform);
|
||||
if (transform && state.pos) {
|
||||
transform.setTarget(state.pos.x, state.pos.y, state.rot);
|
||||
}
|
||||
}
|
||||
getEntityId(netId: number): number | undefined {
|
||||
return this._netIdToEntity.get(netId)
|
||||
}
|
||||
|
||||
private _interpolate(transform: NetworkTransform, deltaTime: number): void {
|
||||
const t = Math.min(1, transform.lerpSpeed * deltaTime);
|
||||
const t = Math.min(1, transform.lerpSpeed * deltaTime)
|
||||
|
||||
transform.currentX += (transform.targetX - transform.currentX) * t;
|
||||
transform.currentY += (transform.targetY - transform.currentY) * t;
|
||||
transform.currentX += (transform.targetX - transform.currentX) * t
|
||||
transform.currentY += (transform.targetY - transform.currentY) * t
|
||||
|
||||
// 角度插值需要处理环绕
|
||||
// Angle interpolation needs to handle wrap-around
|
||||
let angleDiff = transform.targetRotation - transform.currentRotation;
|
||||
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
|
||||
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
|
||||
transform.currentRotation += angleDiff * t;
|
||||
let angleDiff = transform.targetRotation - transform.currentRotation
|
||||
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2
|
||||
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2
|
||||
transform.currentRotation += angleDiff * t
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._netIdToEntity.clear();
|
||||
this._netIdToEntity.clear()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user