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

@@ -0,0 +1,59 @@
---
"@esengine/rpc": minor
"@esengine/network": minor
"@esengine/cli": minor
---
## @esengine/rpc
### 新增 | Added
- 新增类型安全的 RPC 库,支持 WebSocket 通信
- 新增 `RpcClient`connect/disconnect, call/send/on/off/once 方法
- 新增 `RpcServer`Node.js WebSocket 服务端
- 新增编解码系统:支持 JSON 和 MessagePack
- 新增 TextEncoder/TextDecoder polyfill兼容微信小游戏平台
- 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象
---
- Add type-safe RPC library with WebSocket support
- Add `RpcClient` class with connect/disconnect, call/send/on/off/once methods
- Add `RpcServer` class for Node.js WebSocket server
- Add codec system with JSON and MessagePack support
- Add TextEncoder/TextDecoder polyfill for WeChat platform compatibility
- Add WebSocketAdapter interface for cross-platform WebSocket abstraction
## @esengine/network
### 变更 | Changed
- 重构 NetworkService拆分为 `RpcService<P>` 基类和 `GameNetworkService` 游戏服务类
- 新增 `gameProtocol`:类型安全的 API (join/leave) 和消息 (input/sync/spawn/despawn)
- 新增类型安全的便捷方法sendInput(), onSync(), onSpawn(), onDespawn()
- 更新 NetworkPlugin 使用新的服务架构
- 移除 TSRPC 依赖,改用 @esengine/rpc
---
- Refactor NetworkService into `RpcService<P>` base class and `GameNetworkService`
- Add `gameProtocol` with type-safe API (join/leave) and messages (input/sync/spawn/despawn)
- Add type-safe convenience methods: sendInput(), onSync(), onSpawn(), onDespawn()
- Update NetworkPlugin to use new service architecture
- Remove TSRPC dependency, migrate to @esengine/rpc
## @esengine/cli
### 变更 | Changed
- 更新 Node.js 适配器使用 `@esengine/rpc``@esengine/network`
- 生成的服务器代码改用 `RpcServer` + `gameProtocol`
- 添加 `ws``@types/ws` 依赖
- 更新 README 模板中的客户端连接示例
---
- Update Node.js adapter to use `@esengine/rpc` and `@esengine/network`
- Generated server code now uses `RpcServer` + `gameProtocol`
- Add `ws` and `@types/ws` dependencies
- Update client connection example in README template

View File

@@ -72,6 +72,7 @@ jobs:
pnpm --filter @esengine/procgen build pnpm --filter @esengine/procgen build
pnpm --filter @esengine/pathfinding build pnpm --filter @esengine/pathfinding build
pnpm --filter @esengine/network-protocols build pnpm --filter @esengine/network-protocols build
pnpm --filter @esengine/rpc build
pnpm --filter @esengine/network build pnpm --filter @esengine/network build
# 类型检查 (仅 framework 包) # 类型检查 (仅 framework 包)

View File

@@ -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' }, translations: { en: 'Network' },
items: [ items: [
{ label: '概述', slug: 'modules/network', translations: { en: 'Overview' } }, { label: '概述', slug: 'modules/network', translations: { en: 'Overview' } },

View File

@@ -1,41 +1,38 @@
--- ---
title: "Network System" 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 ## Overview
The network module consists of three packages: The network module consists of two core packages:
| Package | Description | | Package | Description |
|---------|-------------| |---------|-------------|
| `@esengine/network` | Client-side ECS plugin | | `@esengine/rpc` | Type-safe RPC communication library |
| `@esengine/network-protocols` | Shared protocol definitions | | `@esengine/network` | RPC-based game networking plugin |
| `@esengine/network-server` | Server-side implementation |
## Installation ## Installation
```bash ```bash
# Client
npm install @esengine/network npm install @esengine/network
# Server
npm install @esengine/network-server
``` ```
> `@esengine/rpc` is automatically installed as a dependency.
## Architecture ## Architecture
``` ```
Client Server Client Server
┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │ │ NetworkPlugin │◄── WS ───►│ RpcServer
│ ├─ Service │ ├─ Room │ ├─ NetworkService │ ├─ Protocol
│ ├─ SyncSystem │ └─ Players │ ├─ SyncSystem │ └─ Handlers │
│ ├─ SpawnSystem │ └────────────────┘ │ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │ │ └─ InputSystem │
└────────────────┘ └────────────────────
``` ```
## Quick Start ## Quick Start
@@ -69,61 +66,81 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
const identity = entity.addComponent(new NetworkIdentity()); const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId; identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId; identity.ownerId = spawn.ownerId;
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId; identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
entity.addComponent(new NetworkTransform()); entity.addComponent(new NetworkTransform());
return entity; return entity;
}); });
// Connect to server // 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) { if (success) {
console.log('Connected!'); console.log('Connected! Player ID:', networkPlugin.localPlayerId);
} }
``` ```
### Server ### Server
```typescript ```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, port: 3000,
roomConfig: { onStart: (port) => console.log(`Server running on ws://localhost:${port}`)
maxPlayers: 16, });
tickRate: 20
} // 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(); 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 ```typescript
mkdir my-game-server && cd my-game-server import { rpc } from '@esengine/rpc';
npm init -y
npx @esengine/cli init -p nodejs
```
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 }>(),
},
});
``` // Create service with custom protocol
my-game-server/ import { RpcService } from '@esengine/network';
├── src/
│ ├── index.ts const service = new RpcService(myProtocol);
│ ├── server/ await service.connect({ url: 'ws://localhost:3000' });
│ │ └── GameServer.ts
│ └── game/ // Type-safe API calls
│ ├── Game.ts const result = await service.call('login', { username: 'test' });
│ ├── scenes/ console.log(result.token);
│ ├── components/
│ └── systems/ // Type-safe message listening
├── tsconfig.json service.on('chat', (data) => {
└── package.json console.log(`${data.from}: ${data.text}`);
});
``` ```
## Documentation ## Documentation

View 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

View File

