Oyun6 Dakika Okuma
Dungeon Mates: Dört Kişilik Tarayıcı Zindanı
Dungeon Mates, oda koduyla dört kişi birlikte oynanan, her katı yeniden üretilen bir zindan oyunu. Oyunda ne var, sunucu neden her şeyi kendisi hesaplıyor ve haritaları hem rastgele hem oynanabilir yapmak için ne yaptım.

Bu Yazıda7 Bölüm
Dungeon Mates, tarayıcıda oynanan piksel grafikli bir zindan oyunu. Bir oda açıyorsun, dört haneli kodu arkadaşlarına veriyorsun, herkes sınıfını seçiyor ve birlikte on katlık bir zindana iniyorsunuz. Kurulum yok, indirme yok; tek başına da oynanabiliyor. Her kat o an üretiliyor, yani aynı zindanı iki kez görmüyorsun.
Arkadaşlarımla akşam bir saat oynayabileceğimiz, kimsenin bir şey kurmasını gerektirmeyen bir oyun istiyordum. Bir de merak ettiğim bir şey vardı: hiç hazır sprite ve ses dosyası kullanmadan, her şeyi kodla çizip kodla sentezleyerek bir oyun ne kadar iyi olabilir? Oyundaki bütün karakterler, canavarlar ve efektler piksel piksel Canvas ile çiziliyor; seslerin hiçbiri dosya değil, Web Audio ile o an üretiliyor.
Oyunda Neler Var
Dört Sınıf
Savaşçı yakın mesafede dayanıklı, Büyücü alan hasarı veriyor, Okçu uzaktan vuruyor, Şifacı takımı ayakta tutuyor. Her sınıfın kendi yeteneği ve kendi saldırı sesi var.
On Kat
Her kat bir öncekinden zor. Üçüncü, beşinci, yedinci ve sekizinci katlarda birer boss, onuncu katta Demon bekliyor. Kata göre değişen kurallar, güçlendirilmiş canavarlar, yetenek seçimi ve bir dükkân her turu bir öncekinden ayırıyor.
Dokuz Canavar
Fare sürüyle gelir, hayalet duvardan geçer, örümcek ağ atıp yavaşlatır, goblin canı azalınca kaçar. Her birinin kendi yapay zekâsı ve kendi davranışı var.
Birlikte Oynamak
Oda, sohbet, minimap ve sandıklar. Zorluk oyuncu sayısına göre ölçekleniyor; dört kişiyle canavarlar daha kalabalık ve daha dayanıklı, tek başına daha çok loot düşüyor.
Oyunun bir de hikâyesi var: yerin yedi kat altında kurulmuş Zephara şehri, kendi halkını kurtarmak isterken onu lanetleyen kral ve en altta onu bekleyen son savaş. Hikâye oyunun içinde satır satır anlatılmıyor; katlar, bosslar ve dükkândaki eşyalar ona göre adlandırılmış.
Nasıl Çalışıyor
Arayüz Next.js, oyun ise bir Node sunucusunda çalışıyor ve ikisi Socket.io ile konuşuyor. Bütün oyun mantığı sunucuda: canavarların yapay zekâsı, çarpışmalar, hasar, loot ve harita üretimi saniyede yirmi kez işleniyor. Tarayıcı yalnızca tuşları gönderiyor, gelen durumu Canvas’a çiziyor. Bu hem hileyi zorlaştırıyor hem de dört oyuncunun aynı dünyayı görmesini garanti ediyor.
Çizim tarafı mantıksal olarak 480×270 piksellik küçük bir ekrana yapılıyor ve sonra büyütülüyor; piksel grafiğin keskinliği oradan geliyor. Zemin, dekor, loot, canavarlar, mermiler, oyuncular, parçacıklar ve görüş sisi ayrı katmanlarda çiziliyor. Cihaz yavaş kalırsa oyun kare hızını izleyip kaliteyi kendisi düşürüyor. Telefonda ekranda bir joystick ve düğmeler var.
Bu yazının geri kalanı oyunun en çok uğraştıran parçası olan harita üretimine ayrılıyor; çünkü rastgele üretimin sorunu şu: çoğu zaman çalışıyor.
Haritayı Nasıl Üretiyorum
Akla ilk gelen yol, rastgele yerlere rastgele boyutta odalar koyup sonra hepsini birbirine bağlamak. Bunun iki sorunu var. Odalar üst üste biner; çakışma kontrolü eklersen bu kez bazı odalar hiç yerleşemez. Odalar arasındaki bağlantılar da kopuk kalabilir: iki oda grubu oluşur ve birinden ötekine geçilemez.
Ben alanı ikiye bölerek ilerleyen yöntemi kullandım (BSP). Haritayı ikiye böl, her yarıyı yine ikiye böl, en alttaki parçalara birer oda koy. Odalar ayrı parçaların içinde olduğu için çakışamıyor. Bağlarken de ağacın kendisini izliyorsun: sol daldaki bir oda sağ daldaki bir odayla birleşiyor, köke çıkınca bütün harita bağlanmış oluyor. Her odaya ulaşılabilmesi yapının kendisinden geliyor.
if (depth > 5) return;
const canSplitH = node.height >= this.minBspSize * 2;
const canSplitV = node.width >= this.minBspSize * 2;
if (!canSplitH && !canSplitV) return;
// Uzun kenarı böl: yoksa içine oda sığmayan ince parçalar çıkıyor.
let splitHorizontally: boolean;
if (canSplitH && canSplitV) {
splitHorizontally = node.height > node.width ? true
: node.width > node.height ? false
: Math.random() > 0.5;
} else {
splitHorizontally = canSplitH;
}Bir parçanın en küçük boyu, en büyük oda artı dört kare. Bir parçaya en büyük oda bile konsa kenarlarda koridorların geçebileceği ikişer karelik pay kalıyor; bu pay olmasa odalar parçanın kenarına yapışır ve koridorlar duvarların içinden geçmek zorunda kalırdı. Koridorlar L biçiminde ve iki kare genişliğinde; hangi kolun önce oyulacağına yazı tura karar veriyor, böylece aynı iki oda arasında iki farklı yol çıkabiliyor.
Oyuncu Sayısı Haritayı Değiştiriyor
Tek kişi için büyük bir harita fazla; dakikalarca boş koridorda yürürsün. Dört kişi için küçük bir harita da fazla; herkes birbirinin üstüne düşer. Bu yüzden harita boyutu oyuncu sayısına bağlı.
| Oyuncu | Harita | Oda Boyutu |
|---|---|---|
| 1 | 48 × 48 | 6-11 |
| 2 | 56 × 56 | 7-12 |
| 3 | 64 × 64 | 7-13 |
| 4 | 72 × 72 | 8-14 |
Kat numarası ikinci bir ayar: yukarı çıktıkça oda sayısı, canavarların canı ve saldırı gücü artıyor. Bu iki ayarın birbirini ezebildiğini bir denge hatasıyla öğrendim. Oyuncu sayısına göre ölçekleyen fonksiyon kat ölçeğini siliyordu; dört kişilik oyunda canavarlar birinci kat seviyesinde kalmış, tek kişilik oyun ise iki kat zorlaşmıştı.
Hiçbir Odaya Ait Olmayan Boşluklar
Bazı katlarda tuhaf bir şey oluyordu: üzerinde yürünebilen ama içinde canavar doğmayan, oraya basınca hiçbir şey tetiklenmeyen boşluklar. Harita üretilmişti, odalar vardı, oyun çalışıyordu. Sebep şuydu: ağaç bazen katın istediğinden fazla oda üretiyor ve ben fazla odaları listeden çıkarıyordum. Ama odalar listeye girmeden önce haritaya oyulmuştu.
// Fazla odaların zemini zaten oyulmuştu; geri duvara çevir.
// Yoksa hiçbir odaya ait olmayan, canavarsız boşluklar kalıyor.
if (this.rooms.length > targetMax) {
for (let i = targetMax; i < this.rooms.length; i++) {
this.uncarveRoom(this.rooms[i]);
}
this.rooms = this.rooms.slice(0, targetMax);
}Diziden silmek odayı oyunun mantığından çıkarıyordu ama zeminden çıkarmıyordu. Oyuncu oraya yürüyebiliyor, oyun ise orayı tanımıyordu. Açılan alanı yeniden duvara çeviren küçük bir fonksiyon yazmam gerekti.
Haritayı Kim Görüyor
Harita sunucuda üretiliyor, çünkü canavarlar ve dövüş zaten orada. Yalnızca bir tohum paylaşıp her tarayıcının aynı haritayı kendisinin üretmesi cazip görünüyor; ağ trafiği neredeyse sıfıra iner. Ama JavaScript’in rastgele sayı üreteci tohum almıyor ve iki tarafın birebir aynı davrandığından emin olmak ayrı bir iş.
Burada bir itirafım var. Sunucu her katın başında haritanın tamamını her oyuncuya bir kez gönderiyor. Görüş sisi, yani görülmemiş, keşfedilmiş ve görünür ayrımı yalnızca çizimde uygulanıyor. Meraklı bir oyuncu tarayıcının ağ sekmesini açıp boss odasının nerede olduğunu görebilir. Co-op bir oyunda kandırılacak bir rakip yok; şimdilik sorun değil.
Sunucu haritayı üretiyor ama oyuncudan saklamıyor. Keşfetme hissi tamamen çizimden geliyor.
Geriye Dönüp Bakınca
Bugün başlasam üretimden hemen sonra bir kontrol adımı koyardım: her yürünebilir karenin bir odaya ya da koridora ait olduğunu ve her odadan merdivene ulaşılabildiğini kontrol eden basit bir dolaşma. Ağaç yapısı odaların bağlı olmasını garanti ediyor ama sahipsiz boşluklar bu garantinin kodun tamamını kapsamadığını gösterdi.
Emin olmadığım bir karar da harita boyutunun oyuncu sayısına bağlanması. Tek kişilik oyun ile dört kişilik oyun artık farklı haritalarda oynanıyor ve ikisinin aynı oyun olması gerekip gerekmediğini hâlâ bilmiyorum. Bir gün rekabetçi bir mod gelirse haritanın tamamını göndermeyi de yeniden düşünmem gerekecek.