服务器端
使用 CLI 创建新的游戏服务器项目:
# 使用 npmnpm create esengine-server my-game-server
# 使用 pnpmpnpm create esengine-server my-game-server
# 使用 yarnyarn create esengine-server my-game-server生成的项目结构:
my-game-server/├── src/│ ├── shared/ # 共享协议(客户端服务端通用)│ │ ├── protocol.ts # 类型定义│ │ └── index.ts│ ├── server/ # 服务端代码│ │ ├── main.ts # 入口│ │ └── rooms/│ │ └── GameRoom.ts # 游戏房间│ └── client/ # 客户端示例│ └── index.ts├── package.json└── tsconfig.json启动服务器:
# 开发模式(热重载)npm run dev
# 生产模式npm run startcreateServer
Section titled “createServer”创建游戏服务器实例:
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' | 消息处理器目录 |
httpDir | string | 'src/http' | HTTP 路由目录 |
httpPrefix | string | '/api' | HTTP 路由前缀 |
cors | boolean | CorsOptions | - | CORS 配置 |
onStart | (port) => void | - | 启动回调 |
onConnect | (conn) => void | - | 连接回调 |
onDisconnect | (conn) => void | - | 断开回调 |
HTTP 路由
Section titled “HTTP 路由”支持 HTTP API 与 WebSocket 共用端口,适用于登录、注册等场景。
const server = await createServer({ port: 3000, httpDir: './src/http', // HTTP 路由目录 httpPrefix: '/api', // 路由前缀 cors: true,
// 或内联定义 http: { '/health': (req, res) => res.json({ status: 'ok' }) }})import { defineHttp } from '@esengine/server'
export default defineHttp<{ username: string; password: string }>({ method: 'POST', handler(req, res) { const { username, password } = req.body // 验证并返回 token... res.json({ token: '...' }) }})详细文档请参考 HTTP 路由
Room 系统
Section titled “Room 系统”Room 是游戏房间的基类,管理玩家和游戏状态。
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 配置
Section titled “Room 配置”| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
maxPlayers | number | 10 | 最大玩家数 |
tickRate | number | 20 | Tick 频率 (Hz) |
autoDispose | boolean | true | 空房间自动销毁 |
Room API
Section titled “Room API”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}生命周期方法
Section titled “生命周期方法”| 方法 | 触发时机 | 用途 |
|---|---|---|
onCreate() | 房间创建时 | 初始化游戏状态 |
onJoin(player) | 玩家加入时 | 欢迎消息、分配位置 |
onLeave(player) | 玩家离开时 | 清理玩家数据 |
onTick(dt) | 每帧调用 | 游戏逻辑、状态同步 |
onDispose() | 房间销毁前 | 保存数据、清理资源 |
Player 类
Section titled “Player 类”Player 代表房间中的一个玩家连接。
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 装饰器
Section titled “@onMessage 装饰器”使用装饰器简化消息处理:
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) { // 处理攻击 }}Schema 验证
Section titled “Schema 验证”使用内置的 Schema 验证系统进行运行时类型验证:
import { s, defineApiWithSchema } from '@esengine/server'
// 定义 Schemaconst MoveSchema = s.object({ x: s.number(), y: s.number(), speed: s.number().optional()})
// 类型自动推断type Move = s.infer<typeof MoveSchema> // { x: number; y: number; speed?: number }
// 使用 Schema 定义 API(自动验证)export default defineApiWithSchema(MoveSchema, { handler(req, ctx) { // req 已验证,类型安全 console.log(req.x, req.y) }})| 类型 | 示例 | 描述 |
|---|---|---|
s.string() | s.string().min(1).max(50) | 字符串,支持长度限制 |
s.number() | s.number().min(0).int() | 数字,支持范围和整数限制 |
s.boolean() | s.boolean() | 布尔值 |
s.literal() | s.literal('admin') | 字面量类型 |
s.object() | s.object({ name: s.string() }) | 对象 |
s.array() | s.array(s.number()) | 数组 |
s.enum() | s.enum(['a', 'b'] as const) | 枚举 |
s.union() | s.union([s.string(), s.number()]) | 联合类型 |
s.record() | s.record(s.any()) | 记录类型 |
// 可选字段s.string().optional()
// 默认值s.number().default(0)
// 可为 nulls.string().nullable()
// 字符串验证s.string().min(1).max(100).email().url().regex(/^[a-z]+$/)
// 数字验证s.number().min(0).max(100).int().positive()
// 数组验证s.array(s.string()).min(1).max(10).nonempty()
// 对象验证s.object({ ... }).strict() // 不允许额外字段s.object({ ... }).partial() // 所有字段可选s.object({ ... }).pick('name', 'age') // 选择字段s.object({ ... }).omit('password') // 排除字段import { s, defineMsgWithSchema } from '@esengine/server'
const InputSchema = s.object({ keys: s.array(s.string()), timestamp: s.number()})
export default defineMsgWithSchema(InputSchema, { handler(msg, ctx) { // msg 已验证 console.log(msg.keys, msg.timestamp) }})import { s, parse, safeParse, createGuard } from '@esengine/server'
const UserSchema = s.object({ name: s.string(), age: s.number().int().min(0)})
// 抛出错误const user = parse(UserSchema, data)
// 返回结果对象const result = safeParse(UserSchema, data)if (result.success) { console.log(result.data)} else { console.error(result.error)}
// 类型守卫const isUser = createGuard(UserSchema)if (isUser(data)) { // data 是 User 类型}在 src/shared/protocol.ts 中定义客户端和服务端共享的类型:
// 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}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 },})ECSRoom
Section titled “ECSRoom”ECSRoom 是带有 ECS World 支持的房间基类,适用于需要 ECS 架构的游戏。
import { Core } from '@esengine/ecs-framework';import { createServer } from '@esengine/server';import { GameRoom } from './rooms/GameRoom.js';
// 初始化 CoreCore.create();
// 全局游戏循环setInterval(() => Core.update(1/60), 16);
// 创建服务器const server = await createServer({ port: 3000 });server.define('game', GameRoom);await server.start();定义 ECSRoom
Section titled “定义 ECSRoom”import { ECSRoom, Player } from '@esengine/server/ecs';import { Component, ECSComponent, sync } from '@esengine/ecs-framework';
// 定义同步组件@ECSComponent('Player')class PlayerComponent extends Component { @sync("string") name: string = ""; @sync("uint16") score: number = 0; @sync("float32") x: number = 0; @sync("float32") y: number = 0;}
// 定义房间class GameRoom extends ECSRoom { onCreate() { this.addSystem(new MovementSystem()); }
onJoin(player: Player) { const entity = this.createPlayerEntity(player.id); const comp = entity.addComponent(new PlayerComponent()); comp.name = player.id; }}ECSRoom API
Section titled “ECSRoom API”abstract class ECSRoom<TState, TPlayerData> extends Room<TState, TPlayerData> { protected readonly world: World; // ECS World protected readonly scene: Scene; // 主场景
// 场景管理 protected addSystem(system: EntitySystem): void; protected createEntity(name?: string): Entity; protected createPlayerEntity(playerId: string, name?: string): Entity; protected getPlayerEntity(playerId: string): Entity | undefined; protected destroyPlayerEntity(playerId: string): void;
// 状态同步 protected sendFullState(player: Player): void; protected broadcastSpawn(entity: Entity, prefabType?: string): void; protected broadcastDelta(): void;}@sync 装饰器
Section titled “@sync 装饰器”标记需要网络同步的组件字段:
| 类型 | 描述 | 字节数 |
|---|---|---|
"boolean" | 布尔值 | 1 |
"int8" / "uint8" | 8位整数 | 1 |
"int16" / "uint16" | 16位整数 | 2 |
"int32" / "uint32" | 32位整数 | 4 |
"float32" | 32位浮点 | 4 |
"float64" | 64位浮点 | 8 |
"string" | 字符串 | 变长 |
-
合理设置 Tick 频率
- 回合制游戏:5-10 Hz
- 休闲游戏:10-20 Hz
- 动作游戏:20-60 Hz
-
使用共享协议
- 在
shared/目录定义所有类型 - 客户端和服务端都从这里导入
- 在
-
状态验证
- 服务器应验证客户端输入
- 不信任客户端发送的任何数据
-
断线处理
- 实现断线重连逻辑
- 使用
onLeave保存玩家状态
-
房间生命周期
- 使用
autoDispose自动清理空房间 - 在
onDispose中保存重要数据
- 使用