Episode ini membangun multiplayer game: arsitektur client-server dengan server otoritatif, sinkronisasi state dengan delta compression, input handling, mitigasi latensi dengan client prediction, dan optimasi khusus game.

Game multiplayer adalah ujian terberat untuk WebSocket: setiap milidetik berarti, cheat harus dicegah, dan pemain di jaringan berbeda harus melihat dunia yang sama. Kegagalan di sini langsung terasa — karakter melompat-lompat, tembakan tidak mengenai sasaran.
Episode 23 membahas multiplayer game basics: arsitektur dengan server otoritatif, sinkronisasi state, pengumpulan input, mitigasi latensi, dan optimasi yang membuat 60 pemain bermain mulus dalam satu world.
Aturan main dijalankan di server, bukan di klien. Klien hanya mengirim input dan menerima state resmi.
client input -> server memvalidasi -> server update state -> broadcastServer otoritatif mencegah cheat: pemain tidak bisa mengirim "saya pindah ke sini" begitu saja. Klien mengirim "tekan tombol kanan", server menghitung ke mana karakter pindah dan mengumumkannya.
P2P menghilangkan biaya server, tapi menimbulkan masalah: satu pemain yang curang bisa memengaruhi semua orang, dan sinkronisasi antar N pemain jadi N kuadrat koneksi. Untuk game skala kecil, server otoritatif sederhana adalah keseimbangan terbaik.
Game berjalan dalam langkah waktu tetap yang disebut tick.
const TICK_RATE = 20; // 20 tick per detik
let tick = 0;
setInterval(() => {
tick += 1;
for (const pemain of pemainList) {
prosesInput(pemain, tick);
updateFisika(pemain, tick);
}
broadcastState(tick);
}, 1000 / TICK_RATE);TICK_RATE = 20 berarti state dihitung 20 kali per detik dan disiarkan setiap tick. Semua pemain melihat tick yang sama, sehingga dunia berjalan konsisten untuk semua orang.
Mengirim posisi semua pemain setiap tick membengkak. Kirim hanya yang berubah.
function broadcastState(tick) {
const delta = [];
for (const pemain of pemainList) {
if (pemain.posisi !== pemain.posisiTerakhir) {
delta.push({
id: pemain.id,
x: Math.round(pemain.posisi.x * 10) / 10,
y: Math.round(pemain.posisi.y * 10) / 10,
});
pemain.posisiTerakhir = pemain.posisi;
}
}
io.emit("state", { tick, delta });
}Math.round(... * 10) / 10 membulatkan posisi ke satu desimal — cukup untuk visual, jauh lebih kecil daripada bilangan mengambang penuh. Pemain yang diam tidak dikirim sama sekali.
Input dari klien datang tidak beraturan; server menyangganya lalu memproses per tick.
const buffer = new Map();
socket.on("input", (input) => {
input.tick = socket.tickTerakhir;
const list = buffer.get(socket.id) || [];
list.push(input);
buffer.set(socket.id, list);
});
function prosesInput(pemain, tick) {
const inputs = (buffer.get(pemain.id) || [])
.filter((i) => i.tick <= tick);
for (const input of inputs) {
terapkan(input);
}
buffer.delete(pemain.id);
}Input yang membawa nomor tick memungkinkan server mengurutkannya dengan benar. Validasi tetap di server: kecepatan maksimal, posisi yang masuk akal, dan aksi yang diizinkan per state game.
Tanpa prediksi, karakter pemain terasa meluncur dengan jeda satu latensi. Client prediction menjalankan gerakan di klien lebih dulu, lalu menyesuaikan saat state server tiba.
function inputKiri() {
// jalankan lokal untuk respons instan
pemainLokal.x -= KECEPATAN / 60;
kirimKeServer("input:kiri");
}Klien menggerakkan karakter seketika untuk memberi respons, lalu server mengirimkan state resmi yang menjadi pembanding. Jika state server berbeda dari prediksi, klien mengoreksi — biasanya tanpa terasa karena perbedaannya kecil.
Untuk karakter pemain lain, jangan lompat ke posisi baru — interpolasi di antara dua snapshot membuat gerakan mulus. Dead reckoning mengekstrapolasi posisi dari kecepatan terakhir sehingga lawan tetap terlihat bergerak saat paket hilang sesaat.
Pemain tidak perlu melihat seluruh peta.
function tetangga(pemain) {
return pemainList.filter((p) => {
const dx = p.x - pemain.x;
const dy = p.y - pemain.y;
return dx * dx + dy * dy < 40000; // radius 200 unit
});
}tetangga(pemain) membatasi broadcast berdasarkan jarak — pola Area of Interest (AOI). Pemain di sisi kiri peta tidak menerima update dari pemain di sisi kanan, mengurangi beban jaringan secara drastis.
Tidak semua update setara: posisi pemain yang berduel lebih penting daripada musuh jauh. Beri prioritas per update dan kirim yang paling mendesak saat bandwidth terbatas.
Episode 23 menunjukkan rahasia game multiplayer yang terasa mulus: server otoritatif menegakkan aturan, tick membagi waktu, delta dan AOI menghemat jaringan, serta prediksi dan interpolasi menyembunyikan latensi.
Inti yang harus dibawa pulang:
Di episode 24 berikutnya kita membahas file transfer over WebSocket: transfer ber-chunk, binary data, progress tracking, serta optimasi ukuran chunk dan kompresi.