* docs(network): 添加网络模块文档和 CLI 支持 - 添加中英文网络模块文档 - 将 network、network-protocols、network-server 加入 CLI 模块列表 * feat(cli): 增强 Node.js 服务端适配器 - 添加 @esengine/network-server 依赖支持 - 生成完整的 ECS 游戏服务器项目结构 - 修复 network-server 包支持 ESM/CJS 双格式 - 添加 ws@8.18.0 解决 Node.js 24 兼容性问题 - 组件使用 @ECSComponent 装饰器注册 - tsconfig 启用 experimentalDecorators
728 lines
16 KiB
Markdown
728 lines
16 KiB
Markdown
# Network System
|
|
|
|
`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation.
|
|
|
|
## Overview
|
|
|
|
The network module consists of three packages:
|
|
|
|
| Package | Description |
|
|
|---------|-------------|
|
|
| `@esengine/network` | Client-side ECS plugin |
|
|
| `@esengine/network-protocols` | Shared protocol definitions |
|
|
| `@esengine/network-server` | Server-side implementation |
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
# Client
|
|
npm install @esengine/network
|
|
|
|
# Server
|
|
npm install @esengine/network-server
|
|
```
|
|
|
|
## Quick Setup with CLI
|
|
|
|
We recommend using ESEngine CLI to quickly create a complete game server project:
|
|
|
|
```bash
|
|
# Create project directory
|
|
mkdir my-game-server && cd my-game-server
|
|
npm init -y
|
|
|
|
# Initialize Node.js server with CLI
|
|
npx @esengine/cli init -p nodejs
|
|
```
|
|
|
|
The CLI will generate the following project structure:
|
|
|
|
```
|
|
my-game-server/
|
|
├── src/
|
|
│ ├── index.ts # Entry point
|
|
│ ├── server/
|
|
│ │ └── GameServer.ts # Network server configuration
|
|
│ └── game/
|
|
│ ├── Game.ts # ECS game class
|
|
│ ├── scenes/
|
|
│ │ └── MainScene.ts # Main scene
|
|
│ ├── components/ # ECS components
|
|
│ │ ├── PositionComponent.ts
|
|
│ │ └── VelocityComponent.ts
|
|
│ └── systems/ # ECS systems
|
|
│ └── MovementSystem.ts
|
|
├── tsconfig.json
|
|
├── package.json
|
|
└── README.md
|
|
```
|
|
|
|
Start the server:
|
|
|
|
```bash
|
|
# Development mode (hot reload)
|
|
npm run dev
|
|
|
|
# Production mode
|
|
npm run start
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
### Client
|
|
|
|
```typescript
|
|
import { Core, Scene } from '@esengine/ecs-framework';
|
|
import {
|
|
NetworkPlugin,
|
|
NetworkIdentity,
|
|
NetworkTransform
|
|
} from '@esengine/network';
|
|
|
|
// Define game scene
|
|
class GameScene extends Scene {
|
|
initialize(): void {
|
|
this.name = 'Game';
|
|
// Network systems are automatically added by NetworkPlugin
|
|
}
|
|
}
|
|
|
|
// Initialize Core
|
|
Core.create({ debug: false });
|
|
const scene = new GameScene();
|
|
Core.setScene(scene);
|
|
|
|
// Install network plugin
|
|
const networkPlugin = new NetworkPlugin();
|
|
await Core.installPlugin(networkPlugin);
|
|
|
|
// Register prefab factory
|
|
networkPlugin.registerPrefab('player', (scene, spawn) => {
|
|
const entity = scene.createEntity(`player_${spawn.netId}`);
|
|
|
|
const identity = entity.addComponent(new NetworkIdentity());
|
|
identity.netId = spawn.netId;
|
|
identity.ownerId = spawn.ownerId;
|
|
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
|
|
|
|
entity.addComponent(new NetworkTransform());
|
|
return entity;
|
|
});
|
|
|
|
// Connect to server
|
|
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
|
if (success) {
|
|
console.log('Connected!');
|
|
}
|
|
|
|
// Game loop
|
|
function gameLoop(dt: number) {
|
|
Core.update(dt);
|
|
}
|
|
|
|
// Disconnect
|
|
await networkPlugin.disconnect();
|
|
```
|
|
|
|
### Server
|
|
|
|
After creating a server project with CLI, the generated code already configures GameServer:
|
|
|
|
```typescript
|
|
import { GameServer } from '@esengine/network-server';
|
|
|
|
const server = new GameServer({
|
|
port: 3000,
|
|
roomConfig: {
|
|
maxPlayers: 16,
|
|
tickRate: 20
|
|
}
|
|
});
|
|
|
|
await server.start();
|
|
console.log('Server started on ws://localhost:3000');
|
|
```
|
|
|
|
## Core Concepts
|
|
|
|
### Architecture
|
|
|
|
```
|
|
Client Server
|
|
┌────────────────┐ ┌────────────────┐
|
|
│ NetworkPlugin │◄──── WS ────► │ GameServer │
|
|
│ ├─ Service │ │ ├─ Room │
|
|
│ ├─ SyncSystem │ │ └─ Players │
|
|
│ ├─ SpawnSystem │ └────────────────┘
|
|
│ └─ InputSystem │
|
|
└────────────────┘
|
|
```
|
|
|
|
### Components
|
|
|
|
#### NetworkIdentity
|
|
|
|
Network identity component, required for every networked entity:
|
|
|
|
```typescript
|
|
class NetworkIdentity extends Component {
|
|
netId: number; // Network unique ID
|
|
ownerId: number; // Owner client ID
|
|
bIsLocalPlayer: boolean; // Whether local player
|
|
bHasAuthority: boolean; // Whether has control authority
|
|
}
|
|
```
|
|
|
|
#### NetworkTransform
|
|
|
|
Network transform component for position and rotation sync:
|
|
|
|
```typescript
|
|
class NetworkTransform extends Component {
|
|
position: { x: number; y: number };
|
|
rotation: number;
|
|
velocity: { x: number; y: number };
|
|
}
|
|
```
|
|
|
|
### Systems
|
|
|
|
#### NetworkSyncSystem
|
|
|
|
Handles server state synchronization and interpolation:
|
|
|
|
- Receives server state snapshots
|
|
- Stores states in snapshot buffer
|
|
- Performs interpolation for remote entities
|
|
|
|
#### NetworkSpawnSystem
|
|
|
|
Handles network entity spawning and despawning:
|
|
|
|
- Listens for Spawn/Despawn messages
|
|
- Creates entities using registered prefab factories
|
|
- Manages networked entity lifecycle
|
|
|
|
#### NetworkInputSystem
|
|
|
|
Handles local player input sending:
|
|
|
|
- Collects local player input
|
|
- Sends input to server
|
|
- Supports movement and action inputs
|
|
|
|
## API Reference
|
|
|
|
### NetworkPlugin
|
|
|
|
```typescript
|
|
class NetworkPlugin {
|
|
constructor(config: INetworkPluginConfig);
|
|
|
|
// Install plugin
|
|
install(services: ServiceContainer): void;
|
|
|
|
// Connect to server
|
|
connect(playerName: string, roomId?: string): Promise<void>;
|
|
|
|
// Disconnect
|
|
disconnect(): void;
|
|
|
|
// Register prefab factory
|
|
registerPrefab(prefab: string, factory: PrefabFactory): void;
|
|
|
|
// Properties
|
|
readonly localPlayerId: number | null;
|
|
readonly isConnected: boolean;
|
|
}
|
|
```
|
|
|
|
**Configuration:**
|
|
|
|
| Property | Type | Required | Description |
|
|
|----------|------|----------|-------------|
|
|
| `serverUrl` | `string` | Yes | WebSocket server URL |
|
|
|
|
### NetworkService
|
|
|
|
Network service managing WebSocket connections:
|
|
|
|
```typescript
|
|
class NetworkService {
|
|
// Connection state
|
|
readonly state: ENetworkState;
|
|
readonly isConnected: boolean;
|
|
readonly clientId: number | null;
|
|
readonly roomId: string | null;
|
|
|
|
// Connection control
|
|
connect(serverUrl: string): Promise<void>;
|
|
disconnect(): void;
|
|
|
|
// Join room
|
|
join(playerName: string, roomId?: string): Promise<ResJoin>;
|
|
|
|
// Send input
|
|
sendInput(input: IPlayerInput): void;
|
|
|
|
// Event callbacks
|
|
setCallbacks(callbacks: Partial<INetworkCallbacks>): void;
|
|
}
|
|
```
|
|
|
|
**Network state enum:**
|
|
|
|
```typescript
|
|
enum ENetworkState {
|
|
Disconnected = 'disconnected',
|
|
Connecting = 'connecting',
|
|
Connected = 'connected',
|
|
Joining = 'joining',
|
|
Joined = 'joined'
|
|
}
|
|
```
|
|
|
|
**Callbacks interface:**
|
|
|
|
```typescript
|
|
interface INetworkCallbacks {
|
|
onConnected?: () => void;
|
|
onDisconnected?: () => void;
|
|
onJoined?: (clientId: number, roomId: string) => void;
|
|
onSync?: (msg: MsgSync) => void;
|
|
onSpawn?: (msg: MsgSpawn) => void;
|
|
onDespawn?: (msg: MsgDespawn) => void;
|
|
}
|
|
```
|
|
|
|
### Prefab Factory
|
|
|
|
```typescript
|
|
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
|
```
|
|
|
|
Register prefab factories for network entity creation:
|
|
|
|
```typescript
|
|
networkPlugin.registerPrefab('enemy', (scene, spawn) => {
|
|
const entity = scene.createEntity(`enemy_${spawn.netId}`);
|
|
|
|
const identity = entity.addComponent(new NetworkIdentity());
|
|
identity.netId = spawn.netId;
|
|
identity.ownerId = spawn.ownerId;
|
|
|
|
entity.addComponent(new NetworkTransform());
|
|
entity.addComponent(new EnemyComponent());
|
|
return entity;
|
|
});
|
|
```
|
|
|
|
### Input System
|
|
|
|
#### NetworkInputSystem
|
|
|
|
```typescript
|
|
class NetworkInputSystem extends EntitySystem {
|
|
// Add movement input
|
|
addMoveInput(x: number, y: number): void;
|
|
|
|
// Add action input
|
|
addActionInput(action: string): void;
|
|
|
|
// Clear input
|
|
clearInput(): void;
|
|
}
|
|
```
|
|
|
|
Usage example:
|
|
|
|
```typescript
|
|
// Send input via NetworkPlugin (recommended)
|
|
networkPlugin.sendMoveInput(0, 1); // Movement
|
|
networkPlugin.sendActionInput('jump'); // Action
|
|
|
|
// Or use inputSystem directly
|
|
const inputSystem = networkPlugin.inputSystem;
|
|
if (keyboard.isPressed('W')) {
|
|
inputSystem.addMoveInput(0, 1);
|
|
}
|
|
if (keyboard.isPressed('Space')) {
|
|
inputSystem.addActionInput('jump');
|
|
}
|
|
```
|
|
|
|
## State Synchronization
|
|
|
|
### Snapshot Buffer
|
|
|
|
Stores server state snapshots for interpolation:
|
|
|
|
```typescript
|
|
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
|
|
|
|
const buffer = createSnapshotBuffer<IStateSnapshot>({
|
|
maxSnapshots: 30, // Max snapshots
|
|
interpolationDelay: 100 // Interpolation delay (ms)
|
|
});
|
|
|
|
// Add snapshot
|
|
buffer.addSnapshot({
|
|
time: serverTime,
|
|
entities: states
|
|
});
|
|
|
|
// Get interpolated state
|
|
const interpolated = buffer.getInterpolatedState(clientTime);
|
|
```
|
|
|
|
### Transform Interpolators
|
|
|
|
#### Linear Interpolator
|
|
|
|
```typescript
|
|
import { createTransformInterpolator } from '@esengine/network';
|
|
|
|
const interpolator = createTransformInterpolator();
|
|
|
|
// Add state
|
|
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
|
|
|
|
// Get interpolated result
|
|
const state = interpolator.getInterpolatedState(currentTime);
|
|
```
|
|
|
|
#### Hermite Interpolator
|
|
|
|
Uses Hermite splines for smoother interpolation:
|
|
|
|
```typescript
|
|
import { createHermiteTransformInterpolator } from '@esengine/network';
|
|
|
|
const interpolator = createHermiteTransformInterpolator({
|
|
bufferSize: 10
|
|
});
|
|
|
|
// Add state with velocity
|
|
interpolator.addState(time, {
|
|
x: 100,
|
|
y: 200,
|
|
rotation: 0,
|
|
vx: 5,
|
|
vy: 0
|
|
});
|
|
|
|
// Get smooth interpolated result
|
|
const state = interpolator.getInterpolatedState(currentTime);
|
|
```
|
|
|
|
### Client Prediction
|
|
|
|
Implement client-side prediction with server reconciliation:
|
|
|
|
```typescript
|
|
import { createClientPrediction } from '@esengine/network';
|
|
|
|
const prediction = createClientPrediction({
|
|
maxPredictedInputs: 60,
|
|
reconciliationThreshold: 0.1
|
|
});
|
|
|
|
// Predict input
|
|
const seq = prediction.predict(inputState, currentState, (state, input) => {
|
|
// Apply input to state
|
|
return applyInput(state, input);
|
|
});
|
|
|
|
// Server reconciliation
|
|
const corrected = prediction.reconcile(
|
|
serverState,
|
|
serverSeq,
|
|
(state, input) => applyInput(state, input)
|
|
);
|
|
```
|
|
|
|
## Server Side
|
|
|
|
### GameServer
|
|
|
|
```typescript
|
|
import { GameServer } from '@esengine/network-server';
|
|
|
|
const server = new GameServer({
|
|
port: 3000,
|
|
roomConfig: {
|
|
maxPlayers: 16, // Max players per room
|
|
tickRate: 20 // Sync rate (Hz)
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
await server.start();
|
|
|
|
// Get room
|
|
const room = server.getOrCreateRoom('room-id');
|
|
|
|
// Stop server
|
|
await server.stop();
|
|
```
|
|
|
|
### Room
|
|
|
|
```typescript
|
|
class Room {
|
|
readonly id: string;
|
|
readonly playerCount: number;
|
|
readonly isFull: boolean;
|
|
|
|
// Add player
|
|
addPlayer(name: string, connection: Connection): IPlayer | null;
|
|
|
|
// Remove player
|
|
removePlayer(clientId: number): void;
|
|
|
|
// Get player
|
|
getPlayer(clientId: number): IPlayer | undefined;
|
|
|
|
// Handle input
|
|
handleInput(clientId: number, input: IPlayerInput): void;
|
|
|
|
// Destroy room
|
|
destroy(): void;
|
|
}
|
|
```
|
|
|
|
**Player interface:**
|
|
|
|
```typescript
|
|
interface IPlayer {
|
|
clientId: number; // Client ID
|
|
name: string; // Player name
|
|
connection: Connection; // Connection object
|
|
netId: number; // Network entity ID
|
|
}
|
|
```
|
|
|
|
## Protocol Types
|
|
|
|
### Message Types
|
|
|
|
```typescript
|
|
// State sync message
|
|
interface MsgSync {
|
|
time: number;
|
|
entities: IEntityState[];
|
|
}
|
|
|
|
// Entity state
|
|
interface IEntityState {
|
|
netId: number;
|
|
pos?: Vec2;
|
|
rot?: number;
|
|
}
|
|
|
|
// Spawn message
|
|
interface MsgSpawn {
|
|
netId: number;
|
|
ownerId: number;
|
|
prefab: string;
|
|
pos: Vec2;
|
|
rot: number;
|
|
}
|
|
|
|
// Despawn message
|
|
interface MsgDespawn {
|
|
netId: number;
|
|
}
|
|
|
|
// Input message
|
|
interface MsgInput {
|
|
input: IPlayerInput;
|
|
}
|
|
|
|
// Player input
|
|
interface IPlayerInput {
|
|
seq?: number;
|
|
moveDir?: Vec2;
|
|
actions?: string[];
|
|
}
|
|
```
|
|
|
|
### API Types
|
|
|
|
```typescript
|
|
// Join request
|
|
interface ReqJoin {
|
|
playerName: string;
|
|
roomId?: string;
|
|
}
|
|
|
|
// Join response
|
|
interface ResJoin {
|
|
clientId: number;
|
|
roomId: string;
|
|
playerCount: number;
|
|
}
|
|
```
|
|
|
|
## Blueprint Nodes
|
|
|
|
The network module provides blueprint nodes for visual scripting:
|
|
|
|
- `IsLocalPlayer` - Check if entity is local player
|
|
- `IsServer` - Check if running on server
|
|
- `HasAuthority` - Check if has authority over entity
|
|
- `GetNetworkId` - Get entity's network ID
|
|
- `GetLocalPlayerId` - Get local player ID
|
|
|
|
## Service Tokens
|
|
|
|
For dependency injection:
|
|
|
|
```typescript
|
|
import {
|
|
NetworkServiceToken,
|
|
NetworkSyncSystemToken,
|
|
NetworkSpawnSystemToken,
|
|
NetworkInputSystemToken
|
|
} from '@esengine/network';
|
|
|
|
// Get service
|
|
const networkService = services.get(NetworkServiceToken);
|
|
```
|
|
|
|
## Practical Example
|
|
|
|
### Complete Multiplayer Client
|
|
|
|
```typescript
|
|
import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
|
import {
|
|
NetworkPlugin,
|
|
NetworkIdentity,
|
|
NetworkTransform
|
|
} from '@esengine/network';
|
|
|
|
// Define game scene
|
|
class GameScene extends Scene {
|
|
initialize(): void {
|
|
this.name = 'MultiplayerGame';
|
|
// Network systems are automatically added by NetworkPlugin
|
|
// Add custom systems
|
|
this.addSystem(new LocalInputHandler());
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
async function initGame() {
|
|
Core.create({ debug: false });
|
|
|
|
const scene = new GameScene();
|
|
Core.setScene(scene);
|
|
|
|
// Install network plugin
|
|
const networkPlugin = new NetworkPlugin();
|
|
await Core.installPlugin(networkPlugin);
|
|
|
|
// Register player prefab
|
|
networkPlugin.registerPrefab('player', (scene, spawn) => {
|
|
const entity = scene.createEntity(`player_${spawn.netId}`);
|
|
|
|
const identity = entity.addComponent(new NetworkIdentity());
|
|
identity.netId = spawn.netId;
|
|
identity.ownerId = spawn.ownerId;
|
|
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
|
|
|
|
entity.addComponent(new NetworkTransform());
|
|
|
|
// If local player, add input marker
|
|
if (identity.isLocalPlayer) {
|
|
entity.addComponent(new LocalInputComponent());
|
|
}
|
|
|
|
return entity;
|
|
});
|
|
|
|
// Connect to server
|
|
const success = await networkPlugin.connect('ws://localhost:3000', 'Player1');
|
|
if (success) {
|
|
console.log('Connected!');
|
|
} else {
|
|
console.error('Connection failed');
|
|
}
|
|
|
|
return networkPlugin;
|
|
}
|
|
|
|
// Game loop
|
|
function gameLoop(deltaTime: number) {
|
|
Core.update(deltaTime);
|
|
}
|
|
|
|
initGame();
|
|
```
|
|
|
|
### Handling Input
|
|
|
|
```typescript
|
|
class LocalInputHandler extends EntitySystem {
|
|
private _networkPlugin: NetworkPlugin | null = null;
|
|
|
|
constructor() {
|
|
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
|
|
}
|
|
|
|
protected onAddedToScene(): void {
|
|
// Get NetworkPlugin reference
|
|
this._networkPlugin = Core.getPlugin(NetworkPlugin);
|
|
}
|
|
|
|
protected processEntity(entity: Entity, dt: number): void {
|
|
if (!this._networkPlugin) return;
|
|
|
|
const identity = entity.getComponent(NetworkIdentity)!;
|
|
if (!identity.isLocalPlayer) return;
|
|
|
|
// Read keyboard input
|
|
let moveX = 0;
|
|
let moveY = 0;
|
|
|
|
if (keyboard.isPressed('A')) moveX -= 1;
|
|
if (keyboard.isPressed('D')) moveX += 1;
|
|
if (keyboard.isPressed('W')) moveY += 1;
|
|
if (keyboard.isPressed('S')) moveY -= 1;
|
|
|
|
if (moveX !== 0 || moveY !== 0) {
|
|
this._networkPlugin.sendMoveInput(moveX, moveY);
|
|
}
|
|
|
|
if (keyboard.isJustPressed('Space')) {
|
|
this._networkPlugin.sendActionInput('jump');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Set appropriate sync rate**: Choose `tickRate` based on game type, action games typically need 20-60 Hz
|
|
|
|
2. **Use interpolation delay**: Set appropriate `interpolationDelay` to balance latency and smoothness
|
|
|
|
3. **Client prediction**: Use client-side prediction for local players to reduce input lag
|
|
|
|
4. **Prefab management**: Register prefab factories for each networked entity type
|
|
|
|
5. **Authority checks**: Use `bHasAuthority` to check entity control permissions
|
|
|
|
6. **Connection state**: Monitor connection state changes, handle reconnection
|
|
|
|
```typescript
|
|
networkService.setCallbacks({
|
|
onConnected: () => console.log('Connected'),
|
|
onDisconnected: () => {
|
|
console.log('Disconnected');
|
|
// Handle reconnection logic
|
|
}
|
|
});
|
|
```
|