Skip to content

Tarayıcıda açık dünya oluşturmak, bölüm 15: Temeli değiştir, sonra senkronize et

Yazan: Oleg Sidorkin, Cinevva CTO'su ve Kurucu Ortağı

Burada yeni misiniz? Seri rehberine göz atın. Rehber, denemenin ne olduğunu açıklıyor ve tüm bölümlere bağlantı veriyor.

İlk on dört bölüm, 1'den 30'a kadar olan denemeleri ele aldı. Bu süreç, gerçek zamanlı şekillendirilebilen bir arazi sistemi ile üzerinde yürüyebilen, süzülebilen ve düşebilen bir karakterle sona erdi. Bu bölüm, seriye 31. denemeden devam ediyor ve vermemiz gereken ilk karar teknik değildi. Tüm bu deneme koduyla ne yapacağımıza karar vermemiz gerekiyordu.

"Geri taşımak yerine değiştir" kararı

Her biri ayrı bir kavramı doğrulayan 30 bağımsız HTML dosyamız vardı ama bunlar arasında hiçbir entegrasyon yoktu. Üretimdeki world/client/ hâlâ eski teknoloji yığınını kullanıyordu: WebGL, basit bir yükseklik haritası, 75 satırlık bir karakter denetleyicisi ve dokuz mesaj türüne sahip bir MessagePack protokolü. Düzenleme, WebGPU, materyal veya bitki örtüsü yoktu.

Bariz plan, deneme sonuçlarını üretim kod tabanına tek tek geri taşımaktı. Bu planı çöpe attık. 30. deneme; arazi, fizik, materyaller, bitki örtüsü ve kamera açısından world/client/ sisteminin ulaştığı her şeyden zaten daha iyiydi. Bunları eski WebGL koduna geri taşımak, bütün süreç boyunca o kodla mücadele etmek anlamına gelecekti. Bu nedenle dünya uygulamasını en başarılı denemeyle değiştirmeye ve buradan ilerlemeye karar verdik. 30. deneme yeni temel oldu ve world/client/ artık kullanılmayan koda dönüştü.

Bu karar, kalan çalışmayı yeniden çerçeveledi. "Harika bir tek oyunculu teknoloji demosundan" "ürüne" geçmek için çok oyunculu desteğe, kalıcılığa, sonsuz dünya akışına ve nesne yerleştirmeye ihtiyacımız vardı. İlk sıraya çok oyunculu arazi senkronizasyonunu koyduk çünkü mimariyi şekillendiren konu buydu. Yanıtladığı soruyu sormak kolay, yanlış yanıtlamanın bedeliyse yüksekti: Oyuncu A araziyi şekillendirdiğinde ağ üzerinden gerçekte ne gönderilecek?

Piksel senkronizasyonu değil, fırça parametrelerini yeniden oynatma

31. denemeyi yeni sekmede açın ↗ · Kaynak kodunu görüntüleyin

Tek bir satır ağ kodu yazmadan önce, bir fırça darbesinin tam olarak ne yaptığını izledik. Yükseklik haritası fırçası, CPU üzerindeki bir Float32Array içinde imlecin çevresindeki yarıçapı dolaşıyor, yumuşak geçişli bir düşüş uyguluyor ve ardından yükseltiyor, alçaltıyor, yumuşatıyor ya da düzleştiriyor. SDF fırçası da aynı işlemi 3B ortamda küresel bir voksel alanı üzerinde yapıyor. Her iki yol da saf CPU dizi matematiğinden oluşuyor. Döngüde GPU hesaplaması, rastlantısallık veya deterministik olmayan kayan nokta işlemi yok. Aynı girdi dizisi ve aynı parametreler, her makinede aynı çıktıyı üretiyor.

İşin püf noktası bu. Düzenlenmiş araziyi göndermiyoruz. Her fırça darbesi adımında 56 baytlık fırça parametrelerini gönderiyoruz ve her istemci aynı deterministik fonksiyonu yeniden çalıştırıyor. Senkronizasyon protokolünde dört mesaj türü var: eş keşfi, {op, wx, wy, wz, radius, strength, flattenTarget} biçimindeki fırça mesajı ve 20Hz frekansındaki oyuncu konumu mesajı.

Denemede sunucuyu tamamen atlayıp tarayıcının aynı kaynaktan açılan sekmeler arasında mesajlaşmayı sağlayan BroadcastChannel API'sini kullandık. İki sekme açtığınızda sıfır altyapıyla birbirleriyle iletişim kuruyorlar. Böylece senkronizasyon sorununu gecikme, kimlik doğrulama ve Durable Object bağlantılarından ayırdık. Parametreleri yeniden oynatma sekmeler arasında yakınsıyorsa WebSocket üzerinden de yakınsar.

