Dasar-dasar multipemain WebSocket
Membangun game multipemain dengan WebSocket: arsitektur server, ruang permainan, dan sinkronisasi state
WebSocket memungkinkan komunikasi real-time untuk game multipemain. Tutorial ini membahas dasar-dasar di sisi klien.
1) Menghubungkan ke server
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) Desain protokol pesan
Tentukan protokol sederhana:
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) Class pengelola koneksi
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) Mengirim input pemain
Kirim input, bukan posisi. Biarkan server menjadi sumber kebenaran:
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) Menerima state permainan
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) Interpolasi untuk gerakan yang mulus
Server mengirim snapshot. Lakukan interpolasi di antaranya:
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) Prediksi sisi klien
Agar kontrol responsif, lakukan prediksi secara lokal dan rekonsiliasi dengan server:
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) Menangani tampilan latensi
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) Pesan biner untuk efisiensi
Untuk pembaruan berfrekuensi tinggi, gunakan format biner:
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) Contoh server dasar (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)
}
}Terkait
- Rilis game web yang dimuat dengan cepat
- Penanganan input game
- Web Worker untuk logika game
- COOP/COEP dan SharedArrayBuffer — header yang diperlukan untuk SharedArrayBuffer dalam konteks multipemain
- Desain Game Kooperatif pada 2026 — alasan game kooperatif "friend slop" sedang meledak dan bagaimana multipemain browser berperan di dalamnya
- Game Jam & Hackathon — game multipemain untuk jam lebih menarik perhatian
Sumber Daya Eksternal
- MDN: API WebSocket — referensi WebSocket untuk browser
- MDN: API WebRTC — koneksi peer-to-peer untuk latensi yang lebih rendah
- MDN: API WebTransport — transportasi HTTP/3 yang lebih baru dan mencapai Baseline lintas browser pada Maret 2026 setelah Safari 26.4 menyediakan dukungan (sebelumnya sudah tersedia di Chrome 97+, Edge, dan Firefox 114+). Tidak seperti satu WebSocket berbasis TCP, WebTransport berjalan di atas QUIC serta menawarkan datagram yang tidak andal dan beberapa stream independen, sehingga satu paket yang hilang tidak menyebabkan head-of-line blocking pada semua hal lainnya. Pola yang umum adalah mengirim pembaruan posisi berfrekuensi tinggi melalui datagram, sementara chat atau papan skor tetap menggunakan stream yang andal. WebSocket masih menjadi pilihan yang paling kompatibel dan semua pembahasan di atas tetap berlaku, tetapi WebTransport kini layak dipertimbangkan untuk game yang sensitif terhadap latensi.
- Dokumentasi Socket.IO — pustaka wrapper WebSocket populer
- Valve Source Multiplayer Networking — pola jaringan otoritatif yang digunakan dalam game produksi