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
- Hızlı yüklenen bir web oyunu yayınlayın
- Oyun girdilerini yönetme
- Oyun mantığı için Web Workers
- COOP/COEP ve SharedArrayBuffer — çok oyunculu bağlamlarda SharedArrayBuffer için gerekli başlıklar
- 2026'da eşli oyun tasarımı — "friend slop" eşli oyunların neden hızla yayıldığı ve tarayıcı tabanlı çok oyunculu oyunların bu alandaki yeri
- Game Jam'ler ve Hackathon'lar — çok oyunculu jam oyunları daha fazla ilgi görür
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ı