跳转到内容

服务器端

使用 CLI 创建新的游戏服务器项目:

Terminal window
# 使用 npm
npm create esengine-server my-game-server
# 使用 pnpm
pnpm create esengine-server my-game-server
# 使用 yarn
yarn 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

启动服务器:

Terminal window
# 开发模式(热重载)
npm run dev
# 生产模式
npm run start

创建游戏服务器实例:

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()
属性类型默认值描述
portnumber3000WebSocket 端口
tickRatenumber20全局 Tick 频率 (Hz)
apiDirstring'src/api'API 处理器目录
msgDirstring'src/msg'消息处理器目录
httpDirstring'src/http'HTTP 路由目录
httpPrefixstring'/api'HTTP 路由前缀
corsboolean | CorsOptions-CORS 配置
onStart(port) => void-启动回调
onConnect(conn) => void-连接回调
onDisconnect(conn) => void-断开回调

支持 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' })
}
})
src/http/login.ts
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 是游戏房间的基类,管理玩家和游戏状态。

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,
})
}
}
属性类型默认值描述
maxPlayersnumber10最大玩家数
tickRatenumber20Tick 频率 (Hz)
autoDisposebooleantrue空房间自动销毁
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 代表房间中的一个玩家连接。

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
}

使用装饰器简化消息处理:

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 验证系统进行运行时类型验证:

import { s, defineApiWithSchema } from '@esengine/server'
// 定义 Schema
const 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)
// 可为 null
s.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 是带有 ECS World 支持的房间基类,适用于需要 ECS 架构的游戏。

import { Core } from '@esengine/ecs-framework';
import { createServer } from '@esengine/server';
import { GameRoom } from './rooms/GameRoom.js';
// 初始化 Core
Core.create();
// 全局游戏循环
setInterval(() => Core.update(1/60), 16);
// 创建服务器
const server = await createServer({ port: 3000 });
server.define('game', GameRoom);
await server.start();
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;
}
}
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;
}

标记需要网络同步的组件字段:

类型描述字节数
"boolean"布尔值1
"int8" / "uint8"8位整数1
"int16" / "uint16"16位整数2
"int32" / "uint32"32位整数4
"float32"32位浮点4
"float64"64位浮点8
"string"字符串变长
  1. 合理设置 Tick 频率

    • 回合制游戏:5-10 Hz
    • 休闲游戏:10-20 Hz
    • 动作游戏:20-60 Hz
  2. 使用共享协议

    • shared/ 目录定义所有类型
    • 客户端和服务端都从这里导入
  3. 状态验证

    • 服务器应验证客户端输入
    • 不信任客户端发送的任何数据
  4. 断线处理

    • 实现断线重连逻辑
    • 使用 onLeave 保存玩家状态
  5. 房间生命周期

    • 使用 autoDispose 自动清理空房间
    • onDispose 中保存重要数据