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:
YHH
2025-12-28 10:54:51 +08:00
committed by GitHub
parent 8605888f11
commit 7940f581a6
39 changed files with 3505 additions and 784 deletions

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}