feat(server): 添加游戏服务器框架 | add game server framework (#366)
**@esengine/server** - 游戏服务器框架 | Game server framework - 文件路由系统 | File-based routing - Room 生命周期 (onCreate, onJoin, onLeave, onTick, onDispose) - @onMessage 装饰器 | Message handler decorator - 玩家管理与断线处理 | Player management with auto-disconnect - 内置 JoinRoom/LeaveRoom API | Built-in room APIs - defineApi/defineMsg 类型安全辅助函数 | Type-safe helpers **create-esengine-server** - CLI 脚手架工具 | CLI scaffolding - 生成 shared/server/client 项目结构 | Project structure - 类型安全的协议定义 | Type-safe protocol definitions - 包含 GameRoom 示例 | Example implementation **其他 | Other** - 删除旧的 network-server 包 | Remove old network-server - 更新服务器文档 | Update server documentation
This commit is contained in:
@@ -1,76 +1,335 @@
|
||||
---
|
||||
title: "Server Side"
|
||||
description: "GameServer and Room management"
|
||||
description: "Build game servers with @esengine/server"
|
||||
---
|
||||
|
||||
## GameServer
|
||||
## Quick Start
|
||||
|
||||
GameServer is the core server-side class managing WebSocket connections and rooms.
|
||||
Create a new game server project using the CLI:
|
||||
|
||||
### Basic Usage
|
||||
```bash
|
||||
# Using npm
|
||||
npm create esengine-server my-game-server
|
||||
|
||||
# Using pnpm
|
||||
pnpm create esengine-server my-game-server
|
||||
|
||||
# Using yarn
|
||||
yarn create esengine-server my-game-server
|
||||
```
|
||||
|
||||
Generated project structure:
|
||||
|
||||
```
|
||||
my-game-server/
|
||||
├── src/
|
||||
│ ├── shared/ # Shared protocol (client & server)
|
||||
│ │ ├── protocol.ts # Type definitions
|
||||
│ │ └── index.ts
|
||||
│ ├── server/ # Server code
|
||||
│ │ ├── main.ts # Entry point
|
||||
│ │ └── rooms/
|
||||
│ │ └── GameRoom.ts # Game room
|
||||
│ └── client/ # Client example
|
||||
│ └── index.ts
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Start the server:
|
||||
|
||||
```bash
|
||||
# Development mode (hot reload)
|
||||
npm run dev
|
||||
|
||||
# Production mode
|
||||
npm run start
|
||||
```
|
||||
|
||||
## createServer
|
||||
|
||||
Create a game server instance:
|
||||
|
||||
```typescript
|
||||
import { GameServer } from '@esengine/network-server';
|
||||
import { createServer } from '@esengine/server'
|
||||
import { GameRoom } from './rooms/GameRoom.js'
|
||||
|
||||
const server = new GameServer({
|
||||
const server = await createServer({
|
||||
port: 3000,
|
||||
roomConfig: {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
onConnect(conn) {
|
||||
console.log('Client connected:', conn.id)
|
||||
},
|
||||
onDisconnect(conn) {
|
||||
console.log('Client disconnected:', conn.id)
|
||||
},
|
||||
})
|
||||
|
||||
// Register room type
|
||||
server.define('game', GameRoom)
|
||||
|
||||
// Start server
|
||||
await server.start()
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `port` | `number` | `3000` | WebSocket port |
|
||||
| `tickRate` | `number` | `20` | Global tick rate (Hz) |
|
||||
| `apiDir` | `string` | `'src/api'` | API handlers directory |
|
||||
| `msgDir` | `string` | `'src/msg'` | Message handlers directory |
|
||||
| `onStart` | `(port) => void` | - | Start callback |
|
||||
| `onConnect` | `(conn) => void` | - | Connection callback |
|
||||
| `onDisconnect` | `(conn) => void` | - | Disconnect callback |
|
||||
|
||||
## Room System
|
||||
|
||||
Room is the base class for game rooms, managing players and game state.
|
||||
|
||||
### Define a Room
|
||||
|
||||
```typescript
|
||||
import { Room, Player, onMessage } from '@esengine/server'
|
||||
import type { MsgMove, MsgChat } from '../../shared/index.js'
|
||||
|
||||
interface PlayerData {
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export class GameRoom extends Room<{ players: any[] }, PlayerData> {
|
||||
// Configuration
|
||||
maxPlayers = 8
|
||||
tickRate = 20
|
||||
autoDispose = true
|
||||
|
||||
// Room state
|
||||
state = {
|
||||
players: [],
|
||||
}
|
||||
});
|
||||
|
||||
await server.start();
|
||||
await server.stop();
|
||||
```
|
||||
// Lifecycle
|
||||
onCreate() {
|
||||
console.log(`Room ${this.id} created`)
|
||||
}
|
||||
|
||||
### Configuration
|
||||
onJoin(player: Player<PlayerData>) {
|
||||
player.data.name = 'Player_' + player.id.slice(-4)
|
||||
player.data.x = Math.random() * 800
|
||||
player.data.y = Math.random() * 600
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `port` | `number` | WebSocket port |
|
||||
| `roomConfig.maxPlayers` | `number` | Max players per room |
|
||||
| `roomConfig.tickRate` | `number` | Sync rate (Hz) |
|
||||
this.broadcast('Joined', {
|
||||
playerId: player.id,
|
||||
playerName: player.data.name,
|
||||
})
|
||||
}
|
||||
|
||||
## Room
|
||||
onLeave(player: Player<PlayerData>) {
|
||||
this.broadcast('Left', { playerId: player.id })
|
||||
}
|
||||
|
||||
```typescript
|
||||
class Room {
|
||||
readonly id: string;
|
||||
readonly playerCount: number;
|
||||
readonly isFull: boolean;
|
||||
onTick(dt: number) {
|
||||
// State synchronization
|
||||
this.broadcast('Sync', { players: this.state.players })
|
||||
}
|
||||
|
||||
addPlayer(name: string, connection: Connection): IPlayer | null;
|
||||
removePlayer(clientId: number): void;
|
||||
getPlayer(clientId: number): IPlayer | undefined;
|
||||
handleInput(clientId: number, input: IPlayerInput): void;
|
||||
destroy(): void;
|
||||
onDispose() {
|
||||
console.log(`Room ${this.id} disposed`)
|
||||
}
|
||||
|
||||
// Message handlers
|
||||
@onMessage('Move')
|
||||
handleMove(data: MsgMove, player: Player<PlayerData>) {
|
||||
player.data.x = data.x
|
||||
player.data.y = data.y
|
||||
}
|
||||
|
||||
@onMessage('Chat')
|
||||
handleChat(data: MsgChat, player: Player<PlayerData>) {
|
||||
this.broadcast('Chat', {
|
||||
from: player.data.name,
|
||||
text: data.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol Types
|
||||
### Room Configuration
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `maxPlayers` | `number` | `10` | Maximum players |
|
||||
| `tickRate` | `number` | `20` | Tick rate (Hz) |
|
||||
| `autoDispose` | `boolean` | `true` | Auto-dispose empty rooms |
|
||||
|
||||
### Room API
|
||||
|
||||
```typescript
|
||||
interface MsgSync {
|
||||
time: number;
|
||||
entities: IEntityState[];
|
||||
class Room<TState, TPlayerData> {
|
||||
readonly id: string // Room ID
|
||||
readonly players: Player[] // All players
|
||||
readonly playerCount: number // Player count
|
||||
readonly isLocked: boolean // Lock status
|
||||
state: TState // Room state
|
||||
|
||||
// Broadcast to all players
|
||||
broadcast<T>(type: string, data: T): void
|
||||
|
||||
// Broadcast to all except one
|
||||
broadcastExcept<T>(type: string, data: T, except: Player): void
|
||||
|
||||
// Get player by ID
|
||||
getPlayer(id: string): Player | undefined
|
||||
|
||||
// Kick a player
|
||||
kick(player: Player, reason?: string): void
|
||||
|
||||
// Lock/unlock room
|
||||
lock(): void
|
||||
unlock(): void
|
||||
|
||||
// Dispose room
|
||||
dispose(): void
|
||||
}
|
||||
```
|
||||
|
||||
### Lifecycle Methods
|
||||
|
||||
| Method | Trigger | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `onCreate()` | Room created | Initialize game state |
|
||||
| `onJoin(player)` | Player joins | Welcome message, assign position |
|
||||
| `onLeave(player)` | Player leaves | Cleanup player data |
|
||||
| `onTick(dt)` | Every frame | Game logic, state sync |
|
||||
| `onDispose()` | Before disposal | Save data, cleanup resources |
|
||||
|
||||
## Player Class
|
||||
|
||||
Player represents a connected player in a room.
|
||||
|
||||
```typescript
|
||||
class Player<TData = Record<string, unknown>> {
|
||||
readonly id: string // Player ID
|
||||
readonly roomId: string // Room ID
|
||||
data: TData // Custom data
|
||||
|
||||
// Send message to this player
|
||||
send<T>(type: string, data: T): void
|
||||
|
||||
// Leave room
|
||||
leave(): void
|
||||
}
|
||||
```
|
||||
|
||||
## @onMessage Decorator
|
||||
|
||||
Use decorators to simplify message handling:
|
||||
|
||||
```typescript
|
||||
import { Room, Player, onMessage } from '@esengine/server'
|
||||
|
||||
class GameRoom extends Room {
|
||||
@onMessage('Move')
|
||||
handleMove(data: { x: number; y: number }, player: Player) {
|
||||
// Handle movement
|
||||
}
|
||||
|
||||
@onMessage('Attack')
|
||||
handleAttack(data: { targetId: string }, player: Player) {
|
||||
// Handle attack
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol Definition
|
||||
|
||||
Define shared types in `src/shared/protocol.ts`:
|
||||
|
||||
```typescript
|
||||
// API request/response
|
||||
export interface JoinRoomReq {
|
||||
roomType: string
|
||||
playerName: string
|
||||
}
|
||||
|
||||
interface MsgSpawn {
|
||||
netId: number;
|
||||
ownerId: number;
|
||||
prefab: string;
|
||||
pos: Vec2;
|
||||
rot: number;
|
||||
export interface JoinRoomRes {
|
||||
roomId: string
|
||||
playerId: string
|
||||
}
|
||||
|
||||
interface MsgDespawn {
|
||||
netId: number;
|
||||
// Game messages
|
||||
export interface MsgMove {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface MsgChat {
|
||||
text: string
|
||||
}
|
||||
|
||||
// Server broadcasts
|
||||
export interface BroadcastSync {
|
||||
players: PlayerState[]
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
id: string
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
```
|
||||
|
||||
## Client Connection
|
||||
|
||||
```typescript
|
||||
import { connect } from '@esengine/rpc/client'
|
||||
|
||||
const client = await connect('ws://localhost:3000')
|
||||
|
||||
// Join room
|
||||
const { roomId, playerId } = await client.call('JoinRoom', {
|
||||
roomType: 'game',
|
||||
playerName: 'Alice',
|
||||
})
|
||||
|
||||
// Listen for broadcasts
|
||||
client.onMessage('Sync', (data) => {
|
||||
console.log('State:', data.players)
|
||||
})
|
||||
|
||||
client.onMessage('Joined', (data) => {
|
||||
console.log('Player joined:', data.playerName)
|
||||
})
|
||||
|
||||
// Send message
|
||||
client.send('RoomMessage', {
|
||||
type: 'Move',
|
||||
payload: { x: 100, y: 200 },
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Set appropriate tick rate**: Choose based on game type (20-60 Hz for action games)
|
||||
2. **Room size control**: Set reasonable `maxPlayers` based on server capacity
|
||||
3. **State validation**: Server should validate client inputs to prevent cheating
|
||||
1. **Set Appropriate Tick Rate**
|
||||
- Turn-based games: 5-10 Hz
|
||||
- Casual games: 10-20 Hz
|
||||
- Action games: 20-60 Hz
|
||||
|
||||
2. **Use Shared Protocol**
|
||||
- Define all types in `shared/` directory
|
||||
- Import from here in both client and server
|
||||
|
||||
3. **State Validation**
|
||||
- Server should validate all client inputs
|
||||
- Never trust client-sent data
|
||||
|
||||
4. **Disconnect Handling**
|
||||
- Implement reconnection logic
|
||||
- Use `onLeave` to save player state
|
||||
|
||||
5. **Room Lifecycle**
|
||||
- Use `autoDispose` to clean up empty rooms
|
||||
- Save important data in `onDispose`
|
||||
|
||||
@@ -1,164 +1,21 @@
|
||||
---
|
||||
title: "服务器端"
|
||||
description: "GameServer 和 Room 管理"
|
||||
description: "使用 @esengine/server 构建游戏服务器"
|
||||
---
|
||||
|
||||
## GameServer
|
||||
## 快速开始
|
||||
|
||||
GameServer 是服务器端的核心类,管理 WebSocket 连接和房间。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```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');
|
||||
|
||||
// 停止服务器
|
||||
await server.stop();
|
||||
```
|
||||
|
||||
### 配置选项
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| `port` | `number` | WebSocket 端口 |
|
||||
| `roomConfig.maxPlayers` | `number` | 房间最大玩家数 |
|
||||
| `roomConfig.tickRate` | `number` | 同步频率 (Hz) |
|
||||
|
||||
### 房间管理
|
||||
|
||||
```typescript
|
||||
// 获取或创建房间
|
||||
const room = server.getOrCreateRoom('room-id');
|
||||
|
||||
// 获取已存在的房间
|
||||
const existingRoom = server.getRoom('room-id');
|
||||
|
||||
// 销毁房间
|
||||
server.destroyRoom('room-id');
|
||||
```
|
||||
|
||||
## Room
|
||||
|
||||
Room 类管理单个游戏房间的玩家和状态。
|
||||
|
||||
### API
|
||||
|
||||
```typescript
|
||||
class Room {
|
||||
readonly id: string;
|
||||
readonly playerCount: number;
|
||||
readonly isFull: boolean;
|
||||
|
||||
// 添加玩家
|
||||
addPlayer(name: string, connection: Connection): IPlayer | null;
|
||||
|
||||
// 移除玩家
|
||||
removePlayer(clientId: number): void;
|
||||
|
||||
// 获取玩家
|
||||
getPlayer(clientId: number): IPlayer | undefined;
|
||||
|
||||
// 处理输入
|
||||
handleInput(clientId: number, input: IPlayerInput): void;
|
||||
|
||||
// 销毁房间
|
||||
destroy(): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 玩家接口
|
||||
|
||||
```typescript
|
||||
interface IPlayer {
|
||||
clientId: number; // 客户端 ID
|
||||
name: string; // 玩家名称
|
||||
connection: Connection; // 连接对象
|
||||
netId: number; // 网络实体 ID
|
||||
}
|
||||
```
|
||||
|
||||
## 协议类型
|
||||
|
||||
### 消息类型
|
||||
|
||||
```typescript
|
||||
// 状态同步消息
|
||||
interface MsgSync {
|
||||
time: number;
|
||||
entities: IEntityState[];
|
||||
}
|
||||
|
||||
// 实体状态
|
||||
interface IEntityState {
|
||||
netId: number;
|
||||
pos?: Vec2;
|
||||
rot?: number;
|
||||
}
|
||||
|
||||
// 生成消息
|
||||
interface MsgSpawn {
|
||||
netId: number;
|
||||
ownerId: number;
|
||||
prefab: string;
|
||||
pos: Vec2;
|
||||
rot: number;
|
||||
}
|
||||
|
||||
// 销毁消息
|
||||
interface MsgDespawn {
|
||||
netId: number;
|
||||
}
|
||||
|
||||
// 输入消息
|
||||
interface MsgInput {
|
||||
input: IPlayerInput;
|
||||
}
|
||||
|
||||
// 玩家输入
|
||||
interface IPlayerInput {
|
||||
seq?: number;
|
||||
moveDir?: Vec2;
|
||||
actions?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### API 类型
|
||||
|
||||
```typescript
|
||||
// 加入请求
|
||||
interface ReqJoin {
|
||||
playerName: string;
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
// 加入响应
|
||||
interface ResJoin {
|
||||
clientId: number;
|
||||
roomId: string;
|
||||
playerCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
## 使用 CLI 创建服务端
|
||||
|
||||
推荐使用 ESEngine CLI 快速创建完整的游戏服务端:
|
||||
使用 CLI 创建新的游戏服务器项目:
|
||||
|
||||
```bash
|
||||
mkdir my-game-server && cd my-game-server
|
||||
npm init -y
|
||||
npx @esengine/cli init -p nodejs
|
||||
# 使用 npm
|
||||
npm create esengine-server my-game-server
|
||||
|
||||
# 使用 pnpm
|
||||
pnpm create esengine-server my-game-server
|
||||
|
||||
# 使用 yarn
|
||||
yarn create esengine-server my-game-server
|
||||
```
|
||||
|
||||
生成的项目结构:
|
||||
@@ -166,24 +23,20 @@ npx @esengine/cli init -p nodejs
|
||||
```
|
||||
my-game-server/
|
||||
├── src/
|
||||
│ ├── index.ts # 入口文件
|
||||
│ ├── server/
|
||||
│ │ └── GameServer.ts # 网络服务器配置
|
||||
│ └── game/
|
||||
│ ├── Game.ts # ECS 游戏主类
|
||||
│ ├── scenes/
|
||||
│ │ └── MainScene.ts # 主场景
|
||||
│ ├── components/ # ECS 组件
|
||||
│ │ ├── PositionComponent.ts
|
||||
│ │ └── VelocityComponent.ts
|
||||
│ └── systems/ # ECS 系统
|
||||
│ └── MovementSystem.ts
|
||||
├── tsconfig.json
|
||||
│ ├── shared/ # 共享协议(客户端服务端通用)
|
||||
│ │ ├── protocol.ts # 类型定义
|
||||
│ │ └── index.ts
|
||||
│ ├── server/ # 服务端代码
|
||||
│ │ ├── main.ts # 入口
|
||||
│ │ └── rooms/
|
||||
│ │ └── GameRoom.ts # 游戏房间
|
||||
│ └── client/ # 客户端示例
|
||||
│ └── index.ts
|
||||
├── package.json
|
||||
└── README.md
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
启动服务端:
|
||||
启动服务器:
|
||||
|
||||
```bash
|
||||
# 开发模式(热重载)
|
||||
@@ -193,15 +46,290 @@ npm run dev
|
||||
npm run start
|
||||
```
|
||||
|
||||
## createServer
|
||||
|
||||
创建游戏服务器实例:
|
||||
|
||||
```typescript
|
||||
import { createServer } from '@esengine/server'
|
||||
import { GameRoom } from './rooms/GameRoom.js'
|
||||
|
||||
const server = await createServer({
|
||||
port: 3000,
|
||||
onConnect(conn) {
|
||||
console.log('Client connected:', conn.id)
|
||||
},
|
||||
onDisconnect(conn) {
|
||||
console.log('Client disconnected:', conn.id)
|
||||
},
|
||||
})
|
||||
|
||||
// 注册房间类型
|
||||
server.define('game', GameRoom)
|
||||
|
||||
// 启动服务器
|
||||
await server.start()
|
||||
```
|
||||
|
||||
### 配置选项
|
||||
|
||||
| 属性 | 类型 | 默认值 | 描述 |
|
||||
|------|------|--------|------|
|
||||
| `port` | `number` | `3000` | WebSocket 端口 |
|
||||
| `tickRate` | `number` | `20` | 全局 Tick 频率 (Hz) |
|
||||
| `apiDir` | `string` | `'src/api'` | API 处理器目录 |
|
||||
| `msgDir` | `string` | `'src/msg'` | 消息处理器目录 |
|
||||
| `onStart` | `(port) => void` | - | 启动回调 |
|
||||
| `onConnect` | `(conn) => void` | - | 连接回调 |
|
||||
| `onDisconnect` | `(conn) => void` | - | 断开回调 |
|
||||
|
||||
## Room 系统
|
||||
|
||||
Room 是游戏房间的基类,管理玩家和游戏状态。
|
||||
|
||||
### 定义房间
|
||||
|
||||
```typescript
|
||||
import { Room, Player, onMessage } from '@esengine/server'
|
||||
import type { MsgMove, MsgChat } from '../../shared/index.js'
|
||||
|
||||
interface PlayerData {
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export class GameRoom extends Room<{ players: any[] }, PlayerData> {
|
||||
// 配置
|
||||
maxPlayers = 8
|
||||
tickRate = 20
|
||||
autoDispose = true
|
||||
|
||||
// 房间状态
|
||||
state = {
|
||||
players: [],
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onCreate() {
|
||||
console.log(`Room ${this.id} created`)
|
||||
}
|
||||
|
||||
onJoin(player: Player<PlayerData>) {
|
||||
player.data.name = 'Player_' + player.id.slice(-4)
|
||||
player.data.x = Math.random() * 800
|
||||
player.data.y = Math.random() * 600
|
||||
|
||||
this.broadcast('Joined', {
|
||||
playerId: player.id,
|
||||
playerName: player.data.name,
|
||||
})
|
||||
}
|
||||
|
||||
onLeave(player: Player<PlayerData>) {
|
||||
this.broadcast('Left', { playerId: player.id })
|
||||
}
|
||||
|
||||
onTick(dt: number) {
|
||||
// 状态同步
|
||||
this.broadcast('Sync', { players: this.state.players })
|
||||
}
|
||||
|
||||
onDispose() {
|
||||
console.log(`Room ${this.id} disposed`)
|
||||
}
|
||||
|
||||
// 消息处理
|
||||
@onMessage('Move')
|
||||
handleMove(data: MsgMove, player: Player<PlayerData>) {
|
||||
player.data.x = data.x
|
||||
player.data.y = data.y
|
||||
}
|
||||
|
||||
@onMessage('Chat')
|
||||
handleChat(data: MsgChat, player: Player<PlayerData>) {
|
||||
this.broadcast('Chat', {
|
||||
from: player.data.name,
|
||||
text: data.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Room 配置
|
||||
|
||||
| 属性 | 类型 | 默认值 | 描述 |
|
||||
|------|------|--------|------|
|
||||
| `maxPlayers` | `number` | `10` | 最大玩家数 |
|
||||
| `tickRate` | `number` | `20` | Tick 频率 (Hz) |
|
||||
| `autoDispose` | `boolean` | `true` | 空房间自动销毁 |
|
||||
|
||||
### Room API
|
||||
|
||||
```typescript
|
||||
class Room<TState, TPlayerData> {
|
||||
readonly id: string // 房间 ID
|
||||
readonly players: Player[] // 所有玩家
|
||||
readonly playerCount: number // 玩家数量
|
||||
readonly isLocked: boolean // 是否锁定
|
||||
state: TState // 房间状态
|
||||
|
||||
// 广播消息给所有玩家
|
||||
broadcast<T>(type: string, data: T): void
|
||||
|
||||
// 广播消息给除某玩家外的所有人
|
||||
broadcastExcept<T>(type: string, data: T, except: Player): void
|
||||
|
||||
// 获取玩家
|
||||
getPlayer(id: string): Player | undefined
|
||||
|
||||
// 踢出玩家
|
||||
kick(player: Player, reason?: string): void
|
||||
|
||||
// 锁定/解锁房间
|
||||
lock(): void
|
||||
unlock(): void
|
||||
|
||||
// 销毁房间
|
||||
dispose(): void
|
||||
}
|
||||
```
|
||||
|
||||
### 生命周期方法
|
||||
|
||||
| 方法 | 触发时机 | 用途 |
|
||||
|------|----------|------|
|
||||
| `onCreate()` | 房间创建时 | 初始化游戏状态 |
|
||||
| `onJoin(player)` | 玩家加入时 | 欢迎消息、分配位置 |
|
||||
| `onLeave(player)` | 玩家离开时 | 清理玩家数据 |
|
||||
| `onTick(dt)` | 每帧调用 | 游戏逻辑、状态同步 |
|
||||
| `onDispose()` | 房间销毁前 | 保存数据、清理资源 |
|
||||
|
||||
## Player 类
|
||||
|
||||
Player 代表房间中的一个玩家连接。
|
||||
|
||||
```typescript
|
||||
class Player<TData = Record<string, unknown>> {
|
||||
readonly id: string // 玩家 ID
|
||||
readonly roomId: string // 所在房间 ID
|
||||
data: TData // 自定义数据
|
||||
|
||||
// 发送消息给此玩家
|
||||
send<T>(type: string, data: T): void
|
||||
|
||||
// 离开房间
|
||||
leave(): void
|
||||
}
|
||||
```
|
||||
|
||||
## @onMessage 装饰器
|
||||
|
||||
使用装饰器简化消息处理:
|
||||
|
||||
```typescript
|
||||
import { Room, Player, onMessage } from '@esengine/server'
|
||||
|
||||
class GameRoom extends Room {
|
||||
@onMessage('Move')
|
||||
handleMove(data: { x: number; y: number }, player: Player) {
|
||||
// 处理移动
|
||||
}
|
||||
|
||||
@onMessage('Attack')
|
||||
handleAttack(data: { targetId: string }, player: Player) {
|
||||
// 处理攻击
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 协议定义
|
||||
|
||||
在 `src/shared/protocol.ts` 中定义客户端和服务端共享的类型:
|
||||
|
||||
```typescript
|
||||
// API 请求/响应
|
||||
export interface JoinRoomReq {
|
||||
roomType: string
|
||||
playerName: string
|
||||
}
|
||||
|
||||
export interface JoinRoomRes {
|
||||
roomId: string
|
||||
playerId: string
|
||||
}
|
||||
|
||||
// 游戏消息
|
||||
export interface MsgMove {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface MsgChat {
|
||||
text: string
|
||||
}
|
||||
|
||||
// 服务端广播
|
||||
export interface BroadcastSync {
|
||||
players: PlayerState[]
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
id: string
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
```
|
||||
|
||||
## 客户端连接
|
||||
|
||||
```typescript
|
||||
import { connect } from '@esengine/rpc/client'
|
||||
|
||||
const client = await connect('ws://localhost:3000')
|
||||
|
||||
// 加入房间
|
||||
const { roomId, playerId } = await client.call('JoinRoom', {
|
||||
roomType: 'game',
|
||||
playerName: 'Alice',
|
||||
})
|
||||
|
||||
// 监听广播
|
||||
client.onMessage('Sync', (data) => {
|
||||
console.log('State:', data.players)
|
||||
})
|
||||
|
||||
client.onMessage('Joined', (data) => {
|
||||
console.log('Player joined:', data.playerName)
|
||||
})
|
||||
|
||||
// 发送消息
|
||||
client.send('RoomMessage', {
|
||||
type: 'Move',
|
||||
payload: { x: 100, y: 200 },
|
||||
})
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **合理设置同步频率**:根据游戏类型选择合适的 `tickRate`
|
||||
1. **合理设置 Tick 频率**
|
||||
- 回合制游戏:5-10 Hz
|
||||
- 休闲游戏:10-20 Hz
|
||||
- 动作游戏:20-60 Hz
|
||||
|
||||
2. **房间大小控制**:根据服务器性能设置合理的 `maxPlayers`
|
||||
2. **使用共享协议**
|
||||
- 在 `shared/` 目录定义所有类型
|
||||
- 客户端和服务端都从这里导入
|
||||
|
||||
3. **连接管理**:监听玩家连接/断开事件,处理异常情况
|
||||
3. **状态验证**
|
||||
- 服务器应验证客户端输入
|
||||
- 不信任客户端发送的任何数据
|
||||
|
||||
4. **状态验证**:服务器应验证客户端输入,防止作弊
|
||||
4. **断线处理**
|
||||
- 实现断线重连逻辑
|
||||
- 使用 `onLeave` 保存玩家状态
|
||||
|
||||
5. **房间生命周期**
|
||||
- 使用 `autoDispose` 自动清理空房间
|
||||
- 在 `onDispose` 中保存重要数据
|
||||
|
||||
Reference in New Issue
Block a user