Yeniden oynatmanın ayrışabileceği tek yer, sıraya bağlı işlemlerdir. Yükseltme ve alçaltma değişmeli işlemlerdir; dolayısıyla val + strength * falloff, önce kimin uyguladığından bağımsız olarak aynı sonuca ulaşır. Yumuşatma ve düzleştirme komşu değerleri okur. Bu yüzden iki istemci aynı anda tam olarak aynı noktayı yumuşatırsa her adımda milimetrenin küçük kesirleri kadar sapma oluşabilir. Pratikte bu durum hiç yaşanmıyor ve üretimdeki çözüm de şimdiden belli: Düzenlemeleri DO üzerinden yönlendirmek, monotonik bir sıra numarası atamasını sağlamak, istemcide iyimser biçimde uygulamak ve yetkili sıra farklıysa sıralamayı düzeltmek. Bu, klasik iyimser eşzamanlılıktır ve DO zaten doğal bir serileştirme noktasıdır.

Sürekli kaybolan eş kapsülü

Düzenlemeler ilk denemede senkronize oldu. Uzak oyuncunun kapsülü ise olmadı. Diğer sekmede görünüp kayboluyordu ve kalıcı hâle gelmesi için üç ayrı hatayı düzeltmemiz gerekti.

Kapsül dünya orijininde oluşturuluyordu; burası arazinin altında kalıyordu çünkü join mesajı herhangi bir konum verisinden önce ulaşıyordu. Çözüm: Başlangıçta gizlemek ve ilk konum güncellemesinde göstermek. Konum yayını render döngüsünün içindeydi ve Chrome, odakta olmayan sekmelerde requestAnimationFrame çağrılarını yavaşlatıyordu. Bu nedenle diğer sekmenin güncellik kontrolü eşi kaldırıyor, sonraki mesaj da onu yeniden oluşturuyordu. Çözüm: Yayını, görünür sekmelerde yavaşlatılmayan bir setInterval içine taşımak. Ayrıca güncellik zaman aşımı fazla agresif bir değer olan 5 saniyeydi ve herhangi bir GC duraklamasında tetikleniyordu. Çözüm: Süreyi 30 saniyeye çıkarmak ve normal kapanışlarda temiz leave mesajına güvenmek.

Kalıcılık ve sonradan katılım, aynı format

Kalıcılığı yeni bir denemeye ayırmak yerine aynı denemeye dâhil ettik çünkü hedef ister IndexedDB ister başka bir sekme olsun, serileştirme formatı aynıydı. Bir anlık görüntü; tam yükseklik haritasından (129×129 boyutunda bir Float32Array, yaklaşık 66 KB), yalnızca düzenlenmiş SDF parçalarından (her biri 653, yaklaşık 1,1 MB) ve marching-cubes moduna kilitlenmiş parça kimliklerinin listesinden oluşuyor. Gecikmeli kaydetme işlemi, son düzenlemeden iki saniye sonra IndexedDB'ye yazıyor. Yükleme sırasında prosedürel arazi eşzamanlı olarak oluşturuluyor, ardından kaydedilmiş durum ilk anlamlı kareden önce onun üzerine yazılıyor. Sonradan katılım da tamamen aynı baytları yeniden kullanıyor: Yeni bir sekme katıldığında, düzenlemelere sahip mevcut bir sekme durumunu serileştirip yeni gelene özel olarak gönderiyor ve beş dakika önce şekillendirdiğiniz dağ onun ekranında beliriyor.

İlk kalıcılık testi güzel bir sıralama hatasını ortaya çıkardı. Çimler başlangıçta prosedürel yükseklikler kullanılarak eşzamanlı biçimde dağıtılıyor ancak IndexedDB'den geri yükleme eşzamansız gerçekleşip daha sonra yükseklik haritasının üzerine yazıyor. Sonuç olarak her çim yaprağı havada kalıyor ya da zemine gömülüyordu. Çözüm, her örneğin altındaki yüksekliği yeniden örnekleyen ve artık uygun olmayan bir eğimde ya da rakımda bulunan yaprakları gizleyen bir refreshAllGrass() geçişidir. Aynı fonksiyon hem yüklemeye hem de sonradan katılıma hizmet ediyor.

Eğim macerası

Şekillendirilmiş arazi, pürüzsüz prosedürel temelden daha engebeliydi ve eski arazinin asla ortaya çıkaramadığı üç fizik hatasını açığa çıkardı. Doğrudan yokuş yukarı yürümek kapsülün yana kaymasına neden oluyordu. Bunun nedeni, hareketi zemine teğet tutmayı amaçlayan ancak normalin yalnızca yatay bileşenleri kullanılarak yazılmış bir hız izdüşümüydü. Normali (0.3,0.9,0.3) olan çapraz bir eğimde kuzeye yürümek, yoktan yere yanal hız oluşturuyordu. Zemine oturtma zaten oyuncuyu yüzeyde tuttuğu için izdüşüm işlemini tamamen sildik.

Kayma, ikinci bir kaynaktan dolayı devam etti. SDF çarpışma sondaları, gövdeyi nüfuz derinliği kadar gradyan boyunca dışarı iter. Herhangi bir eğimde gradyanın yatay bileşenleri vardır. Dolayısıyla 15°'lik bir eğimde 0,1 m'lik nüfuz, her adımda yaklaşık 0,026 m'lik yanal itiş oluşturur; 120Hz'de bu, yaklaşık 3 m/s'lik görünmez bir kaymaya karşılık gelir. Çözüm: Tepkiyi eğime göre bölmek. Yürünebilir zeminde (grady değeri yürüme eşiğinin üzerindeyken) itiş yalnızca dikeydir. Duvarlarda ve uçurumlarda tam 3B itiş korunur çünkü yönünüzün saptırılmasını istediğiniz yerler tam olarak buralardır. Ayrıca fırçayla oluşturulan tepelerin Breath of the Wild'daki gibi tırmanılabilir hissettirmesi için yürünebilir sınırı cos45° değerinden cos60° değerine çıkardık.

Üçüncü hata, kapsülün parça sınırlarında donmasına neden oluyordu çünkü çarpışma sondaları tek bir parçanın SDF'sini örnekliyor ve bir sonda komşu parçaya geçtiğinde "havanın derinlerinde" gözcü değerini alıyordu. Çözüm, herhangi bir dünya konumu için doğru parçayı bulan ve SDF'nin bulunmadığı yerlerde yükseklik haritası mesafe tahminine geri dönen sdfSampleWorld(wx, wy, wz) ile sdfGradientWorld(...) fonksiyonlarıydı. SDF'den yükseklik haritası çarpışmasına geçiş artık kesintisiz.

Su dünyayı tamamlıyor

32. denemeyi yeni sekmede açın ↗ · Kaynak kodunu görüntüleyin

Buraya kadarki her deneme "suyun üzerindeki kara" üzerineydi. 32. deneme bir okyanus ve onunla birlikte yeni bir hareket biçimi ekledi. Yaklaşık 8 ile 58 arasında değişen arazide su seviyesini 22 olarak belirledik. Böylece alçak vadiler suyla doldu, kıyı çizgisinde plajlar oluştu ve oyun için bol miktarda kuru alan kaldı.

Yüzey, araziyle aynı düğüm yaklaşımını kullanan ve TSL ile oluşturulmuş bir MeshStandardNodeMaterial materyalidir. Farklı frekanslardaki üç örtüşen sinüs dalgası köşeleri yerinden oynatıyor ve yüzey normali, örgü normallerinden değil bu dalgaların analitik kosinüs türevlerinden elde ediliyor. Renk, [0,1] aralığına sıkıştırılmış (levely+wave)×0.12 derinlik tahminini kullanarak turkuaz sığlıklardan koyu deniz mavisi derinliklere geçiyor. Bu tahmin kıyı çizgisinde sıfıra yaklaşırken köpük yükseliyor ve alfa derinliği takip ediyor; böylece sığ su şeffaf, derin su ise neredeyse opak görünüyor.

Yüzme, kaldırma kuvveti sağlayan bir yaydır. Ayaklar su seviyesinin altına düştüğünde ve gövdenin merkezi yüzeye kapsül yarı yüksekliği kadar yakın olduğunda oyuncu yüzme moduna giriyor. Bir yay, gövdeyi yüzeyin hemen altındaki hedefe doğru çekiyor. Su sönümlemesi 4'e karşı kaldırma sabiti 12 olduğunda oyuncu, salınım yapmadan ve başı suyun dışında kalacak biçimde dengeli şekilde yüzüyor. Yüzme hızı yürümekten daha düşük; ivmelenme ve sürüklenme daha akışkan. Yüzeye yakınken zıplamak oyuncuyu normal zıplama hızının %60'ıyla sudan dışarı fırlatıyor, suya girişteyse aşağı yönlü hız -5 m/s ile sınırlandırılarak oyuncunun dibe çakılması önleniyor. Arazi çarpışması su altında da çalışmaya devam ediyor; bu nedenle göl yatağının yüzme hedefinin üzerine çıktığı yerlerde yürüyebilirsiniz. Yüzme bayrağı konum yayınına da ekleniyor, böylece diğer oyuncular yüzdüğünüzü görebiliyor. Kamera yüzeyin altına indiğinde bir HTML gradyan katmanı görüntüyü renklendiriyor.

