WebSocket-Multiplayer-Grundlagen
Ein Multiplayer-Spiel mit WebSockets bauen: Serverarchitektur, Spielräume und Zustandssynchronisation
WebSockets ermöglichen Echtzeitkommunikation für Multiplayer-Spiele. Dieses Tutorial behandelt die Grundlagen auf der Client-Seite.
1) Mit einem Server verbinden
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) Ein Nachrichtenprotokoll entwerfen
Leg ein einfaches Protokoll fest:
js
// Client -> Server
{ type: 'join', name: 'Player1' }
{ type: 'input', keys: { left: true, jump: true } }
{ type: 'chat', message: 'Hello!' }
// Server -> Client
{ type: 'state', players: [...], entities: [...] }
{ type: 'playerJoined', id: 'abc', name: 'Player2' }
{ type: 'playerLeft', id: 'abc' }3) Eine Klasse für die Verbindungsverwaltung
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) Spielereingaben senden
Sende Eingaben, keine Positionen. Lass den Server autoritativ sein:
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++,
})
}
// Send input at a fixed rate (e.g., 20 times/sec)
setInterval(() => {
sendInput(currentInput)
}, 50)5) Spielzustand empfangen
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) Interpolation für flüssige Bewegung
Der Server schickt Momentaufnahmen. Interpoliere dazwischen:
js
class EntityInterpolator {
constructor() {
this.buffer = [] // { timestamp, state }
this.renderDelay = 100 // ms behind server
}
addSnapshot(timestamp, state) {
this.buffer.push({ timestamp, state })
// Keep only recent snapshots
while (this.buffer.length > 10) {
this.buffer.shift()
}
}
getState(currentTime) {
const renderTime = currentTime - this.renderDelay
// Find surrounding snapshots
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
}
// Interpolate
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) Clientseitige Vorhersage
Für reaktionsschnelle Steuerung sagst du lokal voraus und gleichst mit dem Server ab:
js
class PredictedPlayer {
constructor() {
this.position = { x: 0, y: 0 }
this.pendingInputs = []
}
applyInput(input) {
// Apply input locally
if (input.left) this.position.x -= 5
if (input.right) this.position.x += 5
// Store for reconciliation
this.pendingInputs.push({ seq: input.seq, input })
}
reconcile(serverState, lastProcessedSeq) {
// Server confirmed position
this.position = { ...serverState.position }
// Remove confirmed inputs
this.pendingInputs = this.pendingInputs.filter(i => i.seq > lastProcessedSeq)
// Re-apply unconfirmed inputs
for (const pending of this.pendingInputs) {
if (pending.input.left) this.position.x -= 5
if (pending.input.right) this.position.x += 5
}
}
}8) Latenzanzeige
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) Binärnachrichten für Effizienz
Für hochfrequente Updates nimmst du Binärdaten:
js
// Sending binary
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)
// Receiving binary
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) Ein einfacher Server (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 })
})
})
// Broadcast state 20 times/sec
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)
}
}Verwandt
- Ein Web-Spiel ausliefern, das schnell lädt
- Eingaben in Spielen
- Web Workers für Spiellogik
- COOP/COEP und SharedArrayBuffer — Header, die SharedArrayBuffer im Multiplayer-Kontext braucht
- Koop-Spieldesign 2026 — warum "Friend-Slop"-Koop-Spiele explodieren und wo Browser-Multiplayer hineinpasst
- Game Jams und Hackathons — Multiplayer-Jam-Spiele bekommen mehr Aufmerksamkeit
Externe Ressourcen
- MDN: WebSocket API — Referenz zur WebSocket-API im Browser
- MDN: WebRTC API — Peer-to-Peer-Verbindungen für geringere Latenz
- MDN: WebTransport API — ein neuerer HTTP/3-Transport, der im März 2026 browserübergreifend Baseline erreichte, als Safari 26.4 die Unterstützung nachlieferte (in Chrome ab 97, Edge und Firefox ab 114 war sie schon da). Anders als ein einzelner TCP-basierter WebSocket läuft WebTransport über QUIC und bietet unzuverlässige Datagramme sowie mehrere unabhängige Streams, ein verlorenes Paket blockiert also nicht alles andere am Kopf der Warteschlange. Ein gängiges Muster ist, hochfrequente Positionsupdates über Datagramme zu schicken und Chat oder Bestenlisten auf einem zuverlässigen Stream zu lassen. WebSockets sind weiterhin die kompatibelste Wahl und alles oben gilt weiter, aber WebTransport ist für latenzempfindliche Spiele inzwischen eine Überlegung wert.
- Socket.IO documentation — beliebte Wrapper-Bibliothek für WebSockets
- Valve Source Multiplayer Networking — bewährte Netzwerkmuster aus Produktionsspielen