@@ -13,10 +13,11 @@ class NetworkPlugin implements IPlugin {
readonly version: string; readonly version: string;
// 访问器 // 访问器
get networkService(): NetworkService; get networkService(): GameNetworkService;
get syncSystem(): NetworkSyncSystem; get syncSystem(): NetworkSyncSystem;
get spawnSystem(): NetworkSpawnSystem; get spawnSystem(): NetworkSpawnSystem;
get inputSystem(): NetworkInputSystem; get inputSystem(): NetworkInputSystem;
get localPlayerId(): number;
get isConnected(): boolean; get isConnected(): boolean;
// 生命周期 // 生命周期
@@ -24,7 +25,10 @@ class NetworkPlugin implements IPlugin {
uninstall(): void; uninstall(): void;
// 连接管理 // 连接管理
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>; connect(options: NetworkServiceOptions & {
playerName: string;
roomId?: string;
}): Promise<boolean>;
disconnect(): Promise<void>; disconnect(): Promise<void>;
// 预制体注册 // 预制体注册
@@ -36,36 +40,65 @@ class NetworkPlugin implements IPlugin {
} }
``` ```
## NetworkService ## RpcService
网络服务,管理 WebSocket 连接 通用 RPC 服务基类,支持自定义协议
```typescript ```typescript
class NetworkService { class RpcService<P extends ProtocolDef> {
// 访问器 // 访问器
get state(): ENetworkState; get state(): NetworkState;
get isConnected(): boolean; get isConnected(): boolean;
get clientId(): number; get client(): RpcClient<P> | null;
get roomId(): string;
constructor(protocol: P);
// 连接管理 // 连接管理
connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean>; connect(options: NetworkServiceOptions): Promise<void>;
disconnect(): Promise<void>; disconnect(): void;
// 输入发送 // RPC 调用
sendInput(input: IPlayerInput): void; 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 ```typescript
const enum ENetworkState { const enum NetworkState {
Disconnected = 0, Disconnected = 0,
Connecting = 1, Connecting = 1,
Connected = 2 Connected = 2
@@ -74,15 +107,21 @@ const enum ENetworkState {
## 接口类型 ## 接口类型
### INetworkCallbacks ### NetworkServiceOptions
```typescript ```typescript
interface INetworkCallbacks { interface NetworkServiceOptions extends RpcClientOptions {
onConnected?: (clientId: number, roomId: string) => void; url: string;
onDisconnected?: () => void; }
onSync?: (msg: MsgSync) => void; ```
onSpawn?: (msg: MsgSpawn) => void;
onDespawn?: (msg: MsgDespawn) => void; ### RpcClientOptions
```typescript
interface RpcClientOptions {
codec?: Codec;
onConnect?: () => void;
onDisconnect?: (reason?: string) => void;
onError?: (error: Error) => void; onError?: (error: Error) => void;
} }
``` ```
@@ -90,15 +129,15 @@ interface INetworkCallbacks {
### PrefabFactory ### PrefabFactory
```typescript ```typescript
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity; type PrefabFactory = (scene: Scene, spawn: SpawnData) => Entity;
``` ```
### IPlayerInput ### PlayerInput
```typescript ```typescript
interface IPlayerInput { interface PlayerInput {
seq?: number; frame: number;
moveDir?: Vec2; moveDir?: { x: number; y: number };
actions?: string[]; actions?: string[];
} }
``` ```
@@ -170,86 +209,128 @@ const networkService = services.get(NetworkServiceToken);
## 服务器端 API ## 服务器端 API
### GameServer ### RpcServer
```typescript ```typescript
class GameServer { class RpcServer<P extends ProtocolDef> {
constructor(config: IGameServerConfig); constructor(protocol: P, options: RpcServerOptions);
// 启动/停止服务器
start(): Promise<void>; start(): Promise<void>;
stop(): Promise<void>; stop(): void;
getOrCreateRoom(roomId: string): Room; // 注册 API 处理器
getRoom(roomId: string): Room | undefined; handle<K extends ApiNames<P>>(
destroyRoom(roomId: string): void; 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 ```typescript
class Room { interface RpcServerOptions {
readonly id: string; port: number;
readonly playerCount: number; codec?: Codec;
readonly isFull: boolean; onStart?: (port: number) => void;
onConnection?: (clientId: string) => void;
addPlayer(name: string, connection: Connection): IPlayer | null; onDisconnection?: (clientId: string, reason?: string) => void;
removePlayer(clientId: number): void; onError?: (error: Error) => void;
getPlayer(clientId: number): IPlayer | undefined;
handleInput(clientId: number, input: IPlayerInput): void;
destroy(): void;
} }
``` ```
### IPlayer ### RpcContext
```typescript ```typescript
interface IPlayer { interface RpcContext {
clientId: number; clientId: string;
name: string;
connection: Connection;
netId: number;
} }
``` ```
## 协议消息 ## 协议定义
### MsgSync ### gameProtocol
默认游戏协议,包含加入/离开 API 和状态同步消息:
```typescript ```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; time: number;
entities: IEntityState[]; entities: IEntityState[];
} }
``` ```
### MsgSpawn ### SpawnData
```typescript ```typescript
interface MsgSpawn { interface SpawnData {
netId: number; netId: number;
ownerId: number; ownerId: number;
prefab: string; prefab: string;
pos: Vec2; position: { x: number; y: number };
rot: number; rotation: number;
} }
``` ```
### MsgDespawn ### DespawnData
```typescript ```typescript
interface MsgDespawn { interface DespawnData {
netId: number; netId: number;
} }
``` ```
### IEntityState ### JoinRequest
```typescript ```typescript
interface IEntityState { interface JoinRequest {
playerName: string;
roomId?: string;
}
```
### JoinResponse
```typescript
interface JoinResponse {
playerId: number;
roomId: string;
}
```
### EntitySyncState
```typescript
interface EntitySyncState {
netId: number; netId: number;
pos?: Vec2; position?: { x: number; y: number };
rot?: number; rotation?: number;
} }
``` ```

View File

@@ -5,7 +5,7 @@ description: "NetworkPlugin、组件和系统的客户端使用指南"
## NetworkPlugin ## NetworkPlugin
NetworkPlugin 是客户端网络功能的核心入口。 NetworkPlugin 是客户端网络功能的核心入口,基于 `@esengine/rpc` 提供类型安全的网络通信
### 基本用法 ### 基本用法
@@ -18,7 +18,11 @@ const networkPlugin = new NetworkPlugin();
await Core.installPlugin(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(); await networkPlugin.disconnect();
@@ -32,14 +36,19 @@ class NetworkPlugin {
readonly version: string; readonly version: string;
// 访问器 // 访问器
get networkService(): NetworkService; get networkService(): GameNetworkService;
get syncSystem(): NetworkSyncSystem; get syncSystem(): NetworkSyncSystem;
get spawnSystem(): NetworkSpawnSystem; get spawnSystem(): NetworkSpawnSystem;
get inputSystem(): NetworkInputSystem; get inputSystem(): NetworkInputSystem;
get localPlayerId(): number;
get isConnected(): boolean; 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>; disconnect(): Promise<void>;
@@ -77,7 +86,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
const identity = entity.addComponent(new NetworkIdentity()); const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId; identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId; identity.ownerId = spawn.ownerId;
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId; identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
return entity; return entity;
}); });
@@ -156,7 +165,7 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
const identity = entity.addComponent(new NetworkIdentity()); const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId; identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId; identity.ownerId = spawn.ownerId;
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId; identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
entity.addComponent(new NetworkTransform()); entity.addComponent(new NetworkTransform());
@@ -230,18 +239,33 @@ class LocalInputHandler extends EntitySystem {
## 连接状态监听 ## 连接状态监听
使用 `GameNetworkService` 的链式 API 监听消息:
```typescript ```typescript
networkPlugin.networkService.setCallbacks({ const { networkService } = networkPlugin;
onConnected: (clientId, roomId) => {
console.log(`已连接: 客户端 ${clientId}, 房间 ${roomId}`); // 监听状态同步
}, networkService.onSync((data) => {
onDisconnected: () => { console.log('收到同步数据:', data.entities.length, '个实体');
console.log('已断开'); });
// 处理重连逻辑
}, // 监听实体生成
onError: (error) => { networkService.onSpawn((data) => {
console.error('网络错误:', error); 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)
}); });
``` ```

View File

@@ -1,41 +1,38 @@
--- ---
title: "网络同步系统 (Network)" title: "网络同步系统 (Network)"
description: "基于 TSRPC 的多人游戏网络同步解决方案" description: "基于 @esengine/rpc 的多人游戏网络同步解决方案"
--- ---
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。 `@esengine/network` 提供基于 `@esengine/rpc` 的类型安全网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
## 概述 ## 概述
网络模块由三个包组成: 网络模块由两个核心包组成:
| 包名 | 描述 | | 包名 | 描述 |
|------|------| |------|------|
| `@esengine/network` | 客户端 ECS 插件 | | `@esengine/rpc` | 类型安全的 RPC 通信库 |
| `@esengine/network-protocols` | 共享协议定义 | | `@esengine/network` | 基于 RPC 的游戏网络插件 |
| `@esengine/network-server` | 服务器端实现 |
## 安装 ## 安装
```bash ```bash
# 客户端
npm install @esengine/network npm install @esengine/network
# 服务器端
npm install @esengine/network-server
``` ```
> `@esengine/rpc` 会作为依赖自动安装。
## 架构 ## 架构
``` ```
客户端 服务器 客户端 服务器
┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │ │ NetworkPlugin │◄── WS ───►│ RpcServer
│ ├─ Service │ ├─ Room │ ├─ NetworkService │ ├─ Protocol
│ ├─ SyncSystem │ └─ Players │ ├─ SyncSystem │ └─ Handlers │
│ ├─ SpawnSystem │ └────────────────┘ │ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │ │ └─ InputSystem │
└────────────────┘ └────────────────────
``` ```
## 快速开始 ## 快速开始
@@ -69,61 +66,81 @@ networkPlugin.registerPrefab('player', (scene, spawn) => {
const identity = entity.addComponent(new NetworkIdentity()); const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId; identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId; identity.ownerId = spawn.ownerId;
identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.networkService.clientId; identity.bIsLocalPlayer = spawn.ownerId === networkPlugin.localPlayerId;
entity.addComponent(new NetworkTransform()); entity.addComponent(new NetworkTransform());
return entity; 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) { if (success) {
console.log('Connected!'); console.log('Connected! Player ID:', networkPlugin.localPlayerId);
} }
``` ```
### 服务器端 ### 服务器端
```typescript ```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, port: 3000,
roomConfig: { onStart: (port) => console.log(`Server running on ws://localhost:${port}`)
maxPlayers: 16, });
tickRate: 20
} // 注册 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(); await server.start();
console.log('Server started on ws://localhost:3000');
``` ```
## 使用 CLI 快速创建 ## 自定义协议
推荐使用 ESEngine CLI 快速创建完整的游戏服务端项目 你可以基于 `@esengine/rpc` 定义自己的协议
```bash ```typescript
mkdir my-game-server && cd my-game-server import { rpc } from '@esengine/rpc';
npm init -y
npx @esengine/cli init -p nodejs
```
生成的项目结构: // 定义自定义协议
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/ import { RpcService } from '@esengine/network';
├── src/
│ ├── index.ts const service = new RpcService(myProtocol);
│ ├── server/ await service.connect({ url: 'ws://localhost:3000' });
│ │ └── GameServer.ts
│ └── game/ // 类型安全的 API 调用
│ ├── Game.ts const result = await service.call('login', { username: 'test' });
│ ├── scenes/ console.log(result.token);
│ ├── components/
│ └── systems/ // 类型安全的消息监听
├── tsconfig.json service.on('chat', (data) => {
└── package.json console.log(`${data.from}: ${data.text}`);
});
``` ```
## 文档导航 ## 文档导航

View 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/) - 自定义编解码器

View File

@@ -42,7 +42,7 @@
"test": "turbo run test", "test": "turbo run test",
"test:coverage": "turbo run test:coverage", "test:coverage": "turbo run test:coverage",
"test:ci": "turbo run test:ci", "test:ci": "turbo run test:ci",
"test:ci:framework": "turbo run test:ci --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network", "test:ci:framework": "turbo run test:ci --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/rpc --filter=@esengine/network",
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs", "prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
"sync:versions": "node scripts/sync-versions.cjs", "sync:versions": "node scripts/sync-versions.cjs",
"publish:all": "npm run prepare:publish && npm run publish:all:dist", "publish:all": "npm run prepare:publish && npm run publish:all:dist",
@@ -68,9 +68,9 @@
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", "format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", "format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"type-check": "turbo run type-check", "type-check": "turbo run type-check",
"type-check:framework": "turbo run type-check --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network", "type-check:framework": "turbo run type-check --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/rpc --filter=@esengine/network",
"lint": "turbo run lint", "lint": "turbo run lint",
"lint:framework": "turbo run lint --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network", "lint:framework": "turbo run lint --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/rpc --filter=@esengine/network",
"lint:fix": "turbo run lint:fix", "lint:fix": "turbo run lint:fix",
"build:wasm": "cd packages/rust/engine && wasm-pack build --dev --out-dir pkg", "build:wasm": "cd packages/rust/engine && wasm-pack build --dev --out-dir pkg",
"build:wasm:release": "cd packages/rust/engine && wasm-pack build --release --out-dir pkg", "build:wasm:release": "cd packages/rust/engine && wasm-pack build --release --out-dir pkg",

View File

@@ -1,10 +1,10 @@
{ {
"name": "@esengine/network", "name": "@esengine/network",
"displayName": "Network", "displayName": "Network",
"description": "TSRPC-based network synchronization for multiplayer games", "description": "RPC-based network synchronization for multiplayer games",
"version": "1.0.0", "version": "2.0.0",
"category": "network", "category": "network",
"dependencies": [], "dependencies": ["@esengine/rpc"],
"components": [ "components": [
{ {
"name": "NetworkIdentity", "name": "NetworkIdentity",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@esengine/network", "name": "@esengine/network",
"version": "1.0.0", "version": "2.0.0",
"description": "Network synchronization for multiplayer games based on TSRPC", "description": "Network synchronization for multiplayer games",
"esengine": { "esengine": {
"plugin": true, "plugin": true,
"pluginExport": "NetworkPlugin", "pluginExport": "NetworkPlugin",
@@ -22,14 +22,13 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "tsup && tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist", "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
"build:watch": "tsup --watch", "build:watch": "tsup --watch",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"clean": "rimraf dist" "clean": "rimraf dist"
}, },
"dependencies": { "dependencies": {
"@esengine/network-protocols": "workspace:*", "@esengine/rpc": "workspace:*"
"tsrpc-browser": "^3.4.16"
}, },
"devDependencies": { "devDependencies": {
"@esengine/blueprint": "workspace:*", "@esengine/blueprint": "workspace:*",

View File

@@ -1,153 +1,199 @@
import { type IPlugin, Core, type ServiceContainer, type Scene } from '@esengine/ecs-framework'; /**
import { NetworkService } from './services/NetworkService'; * @zh 网络插件
import { NetworkSyncSystem } from './systems/NetworkSyncSystem'; * @en Network Plugin
import { NetworkSpawnSystem, type PrefabFactory } from './systems/NetworkSpawnSystem'; */
import { NetworkInputSystem } from './systems/NetworkInputSystem';
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'
/** /**
* 网络插件 * @zh 网络插件
* Network plugin * @en Network plugin
* *
* 提供基于 TSRPC 的网络同步功能 * @zh 提供基于 @esengine/rpc 的网络同步功能
* Provides TSRPC-based network synchronization. * @en Provides @esengine/rpc based network synchronization
* *
* @example * @example
* ```typescript * ```typescript
* import { Core } from '@esengine/ecs-framework'; * import { Core } from '@esengine/ecs-framework'
* import { NetworkPlugin } from '@esengine/network'; * import { NetworkPlugin } from '@esengine/network'
* *
* const networkPlugin = new NetworkPlugin(); * const networkPlugin = new NetworkPlugin()
* await Core.installPlugin(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) => { * networkPlugin.registerPrefab('player', (scene, spawn) => {
* const entity = scene.createEntity('Player'); * const entity = scene.createEntity('Player')
* return entity; * return entity
* }); * })
* ``` * ```
*/ */
export class NetworkPlugin implements IPlugin { export class NetworkPlugin implements IPlugin {
public readonly name = '@esengine/network'; public readonly name = '@esengine/network'
public readonly version = '1.0.0'; public readonly version = '2.0.0'
private _networkService!: NetworkService; private _networkService!: GameNetworkService
private _syncSystem!: NetworkSyncSystem; private _syncSystem!: NetworkSyncSystem
private _spawnSystem!: NetworkSpawnSystem; private _spawnSystem!: NetworkSpawnSystem
private _inputSystem!: NetworkInputSystem; private _inputSystem!: NetworkInputSystem
private _localPlayerId: number = 0
/** /**
* 网络服务 * @zh 网络服务
* Network service * @en Network service
*/ */
get networkService(): NetworkService { get networkService(): GameNetworkService {
return this._networkService; return this._networkService
} }
/** /**
* 同步系统 * @zh 同步系统
* Sync system * @en Sync system
*/ */
get syncSystem(): NetworkSyncSystem { get syncSystem(): NetworkSyncSystem {
return this._syncSystem; return this._syncSystem
} }
/** /**
* 生成系统 * @zh 生成系统
* Spawn system * @en Spawn system
*/ */
get spawnSystem(): NetworkSpawnSystem { get spawnSystem(): NetworkSpawnSystem {
return this._spawnSystem; return this._spawnSystem
} }
/** /**
* 输入系统 * @zh 输入系统
* Input system * @en Input system
*/ */
get inputSystem(): NetworkInputSystem { get inputSystem(): NetworkInputSystem {
return this._inputSystem; return this._inputSystem
} }
/** /**
* 是否已连接 * @zh 本地玩家 ID
* Is connected * @en Local player ID
*/
get localPlayerId(): number {
return this._localPlayerId
}
/**
* @zh 是否已连接
* @en Is connected
*/ */
get isConnected(): boolean { get isConnected(): boolean {
return this._networkService?.isConnected ?? false; return this._networkService?.isConnected ?? false
} }
/** /**
* 安装插件 * @zh 安装插件
* Install plugin * @en Install plugin
*/ */
install(_core: Core, _services: ServiceContainer): void { install(_core: Core, _services: ServiceContainer): void {
this._networkService = new NetworkService(); this._networkService = new GameNetworkService()
// 当场景加载时添加系统 const scene = Core.scene
// Add systems when scene loads
const scene = Core.scene;
if (scene) { if (scene) {
this._setupSystems(scene as Scene); this._setupSystems(scene as Scene)
} }
} }
/** /**
* 卸载插件 * @zh 卸载插件
* Uninstall plugin * @en Uninstall plugin
*/ */
uninstall(): void { uninstall(): void {
this._networkService?.disconnect(); this._networkService?.disconnect()
} }
private _setupSystems(scene: Scene): void { private _setupSystems(scene: Scene): void {
this._syncSystem = new NetworkSyncSystem(this._networkService); this._syncSystem = new NetworkSyncSystem()
this._spawnSystem = new NetworkSpawnSystem(this._networkService, this._syncSystem); this._spawnSystem = new NetworkSpawnSystem(this._syncSystem)
this._inputSystem = new NetworkInputSystem(this._networkService); this._inputSystem = new NetworkInputSystem(this._networkService)
scene.addSystem(this._syncSystem); scene.addSystem(this._syncSystem)
scene.addSystem(this._spawnSystem); scene.addSystem(this._spawnSystem)
scene.addSystem(this._inputSystem); 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)
})
} }
/** /**
* 连接到服务器 * @zh 连接到服务器
* Connect to server * @en Connect to server
*/ */
public async connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean> { public async connect(options: NetworkServiceOptions & { playerName: string; roomId?: string }): Promise<boolean> {
return this._networkService.connect(serverUrl, playerName, roomId); 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
}
} }
/** /**
* 断开连接 * @zh 断开连接
* Disconnect * @en Disconnect
*/ */
public async disconnect(): Promise<void> { public async disconnect(): Promise<void> {
await this._networkService.disconnect(); try {
await this._networkService.call('leave', undefined)
} catch {
// ignore
}
this._networkService.disconnect()
} }
/** /**
* 注册预制体工厂 * @zh 注册预制体工厂
* Register prefab factory * @en Register prefab factory
*/ */
public registerPrefab(prefabType: string, factory: PrefabFactory): void { public registerPrefab(prefabType: string, factory: PrefabFactory): void {
this._spawnSystem?.registerPrefab(prefabType, factory); this._spawnSystem?.registerPrefab(prefabType, factory)
} }
/** /**
* 发送移动输入 * @zh 发送移动输入
* Send move input * @en Send move input
*/ */
public sendMoveInput(x: number, y: number): void { public sendMoveInput(x: number, y: number): void {
this._inputSystem?.addMoveInput(x, y); this._inputSystem?.addMoveInput(x, y)
} }
/** /**
* 发送动作输入 * @zh 发送动作输入
* Send action input * @en Send action input
*/ */
public sendActionInput(action: string): void { public sendActionInput(action: string): void {
this._inputSystem?.addActionInput(action); this._inputSystem?.addActionInput(action)
} }
} }

View File

@@ -1,28 +1,43 @@
/** /**
* @esengine/network * @zh @esengine/network 网络同步模块
* @en @esengine/network Network synchronization module
* *
* 基于 TSRPC 的网络同步模块(客户端) * @zh 基于 @esengine/rpc 的网络同步模块,提供类型安全的多人游戏网络通信
* TSRPC-based network synchronization module (client) * @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 { export type {
ServiceType, ProtocolDef,
Vec2, ApiDef,
IEntityState, MsgDef,
IPlayerInput, ApiInput,
MsgSync, ApiOutput,
MsgInput, MsgData,
MsgSpawn, ApiNames,
MsgDespawn, MsgNames,
ReqJoin, RpcError,
ResJoin } from '@esengine/rpc'
} from '@esengine/network-protocols';
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 | 服务令牌 // Tokens | 服务令牌
@@ -32,37 +47,44 @@ export {
NetworkServiceToken, NetworkServiceToken,
NetworkSyncSystemToken, NetworkSyncSystemToken,
NetworkSpawnSystemToken, NetworkSpawnSystemToken,
NetworkInputSystemToken NetworkInputSystemToken,
} from './tokens'; } from './tokens'
// ============================================================================ // ============================================================================
// Plugin | 插件 // Plugin | 插件
// ============================================================================ // ============================================================================
export { NetworkPlugin } from './NetworkPlugin'; export { NetworkPlugin } from './NetworkPlugin'
// ============================================================================ // ============================================================================
// Services | 服务 // Services | 服务
// ============================================================================ // ============================================================================
export { NetworkService, ENetworkState } from './services/NetworkService'; export {
export type { INetworkCallbacks } from './services/NetworkService'; RpcService,
GameNetworkService,
NetworkService,
NetworkState,
createNetworkService,
} from './services/NetworkService'
export type { NetworkServiceOptions } from './services/NetworkService'
// ============================================================================ // ============================================================================
// Components | 组件 // Components | 组件
// ============================================================================ // ============================================================================
export { NetworkIdentity } from './components/NetworkIdentity'; export { NetworkIdentity } from './components/NetworkIdentity'
export { NetworkTransform } from './components/NetworkTransform'; export { NetworkTransform } from './components/NetworkTransform'
// ============================================================================ // ============================================================================
// Systems | 系统 // Systems | 系统
// ============================================================================ // ============================================================================
export { NetworkSyncSystem } from './systems/NetworkSyncSystem'; export { NetworkSyncSystem } from './systems/NetworkSyncSystem'
export { NetworkSpawnSystem } from './systems/NetworkSpawnSystem'; export type { SyncMessage } from './systems/NetworkSyncSystem'
export type { PrefabFactory } from './systems/NetworkSpawnSystem'; export { NetworkSpawnSystem } from './systems/NetworkSpawnSystem'
export { NetworkInputSystem } from './systems/NetworkInputSystem'; export type { PrefabFactory, SpawnMessage, DespawnMessage } from './systems/NetworkSpawnSystem'
export { NetworkInputSystem } from './systems/NetworkInputSystem'
// ============================================================================ // ============================================================================
// State Sync | 状态同步 // State Sync | 状态同步
@@ -73,8 +95,8 @@ export type {
ITransformState, ITransformState,
ITransformStateWithVelocity, ITransformStateWithVelocity,
ISnapshotBufferConfig, ISnapshotBufferConfig,
ISnapshotBuffer ISnapshotBuffer,
} from './sync'; } from './sync'
export type { export type {
IInterpolator, IInterpolator,
@@ -82,8 +104,8 @@ export type {
IInputSnapshot, IInputSnapshot,
IPredictedState, IPredictedState,
IPredictor, IPredictor,
ClientPredictionConfig ClientPredictionConfig,
} from './sync'; } from './sync'
export { export {
lerp, lerp,
@@ -96,8 +118,8 @@ export {
createTransformInterpolator, createTransformInterpolator,
createHermiteTransformInterpolator, createHermiteTransformInterpolator,
ClientPrediction, ClientPrediction,
createClientPrediction createClientPrediction,
} from './sync'; } from './sync'
// ============================================================================ // ============================================================================
// Blueprint Nodes | 蓝图节点 // Blueprint Nodes | 蓝图节点
@@ -114,5 +136,5 @@ export {
HasAuthorityExecutor, HasAuthorityExecutor,
GetNetworkIdExecutor, GetNetworkIdExecutor,
GetLocalPlayerIdExecutor, GetLocalPlayerIdExecutor,
NetworkNodeDefinitions NetworkNodeDefinitions,
} from './nodes'; } from './nodes'

View 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

View File

@@ -1,172 +1,274 @@
import { WsClient } from 'tsrpc-browser'; /**
* @zh 网络服务模块
* @en Network Service Module
*/
import { import {
serviceProto, RpcClient,
type ServiceType, type ProtocolDef,
type MsgSync, type ApiNames,
type MsgSpawn, type MsgNames,
type MsgDespawn, type ApiInput,
type IPlayerInput type ApiOutput,
} from '@esengine/network-protocols'; type MsgData,
type RpcClientOptions,
} from '@esengine/rpc/client'
import { gameProtocol, type GameProtocol, type PlayerInput } from '../protocol'
// ============================================================================
// Types | 类型定义
// ============================================================================
/** /**
* 连接状态 * @zh 连接状态
* Connection state * @en Connection state
*/ */
export const enum ENetworkState { export const enum NetworkState {
Disconnected = 0, Disconnected = 0,
Connecting = 1, Connecting = 1,
Connected = 2 Connected = 2,
} }
/** /**
* 网络事件回调 * @zh 网络服务配置
* Network event callbacks * @en Network service options
*/ */
export interface INetworkCallbacks { export interface NetworkServiceOptions extends RpcClientOptions {
onConnected?: (clientId: number, roomId: string) => void; /**
onDisconnected?: () => void; * @zh 服务器地址
onSync?: (msg: MsgSync) => void; * @en Server URL
onSpawn?: (msg: MsgSpawn) => void;
onDespawn?: (msg: MsgDespawn) => void;
onError?: (error: Error) => void;
}
/**
* 创建 TSRPC 客户端
* Create TSRPC client
*/ */
function createClient(serverUrl: string): WsClient<ServiceType> { url: string
return new WsClient(serviceProto, {
server: serverUrl,
json: true,
logLevel: 'warn'
});
} }
// ============================================================================
// RpcService - Base Class | RPC 服务基类
// ============================================================================
/** /**
* 网络服务 * @zh RPC 服务基类
* Network service * @en RPC Service base class
* *
* 基于 TSRPC 的网络服务封装,提供类型安全的网络通信。 * @zh 纯粹的 RPC 客户端封装,不包含任何游戏特定逻辑
* TSRPC-based network service wrapper with type-safe communication. * @en Pure RPC client wrapper without any game-specific logic
*
* @typeParam P - @zh 协议定义类型 @en Protocol definition type
*/ */
export class NetworkService { export class RpcService<P extends ProtocolDef> {
private _client: WsClient<ServiceType> | null = null; protected _client: RpcClient<P> | null = null
private _state: ENetworkState = ENetworkState.Disconnected; protected _state: NetworkState = NetworkState.Disconnected
private _clientId: number = 0;
private _roomId: string = '';
private _callbacks: INetworkCallbacks = {};
get state(): ENetworkState { constructor(protected readonly _protocol: P) {}
return this._state;
} /**
* @zh 获取连接状态
get clientId(): number { * @en Get connection state
return this._clientId; */
} get state(): NetworkState {
return this._state
get roomId(): string {
return this._roomId;
} }
/**
* @zh 是否已连接
* @en Whether connected
*/
get isConnected(): boolean { get isConnected(): boolean {
return this._state === ENetworkState.Connected; return this._state === NetworkState.Connected
} }
/** /**
* 设置回调 * @zh 获取底层 RPC 客户端
* Set callbacks * @en Get underlying RPC client
*/ */
setCallbacks(callbacks: INetworkCallbacks): void { get client(): RpcClient<P> | null {
this._callbacks = { ...this._callbacks, ...callbacks }; return this._client
} }
/** /**
* 连接到服务器 * @zh 连接到服务器
* Connect to server * @en Connect to server
*/ */
async connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean> { async connect(options: NetworkServiceOptions): Promise<void> {
if (this._state !== ENetworkState.Disconnected) { if (this._state !== NetworkState.Disconnected) {
return false; throw new Error('Already connected or connecting')
} }
this._state = ENetworkState.Connecting; this._state = NetworkState.Connecting
this._client = createClient(serverUrl);
this._setupListeners();
// 连接 try {
// Connect this._client = new RpcClient(this._protocol, options.url, {
const connectResult = await this._client.connect(); ...options,
if (!connectResult.isSucc) { onConnect: () => {
this._state = ENetworkState.Disconnected; this._state = NetworkState.Connected
this._callbacks.onError?.(new Error(connectResult.errMsg)); options.onConnect?.()
return false; },
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;
} }
/** /**
* 断开连接 * @zh 断开连接
* Disconnect * @en Disconnect
*/ */
async disconnect(): Promise<void> { disconnect(): void {
if (this._client) { this._client?.disconnect()
await this._client.disconnect(); this._client = null
} this._state = NetworkState.Disconnected
this._state = ENetworkState.Disconnected;
this._clientId = 0;
this._roomId = '';
this._client = null;
} }
/** /**
* 发送输入 * @zh 调用 API
* Send input * @en Call API
*/ */
sendInput(input: IPlayerInput): void { call<K extends ApiNames<P>>(
if (!this.isConnected || !this._client) return; name: K,
this._client.sendMsg('Input', { input }); 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); * @zh 监听消息(只触发一次)
}); * @en Listen to message (once)
*/
this._client.flows.postDisconnectFlow.push((v) => { once<K extends MsgNames<P>>(
this._state = ENetworkState.Disconnected; name: K,
this._callbacks.onDisconnected?.(); handler: (data: MsgData<P['msg'][K]>) => void
return v; ): 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()
}

View File

@@ -1,73 +1,74 @@
import { EntitySystem, Matcher } from '@esengine/ecs-framework'; /**
import type { IPlayerInput } from '@esengine/network-protocols'; * @zh 网络输入系统
import type { NetworkService } from '../services/NetworkService'; * @en Network Input System
*/
import { EntitySystem, Matcher } from '@esengine/ecs-framework'
import type { PlayerInput } from '../protocol'
import type { NetworkService } from '../services/NetworkService'
/** /**
* 网络输入系统 * @zh 网络输入系统
* Network input system * @en Network input system
* *
* 收集本地玩家输入并发送到服务器 * @zh 收集本地玩家输入并发送到服务器
* Collects local player input and sends to server. * @en Collects local player input and sends to server
*/ */
export class NetworkInputSystem extends EntitySystem { export class NetworkInputSystem extends EntitySystem {
private _networkService: NetworkService; private _networkService: NetworkService
private _frame: number = 0; private _frame: number = 0
private _inputQueue: IPlayerInput[] = []; private _inputQueue: PlayerInput[] = []
constructor(networkService: NetworkService) { constructor(networkService: NetworkService) {
// 不查询任何实体,此系统只处理输入 super(Matcher.nothing())
// Don't query any entities, this system only handles input this._networkService = networkService
super(Matcher.nothing());
this._networkService = networkService;
} }
/** /**
* 处理输入队列 * @zh 处理输入队列
* Process input queue * @en Process input queue
*/ */
protected override process(): void { 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) { while (this._inputQueue.length > 0) {
const input = this._inputQueue.shift()!; const input = this._inputQueue.shift()!
input.frame = this._frame; input.frame = this._frame
this._networkService.sendInput(input); this._networkService.sendInput(input)
} }
} }
/** /**
* 添加移动输入 * @zh 添加移动输入
* Add move input * @en Add move input
*/ */
public addMoveInput(x: number, y: number): void { public addMoveInput(x: number, y: number): void {
this._inputQueue.push({ this._inputQueue.push({
frame: 0, frame: 0,
moveDir: { x, y } moveDir: { x, y },
}); })
} }
/** /**
* 添加动作输入 * @zh 添加动作输入
* Add action input * @en Add action input
*/ */
public addActionInput(action: string): void { public addActionInput(action: string): void {
const lastInput = this._inputQueue[this._inputQueue.length - 1]; const lastInput = this._inputQueue[this._inputQueue.length - 1]
if (lastInput) { if (lastInput) {
lastInput.actions = lastInput.actions || []; lastInput.actions = lastInput.actions || []
lastInput.actions.push(action); lastInput.actions.push(action)
} else { } else {
this._inputQueue.push({ this._inputQueue.push({
frame: 0, frame: 0,
actions: [action] actions: [action],
}); })
} }
} }
protected override onDestroy(): void { 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 { EntitySystem, Entity, type Scene, Matcher } from '@esengine/ecs-framework'
import type { MsgSpawn, MsgDespawn } from '@esengine/network-protocols'; import { NetworkIdentity } from '../components/NetworkIdentity'
import { NetworkIdentity } from '../components/NetworkIdentity'; import { NetworkTransform } from '../components/NetworkTransform'
import { NetworkTransform } from '../components/NetworkTransform'; import type { NetworkSyncSystem } from './NetworkSyncSystem'
import type { NetworkService } from '../services/NetworkService';
import type { NetworkSyncSystem } from './NetworkSyncSystem';
/** /**
* 预制体工厂函数类型 * @zh 生成消息接口
* Prefab factory function type * @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
}
/** /**
* 网络生成系统 * @zh 销毁消息接口
* Network spawn system * @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
* *
* 处理网络实体的生成和销毁 * @zh 处理网络实体的生成和销毁
* Handles spawning and despawning of networked entities. * @en Handles spawning and despawning of networked entities
*/ */
export class NetworkSpawnSystem extends EntitySystem { export class NetworkSpawnSystem extends EntitySystem {
private _networkService: NetworkService; private _syncSystem: NetworkSyncSystem
private _syncSystem: NetworkSyncSystem; private _prefabFactories: Map<string, PrefabFactory> = new Map()
private _prefabFactories: Map<string, PrefabFactory> = new Map(); private _localPlayerId: number = 0
constructor(networkService: NetworkService, syncSystem: NetworkSyncSystem) { constructor(syncSystem: NetworkSyncSystem) {
// 不查询任何实体,此系统只响应网络消息 super(Matcher.nothing())
// Don't query any entities, this system only responds to network messages this._syncSystem = syncSystem
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)
});
} }
/** /**
* 注册预制体工厂 * @zh 设置本地玩家 ID
* Register prefab factory * @en Set local player ID
*/ */
public registerPrefab(prefabType: string, factory: PrefabFactory): void { setLocalPlayerId(id: number): void {
this._prefabFactories.set(prefabType, factory); this._localPlayerId = id
} }
/** /**
* 注销预制体工厂 * @zh 处理生成消息
* Unregister prefab factory * @en Handle spawn message
*/ */
public unregisterPrefab(prefabType: string): void { handleSpawn(msg: SpawnMessage): Entity | null {
this._prefabFactories.delete(prefabType); if (!this.scene) return null
}
private _handleSpawn(msg: MsgSpawn): void { const factory = this._prefabFactories.get(msg.prefab)
if (!this.scene) return;
const factory = this._prefabFactories.get(msg.prefab);
if (!factory) { if (!factory) {
this.logger.warn(`Unknown prefab: ${msg.prefab}`); this.logger.warn(`Unknown prefab: ${msg.prefab}`)
return; return null
} }
const entity = factory(this.scene, msg); const entity = factory(this.scene, msg)
// 添加网络组件 const identity = entity.addComponent(new NetworkIdentity())
// Add network components identity.netId = msg.netId
const identity = entity.addComponent(new NetworkIdentity()); identity.ownerId = msg.ownerId
identity.netId = msg.netId; identity.prefabType = msg.prefab
identity.ownerId = msg.ownerId; identity.bHasAuthority = msg.ownerId === this._localPlayerId
identity.prefabType = msg.prefab; identity.bIsLocalPlayer = identity.bHasAuthority
identity.bHasAuthority = msg.ownerId === this._networkService.clientId;
identity.bIsLocalPlayer = identity.bHasAuthority;
const transform = entity.addComponent(new NetworkTransform()); const transform = entity.addComponent(new NetworkTransform())
transform.setTarget(msg.pos.x, msg.pos.y, msg.rot); transform.setTarget(msg.pos.x, msg.pos.y, msg.rot ?? 0)
transform.snap(); transform.snap()
// 注册到同步系统 this._syncSystem.registerEntity(msg.netId, entity.id)
// Register to sync system
this._syncSystem.registerEntity(msg.netId, entity.id); return entity
} }
private _handleDespawn(msg: MsgDespawn): void { /**
const entityId = this._syncSystem.getEntityId(msg.netId); * @zh 处理销毁消息
if (entityId === undefined) return; * @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) { 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 { 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 { EntitySystem, Matcher, Time, type Entity } from '@esengine/ecs-framework'
import type { MsgSync } from '@esengine/network-protocols'; import { NetworkIdentity } from '../components/NetworkIdentity'
import { NetworkIdentity } from '../components/NetworkIdentity'; import { NetworkTransform } from '../components/NetworkTransform'
import { NetworkTransform } from '../components/NetworkTransform';
import type { NetworkService } from '../services/NetworkService';
/** /**
* 网络同步系统 * @zh 同步消息接口
* Network sync system * @en Sync message interface
*/
export interface SyncMessage {
entities: Array<{
netId: number
pos?: { x: number; y: number }
rot?: number
}>
}
/**
* @zh 网络同步系统
* @en Network sync system
* *
* 处理网络实体的状态同步和插值 * @zh 处理网络实体的状态同步和插值
* Handles state synchronization and interpolation for networked entities. * @en Handles state synchronization and interpolation for networked entities
*/ */
export class NetworkSyncSystem extends EntitySystem { 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) { constructor() {
super(Matcher.all(NetworkIdentity, NetworkTransform)); super(Matcher.all(NetworkIdentity, NetworkTransform))
this._networkService = networkService;
}
protected override onInitialize(): void {
this._networkService.setCallbacks({
onSync: this._handleSync.bind(this)
});
} }
/** /**
* 处理实体列表 * @zh 处理同步消息
* Process entities * @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 { protected override process(entities: readonly Entity[]): void {
const deltaTime = Time.deltaTime; const deltaTime = Time.deltaTime
for (const entity of entities) { for (const entity of entities) {
const transform = this.requireComponent(entity, NetworkTransform); const transform = this.requireComponent(entity, NetworkTransform)
const identity = this.requireComponent(entity, NetworkIdentity); const identity = this.requireComponent(entity, NetworkIdentity)
// 只有非本地玩家需要插值
// Only non-local players need interpolation
if (!identity.bHasAuthority && transform.bInterpolate) { if (!identity.bHasAuthority && transform.bInterpolate) {
this._interpolate(transform, deltaTime); this._interpolate(transform, deltaTime)
} }
} }
} }
/** /**
* 注册网络实体 * @zh 注册网络实体
* Register network entity * @en Register network entity
*/ */
public registerEntity(netId: number, entityId: number): void { registerEntity(netId: number, entityId: number): void {
this._netIdToEntity.set(netId, entityId); this._netIdToEntity.set(netId, entityId)
} }
/** /**
* 注销网络实体 * @zh 注销网络实体
* Unregister network entity * @en Unregister network entity
*/ */
public unregisterEntity(netId: number): void { unregisterEntity(netId: number): void {
this._netIdToEntity.delete(netId); this._netIdToEntity.delete(netId)
} }
/** /**
* 根据网络 ID 获取实体 ID * @zh 根据网络 ID 获取实体 ID
* Get entity ID by network ID * @en Get entity ID by network ID
*/ */
public getEntityId(netId: number): number | undefined { getEntityId(netId: number): number | undefined {
return this._netIdToEntity.get(netId); 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);
}
}
} }
private _interpolate(transform: NetworkTransform, deltaTime: number): void { 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.currentX += (transform.targetX - transform.currentX) * t
transform.currentY += (transform.targetY - transform.currentY) * t; transform.currentY += (transform.targetY - transform.currentY) * t
// 角度插值需要处理环绕 let angleDiff = transform.targetRotation - transform.currentRotation
// Angle interpolation needs to handle wrap-around while (angleDiff > Math.PI) angleDiff -= Math.PI * 2
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
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
transform.currentRotation += angleDiff * t;
} }
protected override onDestroy(): void { protected override onDestroy(): void {
this._netIdToEntity.clear(); this._netIdToEntity.clear()
} }
} }

View File

@@ -5,12 +5,8 @@
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src"
"moduleResolution": "node"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts"]
"references": [
{ "path": "../blueprint" }
]
} }

View File

@@ -3,25 +3,12 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src"
"moduleResolution": "node"
}, },
"include": [ "include": ["src/**/*"],
"src/**/*" "exclude": ["node_modules", "dist"],
],
"exclude": [
"node_modules",
"dist"
],
"references": [ "references": [
{ { "path": "../core" },
"path": "../core" { "path": "../blueprint" }
},
{
"path": "../network-protocols"
},
{
"path": "../blueprint"
}
] ]
} }

View File

@@ -1,14 +1,10 @@
import { defineConfig } from 'tsup'; import { defineConfig } from 'tsup'
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup'; import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup'
export default defineConfig({ export default defineConfig({
...runtimeOnlyPreset({ ...runtimeOnlyPreset({}),
external: [/^tsrpc/, 'tsbuffer', 'tsbuffer-schema']
}),
tsconfig: 'tsconfig.build.json', tsconfig: 'tsconfig.build.json',
// 禁用 tsup 的 DTS 捆绑器,改用 tsc 生成声明文件 // tsup 的 DTS bundler 无法正确解析 workspace 包的类型继承链
// tsup DTS bundler 无法正确解析 tsrpc 的类型继承链 // tsup's DTS bundler cannot correctly resolve workspace package type inheritance
// Disable tsup's DTS bundler, use tsc to generate declarations dts: false,
// tsup's DTS bundler cannot correctly resolve tsrpc's type inheritance chain })
dts: false
});

View File

@@ -0,0 +1,20 @@
{
"id": "rpc",
"name": "@esengine/rpc",
"globalKey": "rpc",
"displayName": "RPC",
"description": "类型安全的 RPC 通信库 | Type-safe RPC communication library",
"version": "1.0.0",
"category": "Network",
"icon": "Network",
"tags": ["rpc", "websocket", "network", "multiplayer"],
"isCore": false,
"defaultEnabled": true,
"isEngineModule": true,
"canContainContent": false,
"platforms": ["web", "desktop", "node"],
"dependencies": [],
"exports": {},
"requiresWasm": false,
"outputPath": "dist/index.js"
}

View File

@@ -0,0 +1,60 @@
{
"name": "@esengine/rpc",
"version": "1.0.0",
"description": "Elegant type-safe RPC library for ESEngine",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./server": {
"import": "./dist/server/index.js",
"types": "./dist/server/index.d.ts"
},
"./client": {
"import": "./dist/client/index.js",
"types": "./dist/client/index.d.ts"
},
"./codec": {
"import": "./dist/codec/index.js",
"types": "./dist/codec/index.d.ts"
}
},
"files": [
"dist",
"module.json"
],
"scripts": {
"build": "tsup && tsc --emitDeclarationOnly",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {},
"optionalDependencies": {
"msgpackr": "^1.11.0"
},
"devDependencies": {
"@types/ws": "^8.5.13",
"msgpackr": "^1.11.0",
"ws": "^8.18.0",
"tsup": "^8.0.0",
"typescript": "^5.7.0"
},
"peerDependencies": {
"typescript": ">=5.0.0",
"ws": ">=8.0.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
}
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,461 @@
/**
* @zh RPC 客户端模块
* @en RPC Client Module
*/
import type {
ProtocolDef,
ApiNames,
MsgNames,
ApiInput,
ApiOutput,
MsgData,
Packet,
ConnectionStatus,
} from '../types'
import { RpcError, ErrorCode } from '../types'
import { json } from '../codec/json'
import type { Codec } from '../codec/types'
// ============================================================================
// Re-exports | 类型重导出
// ============================================================================
export type {
ProtocolDef,
ApiNames,
MsgNames,
ApiInput,
ApiOutput,
MsgData,
ConnectionStatus,
Codec,
}
export { RpcError, ErrorCode }
// ============================================================================
// Types | 类型定义
// ============================================================================
/**
* @zh WebSocket 适配器接口
* @en WebSocket adapter interface
*
* @zh 用于适配不同平台的 WebSocket 实现(浏览器、微信小游戏等)
* @en Used to adapt different platform WebSocket implementations (browser, WeChat Mini Games, etc.)
*/
export interface WebSocketAdapter {
readonly readyState: number
send(data: string | ArrayBuffer): void
close(code?: number, reason?: string): void
onopen: ((ev: Event) => void) | null
onclose: ((ev: { code: number; reason: string }) => void) | null
onerror: ((ev: Event) => void) | null
onmessage: ((ev: { data: string | ArrayBuffer }) => void) | null
}
/**
* @zh WebSocket 工厂函数类型
* @en WebSocket factory function type
*/
export type WebSocketFactory = (url: string) => WebSocketAdapter
/**
* @zh 客户端配置
* @en Client options
*/
export interface RpcClientOptions {
/**
* @zh 编解码器
* @en Codec
* @defaultValue json()
*/
codec?: Codec
/**
* @zh API 调用超时(毫秒)
* @en API call timeout in milliseconds
* @defaultValue 30000
*/
timeout?: number
/**
* @zh 自动重连
* @en Auto reconnect
* @defaultValue true
*/
autoReconnect?: boolean
/**
* @zh 重连间隔(毫秒)
* @en Reconnect interval in milliseconds
* @defaultValue 3000
*/
reconnectInterval?: number
/**
* @zh WebSocket 工厂函数
* @en WebSocket factory function
*
* @zh 用于自定义 WebSocket 实现,如微信小游戏
* @en Used for custom WebSocket implementation, e.g., WeChat Mini Games
*/
webSocketFactory?: WebSocketFactory
/**
* @zh 连接成功回调
* @en Connection established callback
*/
onConnect?: () => void
/**
* @zh 连接断开回调
* @en Connection closed callback
*/
onDisconnect?: (reason?: string) => void
/**
* @zh 错误回调
* @en Error callback
*/
onError?: (error: Error) => void
}
/** @deprecated Use RpcClientOptions instead */
export type ConnectOptions = RpcClientOptions
// ============================================================================
// Constants | 常量
// ============================================================================
const PacketType = {
ApiRequest: 0,
ApiResponse: 1,
ApiError: 2,
Message: 3,
Heartbeat: 9,
} as const
const defaultWebSocketFactory: WebSocketFactory = (url) =>
new WebSocket(url) as unknown as WebSocketAdapter
// ============================================================================
// RpcClient Class | RPC 客户端类
// ============================================================================
interface PendingCall {
resolve: (value: unknown) => void
reject: (error: Error) => void
timer: ReturnType<typeof setTimeout>
}
/**
* @zh RPC 客户端
* @en RPC Client
*
* @example
* ```typescript
* const client = new RpcClient(protocol, 'ws://localhost:3000')
* await client.connect()
*
* const result = await client.call('join', { name: 'Alice' })
*
* client.on('chat', (msg) => console.log(msg.text))
* ```
*/
export class RpcClient<P extends ProtocolDef> {
private readonly _url: string
private readonly _codec: Codec
private readonly _timeout: number
private readonly _reconnectInterval: number
private readonly _wsFactory: WebSocketFactory
private readonly _options: RpcClientOptions
private _ws: WebSocketAdapter | null = null
private _status: ConnectionStatus = 'closed'
private _callIdCounter = 0
private _shouldReconnect: boolean
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null
private readonly _pendingCalls = new Map<number, PendingCall>()
private readonly _msgHandlers = new Map<string, Set<(data: unknown) => void>>()
constructor(
_protocol: P,
url: string,
options: RpcClientOptions = {}
) {
this._url = url
this._options = options
this._codec = options.codec ?? json()
this._timeout = options.timeout ?? 30000
this._shouldReconnect = options.autoReconnect ?? true
this._reconnectInterval = options.reconnectInterval ?? 3000
this._wsFactory = options.webSocketFactory ?? defaultWebSocketFactory
}
/**
* @zh 连接状态
* @en Connection status
*/
get status(): ConnectionStatus {
return this._status
}
/**
* @zh 是否已连接
* @en Whether connected
*/
get isConnected(): boolean {
return this._status === 'open'
}
/**
* @zh 连接到服务器
* @en Connect to server
*/
connect(): Promise<this> {
return new Promise((resolve, reject) => {
if (this._status === 'open' || this._status === 'connecting') {
resolve(this)
return
}
this._status = 'connecting'
this._ws = this._wsFactory(this._url)
this._ws.onopen = () => {
this._status = 'open'
this._options.onConnect?.()
resolve(this)
}
this._ws.onclose = (e) => {
this._status = 'closed'
this._rejectAllPending()
this._options.onDisconnect?.(e.reason)
this._scheduleReconnect()
}
this._ws.onerror = () => {
const err = new Error('WebSocket error')
this._options.onError?.(err)
if (this._status === 'connecting') {
reject(err)
}
}
this._ws.onmessage = (e) => {
this._handleMessage(e.data as string | ArrayBuffer)
}
})
}
/**
* @zh 断开连接
* @en Disconnect
*/
disconnect(): void {
this._shouldReconnect = false
this._clearReconnectTimer()
if (this._ws) {
this._status = 'closing'
this._ws.close()
this._ws = null
}
}
/**
* @zh 调用 API
* @en Call API
*/
call<K extends ApiNames<P>>(
name: K,
input: ApiInput<P['api'][K]>
): Promise<ApiOutput<P['api'][K]>> {
return new Promise((resolve, reject) => {
if (this._status !== 'open') {
reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Not connected'))
return
}
const id = ++this._callIdCounter
const timer = setTimeout(() => {
this._pendingCalls.delete(id)
reject(new RpcError(ErrorCode.TIMEOUT, 'Request timeout'))
}, this._timeout)
this._pendingCalls.set(id, {
resolve: resolve as (v: unknown) => void,
reject,
timer,
})
const packet: Packet = [PacketType.ApiRequest, id, name as string, input]
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer)
})
}
/**
* @zh 发送消息
* @en Send message
*/
send<K extends MsgNames<P>>(name: K, data: MsgData<P['msg'][K]>): void {
if (this._status !== 'open') return
const packet: Packet = [PacketType.Message, name as string, data]
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer)
}
/**
* @zh 监听消息
* @en Listen to message
*/
on<K extends MsgNames<P>>(
name: K,
handler: (data: MsgData<P['msg'][K]>) => void
): this {
const key = name as string
let handlers = this._msgHandlers.get(key)
if (!handlers) {
handlers = new Set()
this._msgHandlers.set(key, handlers)
}
handlers.add(handler as (data: unknown) => void)
return this
}
/**
* @zh 取消监听消息
* @en Remove message listener
*/
off<K extends MsgNames<P>>(
name: K,
handler?: (data: MsgData<P['msg'][K]>) => void
): this {
const key = name as string
if (handler) {
this._msgHandlers.get(key)?.delete(handler as (data: unknown) => void)
} else {
this._msgHandlers.delete(key)
}
return this
}
/**
* @zh 监听消息(只触发一次)
* @en Listen to message (once)
*/
once<K extends MsgNames<P>>(
name: K,
handler: (data: MsgData<P['msg'][K]>) => void
): this {
const wrapper = (data: MsgData<P['msg'][K]>) => {
this.off(name, wrapper)
handler(data)
}
return this.on(name, wrapper)
}
// ========================================================================
// Private Methods | 私有方法
// ========================================================================
private _handleMessage(raw: string | ArrayBuffer): void {
try {
const data = typeof raw === 'string' ? raw : new Uint8Array(raw)
const packet = this._codec.decode(data)
const type = packet[0]
switch (type) {
case PacketType.ApiResponse:
this._handleApiResponse(packet as [number, number, unknown])
break
case PacketType.ApiError:
this._handleApiError(packet as [number, number, string, string])
break
case PacketType.Message:
this._handleMsg(packet as [number, string, unknown])
break
}
} catch (err) {
this._options.onError?.(err as Error)
}
}
private _handleApiResponse([, id, result]: [number, number, unknown]): void {
const pending = this._pendingCalls.get(id)
if (pending) {
clearTimeout(pending.timer)
this._pendingCalls.delete(id)
pending.resolve(result)
}
}
private _handleApiError([, id, code, message]: [number, number, string, string]): void {
const pending = this._pendingCalls.get(id)
if (pending) {
clearTimeout(pending.timer)
this._pendingCalls.delete(id)
pending.reject(new RpcError(code, message))
}
}
private _handleMsg([, path, data]: [number, string, unknown]): void {
const handlers = this._msgHandlers.get(path)
if (handlers) {
for (const handler of handlers) {
try {
handler(data)
} catch (err) {
this._options.onError?.(err as Error)
}
}
}
}
private _rejectAllPending(): void {
for (const [, pending] of this._pendingCalls) {
clearTimeout(pending.timer)
pending.reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Connection closed'))
}
this._pendingCalls.clear()
}
private _scheduleReconnect(): void {
if (this._shouldReconnect && !this._reconnectTimer) {
this._reconnectTimer = setTimeout(() => {
this._reconnectTimer = null
this.connect().catch(() => {})
}, this._reconnectInterval)
}
}
private _clearReconnectTimer(): void {
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer)
this._reconnectTimer = null
}
}
}
// ============================================================================
// Factory Function | 工厂函数
// ============================================================================
/**
* @zh 连接到 RPC 服务器(便捷函数)
* @en Connect to RPC server (convenience function)
*
* @example
* ```typescript
* const client = await connect(protocol, 'ws://localhost:3000')
* const result = await client.call('join', { name: 'Alice' })
* ```
*/
export function connect<P extends ProtocolDef>(
protocol: P,
url: string,
options: RpcClientOptions = {}
): Promise<RpcClient<P>> {
return new RpcClient(protocol, url, options).connect()
}

View File

@@ -0,0 +1,9 @@
/**
* @zh 编解码器模块
* @en Codec Module
*/
export type { Codec } from './types'
export { json } from './json'
export { msgpack } from './msgpack'
export { textEncode, textDecode } from './polyfill'

View File

@@ -0,0 +1,30 @@
/**
* @zh JSON 编解码器
* @en JSON Codec
*/
import type { Packet } from '../types'
import type { Codec } from './types'
import { textDecode } from './polyfill'
/**
* @zh 创建 JSON 编解码器
* @en Create JSON codec
*
* @zh 适用于开发调试,可读性好
* @en Suitable for development, human-readable
*/
export function json(): Codec {
return {
encode(packet: Packet): string {
return JSON.stringify(packet)
},
decode(data: string | Uint8Array): Packet {
const str = typeof data === 'string'
? data
: textDecode(data)
return JSON.parse(str) as Packet
},
}
}

View File

@@ -0,0 +1,34 @@
/**
* @zh MessagePack 编解码器
* @en MessagePack Codec
*/
import { Packr, Unpackr } from 'msgpackr'
import type { Packet } from '../types'
import type { Codec } from './types'
import { textEncode } from './polyfill'
/**
* @zh 创建 MessagePack 编解码器
* @en Create MessagePack codec
*
* @zh 适用于生产环境,体积更小、速度更快
* @en Suitable for production, smaller size and faster speed
*/
export function msgpack(): Codec {
const encoder = new Packr({ structuredClone: true })
const decoder = new Unpackr({ structuredClone: true })
return {
encode(packet: Packet): Uint8Array {
return encoder.pack(packet)
},
decode(data: string | Uint8Array): Packet {
const buf = typeof data === 'string'
? textEncode(data)
: data
return decoder.unpack(buf) as Packet
},
}
}

View File

@@ -0,0 +1,112 @@
/**
* @zh 平台兼容性 polyfill
* @en Platform compatibility polyfill
*
* @zh 为微信小游戏等不支持原生 TextEncoder/TextDecoder 的平台提供兼容层
* @en Provides compatibility layer for platforms like WeChat Mini Games that don't support native TextEncoder/TextDecoder
*/
/**
* @zh 获取全局 TextEncoder 实现
* @en Get global TextEncoder implementation
*/
function getTextEncoder(): { encode(str: string): Uint8Array } {
if (typeof TextEncoder !== 'undefined') {
return new TextEncoder()
}
return {
encode(str: string): Uint8Array {
const utf8: number[] = []
for (let i = 0; i < str.length; i++) {
let charCode = str.charCodeAt(i)
if (charCode < 0x80) {
utf8.push(charCode)
} else if (charCode < 0x800) {
utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f))
} else if (charCode >= 0xd800 && charCode <= 0xdbff) {
i++
const low = str.charCodeAt(i)
charCode = 0x10000 + ((charCode - 0xd800) << 10) + (low - 0xdc00)
utf8.push(
0xf0 | (charCode >> 18),
0x80 | ((charCode >> 12) & 0x3f),
0x80 | ((charCode >> 6) & 0x3f),
0x80 | (charCode & 0x3f)
)
} else {
utf8.push(
0xe0 | (charCode >> 12),
0x80 | ((charCode >> 6) & 0x3f),
0x80 | (charCode & 0x3f)
)
}
}
return new Uint8Array(utf8)
},
}
}
/**
* @zh 获取全局 TextDecoder 实现
* @en Get global TextDecoder implementation
*/
function getTextDecoder(): { decode(data: Uint8Array): string } {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder()
}
return {
decode(data: Uint8Array): string {
let str = ''
let i = 0
while (i < data.length) {
const byte1 = data[i++]
if (byte1 < 0x80) {
str += String.fromCharCode(byte1)
} else if ((byte1 & 0xe0) === 0xc0) {
const byte2 = data[i++]
str += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f))
} else if ((byte1 & 0xf0) === 0xe0) {
const byte2 = data[i++]
const byte3 = data[i++]
str += String.fromCharCode(
((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
)
} else if ((byte1 & 0xf8) === 0xf0) {
const byte2 = data[i++]
const byte3 = data[i++]
const byte4 = data[i++]
const codePoint =
((byte1 & 0x07) << 18) |
((byte2 & 0x3f) << 12) |
((byte3 & 0x3f) << 6) |
(byte4 & 0x3f)
const offset = codePoint - 0x10000
str += String.fromCharCode(
0xd800 + (offset >> 10),
0xdc00 + (offset & 0x3ff)
)
}
}
return str
},
}
}
const encoder = getTextEncoder()
const decoder = getTextDecoder()
/**
* @zh 将字符串编码为 UTF-8 字节数组
* @en Encode string to UTF-8 byte array
*/
export function textEncode(str: string): Uint8Array {
return encoder.encode(str)
}
/**
* @zh 将 UTF-8 字节数组解码为字符串
* @en Decode UTF-8 byte array to string
*/
export function textDecode(data: Uint8Array): string {
return decoder.decode(data)
}

View File

@@ -0,0 +1,24 @@
/**
* @zh 编解码器类型定义
* @en Codec Type Definitions
*/
import type { Packet } from '../types'
/**
* @zh 编解码器接口
* @en Codec interface
*/
export interface Codec {
/**
* @zh 编码数据包
* @en Encode packet
*/
encode(packet: Packet): string | Uint8Array
/**
* @zh 解码数据包
* @en Decode packet
*/
decode(data: string | Uint8Array): Packet
}

View File

@@ -0,0 +1,63 @@
/**
* @zh 协议定义模块
* @en Protocol Definition Module
*/
import type { ApiDef, MsgDef, ProtocolDef } from './types'
/**
* @zh 创建 API 定义
* @en Create API definition
*
* @example
* ```typescript
* const join = rpc.api<{ name: string }, { id: string }>()
* ```
*/
function api<TInput = void, TOutput = void>(): ApiDef<TInput, TOutput> {
return { _type: 'api' } as ApiDef<TInput, TOutput>
}
/**
* @zh 创建消息定义
* @en Create message definition
*
* @example
* ```typescript
* const chat = rpc.msg<{ from: string; text: string }>()
* ```
*/
function msg<TData = void>(): MsgDef<TData> {
return { _type: 'msg' } as MsgDef<TData>
}
/**
* @zh 定义协议
* @en Define protocol
*
* @example
* ```typescript
* export const protocol = rpc.define({
* api: {
* join: rpc.api<{ name: string }, { id: string }>(),
* leave: rpc.api<void, void>(),
* },
* msg: {
* chat: rpc.msg<{ from: string; text: string }>(),
* },
* })
* ```
*/
function define<T extends ProtocolDef>(protocol: T): T {
return protocol
}
/**
* @zh RPC 协议定义工具
* @en RPC protocol definition utilities
*/
export const rpc = {
define,
api,
msg,
} as const

View File

@@ -0,0 +1,42 @@
/**
* @zh ESEngine RPC 库
* @en ESEngine RPC Library
*
* @zh 类型安全的 RPC 通信库,支持 WebSocket 长连接
* @en Type-safe RPC communication library with WebSocket support
*
* @example
* ```typescript
* // 1. 定义协议(共享)
* import { rpc } from '@esengine/rpc'
*
* export const protocol = rpc.define({
* api: {
* join: rpc.api<{ name: string }, { id: string }>(),
* },
* msg: {
* chat: rpc.msg<{ from: string; text: string }>(),
* },
* })
*
* // 2. 服务端
* import { serve } from '@esengine/rpc/server'
*
* const server = serve(protocol, {
* port: 3000,
* api: {
* join: async (input, conn) => ({ id: conn.id }),
* },
* })
* await server.start()
*
* // 3. 客户端
* import { connect } from '@esengine/rpc/client'
*
* const client = await connect(protocol, 'ws://localhost:3000')
* const result = await client.call('join', { name: 'Alice' })
* ```
*/
export { rpc } from './define'
export * from './types'

View File

@@ -0,0 +1,68 @@
/**
* @zh 服务端连接模块
* @en Server Connection Module
*/
import type { Connection, ConnectionStatus } from '../types'
/**
* @zh 服务端连接实现
* @en Server connection implementation
*/
export class ServerConnection<TData = unknown> implements Connection<TData> {
readonly id: string
readonly ip: string
data: TData
private _status: ConnectionStatus = 'open'
private _socket: any
private _onClose?: () => void
constructor(options: {
id: string
ip: string
socket: any
initialData: TData
onClose?: () => void
}) {
this.id = options.id
this.ip = options.ip
this.data = options.initialData
this._socket = options.socket
this._onClose = options.onClose
}
get status(): ConnectionStatus {
return this._status
}
/**
* @zh 发送原始数据
* @en Send raw data
*/
send(data: string | Uint8Array): void {
if (this._status !== 'open') return
this._socket.send(data)
}
/**
* @zh 关闭连接
* @en Close connection
*/
close(reason?: string): void {
if (this._status !== 'open') return
this._status = 'closing'
this._socket.close(1000, reason)
this._status = 'closed'
this._onClose?.()
}
/**
* @zh 标记连接已关闭(内部使用)
* @en Mark connection as closed (internal use)
*/
_markClosed(): void {
this._status = 'closed'
}
}

View File

@@ -0,0 +1,372 @@
/**
* @zh RPC 服务端模块
* @en RPC Server Module
*/
import { WebSocketServer, WebSocket } from 'ws'
import type {
ProtocolDef,
ApiNames,
MsgNames,
ApiInput,
ApiOutput,
MsgData,
Packet,
PacketType,
Connection,
} from '../types'
import { RpcError, ErrorCode } from '../types'
import { json } from '../codec/json'
import type { Codec } from '../codec/types'
import { ServerConnection } from './connection'
// ============ Types ============
/**
* @zh API 处理函数
* @en API handler function
*/
type ApiHandler<TInput, TOutput, TConnData> = (
input: TInput,
conn: Connection<TConnData>
) => TOutput | Promise<TOutput>
/**
* @zh 消息处理函数
* @en Message handler function
*/
type MsgHandler<TData, TConnData> = (
data: TData,
conn: Connection<TConnData>
) => void | Promise<void>
/**
* @zh API 处理器映射
* @en API handlers map
*/
type ApiHandlers<P extends ProtocolDef, TConnData> = {
[K in ApiNames<P>]: ApiHandler<
ApiInput<P['api'][K]>,
ApiOutput<P['api'][K]>,
TConnData
>
}
/**
* @zh 消息处理器映射
* @en Message handlers map
*/
type MsgHandlers<P extends ProtocolDef, TConnData> = {
[K in MsgNames<P>]?: MsgHandler<MsgData<P['msg'][K]>, TConnData>
}
/**
* @zh 服务器配置
* @en Server options
*/
export interface ServeOptions<P extends ProtocolDef, TConnData = unknown> {
/**
* @zh 监听端口
* @en Listen port
*/
port: number
/**
* @zh API 处理器
* @en API handlers
*/
api: ApiHandlers<P, TConnData>
/**
* @zh 消息处理器
* @en Message handlers
*/
msg?: MsgHandlers<P, TConnData>
/**
* @zh 编解码器
* @en Codec
* @defaultValue json()
*/
codec?: Codec
/**
* @zh 连接初始数据工厂
* @en Connection initial data factory
*/
createConnData?: () => TConnData
/**
* @zh 连接建立回调
* @en Connection established callback
*/
onConnect?: (conn: Connection<TConnData>) => void | Promise<void>
/**
* @zh 连接断开回调
* @en Connection closed callback
*/
onDisconnect?: (conn: Connection<TConnData>, reason?: string) => void | Promise<void>
/**
* @zh 错误回调
* @en Error callback
*/
onError?: (error: Error, conn?: Connection<TConnData>) => void
/**
* @zh 服务器启动回调
* @en Server started callback
*/
onStart?: (port: number) => void
}
/**
* @zh RPC 服务器实例
* @en RPC Server instance
*/
export interface RpcServer<P extends ProtocolDef, TConnData = unknown> {
/**
* @zh 启动服务器
* @en Start server
*/
start(): Promise<void>
/**
* @zh 停止服务器
* @en Stop server
*/
stop(): Promise<void>
/**
* @zh 获取所有连接
* @en Get all connections
*/
readonly connections: ReadonlyArray<Connection<TConnData>>
/**
* @zh 向单个连接发送消息
* @en Send message to a single connection
*/
send<K extends MsgNames<P>>(
conn: Connection<TConnData>,
name: K,
data: MsgData<P['msg'][K]>
): void
/**
* @zh 广播消息给所有连接
* @en Broadcast message to all connections
*/
broadcast<K extends MsgNames<P>>(
name: K,
data: MsgData<P['msg'][K]>,
options?: { exclude?: Connection<TConnData> | Connection<TConnData>[] }
): void
}
// ============ Implementation ============
const PT = {
ApiRequest: 0,
ApiResponse: 1,
ApiError: 2,
Message: 3,
Heartbeat: 9,
} as const
/**
* @zh 创建 RPC 服务器
* @en Create RPC server
*
* @example
* ```typescript
* const server = serve(protocol, {
* port: 3000,
* api: {
* join: async (input, conn) => {
* return { id: conn.id }
* },
* },
* })
* await server.start()
* ```
*/
export function serve<P extends ProtocolDef, TConnData = unknown>(
_protocol: P,
options: ServeOptions<P, TConnData>
): RpcServer<P, TConnData> {
const codec = options.codec ?? json()
const connections: ServerConnection<TConnData>[] = []
let wss: WebSocketServer | null = null
let connIdCounter = 0
const getClientIp = (ws: WebSocket, req: any): string => {
return req?.headers?.['x-forwarded-for']?.split(',')[0]?.trim()
|| req?.socket?.remoteAddress
|| 'unknown'
}
const handleMessage = async (
conn: ServerConnection<TConnData>,
data: string | Buffer
): Promise<void> => {
try {
const packet = codec.decode(
typeof data === 'string' ? data : new Uint8Array(data)
)
const type = packet[0]
if (type === PT.ApiRequest) {
const [, id, path, input] = packet as [number, number, string, unknown]
await handleApiRequest(conn, id, path, input)
} else if (type === PT.Message) {
const [, path, msgData] = packet as [number, string, unknown]
await handleMsg(conn, path, msgData)
} else if (type === PT.Heartbeat) {
conn.send(codec.encode([PT.Heartbeat]))
}
} catch (err) {
options.onError?.(err as Error, conn)
}
}
const handleApiRequest = async (
conn: ServerConnection<TConnData>,
id: number,
path: string,
input: unknown
): Promise<void> => {
const handler = (options.api as any)[path]
if (!handler) {
const errPacket: Packet = [PT.ApiError, id, ErrorCode.NOT_FOUND, `API not found: ${path}`]
conn.send(codec.encode(errPacket))
return
}
try {
const result = await handler(input, conn)
const resPacket: Packet = [PT.ApiResponse, id, result]
conn.send(codec.encode(resPacket))
} catch (err) {
if (err instanceof RpcError) {
const errPacket: Packet = [PT.ApiError, id, err.code, err.message]
conn.send(codec.encode(errPacket))
} else {
const errPacket: Packet = [PT.ApiError, id, ErrorCode.INTERNAL_ERROR, 'Internal server error']
conn.send(codec.encode(errPacket))
options.onError?.(err as Error, conn)
}
}
}
const handleMsg = async (
conn: ServerConnection<TConnData>,
path: string,
data: unknown
): Promise<void> => {
const handler = options.msg?.[path as MsgNames<P>]
if (handler) {
await (handler as any)(data, conn)
}
}
const server: RpcServer<P, TConnData> = {
get connections() {
return connections as ReadonlyArray<Connection<TConnData>>
},
async start() {
return new Promise((resolve) => {
wss = new WebSocketServer({ port: options.port })
wss.on('connection', async (ws, req) => {
const id = String(++connIdCounter)
const ip = getClientIp(ws, req)
const initialData = options.createConnData?.() ?? ({} as TConnData)
const conn = new ServerConnection<TConnData>({
id,
ip,
socket: ws,
initialData,
onClose: () => {
const idx = connections.indexOf(conn)
if (idx !== -1) connections.splice(idx, 1)
},
})
connections.push(conn)
ws.on('message', (data) => {
handleMessage(conn, data as string | Buffer)
})
ws.on('close', async (code, reason) => {
conn._markClosed()
const idx = connections.indexOf(conn)
if (idx !== -1) connections.splice(idx, 1)
await options.onDisconnect?.(conn, reason?.toString())
})
ws.on('error', (err) => {
options.onError?.(err, conn)
})
await options.onConnect?.(conn)
})
wss.on('listening', () => {
options.onStart?.(options.port)
resolve()
})
})
},
async stop() {
return new Promise((resolve, reject) => {
if (!wss) {
resolve()
return
}
for (const conn of connections) {
conn.close('Server shutting down')
}
wss.close((err) => {
if (err) reject(err)
else resolve()
})
})
},
send(conn, name, data) {
const packet: Packet = [PT.Message, name as string, data]
;(conn as ServerConnection<TConnData>).send(codec.encode(packet))
},
broadcast(name, data, opts) {
const packet: Packet = [PT.Message, name as string, data]
const encoded = codec.encode(packet)
const excludeSet = new Set(
Array.isArray(opts?.exclude)
? opts.exclude
: opts?.exclude
? [opts.exclude]
: []
)
for (const conn of connections) {
if (!excludeSet.has(conn)) {
conn.send(encoded)
}
}
},
}
return server
}

View File

@@ -0,0 +1,217 @@
/**
* @zh RPC 核心类型定义
* @en RPC Core Type Definitions
*/
// ============ Protocol Types ============
/**
* @zh API 定义标记
* @en API definition marker
*/
export interface ApiDef<TInput = unknown, TOutput = unknown> {
readonly _type: 'api'
readonly _input: TInput
readonly _output: TOutput
}
/**
* @zh 消息定义标记
* @en Message definition marker
*/
export interface MsgDef<TData = unknown> {
readonly _type: 'msg'
readonly _data: TData
}
/**
* @zh 协议定义
* @en Protocol definition
*/
export interface ProtocolDef {
readonly api: Record<string, ApiDef<any, any>>
readonly msg: Record<string, MsgDef<any>>
}
// ============ Type Inference ============
/**
* @zh 提取 API 输入类型
* @en Extract API input type
*/
export type ApiInput<T> = T extends ApiDef<infer I, any> ? I : never
/**
* @zh 提取 API 输出类型
* @en Extract API output type
*/
export type ApiOutput<T> = T extends ApiDef<any, infer O> ? O : never
/**
* @zh 提取消息数据类型
* @en Extract message data type
*/
export type MsgData<T> = T extends MsgDef<infer D> ? D : never
/**
* @zh 提取协议中所有 API 名称
* @en Extract all API names from protocol
*/
export type ApiNames<P extends ProtocolDef> = keyof P['api'] & string
/**
* @zh 提取协议中所有消息名称
* @en Extract all message names from protocol
*/
export type MsgNames<P extends ProtocolDef> = keyof P['msg'] & string
// ============ Connection Types ============
/**
* @zh 连接状态
* @en Connection status
*/
export type ConnectionStatus = 'connecting' | 'open' | 'closing' | 'closed'
/**
* @zh 连接接口
* @en Connection interface
*/
export interface Connection<TData = unknown> {
/**
* @zh 连接唯一标识
* @en Connection unique identifier
*/
readonly id: string
/**
* @zh 客户端 IP 地址
* @en Client IP address
*/
readonly ip: string
/**
* @zh 连接状态
* @en Connection status
*/
readonly status: ConnectionStatus
/**
* @zh 用户自定义数据
* @en User-defined data
*/
data: TData
/**
* @zh 关闭连接
* @en Close connection
*/
close(reason?: string): void
}
// ============ Packet Types ============
/**
* @zh 数据包类型
* @en Packet types
*/
export const PacketType = {
ApiRequest: 0,
ApiResponse: 1,
ApiError: 2,
Message: 3,
Heartbeat: 9,
} as const
export type PacketType = typeof PacketType[keyof typeof PacketType]
/**
* @zh API 请求包
* @en API request packet
*/
export type ApiRequestPacket = [
type: typeof PacketType.ApiRequest,
id: number,
path: string,
data: unknown
]
/**
* @zh API 响应包(成功)
* @en API response packet (success)
*/
export type ApiResponsePacket = [
type: typeof PacketType.ApiResponse,
id: number,
data: unknown
]
/**
* @zh API 错误包
* @en API error packet
*/
export type ApiErrorPacket = [
type: typeof PacketType.ApiError,
id: number,
code: string,
message: string
]
/**
* @zh 消息包
* @en Message packet
*/
export type MessagePacket = [
type: typeof PacketType.Message,
path: string,
data: unknown
]
/**
* @zh 心跳包
* @en Heartbeat packet
*/
export type HeartbeatPacket = [type: typeof PacketType.Heartbeat]
/**
* @zh 所有数据包类型
* @en All packet types
*/
export type Packet =
| ApiRequestPacket
| ApiResponsePacket
| ApiErrorPacket
| MessagePacket
| HeartbeatPacket
// ============ Error Types ============
/**
* @zh RPC 错误
* @en RPC Error
*/
export class RpcError extends Error {
constructor(
public readonly code: string,
message: string,
public readonly details?: unknown
) {
super(message)
this.name = 'RpcError'
}
}
/**
* @zh 预定义错误码
* @en Predefined error codes
*/
export const ErrorCode = {
INVALID_REQUEST: 'INVALID_REQUEST',
NOT_FOUND: 'NOT_FOUND',
UNAUTHORIZED: 'UNAUTHORIZED',
INTERNAL_ERROR: 'INTERNAL_ERROR',
TIMEOUT: 'TIMEOUT',
CONNECTION_CLOSED: 'CONNECTION_CLOSED',
} as const
export type ErrorCode = typeof ErrorCode[keyof typeof ErrorCode]

View File

@@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: {
index: 'src/index.ts',
'server/index': 'src/server/index.ts',
'client/index': 'src/client/index.ts',
'codec/index': 'src/codec/index.ts',
},
format: ['esm'],
dts: false,
clean: true,
sourcemap: true,
external: ['ws', 'msgpackr'],
})

View File

@@ -12,13 +12,16 @@ export const nodejsAdapter: PlatformAdapter = {
getDependencies() { getDependencies() {
return { return {
'@esengine/ecs-framework': 'latest', '@esengine/ecs-framework': 'latest',
'@esengine/network-server': 'latest' '@esengine/rpc': 'latest',
'@esengine/network': 'latest',
'ws': '^8.18.0'
}; };
}, },
getDevDependencies() { getDevDependencies() {
return { return {
'@types/node': '^20.0.0', '@types/node': '^20.0.0',
'@types/ws': '^8.5.13',
'tsx': '^4.0.0', 'tsx': '^4.0.0',
'typescript': '^5.0.0' 'typescript': '^5.0.0'
}; };
@@ -54,7 +57,7 @@ function generateIndex(config: ProjectConfig): string {
const PORT = Number(process.env.PORT) || 3000; const PORT = Number(process.env.PORT) || 3000;
async function main() { async function main() {
const server = createGameServer({ port: PORT }); const { server } = createGameServer({ port: PORT });
await server.start(); await server.start();
console.log('========================================'); console.log('========================================');
@@ -75,28 +78,73 @@ main().catch(console.error);
} }
function generateGameServer(config: ProjectConfig): string { function generateGameServer(config: ProjectConfig): string {
return `import { GameServer, type IServerConfig } from '@esengine/network-server'; return `import { RpcServer } from '@esengine/rpc/server';
import { gameProtocol, type JoinRequest, type JoinResponse } from '@esengine/network';
import { Game } from '../game/Game'; import { Game } from '../game/Game';
/**
* @zh 服务器配置
* @en Server configuration
*/
export interface ServerConfig {
port: number;
maxPlayers?: number;
tickRate?: number;
}
/** /**
* @zh 创建游戏服务器 * @zh 创建游戏服务器
* @en Create game server * @en Create game server
*/ */
export function createGameServer(config: Partial<IServerConfig> = {}): GameServer { export function createGameServer(config: Partial<ServerConfig> = {}) {
const server = new GameServer({ const port = config.port ?? 3000;
port: config.port ?? 3000, const maxPlayers = config.maxPlayers ?? 16;
roomConfig: { const tickRate = config.tickRate ?? 20;
maxPlayers: 16,
tickRate: 20, // 创建 RPC 服务器
...config.roomConfig const server = new RpcServer(gameProtocol, {
port,
onStart: (p) => console.log(\`[Server] Started on ws://localhost:\${p}\`),
onConnection: (id) => console.log(\`[Server] Client connected: \${id}\`),
onDisconnection: (id) => console.log(\`[Server] Client disconnected: \${id}\`),
});
// 玩家管理
const players = new Map<string, { id: number; name: string }>();
let nextPlayerId = 1;
// 注册 API 处理器
server.handle('join', async (input: JoinRequest, ctx): Promise<JoinResponse> => {
const playerId = nextPlayerId++;
players.set(ctx.clientId, { id: playerId, name: input.playerName });
console.log(\`[Server] Player joined: \${input.playerName} (ID: \${playerId})\`);
return {
playerId,
roomId: input.roomId ?? 'default',
};
});
server.handle('leave', async (_input, ctx) => {
const player = players.get(ctx.clientId);
if (player) {
console.log(\`[Server] Player left: \${player.name}\`);
players.delete(ctx.clientId);
} }
}); });
// 初始化 ECS 游戏逻辑 // 初始化 ECS 游戏逻辑
const game = new Game(); const game = new Game({ targetFPS: tickRate });
game.start(); game.start();
return server; // 游戏循环:广播状态同步
setInterval(() => {
// 在这里广播游戏状态
// server.broadcast('sync', { entities: [...] });
}, 1000 / tickRate);
return { server, game, players };
} }
`; `;
} }
@@ -319,18 +367,24 @@ src/
## 客户端连接 ## 客户端连接
\`\`\`typescript \`\`\`typescript
import { Core } from '@esengine/ecs-framework';
import { NetworkPlugin } from '@esengine/network'; import { NetworkPlugin } from '@esengine/network';
const networkPlugin = new NetworkPlugin({ // 安装网络插件
serverUrl: 'ws://localhost:3000' const networkPlugin = new NetworkPlugin();
}); await Core.installPlugin(networkPlugin);
await networkPlugin.connect('PlayerName'); // 连接服务器
await networkPlugin.connect({
url: 'ws://localhost:3000',
playerName: 'Player1'
});
\`\`\` \`\`\`
## 文档 ## 文档
- [ESEngine 文档](https://esengine.github.io/esengine/) - [ESEngine 文档](https://esengine.github.io/esengine/)
- [RPC 模块](https://esengine.github.io/esengine/modules/rpc/)
- [Network 模块](https://esengine.github.io/esengine/modules/network/) - [Network 模块](https://esengine.github.io/esengine/modules/network/)
`; `;
} }

530
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff