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:
@@ -251,7 +251,14 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '网络',
|
||||
label: 'RPC 通信',
|
||||
translations: { en: 'RPC' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/rpc', translations: { en: 'Overview' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '网络同步',
|
||||
translations: { en: 'Network' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/network', translations: { en: 'Overview' } },
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
---
|
||||
title: "Network System"
|
||||
description: "TSRPC-based multiplayer game network synchronization solution"
|
||||
description: "Type-safe multiplayer game network synchronization based on @esengine/rpc"
|
||||
---
|
||||
|
||||
`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation.
|
||||
`@esengine/network` provides a type-safe network synchronization solution based on `@esengine/rpc` for multiplayer games, including entity synchronization, input handling, and state interpolation.
|
||||
|
||||
## Overview
|
||||
|
||||
The network module consists of three packages:
|
||||
The network module consists of two core packages:
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@esengine/network` | Client-side ECS plugin |
|
||||
| `@esengine/network-protocols` | Shared protocol definitions |
|
||||
| `@esengine/network-server` | Server-side implementation |
|
||||
| `@esengine/rpc` | Type-safe RPC communication library |
|
||||
| `@esengine/network` | RPC-based game networking plugin |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Client
|
||||
npm install @esengine/network
|
||||
|
||||
# Server
|
||||
npm install @esengine/network-server
|
||||
```
|
||||
|
||||
> `@esengine/rpc` is automatically installed as a dependency.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Client Server
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ NetworkPlugin │◄──── WS ────► │ GameServer │
|
||||
│ ├─ Service │ │ ├─ Room │
|
||||
│ ├─ SyncSystem │ │ └─ Players │
|
||||
│ ├─ SpawnSystem │ └────────────────┘
|
||||
│ └─ InputSystem │
|
||||
└────────────────┘
|
||||
┌────────────────────┐ ┌────────────────┐
|
||||
│ NetworkPlugin │◄── WS ───►│ RpcServer │
|
||||
│ ├─ NetworkService │ │ ├─ Protocol │
|
||||
│ ├─ SyncSystem │ │ └─ Handlers │
|
||||
│ ├─ SpawnSystem │ └────────────────┘
|
||||
│ └─ InputSystem │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
@@ -69,61 +66,81 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||
const identity = entity.addComponent(new NetworkIdentity());
|
||||
identity.netId = spawn.netId;
|
||||
identity.ownerId = spawn.ownerId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
|
||||
|
||||
entity.addComponent(new NetworkTransform());
|
||||
return entity;
|
||||
});
|
||||
|
||||
// Connect to server
|
||||
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
||||
const success = await networkPlugin.connect({
|
||||
url: 'ws://localhost:3000',
|
||||
playerName: 'Player1',
|
||||
roomId: 'room-1' // optional
|
||||
});
|
||||
|
||||
if (success) {
|
||||
console.log('Connected!');
|
||||
console.log('Connected! Player ID:', networkPlugin.localPlayerId);
|
||||
}
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
```typescript
|
||||
import { GameServer } from '@esengine/network-server';
|
||||
import { RpcServer } from '@esengine/rpc/server';
|
||||
import { gameProtocol } from '@esengine/network';
|
||||
|
||||
const server = new GameServer({
|
||||
const server = new RpcServer(gameProtocol, {
|
||||
port: 3000,
|
||||
roomConfig: {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
}
|
||||
onStart: (port) => console.log(`Server running on ws://localhost:${port}`)
|
||||
});
|
||||
|
||||
// Register API handlers
|
||||
server.handle('join', async (input, ctx) => {
|
||||
const playerId = generatePlayerId();
|
||||
return { playerId, roomId: input.roomId ?? 'default' };
|
||||
});
|
||||
|
||||
server.handle('leave', async (input, ctx) => {
|
||||
// Handle player leaving
|
||||
});
|
||||
|
||||
await server.start();
|
||||
console.log('Server started on ws://localhost:3000');
|
||||
```
|
||||
|
||||
## Quick Setup with CLI
|
||||
## Custom Protocol
|
||||
|
||||
We recommend using ESEngine CLI to quickly create a complete game server project:
|
||||
You can define your own protocol using `@esengine/rpc`:
|
||||
|
||||
```bash
|
||||
mkdir my-game-server && cd my-game-server
|
||||
npm init -y
|
||||
npx @esengine/cli init -p nodejs
|
||||
```
|
||||
```typescript
|
||||
import { rpc } from '@esengine/rpc';
|
||||
|
||||
Generated project structure:
|
||||
// Define custom protocol
|
||||
export const myProtocol = rpc.define({
|
||||
api: {
|
||||
login: rpc.api<{ username: string }, { token: string }>(),
|
||||
getData: rpc.api<{ id: number }, { data: object }>(),
|
||||
},
|
||||
msg: {
|
||||
chat: rpc.msg<{ from: string; text: string }>(),
|
||||
notification: rpc.msg<{ type: string; content: string }>(),
|
||||
},
|
||||
});
|
||||
|
||||
```
|
||||
my-game-server/
|
||||
├── src/
|
||||
│ ├── index.ts
|
||||
│ ├── server/
|
||||
│ │ └── GameServer.ts
|
||||
│ └── game/
|
||||
│ ├── Game.ts
|
||||
│ ├── scenes/
|
||||
│ ├── components/
|
||||
│ └── systems/
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
// Create service with custom protocol
|
||||
import { RpcService } from '@esengine/network';
|
||||
|
||||
const service = new RpcService(myProtocol);
|
||||
await service.connect({ url: 'ws://localhost:3000' });
|
||||
|
||||
// Type-safe API calls
|
||||
const result = await service.call('login', { username: 'test' });
|
||||
console.log(result.token);
|
||||
|
||||
// Type-safe message listening
|
||||
service.on('chat', (data) => {
|
||||
console.log(`${data.from}: ${data.text}`);
|
||||
});
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
165
docs/src/content/docs/en/modules/rpc/index.md
Normal file
165
docs/src/content/docs/en/modules/rpc/index.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "RPC Library"
|
||||
description: "Type-safe WebSocket RPC communication library"
|
||||
---
|
||||
|
||||
`@esengine/rpc` is a lightweight, type-safe RPC (Remote Procedure Call) library designed for games and real-time applications.
|
||||
|
||||
## Features
|
||||
|
||||
- **Type-Safe**: Complete TypeScript type inference for API calls and message handling
|
||||
- **Bidirectional**: Supports both request-response (API) and publish-subscribe (message) patterns
|
||||
- **Flexible Codecs**: Built-in JSON and MessagePack codecs
|
||||
- **Cross-Platform**: Works in browsers, Node.js, and WeChat Mini Games
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @esengine/rpc
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Define Protocol
|
||||
|
||||
```typescript
|
||||
import { rpc } from '@esengine/rpc';
|
||||
|
||||
// Define protocol (shared between client and server)
|
||||
export const chatProtocol = rpc.define({
|
||||
api: {
|
||||
// Request-response APIs
|
||||
login: rpc.api<{ username: string }, { userId: string; token: string }>(),
|
||||
sendMessage: rpc.api<{ text: string }, { messageId: string }>(),
|
||||
},
|
||||
msg: {
|
||||
// One-way messages (publish-subscribe)
|
||||
newMessage: rpc.msg<{ from: string; text: string; time: number }>(),
|
||||
userJoined: rpc.msg<{ username: string }>(),
|
||||
userLeft: rpc.msg<{ username: string }>(),
|
||||
},
|
||||
});
|
||||
|
||||
export type ChatProtocol = typeof chatProtocol;
|
||||
```
|
||||
|
||||
### 2. Create Client
|
||||
|
||||
```typescript
|
||||
import { RpcClient } from '@esengine/rpc/client';
|
||||
import { chatProtocol } from './protocol';
|
||||
|
||||
// Create client
|
||||
const client = new RpcClient(chatProtocol, 'ws://localhost:3000', {
|
||||
onConnect: () => console.log('Connected'),
|
||||
onDisconnect: (reason) => console.log('Disconnected:', reason),
|
||||
onError: (error) => console.error('Error:', error),
|
||||
});
|
||||
|
||||
// Connect to server
|
||||
await client.connect();
|
||||
|
||||
// Call API (type-safe)
|
||||
const { userId, token } = await client.call('login', { username: 'player1' });
|
||||
console.log('Logged in:', userId);
|
||||
|
||||
// Listen to messages (type-safe)
|
||||
client.on('newMessage', (data) => {
|
||||
console.log(`${data.from}: ${data.text}`);
|
||||
});
|
||||
|
||||
// Send message
|
||||
await client.call('sendMessage', { text: 'Hello!' });
|
||||
```
|
||||
|
||||
### 3. Create Server
|
||||
|
||||
```typescript
|
||||
import { RpcServer } from '@esengine/rpc/server';
|
||||
import { chatProtocol } from './protocol';
|
||||
|
||||
// Create server
|
||||
const server = new RpcServer(chatProtocol, {
|
||||
port: 3000,
|
||||
onStart: (port) => console.log(`Server started: ws://localhost:${port}`),
|
||||
});
|
||||
|
||||
// Register API handlers
|
||||
server.handle('login', async (input, ctx) => {
|
||||
const userId = generateUserId();
|
||||
const token = generateToken();
|
||||
|
||||
// Broadcast user joined
|
||||
server.broadcast('userJoined', { username: input.username });
|
||||
|
||||
return { userId, token };
|
||||
});
|
||||
|
||||
server.handle('sendMessage', async (input, ctx) => {
|
||||
const messageId = generateMessageId();
|
||||
|
||||
// Broadcast new message
|
||||
server.broadcast('newMessage', {
|
||||
from: ctx.clientId,
|
||||
text: input.text,
|
||||
time: Date.now(),
|
||||
});
|
||||
|
||||
return { messageId };
|
||||
});
|
||||
|
||||
// Start server
|
||||
await server.start();
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Protocol Definition
|
||||
|
||||
A protocol is the contract between client and server:
|
||||
|
||||
```typescript
|
||||
const protocol = rpc.define({
|
||||
api: {
|
||||
// API: Request-response pattern, client initiates, server handles and returns
|
||||
methodName: rpc.api<InputType, OutputType>(),
|
||||
},
|
||||
msg: {
|
||||
// Message: Publish-subscribe pattern, either side can send, other side listens
|
||||
messageName: rpc.msg<DataType>(),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### API vs Messages
|
||||
|
||||
| Feature | API | Message |
|
||||
|---------|-----|---------|
|
||||
| Pattern | Request-Response | Publish-Subscribe |
|
||||
| Return Value | Yes (Promise) | No |
|
||||
| Confirmation | Waits for response | Fire-and-forget |
|
||||
| Use Cases | Login, queries, operations | Real-time notifications, state sync |
|
||||
|
||||
## Codecs
|
||||
|
||||
Two built-in codecs are available:
|
||||
|
||||
```typescript
|
||||
import { json, msgpack } from '@esengine/rpc/codec';
|
||||
|
||||
// JSON (default, human-readable)
|
||||
const client = new RpcClient(protocol, url, {
|
||||
codec: json(),
|
||||
});
|
||||
|
||||
// MessagePack (binary, more efficient)
|
||||
const client = new RpcClient(protocol, url, {
|
||||
codec: msgpack(),
|
||||
});
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Client API](/en/modules/rpc/client/) - Detailed RpcClient API
|
||||
- [Server API](/en/modules/rpc/server/) - Detailed RpcServer API
|
||||
- [Codecs](/en/modules/rpc/codec/) - Custom codecs
|
||||
@@ -13,10 +13,11 @@ class NetworkPlugin implements IPlugin {
|
||||
readonly version: string;
|
||||
|
||||
// 访问器
|
||||
get networkService(): NetworkService;
|
||||
get networkService(): GameNetworkService;
|
||||
get syncSystem(): NetworkSyncSystem;
|
||||
get spawnSystem(): NetworkSpawnSystem;
|
||||
get inputSystem(): NetworkInputSystem;
|
||||
get localPlayerId(): number;
|
||||
get isConnected(): boolean;
|
||||
|
||||
// 生命周期
|
||||
@@ -24,7 +25,10 @@ class NetworkPlugin implements IPlugin {
|
||||
uninstall(): void;
|
||||
|
||||
// 连接管理
|
||||
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||
connect(options: NetworkServiceOptions & {
|
||||
playerName: string;
|
||||
roomId?: string;
|
||||
}): Promise<boolean>;
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
// 预制体注册
|
||||
@@ -36,36 +40,65 @@ class NetworkPlugin implements IPlugin {
|
||||
}
|
||||
```
|
||||
|
||||
## NetworkService
|
||||
## RpcService
|
||||
|
||||
网络服务,管理 WebSocket 连接。
|
||||
通用 RPC 服务基类,支持自定义协议。
|
||||
|
||||
```typescript
|
||||
class NetworkService {
|
||||
class RpcService<P extends ProtocolDef> {
|
||||
// 访问器
|
||||
get state(): ENetworkState;
|
||||
get state(): NetworkState;
|
||||
get isConnected(): boolean;
|
||||
get clientId(): number;
|
||||
get roomId(): string;
|
||||
get client(): RpcClient<P> | null;
|
||||
|
||||
constructor(protocol: P);
|
||||
|
||||
// 连接管理
|
||||
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||
disconnect(): Promise<void>;
|
||||
connect(options: NetworkServiceOptions): Promise<void>;
|
||||
disconnect(): void;
|
||||
|
||||
// 输入发送
|
||||
sendInput(input: IPlayerInput): void;
|
||||
// RPC 调用
|
||||
call<K extends ApiNames<P>>(
|
||||
name: K,
|
||||
input: ApiInput<P['api'][K]>
|
||||
): Promise<ApiOutput<P['api'][K]>>;
|
||||
|
||||
// 回调设置
|
||||
setCallbacks(callbacks: INetworkCallbacks): void;
|
||||
// 消息发送
|
||||
send<K extends MsgNames<P>>(name: K, data: MsgData<P['msg'][K]>): void;
|
||||
|
||||
// 消息监听
|
||||
on<K extends MsgNames<P>>(name: K, handler: (data: MsgData<P['msg'][K]>) => void): this;
|
||||
off<K extends MsgNames<P>>(name: K, handler?: (data: MsgData<P['msg'][K]>) => void): this;
|
||||
once<K extends MsgNames<P>>(name: K, handler: (data: MsgData<P['msg'][K]>) => void): this;
|
||||
}
|
||||
```
|
||||
|
||||
## GameNetworkService
|
||||
|
||||
游戏网络服务,继承自 RpcService,提供游戏特定的便捷方法。
|
||||
|
||||
```typescript
|
||||
class GameNetworkService extends RpcService<GameProtocol> {
|
||||
// 发送玩家输入
|
||||
sendInput(input: PlayerInput): void;
|
||||
|
||||
// 监听状态同步(链式调用)
|
||||
onSync(handler: (data: SyncData) => void): this;
|
||||
|
||||
// 监听实体生成
|
||||
onSpawn(handler: (data: SpawnData) => void): this;
|
||||
|
||||
// 监听实体销毁
|
||||
onDespawn(handler: (data: DespawnData) => void): this;
|
||||
}
|
||||
```
|
||||
|
||||
## 枚举类型
|
||||
|
||||
### ENetworkState
|
||||
### NetworkState
|
||||
|
||||
```typescript
|
||||
const enum ENetworkState {
|
||||
const enum NetworkState {
|
||||
Disconnected = 0,
|
||||
Connecting = 1,
|
||||
Connected = 2
|
||||
@@ -74,15 +107,21 @@ const enum ENetworkState {
|
||||
|
||||
## 接口类型
|
||||
|
||||
### INetworkCallbacks
|
||||
### NetworkServiceOptions
|
||||
|
||||
```typescript
|
||||
interface INetworkCallbacks {
|
||||
onConnected?: (clientId: number, roomId: string) => void;
|
||||
onDisconnected?: () => void;
|
||||
onSync?: (msg: MsgSync) => void;
|
||||
onSpawn?: (msg: MsgSpawn) => void;
|
||||
onDespawn?: (msg: MsgDespawn) => void;
|
||||
interface NetworkServiceOptions extends RpcClientOptions {
|
||||
url: string;
|
||||
}
|
||||
```
|
||||
|
||||
### RpcClientOptions
|
||||
|
||||
```typescript
|
||||
interface RpcClientOptions {
|
||||
codec?: Codec;
|
||||
onConnect?: () => void;
|
||||
onDisconnect?: (reason?: string) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
```
|
||||
@@ -90,15 +129,15 @@ interface INetworkCallbacks {
|
||||
### PrefabFactory
|
||||
|
||||
```typescript
|
||||
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||
type PrefabFactory = (scene: Scene, spawn: SpawnData) => Entity;
|
||||
```
|
||||
|
||||
### IPlayerInput
|
||||
### PlayerInput
|
||||
|
||||
```typescript
|
||||
interface IPlayerInput {
|
||||
seq?: number;
|
||||
moveDir?: Vec2;
|
||||
interface PlayerInput {
|
||||
frame: number;
|
||||
moveDir?: { x: number; y: number };
|
||||
actions?: string[];
|
||||
}
|
||||
```
|
||||
@@ -170,86 +209,128 @@ const networkService = services.get(NetworkServiceToken);
|
||||
|
||||
## 服务器端 API
|
||||
|
||||
### GameServer
|
||||
### RpcServer
|
||||
|
||||
```typescript
|
||||
class GameServer {
|
||||
constructor(config: IGameServerConfig);
|
||||
class RpcServer<P extends ProtocolDef> {
|
||||
constructor(protocol: P, options: RpcServerOptions);
|
||||
|
||||
// 启动/停止服务器
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
stop(): void;
|
||||
|
||||
getOrCreateRoom(roomId: string): Room;
|
||||
getRoom(roomId: string): Room | undefined;
|
||||
destroyRoom(roomId: string): void;
|
||||
// 注册 API 处理器
|
||||
handle<K extends ApiNames<P>>(
|
||||
name: K,
|
||||
handler: (input: ApiInput<P['api'][K]>, ctx: RpcContext) => Promise<ApiOutput<P['api'][K]>>
|
||||
): void;
|
||||
|
||||
// 广播消息
|
||||
broadcast<K extends MsgNames<P>>(name: K, data: MsgData<P['msg'][K]>): void;
|
||||
|
||||
// 发送给特定客户端
|
||||
sendTo<K extends MsgNames<P>>(clientId: string, name: K, data: MsgData<P['msg'][K]>): void;
|
||||
}
|
||||
```
|
||||
|
||||
### Room
|
||||
### RpcServerOptions
|
||||
|
||||
```typescript
|
||||
class Room {
|
||||
readonly id: string;
|
||||
readonly playerCount: number;
|
||||
readonly isFull: boolean;
|
||||
|
||||
addPlayer(name: string, connection: Connection): IPlayer | null;
|
||||
removePlayer(clientId: number): void;
|
||||
getPlayer(clientId: number): IPlayer | undefined;
|
||||
handleInput(clientId: number, input: IPlayerInput): void;
|
||||
destroy(): void;
|
||||
interface RpcServerOptions {
|
||||
port: number;
|
||||
codec?: Codec;
|
||||
onStart?: (port: number) => void;
|
||||
onConnection?: (clientId: string) => void;
|
||||
onDisconnection?: (clientId: string, reason?: string) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### IPlayer
|
||||
### RpcContext
|
||||
|
||||
```typescript
|
||||
interface IPlayer {
|
||||
clientId: number;
|
||||
name: string;
|
||||
connection: Connection;
|
||||
netId: number;
|
||||
interface RpcContext {
|
||||
clientId: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 协议消息
|
||||
## 协议定义
|
||||
|
||||
### MsgSync
|
||||
### gameProtocol
|
||||
|
||||
默认游戏协议,包含加入/离开 API 和状态同步消息:
|
||||
|
||||
```typescript
|
||||
interface MsgSync {
|
||||
const gameProtocol = rpc.define({
|
||||
api: {
|
||||
join: rpc.api<JoinRequest, JoinResponse>(),
|
||||
leave: rpc.api<void, void>(),
|
||||
},
|
||||
msg: {
|
||||
input: rpc.msg<PlayerInput>(),
|
||||
sync: rpc.msg<SyncData>(),
|
||||
spawn: rpc.msg<SpawnData>(),
|
||||
despawn: rpc.msg<DespawnData>(),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 协议消息类型
|
||||
|
||||
### SyncData
|
||||
|
||||
```typescript
|
||||
interface SyncData {
|
||||
time: number;
|
||||
entities: IEntityState[];
|
||||
}
|
||||
```
|
||||
|
||||
### MsgSpawn
|
||||
### SpawnData
|
||||
|
||||
```typescript
|
||||
interface MsgSpawn {
|
||||
interface SpawnData {
|
||||
netId: number;
|
||||
ownerId: number;
|
||||
prefab: string;
|
||||
pos: Vec2;
|
||||
rot: number;
|
||||
position: { x: number; y: number };
|
||||
rotation: number;
|
||||
}
|
||||
```
|
||||
|
||||
### MsgDespawn
|
||||
### DespawnData
|
||||
|
||||
```typescript
|
||||
interface MsgDespawn {
|
||||
interface DespawnData {
|
||||
netId: number;
|
||||
}
|
||||
```
|
||||
|
||||
### IEntityState
|
||||
### JoinRequest
|
||||
|
||||
```typescript
|
||||
interface IEntityState {
|
||||
interface JoinRequest {
|
||||
playerName: string;
|
||||
roomId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### JoinResponse
|
||||
|
||||
```typescript
|
||||
interface JoinResponse {
|
||||
playerId: number;
|
||||
roomId: string;
|
||||
}
|
||||
```
|
||||
|
||||
### EntitySyncState
|
||||
|
||||
```typescript
|
||||
interface EntitySyncState {
|
||||
netId: number;
|
||||
pos?: Vec2;
|
||||
rot?: number;
|
||||
position?: { x: number; y: number };
|
||||
rotation?: number;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "NetworkPlugin、组件和系统的客户端使用指南"
|
||||
|
||||
## NetworkPlugin
|
||||
|
||||
NetworkPlugin 是客户端网络功能的核心入口。
|
||||
NetworkPlugin 是客户端网络功能的核心入口,基于 `@esengine/rpc` 提供类型安全的网络通信。
|
||||
|
||||
### 基本用法
|
||||
|
||||
@@ -18,7 +18,11 @@ const networkPlugin = new NetworkPlugin();
|
||||
await Core.installPlugin(networkPlugin);
|
||||
|
||||
// 连接服务器
|
||||
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
||||
const success = await networkPlugin.connect({
|
||||
url: 'ws://localhost:3000',
|
||||
playerName: 'PlayerName',
|
||||
roomId: 'room-1' // 可选
|
||||
});
|
||||
|
||||
// 断开连接
|
||||
await networkPlugin.disconnect();
|
||||
@@ -32,14 +36,19 @@ class NetworkPlugin {
|
||||
readonly version: string;
|
||||
|
||||
// 访问器
|
||||
get networkService(): NetworkService;
|
||||
get networkService(): GameNetworkService;
|
||||
get syncSystem(): NetworkSyncSystem;
|
||||
get spawnSystem(): NetworkSpawnSystem;
|
||||
get inputSystem(): NetworkInputSystem;
|
||||
get localPlayerId(): number;
|
||||
get isConnected(): boolean;
|
||||
|
||||
// 连接服务器
|
||||
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>;
|
||||
connect(options: {
|
||||
url: string;
|
||||
playerName: string;
|
||||
roomId?: string;
|
||||
}): Promise<boolean>;
|
||||
|
||||
// 断开连接
|
||||
disconnect(): Promise<void>;
|
||||
@@ -77,7 +86,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||
const identity = entity.addComponent(new NetworkIdentity());
|
||||
identity.netId = spawn.netId;
|
||||
identity.ownerId = spawn.ownerId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
|
||||
|
||||
return entity;
|
||||
});
|
||||
@@ -156,7 +165,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||
const identity = entity.addComponent(new NetworkIdentity());
|
||||
identity.netId = spawn.netId;
|
||||
identity.ownerId = spawn.ownerId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
|
||||
|
||||
entity.addComponent(new NetworkTransform());
|
||||
|
||||
@@ -230,18 +239,33 @@ class LocalInputHandler extends EntitySystem {
|
||||
|
||||
## 连接状态监听
|
||||
|
||||
使用 `GameNetworkService` 的链式 API 监听消息:
|
||||
|
||||
```typescript
|
||||
networkPlugin.networkService.setCallbacks({
|
||||
onConnected: (clientId, roomId) => {
|
||||
console.log(`已连接: 客户端 ${clientId}, 房间 ${roomId}`);
|
||||
},
|
||||
onDisconnected: () => {
|
||||
console.log('已断开');
|
||||
// 处理重连逻辑
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('网络错误:', error);
|
||||
}
|
||||
const { networkService } = networkPlugin;
|
||||
|
||||
// 监听状态同步
|
||||
networkService.onSync((data) => {
|
||||
console.log('收到同步数据:', data.entities.length, '个实体');
|
||||
});
|
||||
|
||||
// 监听实体生成
|
||||
networkService.onSpawn((data) => {
|
||||
console.log('生成实体:', data.prefab, 'netId:', data.netId);
|
||||
});
|
||||
|
||||
// 监听实体销毁
|
||||
networkService.onDespawn((data) => {
|
||||
console.log('销毁实体:', data.netId);
|
||||
});
|
||||
|
||||
// 通过连接选项设置回调
|
||||
await networkPlugin.connect({
|
||||
url: 'ws://localhost:3000',
|
||||
playerName: 'Player1',
|
||||
onConnect: () => console.log('已连接'),
|
||||
onDisconnect: (reason) => console.log('已断开:', reason),
|
||||
onError: (error) => console.error('网络错误:', error)
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
---
|
||||
title: "网络同步系统 (Network)"
|
||||
description: "基于 TSRPC 的多人游戏网络同步解决方案"
|
||||
description: "基于 @esengine/rpc 的多人游戏网络同步解决方案"
|
||||
---
|
||||
|
||||
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
|
||||
`@esengine/network` 提供基于 `@esengine/rpc` 的类型安全网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
|
||||
|
||||
## 概述
|
||||
|
||||
网络模块由三个包组成:
|
||||
网络模块由两个核心包组成:
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/network` | 客户端 ECS 插件 |
|
||||
| `@esengine/network-protocols` | 共享协议定义 |
|
||||
| `@esengine/network-server` | 服务器端实现 |
|
||||
| `@esengine/rpc` | 类型安全的 RPC 通信库 |
|
||||
| `@esengine/network` | 基于 RPC 的游戏网络插件 |
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 客户端
|
||||
npm install @esengine/network
|
||||
|
||||
# 服务器端
|
||||
npm install @esengine/network-server
|
||||
```
|
||||
|
||||
> `@esengine/rpc` 会作为依赖自动安装。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
客户端 服务器
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ NetworkPlugin │◄──── WS ────► │ GameServer │
|
||||
│ ├─ Service │ │ ├─ Room │
|
||||
│ ├─ SyncSystem │ │ └─ Players │
|
||||
│ ├─ SpawnSystem │ └────────────────┘
|
||||
│ └─ InputSystem │
|
||||
└────────────────┘
|
||||
┌────────────────────┐ ┌────────────────┐
|
||||
│ NetworkPlugin │◄── WS ───►│ RpcServer │
|
||||
│ ├─ NetworkService │ │ ├─ Protocol │
|
||||
│ ├─ SyncSystem │ │ └─ Handlers │
|
||||
│ ├─ SpawnSystem │ └────────────────┘
|
||||
│ └─ InputSystem │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
@@ -69,61 +66,81 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||
const identity = entity.addComponent(new NetworkIdentity());
|
||||
identity.netId = spawn.netId;
|
||||
identity.ownerId = spawn.ownerId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId;
|
||||
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
|
||||
|
||||
entity.addComponent(new NetworkTransform());
|
||||
return entity;
|
||||
});
|
||||
|
||||
// 连接服务器
|
||||
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
||||
const success = await networkPlugin.connect({
|
||||
url: 'ws://localhost:3000',
|
||||
playerName: 'Player1',
|
||||
roomId: 'room-1' // 可选
|
||||
});
|
||||
|
||||
if (success) {
|
||||
console.log('Connected!');
|
||||
console.log('Connected! Player ID:', networkPlugin.localPlayerId);
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器端
|
||||
|
||||
```typescript
|
||||
import { GameServer } from '@esengine/network-server';
|
||||
import { RpcServer } from '@esengine/rpc/server';
|
||||
import { gameProtocol } from '@esengine/network';
|
||||
|
||||
const server = new GameServer({
|
||||
const server = new RpcServer(gameProtocol, {
|
||||
port: 3000,
|
||||
roomConfig: {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
}
|
||||
onStart: (port) => console.log(`Server running on ws://localhost:${port}`)
|
||||
});
|
||||
|
||||
// 注册 API 处理器
|
||||
server.handle('join', async (input, ctx) => {
|
||||
const playerId = generatePlayerId();
|
||||
return { playerId, roomId: input.roomId ?? 'default' };
|
||||
});
|
||||
|
||||
server.handle('leave', async (input, ctx) => {
|
||||
// 处理玩家离开
|
||||
});
|
||||
|
||||
await server.start();
|
||||
console.log('Server started on ws://localhost:3000');
|
||||
```
|
||||
|
||||
## 使用 CLI 快速创建
|
||||
## 自定义协议
|
||||
|
||||
推荐使用 ESEngine CLI 快速创建完整的游戏服务端项目:
|
||||
你可以基于 `@esengine/rpc` 定义自己的协议:
|
||||
|
||||
```bash
|
||||
mkdir my-game-server && cd my-game-server
|
||||
npm init -y
|
||||
npx @esengine/cli init -p nodejs
|
||||
```
|
||||
```typescript
|
||||
import { rpc } from '@esengine/rpc';
|
||||
|
||||
生成的项目结构:
|
||||
// 定义自定义协议
|
||||
export const myProtocol = rpc.define({
|
||||
api: {
|
||||
login: rpc.api<{ username: string }, { token: string }>(),
|
||||
getData: rpc.api<{ id: number }, { data: object }>(),
|
||||
},
|
||||
msg: {
|
||||
chat: rpc.msg<{ from: string; text: string }>(),
|
||||
notification: rpc.msg<{ type: string; content: string }>(),
|
||||
},
|
||||
});
|
||||
|
||||
```
|
||||
my-game-server/
|
||||
├── src/
|
||||
│ ├── index.ts
|
||||
│ ├── server/
|
||||
│ │ └── GameServer.ts
|
||||
│ └── game/
|
||||
│ ├── Game.ts
|
||||
│ ├── scenes/
|
||||
│ ├── components/
|
||||
│ └── systems/
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
// 使用自定义协议创建服务
|
||||
import { RpcService } from '@esengine/network';
|
||||
|
||||
const service = new RpcService(myProtocol);
|
||||
await service.connect({ url: 'ws://localhost:3000' });
|
||||
|
||||
// 类型安全的 API 调用
|
||||
const result = await service.call('login', { username: 'test' });
|
||||
console.log(result.token);
|
||||
|
||||
// 类型安全的消息监听
|
||||
service.on('chat', (data) => {
|
||||
console.log(`${data.from}: ${data.text}`);
|
||||
});
|
||||
```
|
||||
|
||||
## 文档导航
|
||||
|
||||
165
docs/src/content/docs/modules/rpc/index.md
Normal file
165
docs/src/content/docs/modules/rpc/index.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "RPC 通信库"
|
||||
description: "类型安全的 WebSocket RPC 通信库"
|
||||
---
|
||||
|
||||
`@esengine/rpc` 是一个轻量级、类型安全的 RPC(远程过程调用)库,专为游戏和实时应用设计。
|
||||
|
||||
## 特性
|
||||
|
||||
- **类型安全**:完整的 TypeScript 类型推断,API 调用和消息处理都有类型检查
|
||||
- **双向通信**:支持请求-响应(API)和发布-订阅(消息)两种模式
|
||||
- **编解码灵活**:内置 JSON 和 MessagePack 编解码器
|
||||
- **跨平台**:支持浏览器、Node.js 和微信小游戏
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/rpc
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 定义协议
|
||||
|
||||
```typescript
|
||||
import { rpc } from '@esengine/rpc';
|
||||
|
||||
// 定义协议(共享于客户端和服务器)
|
||||
export const chatProtocol = rpc.define({
|
||||
api: {
|
||||
// 请求-响应 API
|
||||
login: rpc.api<{ username: string }, { userId: string; token: string }>(),
|
||||
sendMessage: rpc.api<{ text: string }, { messageId: string }>(),
|
||||
},
|
||||
msg: {
|
||||
// 单向消息(发布-订阅)
|
||||
newMessage: rpc.msg<{ from: string; text: string; time: number }>(),
|
||||
userJoined: rpc.msg<{ username: string }>(),
|
||||
userLeft: rpc.msg<{ username: string }>(),
|
||||
},
|
||||
});
|
||||
|
||||
export type ChatProtocol = typeof chatProtocol;
|
||||
```
|
||||
|
||||
### 2. 创建客户端
|
||||
|
||||
```typescript
|
||||
import { RpcClient } from '@esengine/rpc/client';
|
||||
import { chatProtocol } from './protocol';
|
||||
|
||||
// 创建客户端
|
||||
const client = new RpcClient(chatProtocol, 'ws://localhost:3000', {
|
||||
onConnect: () => console.log('已连接'),
|
||||
onDisconnect: (reason) => console.log('已断开:', reason),
|
||||
onError: (error) => console.error('错误:', error),
|
||||
});
|
||||
|
||||
// 连接服务器
|
||||
await client.connect();
|
||||
|
||||
// 调用 API(类型安全)
|
||||
const { userId, token } = await client.call('login', { username: 'player1' });
|
||||
console.log('登录成功:', userId);
|
||||
|
||||
// 监听消息(类型安全)
|
||||
client.on('newMessage', (data) => {
|
||||
console.log(`${data.from}: ${data.text}`);
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
await client.call('sendMessage', { text: 'Hello!' });
|
||||
```
|
||||
|
||||
### 3. 创建服务器
|
||||
|
||||
```typescript
|
||||
import { RpcServer } from '@esengine/rpc/server';
|
||||
import { chatProtocol } from './protocol';
|
||||
|
||||
// 创建服务器
|
||||
const server = new RpcServer(chatProtocol, {
|
||||
port: 3000,
|
||||
onStart: (port) => console.log(`服务器启动: ws://localhost:${port}`),
|
||||
});
|
||||
|
||||
// 注册 API 处理器
|
||||
server.handle('login', async (input, ctx) => {
|
||||
const userId = generateUserId();
|
||||
const token = generateToken();
|
||||
|
||||
// 广播用户加入
|
||||
server.broadcast('userJoined', { username: input.username });
|
||||
|
||||
return { userId, token };
|
||||
});
|
||||
|
||||
server.handle('sendMessage', async (input, ctx) => {
|
||||
const messageId = generateMessageId();
|
||||
|
||||
// 广播新消息
|
||||
server.broadcast('newMessage', {
|
||||
from: ctx.clientId,
|
||||
text: input.text,
|
||||
time: Date.now(),
|
||||
});
|
||||
|
||||
return { messageId };
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
await server.start();
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 协议定义
|
||||
|
||||
协议是客户端和服务器之间通信的契约:
|
||||
|
||||
```typescript
|
||||
const protocol = rpc.define({
|
||||
api: {
|
||||
// API:请求-响应模式,客户端发起,服务器处理并返回结果
|
||||
methodName: rpc.api<InputType, OutputType>(),
|
||||
},
|
||||
msg: {
|
||||
// 消息:发布-订阅模式,任意一方发送,对方监听
|
||||
messageName: rpc.msg<DataType>(),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### API vs 消息
|
||||
|
||||
| 特性 | API | 消息 |
|
||||
|------|-----|------|
|
||||
| 模式 | 请求-响应 | 发布-订阅 |
|
||||
| 返回值 | 有(Promise) | 无 |
|
||||
| 确认 | 等待响应 | 发送即忘 |
|
||||
| 用途 | 登录、查询、操作 | 实时通知、状态同步 |
|
||||
|
||||
## 编解码器
|
||||
|
||||
内置两种编解码器:
|
||||
|
||||
```typescript
|
||||
import { json, msgpack } from '@esengine/rpc/codec';
|
||||
|
||||
// JSON(默认,可读性好)
|
||||
const client = new RpcClient(protocol, url, {
|
||||
codec: json(),
|
||||
});
|
||||
|
||||
// MessagePack(二进制,更高效)
|
||||
const client = new RpcClient(protocol, url, {
|
||||
codec: msgpack(),
|
||||
});
|
||||
```
|
||||
|
||||
## 文档导航
|
||||
|
||||
- [客户端 API](/modules/rpc/client/) - RpcClient 详细 API
|
||||
- [服务器 API](/modules/rpc/server/) - RpcServer 详细 API
|
||||
- [编解码器](/modules/rpc/codec/) - 自定义编解码器
|
||||
Reference in New Issue
Block a user