Skip to content

WebSocket ile çok oyunculu oyunların temelleri

WebSocket ile çok oyunculu oyun geliştirme: sunucu mimarisi, oyun odaları ve durum senkronizasyonu

WebSocket'ler, çok oyunculu oyunlarda gerçek zamanlı iletişimi mümkün kılar. Bu eğitim, istemci tarafındaki temel konuları ele alır.

1) Bir sunucuya bağlanma

js
const ws = new WebSocket('wss://game-server.example.com')

ws.onopen = () => {
  console.log('Bağlandı')
  ws.send(JSON.stringify({ type: 'join', name: 'Player1' }))
}

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  handleMessage(msg)
}

ws.onclose = () => {
  console.log('Bağlantı kesildi')
}

ws.onerror = (err) => {
  console.error('WebSocket hatası:', err)
}

2) Mesaj protokolü tasarımı

Basit bir protokol tanımlayın:

js
// İstemci -> Sunucu
{ type: 'join', name: 'Player1' }
{ type: 'input', keys: { left: true, jump: true } }
{ type: 'chat', message: 'Merhaba!' }

// Sunucu -> İstemci
{ type: 'state', players: [...], entities: [...] }
{ type: 'playerJoined', id: 'abc', name: 'Player2' }
{ type: 'playerLeft', id: 'abc' }

3) Bağlantı yöneticisi sınıfı

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) Oyuncu girdilerini gönderme

Konumları değil, girdileri gönderin. Yetkili taraf sunucu olsun:

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++,
  })
}

// Girdiyi sabit bir hızda gönder (ör. saniyede 20 kez)
setInterval(() => {
  sendInput(currentInput)
}, 50)

5) Oyun durumunu alma

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) Akıcı hareket için enterpolasyon

Sunucu anlık durum görüntüleri gönderir. Bunların arasında enterpolasyon yapın:

js
class EntityInterpolator {
  constructor() {
    this.buffer = [] // { zaman damgası, durum }
    this.renderDelay = 100 // sunucunun ms cinsinden gerisinde
  }
  
  addSnapshot(timestamp, state) {
    this.buffer.push({ timestamp, state })
    
    // Yalnızca son anlık durum görüntülerini tut
    while (this.buffer.length > 10) {
      this.buffer.shift()
    }
  }
  
  getState(currentTime) {
    const renderTime = currentTime - this.renderDelay
    
    // Çevreleyen anlık durum görüntülerini bul
    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
    }
    
    // Enterpolasyon yap
    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) İstemci taraflı tahmin

Kontrollerin hızlı tepki vermesi için yerel olarak tahmin yapın ve sunucuyla uzlaştırın:

js
class PredictedPlayer {
  constructor() {
    this.position = { x: 0, y: 0 }
    this.pendingInputs = []
  }
  
  applyInput(input) {
    // Girdiyi yerel olarak uygula
    if (input.left) this.position.x -= 5
    if (input.right) this.position.x += 5
    
    // Uzlaştırma için sakla
    this.pendingInputs.push({ seq: input.seq, input })
  }
  
  reconcile(serverState, lastProcessedSeq) {
    // Sunucu tarafından doğrulanan konum
    this.position = { ...serverState.position }
    
    // Doğrulanan girdileri kaldır
    this.pendingInputs = this.pendingInputs.filter(i => i.seq > lastProcessedSeq)
    
    // Doğrulanmamış girdileri yeniden uygula
    for (const pending of this.pendingInputs) {
      if (pending.input.left) this.position.x -= 5
      if (pending.input.right) this.position.x += 5
    }
  }
}

8) Gecikme göstergesini yönetme

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) Verimlilik için ikili mesajlar

Yüksek frekanslı güncellemelerde ikili verileri kullanın:

js
// İkili veri gönderme
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)

// İkili veri alma
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) Temel sunucu örneği (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 })
  })
})

// Durumu saniyede 20 kez yayınla
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)
  }
}

İlgili içerikler

Harici kaynaklar

  • MDN: WebSocket API'si — tarayıcı WebSocket referansı
  • MDN: WebRTC API'si — daha düşük gecikme için eşler arası bağlantılar
  • MDN: WebTransport API'si — Safari 26.4'ün destek sunmasıyla Mart 2026'da tarayıcılar arası Baseline düzeyine ulaşan daha yeni bir HTTP/3 aktarım teknolojisi (Chrome 97+, Edge ve Firefox 114+ sürümlerinde zaten destekleniyordu). TCP tabanlı tek bir WebSocket bağlantısının aksine WebTransport, QUIC üzerinden çalışır; güvenilir olmayan datagramların yanı sıra birbirinden bağımsız birden fazla akış sunar. Böylece düşen tek bir paket, diğer tüm verileri sıra başı engellemesine maruz bırakmaz. Yaygın bir yaklaşım, yüksek frekanslı konum güncellemelerini datagramlar üzerinden gönderirken sohbeti veya skor tablolarını güvenilir bir akışta tutmaktır. WebSocket'ler hâlâ en uyumlu seçenektir ve yukarıdaki her şey geçerliliğini korur; ancak gecikmeye duyarlı oyunlarda artık WebTransport da değerlendirilmeye değer.
  • Socket.IO belgeleri — popüler WebSocket sarmalayıcı kütüphanesi
  • Valve Source Çok Oyunculu Ağ İletişimi — yayınlanmış oyunlarda kullanılan yetkili ağ iletişimi kalıpları