WebSocket 多人游戏基础
用 WebSocket 构建多人游戏:服务器架构、游戏房间和状态同步
WebSocket 让多人游戏的实时通信成为可能。这篇教程介绍客户端基础。
1)连接到服务器
js
const ws = new WebSocket('wss://game-server.example.com')
ws.onopen = () => {
console.log('Connected')
ws.send(JSON.stringify({ type: 'join', name: 'Player1' }))
}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
handleMessage(msg)
}
ws.onclose = () => {
console.log('Disconnected')
}
ws.onerror = (err) => {
console.error('WebSocket error:', err)
}2)消息协议设计
定义一个简单协议:
js
// 客户端 -> 服务端
{ type: 'join', name: 'Player1' }
{ type: 'input', keys: { left: true, jump: true } }
{ type: 'chat', message: 'Hello!' }
// 服务端 -> 客户端
{ type: 'state', players: [...], entities: [...] }
{ type: 'playerJoined', id: 'abc', name: 'Player2' }
{ type: 'playerLeft', id: 'abc' }3)连接管理器类
js
class GameConnection {
constructor(url) {
this.url = url
this.ws = null
this.handlers = new Map()
this.reconnectDelay = 1000
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
this.reconnectDelay = 1000
this.emit('connected')
}
this.ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
this.emit(msg.type, msg)
}
this.ws.onclose = () => {
this.emit('disconnected')
this.scheduleReconnect()
}
this.ws.onerror = () => {
this.ws.close()
}
}
scheduleReconnect() {
setTimeout(() => {
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
this.connect()
}, this.reconnectDelay)
}
send(type, data = {}) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type, ...data }))
}
}
on(type, handler) {
if (!this.handlers.has(type)) {
this.handlers.set(type, [])
}
this.handlers.get(type).push(handler)
}
emit(type, data) {
const handlers = this.handlers.get(type) || []
handlers.forEach(h => h(data))
}
disconnect() {
this.ws?.close()
}
}4)发送玩家输入
发送输入,而不是位置。让服务器作为权威:
js
const connection = new GameConnection('wss://server.example.com')
function sendInput(input) {
connection.send('input', {
left: input.left,
right: input.right,
jump: input.jump,
shoot: input.shoot,
seq: inputSequence++,
})
}
// 以固定频率发送输入(例如每秒 20 次)
setInterval(() => {
sendInput(currentInput)
}, 50)5)接收游戏状态
js
let gameState = { players: [], entities: [] }
connection.on('state', (msg) => {
gameState = msg
})
connection.on('playerJoined', (msg) => {
gameState.players.push({ id: msg.id, name: msg.name })
})
connection.on('playerLeft', (msg) => {
gameState.players = gameState.players.filter(p => p.id !== msg.id)
})6)插值让运动平滑
服务器发送快照,本地在快照之间插值:
js
class EntityInterpolator {
constructor() {
this.buffer = [] // { timestamp, state }
this.renderDelay = 100 // 比服务器滞后 ms
}
addSnapshot(timestamp, state) {
this.buffer.push({ timestamp, state })
// 只保留最近的快照
while (this.buffer.length > 10) {
this.buffer.shift()
}
}
getState(currentTime) {
const renderTime = currentTime - this.renderDelay
// 找到包围的快照
let before = null
let after = null
for (let i = 0; i < this.buffer.length - 1; i++) {
if (this.buffer[i].timestamp <= renderTime &&
this.buffer[i + 1].timestamp >= renderTime) {
before = this.buffer[i]
after = this.buffer[i + 1]
break
}
}
if (!before || !after) {
return this.buffer[this.buffer.length - 1]?.state
}
// 插值
const t = (renderTime - before.timestamp) / (after.timestamp - before.timestamp)
return this.lerp(before.state, after.state, t)
}
lerp(a, b, t) {
return {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t,
}
}
}7)客户端预测
为了响应灵敏的控制,本地预测并与服务器对账:
js
class PredictedPlayer {
constructor() {
this.position = { x: 0, y: 0 }
this.pendingInputs = []
}
applyInput(input) {
// 本地应用输入
if (input.left) this.position.x -= 5
if (input.right) this.position.x += 5
// 存起来以便对账
this.pendingInputs.push({ seq: input.seq, input })
}
reconcile(serverState, lastProcessedSeq) {
// 服务器确认的位置
this.position = { ...serverState.position }
// 删除已确认的输入
this.pendingInputs = this.pendingInputs.filter(i => i.seq > lastProcessedSeq)
// 重放尚未确认的输入
for (const pending of this.pendingInputs) {
if (pending.input.left) this.position.x -= 5
if (pending.input.right) this.position.x += 5
}
}
}8)显示延迟
js
class LatencyMonitor {
constructor(connection) {
this.connection = connection
this.pingStart = 0
this.latency = 0
connection.on('pong', () => {
this.latency = Date.now() - this.pingStart
})
setInterval(() => this.ping(), 1000)
}
ping() {
this.pingStart = Date.now()
this.connection.send('ping')
}
getLatency() {
return this.latency
}
}9)用二进制消息提高效率
高频更新用二进制:
js
// 发送二进制
const buffer = new ArrayBuffer(12)
const view = new DataView(buffer)
view.setFloat32(0, player.x)
view.setFloat32(4, player.y)
view.setUint32(8, inputFlags)
ws.send(buffer)
// 接收二进制
ws.binaryType = 'arraybuffer'
ws.onmessage = (e) => {
if (e.data instanceof ArrayBuffer) {
const view = new DataView(e.data)
const x = view.getFloat32(0)
const y = view.getFloat32(4)
// ...
}
}10)基础服务器示例(Node.js)
js
import { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
const players = new Map()
wss.on('connection', (ws) => {
const id = crypto.randomUUID()
players.set(id, { id, x: 0, y: 0, ws })
ws.on('message', (data) => {
const msg = JSON.parse(data)
if (msg.type === 'input') {
const player = players.get(id)
if (msg.left) player.x -= 5
if (msg.right) player.x += 5
}
})
ws.on('close', () => {
players.delete(id)
broadcast({ type: 'playerLeft', id })
})
})
// 每秒广播 20 次状态
setInterval(() => {
const state = {
type: 'state',
players: Array.from(players.values()).map(p => ({
id: p.id, x: p.x, y: p.y
}))
}
broadcast(state)
}, 50)
function broadcast(msg) {
const data = JSON.stringify(msg)
for (const player of players.values()) {
player.ws.send(data)
}
}相关阅读
- 发布一个快速加载的网页游戏
- 游戏输入处理
- 游戏逻辑的 Web Workers
- COOP/COEP 与 SharedArrayBuffer —— 多人游戏场景里使用 SharedArrayBuffer 所需的响应头
- 2026 年合作游戏设计 —— 为什么"朋友联机"合作游戏在爆发,以及浏览器多人游戏如何融入
- Game Jam 与黑客松 —— 多人 jam 游戏更容易获得关注
外部资源
- MDN: WebSocket API —— 浏览器 WebSocket 参考
- MDN: WebRTC API —— 用 P2P 连接降低延迟
- Socket.IO documentation —— 流行的 WebSocket 封装库
- Valve Source Multiplayer Networking —— 商业游戏使用的权威网络模式