Çok oyunculu 3B web oyunu
MoonLite
MoonLite ücretsiz, çok oyunculu, 3B bir "gece parkı": yabancılar ortak bir zaman çizelgesinde birlikte müzik yapar, açık havada film izler, go-kart yarışır, bowling ve satranç oynar. Netcode'dan ses motoruna, sunuculardan hukuki metinlere kadar hepsini tek başıma geliştirdim ve ilk commit'ten halka açık yayına yaklaşık üç haftada çıkardım.
MoonLite'a Türkiye'den erişim şu an kısıtlı; site ve oyun diğer ülkelerden normal açılıyor.
Problem
Kalabalık 3B odalar genelde aynı şekilde çöker: bir oyuncu hareket eder, sunucu herkese söyler, herkes hareket eder, sunucu yine herkese söyler. Olay başına dağıtım, oda büyüklüğünün karesiyle büyür.
Daha kötüsü, kalabalığın trafiği asla aksamaması gereken tek şeyle aynı kanalı paylaşır: müzik. Bir emoji fırtınası bir mikser komutunu geciktirebiliyorsa ürün bozuktur. Üstelik hepsinin bağımsız bir stüdyonun karşılayabileceği donanımda çalışması gerekiyordu.
Önemli kararlar
- 01
Asla aynı kuyruğu paylaşmayan üç trafik düzlemi
Kaybolmaması gereken durum için güvenilir bir kontrol düzlemi, kayıplı ikili bir konum düzlemi (8 baytlık konum paketleri, 15 Hz sunucu tick'i, ilgi alanı ayıklama ve delta snapshot'lar) ve toplu gönderilen bir tepki düzlemi. Sıkışma anında kayıplı trafik düşürülür, kontrol trafiği asla. Sunucunun olay başına işi, istemcilerin ne kadar yüklendiğine değil tick hızına bağlı.
- 02
Ağ üzerinden hiç kare göndermeyen mini oyunlar
On iki etkinliğin tamamı (satranç, bilardo, bowling, pinball, go-kart, lunapark araçları, senkron sinema...) tarayıcının ve sunucunun paylaştığı saf, determinist step(durum, girdiler) fonksiyonları. Ağda sadece zaman damgalı girdiler gidiyor; sonradan katılan birinin tüm durumu 54 bayta kadar iniyor. Ek kazanç: simülasyonlar bir kütüphane gibi test ediliyor ve testler gerçek hataları yakaladı.
- 03
Medya baytları sunucuya hiç uğramıyor
Ses kanalları içerik adresli (SHA-256): önce yerel IndexedDB önbelleğinden, sonra çıkış ücreti olmayan Cloudflare R2'den geliyor. Ortak bir transport saati tüm istemcilerde çalmayı hizalı tutuyor. Bunun için önce tam bir WebRTC eşler arası katman yazdım, sonra sildim: mobil ağlarda NAT aşma nadiren çalışıyordu, oyuncuların IP adreslerini sızdırıyordu ve R2 zaten maliyeti sıfırlamıştı.
- 04
Sıfırdan bir tarayıcı ses motoru
Ableton tarzı otomatik warp'lı çok kanallı mikser, Web Worker'larda perde koruyan WSOLA zaman esnetme, efekt hatlı bir AudioWorklet wavetable synth ve dalga formu klipleri, piyano rulosu ve ortak düzenleme içeren beat ızgaralı bir DAW.
- 05
Üretim, güvenlik ve operasyon uçtan uca
Cloudflare Pages ve ~68 rotalı bir Worker API (D1, KV, R2), drain tabanlı kademeli dağıtım ve istemci otomatik güncellemesi olan çoklu VPS relay filosu, Google OAuth, CSP/HSTS, Cloudflare Access arkasında denetim kayıtlı yönetim paneli ve hibrit post-kuantum şemayla (X25519 + ML-KEM-768) uçtan uca şifreli mesajlaşma. Tehdit modelini ve EN/TR GDPR–KVKK hukuki metinlerini de ben yazdım.
Sonuçlar
- 480
- eşzamanlı oyuncu, aylık 4 €'luk sunucuda
- ~2 ms
- tam yükte en kötü tick (bütçe: 66 ms)
- 0
- yük testlerinde kopan bağlantı
- ~53k
- satır strict TypeScript, tek başıma
- 418
- otomatik test
- 900 → 12
- sahne toplulaştırmasıyla draw call
Ekran görüntüleri
Öğrendiklerim
- Hangi trafiğin düşürülebileceğine, hangisinin asla düşürülemeyeceğine karar ver ve onlara ayrı borular ver.
- Simülasyonu saf bir fonksiyona koy ve bir kütüphane gibi test et. Netcode hatalarının çoğu, üstüne gecikme binmiş simülasyon hatalarıdır.
- Daha büyük bir sunucu almadan önce gerçek makinede, gerçek yoldan ölç. Benim "ölçek" sorunum bir oda limiti ayarı çıktı.
- En gurur duyduğun parçayı silmeye hazır ol. P2P katmanını kaldırmak ürünü daha iyi yaptı.