Bu bölümde kullanılan teknolojiler

Deterministik fırça parametresi yeniden oynatması. Her istemci, düzenlenmiş araziyi aktarmak yerine yalnızca fırça parametrelerini gönderiyor ve aynı CPU fonksiyonunu yeniden çalıştırıyor. Bu mümkün çünkü hem yükseklik haritası hem de SDF fırçaları, rastlantısallık ya da GPU kaynaklı deterministik olmayan davranış içermeyen saf Float32Array matematiğine dayanıyor. Böylece aynı girdiler her yerde bit düzeyinde aynı çıktıları üretiyor. Yük, her fırça darbesi adımında 56 bayt. Değişmeli işlemler (yükseltme, alçaltma) sıradan bağımsız olarak yakınsarken komşuları okuyan işlemlerin (yumuşatma, düzleştirme) yakınsamayı garanti etmek için bir serileştirme noktasına ihtiyacı vardır. Üretimdeki Durable Object bunu monotonik sıra numaralarıyla sağlar.

WebSocket yerine BroadcastChannel. Sıfır sunucuyla, aynı kaynaktan açılmış sekmeler arasında mesajlaşmayı sağlayan bir tarayıcı API'si. Burada senkronizasyon protokolünü ağ gecikmesi ve kimlik doğrulamadan bağımsız olarak test etmek için kullanıldı. Serileştirme formatı (ham Float32Array yükseklik haritası, düzenlenmiş SDF parçaları ve MC kilitli parça kimlikleri), IndexedDB kalıcılığı ve sonradan katılım durum aktarımında kullanılan baytlarla aynıdır. Böylece tek format üç işi birden kapsar.

Eğime göre bölünmüş SDF çarpışma tepkisi. Bir kapsül sondası hacimsel araziye nüfuz ettiğinde en basit çözüm, gövdeyi nüfuz derinliği kadar SDF gradyanı boyunca dışarı itmektir. Eğimlerde bu gradyanın yatay bileşenleri bulunur ve bu da yanal kayma oluşturur. Tepkiyi, yürünebilir yüzeylerin (grady eşik değerinin üzerindeyken) yalnızca dikey bir itiş alacağı, dik yüzeylerinse tam 3B itişi ve hız izdüşümünü koruyacağı şekilde bölmek; duvar çarpışmasını kaybetmeden kaymayı ortadan kaldırır. Bkz. SDF arazi çarpışmaları.

Analitik dalga normallerine sahip TSL suyu. Okyanus, köşeleri üst üste eklenen üç sinüs dalgasıyla yerinden oynatılan bir düğüm materyalidir. Yüzey normali, yer değiştirmeden sonra örgü normallerini yeniden hesaplamak yerine dalga fonksiyonlarının kosinüs türevlerinden analitik olarak elde edilir. Bu yöntem daha ucuzdur ve kaba bir ızgarada sonlu fark normallerinin oluşturduğu bozulmaları önler. Derinliğe bağlı renk, kıyı köpüğü ve derinliğe bağlı şeffaflık aynı derinlik tahminini temel alır.

Kaldırma yaylı yüzme. Yüzme fiziği, gövdeyi yüzeyin hemen altındaki bir hedefe çeken sönümlü bir yay olarak modeller. Kaldırma sabiti 12 ve sönümleme 4 olduğunda oyuncu salınım yapmadan yüzeyde dengelenir. Farklı hareket sabitleri (daha düşük hız, akışkan ivmelenme ve güçlü sürüklenme) yüzmeye yürümekten farklı bir his kazandırır; mevcut kapsül-arazi çarpışması da su altında çalışmayı sürdürür.


29 bölümün 15.'si. Önceki: Bölüm 14 - Dünya canlanıyor Sonraki: Bölüm 16 - Büyümeye devam eden bir dünya için yapı Seri rehberi: /tr/blog/2026-02-25-open-world-browser-series-guide