Skip to content

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

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