Her oyun geliştirme eğitimi aynı şekilde başlar: gri bir tuval üzerinde kayan renkli dikdörtgenler. Bu, söz dizimini (syntax) öğrenmek için iyidir ancak bir oyun motorunun gerçekte nasıl nefes aldığına dair size hiçbir şey öğretmez. Ben dikdörtgen işinden çıkmak istiyordum. Hareket, dövüş, bir HUD ve ses içeren, gerçek bir oyun gibi hissettiren üstten görünümlü (top-down) bir zindan gezgini (dungeon crawler) inşa etmek istiyordum.
Phaser v4 kullanmaya karar verdim ve kendime katı bir kural koydum: Sıfır harici varlık (asset). Resim dosyası yok, ses klibi yok ve Webpack veya Vite gibi derleme araçları yok. Tüm proje, düz JavaScript ile yazılmış tek bir HTML dosyasının içinde yaşamak zorundaydı. Bu kısıtlama, sırf minimalizm olsun diye yapılmamıştı. Her türlü mazereti ve her türlü "kara kutuyu" ortadan kaldırmakla ilgiliydi. Bilginizdeki bir boşluğu kapatmak için bir sprite paketi indiremediğinizde, motorun kaputun altında dokuları (textures), animasyonları, sesi ve durumu (state) nasıl yönettiğini öğrenmeye zorlanırsınız.
Dünyayı Kodla Çizmek
Normal bir Phaser projesinde, bir preload fonksiyonu içinde this.load.image() çağırırsınız ve motoru bir PNG dosyasına yönlendirirsiniz. Bu seçenek olmadan, Graphics nesnesine yönelirsiniz. Onu örneklendirirsiniz (instantiate), temel şekiller çizersiniz —zemin karoları için dikdörtgenler, duvarlar için daha kalın çizgiler, belki bir oyuncu simgesi için bir daire— ve ardından generateTexture çağrısını yaparsınız. Bu yöntem, grafik tamponunu (buffer) yakalar ve seçtiğiniz bir anahtar (key) altında Phaser'ın doku yöneticisine (texture manager) kaydeder.
O andan itibaren motor, oluşturulan bu bit eşlemi (bitmap) tıpkı yüklenmiş bir resim dosyası gibi işler. Onu tilemap'lere atayabilir, sprite'lara bölebilir veya renklendirebilirsiniz (tint). Zindan gezgini için bu, kod editörümden hiç ayrılmadan prosedürel olarak bir zemin ızgarası oluşturabileceğim, duvar segmentlerini yerleştirebileceğim ve renk paleti üzerinde yinelemeler yapabileceğim anlamına geliyordu. Buradaki pratik ders, bir dokunun (texture) bellekte duran bir bit eşlem veri yığınından ibaret olduğudur. Phaser, bunun bir HTTP isteğiyle mi yoksa elle yazılmış bir Graphics çağrısıyla mı geldiğini umursamaz.
Bu yaklaşım ayrıca sizi çizim sırası (draw order) ve gruplandırma (batching) üzerine bilinçli düşünmeye iter. Her duvar ve zemin karosu aynı üretilmiş doku ailesinden geldiğinde, Phaser'ın render çağrılarını nasıl gruplandırdığına dikkat etmeye başlarsınız. Statik bir tilemap katmanı ile tekil nesnelerden oluşan bir spritemap arasındaki farkı fark edersiniz; çünkü hangisinin bir doku örneği (texture instance) olarak var olmayı hak ettiğine manuel olarak siz karar veriyorsunuzdur.
Spritesheet Olmadan Animasyon Yapmak
Statik kareler çabuk sıkıcılaşır, ancak Phaser'ın animasyon sistemi bir spritesheet —genellikle kareler bir ızgara düzeninde yerleştirilmiş tek bir PNG— bekler. Bu şeridi, ekran dışı (offscreen) bir HTML canvas öğesi kullanarak taklit ettim. Bir animasyonun her karesi için canvas'ı temizledim ve yeni bir poz çizdim: basit bir kılıç savurma, iki adımlık bir yürüme döngüsü veya bir düşmanın bekleme (idle) hareketi. Şerit tamamlandığında, motorun bir pozun nerede bittiğini ve bir sonrakinin nerede başladığını bilmesi için kare genişliğini ve yüksekliğini tanımlayarak onu Phaser'a bir spritesheet olarak kaydettim.
Bunu manuel olarak yapmak, bir animasyonun yüzeyin altında tam olarak ne olduğunu ortaya çıkarır: paylaşılan bir doku üzerindeki kare sınırları kümesi. Phaser'ın animasyon bileşeni bir başlangıç karesi, bir bitiş karesi ve bir kare hızı (frame rate) ister. Ardından, oyun saatinin her tıkında (tick) bu dikdörtgen dilimler arasında bir işaretçiyi (pointer) ilerletir. Spritesheet'leri sanatçılar tarafından üretilen sihirli varlıklar olarak görmeyi bırakır ve onları koordinat matematiği olarak görmeye başlarsınız. Bu bakış açısı, daha sonra gerçek bir sanatı dilimlerken veya bir animasyonun neden kareleri sırasız oynattığını hata ayıklarken (debug) paha biçilemezdir.
Hiçlikten Gelen Ses
Ses dosyaları yasaklanmıştı, bu yüzden doğrudan Web Audio API'yi kullandım. Birkaç satır JavaScript; bir osilatör düğümü (oscillator node) oluşturabilir, onu bir kare veya sinüs dalgasına ayarlayabilir, bir kazanç düğümünden (gain node) geçirebilir ve kısa bir ses patlaması planlayabilir. Yaygın olaylar için küçük yardımcılar yazdım: ayak sesleri için düşük frekanslı bir bip sesi, ganimet toplamak için yükselen bir cıvıltı ve hasar almak için sert bir kare dalga tonu.
Bu sentezlenmiş sesler tasarım gereği yer tutucudur (placeholder), ancak oyuna anında mekanik geri bildirim sağlarlar. Gerçek ses kaydı yapmaya veya ses kaynağı bulmaya karar vermeden önce zamanlamanın doğru olup olmadığını hissedebilirsiniz. Asıl kazanç mimaride gelir. Ses tetikleyicisini basit bir fonksiyon içine sardığınız için, sentezlenmiş bip sesini daha sonra yüklenmiş bir ses tamponuyla (sound buffer) değiştirmek tek bir satırlık değişiklikle gerçekleşir. Oyunun geri kalanı —çarpışma olayı, UI parlaması, skor artışı— bozulmadan kalır. Önce hissi prototipleyip, ardından kaliteyi (fidelity) artırırsınız.
Birden Fazla Dünyayı Yönetmek
Gerçek bir oyun birden fazla ekrana ihtiyaç duyar, bu yüzden projeyi ayrı Phaser sahnelerine böldüm: Menu, Game, UI ve Pause. UI sahnesi, Game sahnesiyle paralel olarak çalışır; can barı ve skor sayacı aşağıda zindan ilerlerken kendi kum havuzlarında yaşasın diye aynı anda başlatılırlar. Sadece olaylar (events) aracılığıyla iletişim kurarlar. Oyuncu hasar aldığında, Game sahnesi bir değişiklik yayar. UI sahnesi bunu dinler ve metin nesnelerini günceller. Game sahnesi UI'ı içe aktarmaz, onun metodlarını çağırmaz, hatta var olup olmadığını bile kontrol etmez. Sadece veriyi boşluğa gönderir. Bu ayrıştırma (decoupling), çekirdek oyun döngüsüne dokunmadan test için HUD'ı yerinden sökebileceğiniz veya tamamen değiştirebileceğiniz anlamına gelir.
Duraklatma için, Game sahnesinin üzerine binen üst üste binmiş bir Pause sahnesi kullandım. Kritik bir nokta olarak, Game sahnesinde scene.pause() çağırmak aslında fizik dünyasını dondurur ve zamanlayıcıları durdurur. Game sahnesi güncellenmeyi durdurur ancak Pause sahnesi bir menü oluşturmak ve duraklatmayı kaldırma (unpause) sinyali beklemek için aktif kalmaya devam eder. Eğer duraklatma durumlarını şimdiye kadar tek bir devasa güncelleme döngüsü içinde bir boolean bayrağı ile yönettiyseniz, bu bir ışık anahtarı keşfetmek gibi hissettiriyor. Motor, sizi kodunuzun her yerine if (isPaused) return korumaları serpiştirmeye zorlamak yerine gerçek bir duraklatma yaşam döngüsü sunar.
Gerçek Hatalardan Alınan Zor Dersler
Donanıma bu kadar yakın çalışmak, değiştirmem gereken iki alışkanlığı ortaya çıkardı.
İlk olarak, Phaser v4'te kamuya açık (public) görünen ancak belgelenmiş API'nin bir parçası olmayan bir metodu kullanmaya çalıştım. Bu metot versiyonlar arasında değişti ve derlememi (build) bozdu. Kararlı ve belgelenmiş bir public metot olan getChildren() metodunu kullanacak şekilde kodu yeniden düzenledim ve istikrarsızlık ortadan kalktı. Ders oldukça net: Eğer bir metot resmi dokümantasyonda yoksa, oyununuzu onun üzerine inşa etmeyin. Dahili (internal) API'ler bir sebeple dahildir. Public yüzey alanına sadık kalın, böylece projeniz motor güncellemelerinden sağ çıkacaktır.
İkinci olarak, her şey için olaylara (events) asla güvenmemem gerektiğini öğrendim. Event callback'leri, bir bozuk para toplamak veya bir sandığı açmak gibi düşük riskli etkileşimler için mükemmel çalışır. Ancak kritik durum geçişleri —özellikle Game Over— için ana güncelleme döngüsü içine yedekli bir kontrol ekledim. Bir dinleyici (listener) kaldırılırsa, bir sahne uygunsuz bir mikrosaniyede duraklatılırsa veya yayınlama (emission) ile işleme (handling) arasında bir yarış durumu (race condition) oluşursa olaylar hatalı tetiklenebilir. Oyuncunun canını doğrudan güncelleme döngüsünde kontrol ederek ve sıfıra inerse oyun bitme (game-over) durumunu zorlayarak, bir olayın tetiklenememesi durumunda oyunun asla bir limbo durumunda takılı kalmamasını sağladım. Olaylar hala ikincil etkileri —ekran sarsıntısı, ses ipuçları, skor gönderimi— yönetir ancak yetkili mantık (authoritative logic), oyun saatinin bulunduğu yerde yaşar.
Bunu Neden Denemelisiniz
Eğer oyun geliştirme öğreniyorsanız, bir sonraki projenize tam olarak şu kısıtlamayı getirin: harici varlık (asset) yok, tek bir HTML dosyası. Kısıtlayıcı görünüyor ama her türlü mazereti ortadan kaldırıyor. Bir bundler yapılandırmanıza, yerel ses dosyalarındaki CORS hatalarıyla boğuşmanıza veya bir öğleden sonranızı ücretsiz asset paketleri seçmekle harcamanıza gerek kalmaz. Kodu yazarsınız, tarayıcıyı yenilersiniz ve sonuçları görürsünüz.
Daha da önemlisi, motorun neden bu şekilde davrandığını anlayacaksınız. Bir dokunun (texture) GPU'ya nasıl girdiğini bileceksiniz çünkü generateTexture metodunu siz çağırdınız. Animasyon karelerinin nasıl indekslendiğini bileceksiniz çünkü sınırları elle siz kaydettiniz. Sesin hoparlörlere nasıl ulaştığını bileceksiniz çünkü osilatörü siz bağladınız. Bu bilgi, harici varlıklar kullanan daha büyük projelere doğrudan aktarılır, çünkü temel mekanikler asla değişmez—motor
