World Wide Maze, web sitelerinden oluşturulan 3D labirentlerde hedef noktalara ulaşmaya çalışırken akıllı telefonunuzu kullanarak yuvarlanan bir topu yönlendirdiğiniz bir oyundur.
Oyunda HTML5 özellikleri bolca kullanılıyor. Örneğin, DeviceOrientation etkinliği, akıllı telefondan eğim verilerini alır. Bu veriler daha sonra WebSocket aracılığıyla PC'ye gönderilir. Burada oyuncular, WebGL ve Web İşleyiciler tarafından oluşturulan 3D alanlarda yollarını bulurlar.
Bu makalede, bu özelliklerin tam olarak nasıl kullanıldığını, genel geliştirme sürecini ve optimizasyonla ilgili önemli noktaları açıklayacağım.
DeviceOrientation
DeviceOrientation etkinliği (örnek), akıllı telefondan eğim verilerini almak için kullanılır. addEventListener
, DeviceOrientation
etkinliğiyle birlikte kullanıldığında düzenli aralıklarla bağımsız değişken olarak DeviceOrientationEvent
nesnesini içeren bir geri çağırma işlevi çağrılır. Aralıklar, kullanılan cihaza göre değişir. Örneğin, iOS + Chrome ve iOS + Safari'de geri çağırma yaklaşık 1/20 saniyede bir, Android 4 + Chrome'da ise yaklaşık 1/10 saniyede bir çağrılır.
window.addEventListener('deviceorientation', function (e) {
// do something here..
});
DeviceOrientationEvent
nesnesi, X
, Y
ve Z
eksenlerinin her biri için derece cinsinden (radyan değil) eğim verilerini içerir (HTML5Rocks'ta daha fazla bilgi edinin). Ancak döndürülen değerler, kullanılan cihaz ve tarayıcı kombinasyonuna göre de değişiklik gösterir. Gerçek döndürülen değerlerin aralıkları aşağıdaki tabloda açıklanmıştır:
Üst tarafta mavi renkle vurgulanan değerler, W3C spesifikasyonlarında tanımlanan değerlerdir. Yeşil renkle vurgulanan değerler bu özelliklerle eşleşirken kırmızı renkle vurgulanan değerler bu özelliklerden farklıdır. Şaşırtıcı bir şekilde, yalnızca Android-Firefox kombinasyonu spesifikasyonlarla eşleşen değerler döndürdü. Yine de uygulama söz konusu olduğunda, sık karşılaşılan değerlerin kullanılması daha mantıklı olur. Bu nedenle World Wide Maze, standart olarak iOS döndürme değerlerini kullanır ve Android cihazlara göre ayarlamalar yapar.
if android and event.gamma > 180 then event.gamma -= 360
Ancak Nexus 10 bu sürümde desteklenmiyor. Nexus 10, diğer Android cihazlarla aynı değer aralığını döndürse de beta ve gama değerlerini tersine çeviren bir hata vardır. Bu sorun ayrı olarak ele alınıyor. (Varsayılan olarak yatay yöne ayarlanmış olabilir mi?)
Bu örnekte görüldüğü gibi, fiziksel cihazları içeren API'lerde belirli özellikler olsa bile döndürülen değerlerin bu özelliklerle eşleşeceği garanti edilmez. Bu nedenle, bunları tüm potansiyel cihazlarda test etmek çok önemlidir. Ayrıca, beklenmedik değerlerin girilebilmesi de söz konusudur. Bu durumda, geçici çözümler oluşturmanız gerekir. World Wide Maze, ilk kez oynayan kullanıcılardan eğitimlerinin 1. adımı olarak cihazlarını kalibre etmelerini ister ancak beklenmedik eğim değerleri alırsa sıfır konuma doğru şekilde kalibre edilmez. Bu nedenle, dahili bir zaman sınırı vardır ve bu süre içinde kalibre edilemezse oynatıcıdan klavye kontrollerine geçmesini ister.
WebSocket
World Wide Maze'de akıllı telefonunuz ve bilgisayarınız WebSocket üzerinden bağlanır. Daha doğrusu, aralarında bir geçiş sunucusu (ör. akıllı telefondan sunucuya, oradan da bilgisayara) üzerinden bağlanırlar. Bunun nedeni, WebSocket'in tarayıcıları doğrudan birbirine bağlama özelliğinin olmamasıdır. (WebRTC veri kanallarını kullanmak, eşler arası bağlantıya olanak tanır ve aktarıcı sunucu ihtiyacını ortadan kaldırır ancak uygulama sırasında bu yöntem yalnızca Chrome Canary ve Firefox Nightly ile kullanılabilirdi.)
Bağlantı zaman aşımı veya bağlantı kesilmesi durumunda yeniden bağlanma özelliklerini içeren Socket.IO (v0.9.11) adlı bir kitaplığı kullanmayı tercih ettim. Bu NodeJS + Socket.IO kombinasyonu, çeşitli WebSocket uygulama testlerinde en iyi sunucu tarafı performansını gösterdiğinden bu kombinasyonu NodeJS ile birlikte kullandım.
Sayılara göre eşleme
- PC'niz sunucuya bağlanır.
- Sunucu, PC'nize rastgele oluşturulmuş bir sayı verir ve sayı ile PC'nin kombinasyonunu hatırlar.
- Mobil cihazınızdan bir numara belirtin ve sunucuya bağlanın.
- Belirtilen numara, bağlı bir bilgisayardaki numarayla aynıysa mobil cihazınız o bilgisayarla eşlenmiştir.
- Belirlenmiş bir PC yoksa hata oluşur.
- Mobil cihazınızdan gelen veriler, eşlendiği PC'ye gönderilir ve bunun tersi de geçerlidir.
İlk bağlantıyı mobil cihazınızdan da yapabilirsiniz. Bu durumda cihazlar tersine çevrilir.
Sekme senkronizasyonu
Chrome'a özel Sekme Senkronizasyonu özelliği, eşleme işlemini daha da kolaylaştırır. Bu sayede, PC'de açık olan sayfalar mobil cihazda kolayca açılabilir (ve bunun tersi de geçerlidir). PC, sunucu tarafından verilen bağlantı numarasını alır ve history.replaceState
kullanarak bir sayfanın URL'sine ekler.
history.replaceState(null, null, '/maze/' + connectionNumber)
Sekme senkronizasyonu etkinse URL birkaç saniye sonra senkronize edilir ve aynı sayfa mobil cihazda açılabilir. Mobil cihaz, açık sayfanın URL'sini kontrol eder ve bir sayı eklenmişse hemen bağlanmaya başlar. Bu sayede, sayıları manuel olarak girmek veya QR kodlarını kamerayla taramak zorunda kalmazsınız.
Gecikme
Aktarım sunucusu ABD'de bulunduğundan, Japonya'dan bu sunucuya erişmek, akıllı telefonun eğim verilerinin PC'ye ulaşmasına yaklaşık 200 ms gecikmeyle neden olur. Yanıt süreleri, geliştirme sırasında kullanılan yerel ortama kıyasla belirgin şekilde yavaştı ancak düşük geçirimli filtre gibi bir şey eklemek (EMA kullandım) bu durumu rahatsız edici olmayan seviyelere getirdi. (Uygulamada, sunum amacıyla da düşük geçiren filtreye ihtiyaç duyuluyordu; Eğim sensöründen döndürülen değerler önemli miktarda gürültü içeriyordu ve bu değerlerin ekrana uygulanması çok fazla titreşime neden oluyordu.) Bu, açıkça yavaş olan sıçramalarda işe yaramadı ancak bu sorunu çözmek için hiçbir şey yapılamadı.
Başından itibaren gecikme sorunları beklediğim için istemcilerin en yakın sunucuya bağlanabilmesi (böylece gecikmeyi en aza indirmek) için dünya genelinde geçiş sunucuları kurmayı düşündüm. Ancak o zamanlar yalnızca ABD'de mevcut olan Google Compute Engine (GCE)'i kullandım. Bu nedenle bu mümkün değildi.
Nagle algoritması sorunu
Nagle algoritması, TCP düzeyinde arabelleğe alarak verimli iletişim için genellikle işletim sistemlerine dahil edilir. Ancak bu algoritma etkinken gerçek zamanlı veri gönderemediğimi fark ettim. (Özellikle TCP gecikmeli onay ile birlikte kullanıldığında. ACK
gecikmesi olmasa bile, sunucunun yurtdışında olması gibi faktörler nedeniyle ACK
belirli bir ölçüde gecikirse aynı sorun ortaya çıkar.)
Nagle gecikmesi sorunu, Nagle'i devre dışı bırakmak için TCP_NODELAY
seçeneğini içeren Android için Chrome'daki WebSocket'te yaşanmadı ancak iOS için Chrome'da kullanılan ve bu seçeneğin etkin olmadığı WebKit WebSocket'te yaşandı. (Aynı WebKit'i kullanan Safari'de de bu sorun yaşanıyordu. Sorun Google aracılığıyla Apple'a bildirildi ve WebKit'in geliştirme sürümünde çözüldü.
Bu sorun oluştuğunda, 100 ms'de bir gönderilen eğim verileri yalnızca 500 ms'de bir PC'ye ulaşan parçalara birleştirilir. Oyun bu koşullarda çalışamaz. Bu nedenle, sunucu tarafının verileri kısa aralıklarla (yaklaşık 50 ms) göndermesini sağlayarak bu gecikmeyi önler. ACK
değerinin kısa aralıklarla alınmasının, Nagle algoritmasını verileri göndermenin sorun olmadığını düşünmeye yönlendirdiğini düşünüyorum.
Yukarıdaki grafikte, alınan gerçek verilerin aralıkları gösterilmektedir. Paketler arasındaki zaman aralıklarını gösterir. Yeşil, çıkış aralıklarını, kırmızı ise giriş aralıklarını temsil eder. Minimum değer 54 ms, maksimum değer 158 ms, orta değer ise 100 ms'ye yakındır. Burada, Japonya'da bulunan bir geçiş sunucusu olan bir iPhone kullandım. Hem çıkış hem de giriş yaklaşık 100 ms. sürer ve işlem sorunsuzdur.
Buna karşılık bu grafikte, sunucunun ABD'de kullanılmasının sonuçları gösterilmektedir. Yeşil çıkış aralıkları 100 ms'de sabit kalırken giriş aralıkları 0 ms ile 500 ms arasında dalgalanır. Bu durum, PC'nin verileri parçalar halinde aldığını gösterir.
Son olarak bu grafikte, sunucunun yer tutucu veriler göndermesiyle gecikme yaşanmasının önlenmesi sonucunda elde edilen sonuçlar gösterilmektedir. Japon sunucusunu kullanmak kadar iyi performans göstermese de giriş aralıkları yaklaşık 100 ms'de nispeten sabit kalıyor.
Bir hata mı?
Android 4'teki (ICS) varsayılan tarayıcıda WebSocket API olmasına rağmen bağlantı kurulamaz ve Socket.IO connect_failed etkinliği oluşur. Dahili olarak zaman aşımına uğrar ve sunucu tarafı da bağlantıyı doğrulayamaz. (Bunu yalnızca WebSocket ile test etmedim, bu nedenle Socket.IO sorunu olabilir.)
Geçiş sunucularını ölçeklendirme
Aktarım sunucusunun rolü o kadar karmaşık olmadığından, aynı PC ve mobil cihazın her zaman aynı sunucuya bağlı olduğundan emin olduğunuz sürece ölçeklendirme ve sunucu sayısını artırma işlemi zor olmayacaktır.
Fizik
Oyun içi top hareketlerinin tümü (yokuşta yuvarlanma, zeminle çarpışma, duvarlarla çarpışma, öğe toplama vb.) 3D fizik simülasyonuyla yapılır. Yaygın olarak kullanılan Bullet fizik motorunun Emscripten kullanılarak JavaScript'e aktarılmış hali olan Ammo.js'i ve Physijs'i "Web Çalışanı" olarak kullanmak için kullandım.
Web İşçileri
Web Workers, JavaScript'i ayrı iş parçacıklarında çalıştırmak için kullanılan bir API'dir. Web İşleyici olarak başlatılan JavaScript, kendisini ilk kez çağıran işleyiciden ayrı bir işleyici olarak çalışır. Böylece, sayfanın duyarlı kalması sağlanırken ağır görevler gerçekleştirilebilir. Physijs, normalde yoğun olan 3D fizik motorunun sorunsuz çalışmasına yardımcı olmak için Web İşleyicileri verimli bir şekilde kullanır. World Wide Maze, fizik motorunu ve WebGL resim oluşturma işlemini tamamen farklı kare hızlarında yönetir. Bu nedenle, düşük özellikli bir makinede kare hızı yoğun WebGL oluşturma yükü nedeniyle düşse bile fizik motoru aşağı yukarı 60 fps'yi korur ve oyun kontrollerini engellemez.
Bu resimde, Lenovo G570'da elde edilen kare hızları gösterilmektedir. Üst kutuda WebGL'nin (resim oluşturma) kare hızı, alt kutuda ise fizik motorunun kare hızı gösterilir. GPU, entegre bir Intel HD Graphics 3000 çipi olduğundan resim oluşturma kare hızı beklenen 60 fps'ye ulaşamadı. Ancak fizik motoru beklenen kare hızına ulaştığı için oyun deneyimi, yüksek özellikli bir makinedeki performanstan çok farklı değil.
Etkin Web İşleyici'leri olan iş parçalarında konsol nesneleri olmadığından, hata ayıklama günlükleri oluşturmak için veriler postMessage aracılığıyla ana iş parçacığına gönderilmelidir. console4Worker kullanıldığında, Worker'da bir konsol nesnesine eşdeğer bir nesne oluşturulur. Bu da hata ayıklama sürecini önemli ölçüde kolaylaştırır.
Chrome'un son sürümleri, Web İşleyicileri başlatırken kesme noktaları belirlemenize olanak tanır. Bu, hata ayıklama için de yararlıdır. Bu bilgiyi Geliştirici Araçları'ndaki "İşçiler" panelinde bulabilirsiniz.
Performans
Poligon sayısı yüksek olan sahneler bazen 100.000 poligonu aşar ancak tamamen Physijs.ConcaveMesh
(Bullet'ta btBvhTriangleMeshShape
) olarak oluşturulsalar bile performansta önemli bir düşüş yaşanmaz.
Başlangıçta, çarpışma algılaması gerektiren nesnelerin sayısı arttıkça kare hızı düştü, ancak Physijs'te gereksiz işleme ortadan kaldırılarak performans iyileştirildi. Bu iyileştirme, orijinal Physijs'in bir çatalında yapıldı.
Hayalet nesneler
Çarpışma algılaması olan ancak çarpışma sırasında etkisi olmayan ve dolayısıyla diğer nesneler üzerinde etkisi olmayan nesnelere Bullet'ta "hayalet nesneler" denir. Physijs resmi olarak hayalet nesneleri desteklemese de Physijs.Mesh
oluşturduktan sonra işaretlerle oynayarak burada oluşturabilirsiniz. World Wide Maze, öğelerin ve hedef noktalarının çarpışma algılaması için hayalet nesneler kullanır.
hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)
collision_flags
için 1 CF_STATIC_OBJECT
, 4 ise CF_NO_CONTACT_RESPONSE
değerini alır. Daha fazla bilgi için Bullet forumunda, Stack Overflow'da veya Bullet belgelerinde arama yapmayı deneyin. Physijs, Ammo.js için bir sarmalayıcı olduğundan ve Ammo.js temelde Bullet ile aynı olduğundan, Bullet'ta yapılabilecek çoğu şey Physijs'te de yapılabilir.
Firefox 18 sorunu
Firefox 17'den 18'e yapılan güncelleme, Web Çalışanlarının veri alışverişi şeklini değiştirdi ve bunun sonucunda Physijs çalışmayı durdurdu. Sorun GitHub'da bildirildi ve birkaç gün sonra çözüldü. Bu açık kaynak verimliliği beni etkilemekle birlikte, World Wide Maze'in çeşitli farklı açık kaynak çerçevelerinden oluştuğunu da hatırlattı. Bu makaleyi, bir tür geri bildirim sağlamak amacıyla yazıyorum.
asm.js
Bu durum World Wide Maze ile doğrudan ilgili olmasa da Ammo.js, Mozilla'nın kısa süre önce duyurduğu asm.js'yi zaten destekliyor (asm.js temel olarak Emscripten tarafından oluşturulan JavaScript'i hızlandırmak için oluşturulduğundan ve Emscripten'in yaratıcısı Ammo.js'in de yaratıcısı olduğundan bu durum şaşırtıcı değildir). Chrome, asm.js'yi de destekliyorsa fizik motorunun hesaplama yükü önemli ölçüde azalır. Firefox Nightly ile test edildiğinde hız belirgin şekilde daha yüksekti. Daha fazla hız gerektiren bölümleri C/C++'da yazıp ardından Emscripten'i kullanarak JavaScript'e taşımak en iyi seçenek olabilir mi?
WebGL
WebGL uygulaması için en aktif şekilde geliştirilen kitaplığı, three.js'i (r53) kullandım. 57. düzeltme, geliştirmenin son aşamalarında yayınlanmış olsa da API'de önemli değişiklikler yapılmıştı. Bu nedenle, yayınlamak için orijinal düzeltmeyi kullandım.
Parıltı efekti
Topun çekirdeğine ve öğelere eklenen parlama efekti, "Kawase Yöntemi MGF" olarak bilinen yöntemin basit bir versiyonu kullanılarak uygulanır. Ancak Kawase yöntemi tüm parlak alanları parlatırken World Wide Maze, parlatılması gereken alanlar için ayrı oluşturma hedefleri oluşturur. Bunun nedeni, sahne dokuları için bir web sitesi ekran görüntüsünün kullanılması gerektiğidir. Örneğin, web sitesinin arka planı beyazsa tüm parlak alanların ayıklanması, web sitesinin tamamının parlamasına neden olur. Her şeyi HDR olarak işleme fikrini de düşündüm ancak uygulamanın oldukça karmaşık hale gelmesi nedeniyle bu sefer buna karşı karar verdim.
Sol üstte, parıltı alanlarının ayrı olarak oluşturulduğu ve ardından bulanıklık uygulandığı ilk geçiş gösterilmektedir. Sağ altta, resim boyutunun% 50 oranında azaltıldığı ve ardından bulanıklık uygulandığı ikinci geçiş gösterilmektedir. Sağ üstte, resmin tekrar% 50 oranında azaltıldığı ve ardından bulanıklaştırıldığı üçüncü geçiş gösterilmektedir. Ardından, sol altta gösterilen nihai birleştirilmiş resmi oluşturmak için bu üç resim üst üste yerleştirildi. Bulanıklaştırma için three.js'ye dahil olan VerticalBlurShader
ve HorizontalBlurShader
işlevlerini kullandım. Bu nedenle, daha fazla optimizasyon yapılabilir.
Yansıtıcı top
Toptaki yansıma, three.js'deki bir örneğe dayanır. Tüm yönler topun konumundan oluşturulur ve ortam haritaları olarak kullanılır. Çevre haritalarının, top her hareket ettiğinde güncellenmesi gerekir ancak 60 fps'de güncelleme yoğun olduğundan bunun yerine her üç karede bir güncellenir. Sonuç, her kareyi güncellemek kadar akıcı olmasa da fark, belirtilmediği sürece neredeyse fark edilmez.
Gölgelendirici, gölgelendirici, gölgelendirici…
WebGL, tüm oluşturma işlemleri için gölgelendiricilere (köşe gölgelendiricileri, parça gölgelendiricileri) ihtiyaç duyar. three.js'ye dahil edilen gölgelendiriciler zaten çok çeşitli efektlere izin verse de daha ayrıntılı gölgelendirme ve optimizasyon için kendi gölgelendiricinizi yazmanız gerekir. World Wide Maze, CPU'yu fizik motoruyla meşgul tuttuğu için CPU işleme (JavaScript aracılığıyla) daha kolay olsa bile mümkün olduğunca gölgelendirme dilinde (GLSL) yazarak GPU'yu kullanmaya çalıştım. Okyanus dalga efektleri, gol noktalarında gösterilen havai fişekler ve top göründüğünde kullanılan ağ efekti gibi doğal olarak gölgelendiricilere dayanır.
Yukarıdaki resim, top göründüğünde kullanılan ağ efektiyle ilgili testlerden alınmıştır. Soldaki, oyunda kullanılan ve 320 poligondan oluşan modeldir. Ortadaki resim yaklaşık 5.000, sağdaki resim ise yaklaşık 300.000 çokgen kullanır. Bu kadar çok poligon olsa bile gölgelendiricilerle işleme, 30 fps'lik sabit bir kare hızını koruyabilir.
Sahneye dağılmış küçük öğelerin tümü tek bir ağaca entegre edilmiştir ve ayrı hareketler, poligon uçlarının her birini hareket ettiren gölgelendiricilere bağlıdır. Bu, çok sayıda nesne bulunduğunda performansın etkilenip etkilenmeyeceğini görmek için yapılan bir testten alınmıştır. Yaklaşık 20.000 poligondan oluşan yaklaşık 5.000 nesne burada düzenlenmiştir. Performans hiç etkilenmedi.
poly2tri
Aşamalar, sunucudan alınan ana hat bilgilerine göre oluşturulur ve ardından JavaScript tarafından poligonlaştırılır. Bu sürecin önemli bir parçası olan üçgenleme, three.js tarafından kötü bir şekilde uygulanır ve genellikle başarısız olur. Bu nedenle, poly2tri adlı farklı bir üçgenleme kitaplığını kendim entegre etmeye karar verdim. Görünüşe göre three.js geçmişte aynı şeyi denemiş. Bu yüzden, bir kısmını yorumlayarak bu özelliği çalıştırmayı başardım. Sonuç olarak hatalar önemli ölçüde azaldı ve daha fazla oynanabilir aşamaya yer verildi. Ara sıra hata devam ediyor ve poly2tri, bir nedenden dolayı hataları uyarı yayınlayarak ele alıyor. Bu nedenle, bunun yerine istisna atacak şekilde değiştirdim.
Yukarıda, mavi dış çizginin nasıl üçgenleştirildiği ve kırmızı poligonların nasıl oluşturulduğu gösterilmektedir.
Anizotropik filtreleme
Standart izotropik MIP eşleme, görüntüleri hem yatay hem de dikey eksenlerde küçülttüğünden, poligonları eğik açılardan görüntülemek, Dünya Çapında Labirent aşamalarının uzak ucundaki dokuların yatay olarak uzatılmış, düşük çözünürlüklü dokular gibi görünmesine neden olur. Bu Wikipedia sayfasının sağ üst kısmındaki resim, bunun iyi bir örneğidir. Pratikte daha fazla yatay çözünürlük gerekir. WebGL (OpenGL), bu sorunu anizotropik filtreleme adı verilen bir yöntem kullanarak çözer. three.js'de THREE.Texture.anisotropy
için 1'den büyük bir değer ayarlamak anizotropik filtrelemeyi etkinleştirir. Ancak bu özellik bir uzantı olduğundan tüm GPU'lar tarafından desteklenmeyebilir.
Optimize etme
Bu WebGL en iyi uygulamaları makalesinde de belirtildiği gibi, WebGL (OpenGL) performansını artırmanın en önemli yolu, çizim çağrılarını en aza indirmektir. World Wide Maze'in ilk geliştirme aşamasında tüm oyun içi adalar, köprüler ve korkuluklar ayrı nesnelerdi. Bu durum bazen 2.000'den fazla çizim çağrısına neden olarak karmaşık aşamaları hantal hale getiriyordu. Ancak aynı türdeki tüm nesneleri tek bir ağaca yerleştirdiğimde, çizim çağrıları yaklaşık elliye düştü ve performans önemli ölçüde iyileşti.
Daha fazla optimizasyon için Chrome izleme özelliğini kullandım. Chrome'un Geliştirici Araçları'na dahil edilen profilleyiciler, genel yöntem işleme sürelerini bir dereceye kadar belirleyebilir ancak izleme, her bir bölümün ne kadar sürdüğünü saniyenin binde birine kadar kesin olarak söyleyebilir. İzlemenin nasıl kullanılacağıyla ilgili ayrıntılı bilgi için bu makaleye göz atın.
Yukarıdakiler, topun yansıması için ortam haritaları oluşturma işleminin izleme sonuçlarıdır. three.js'de alakalı görünen konumlara console.time
ve console.timeEnd
eklediğimizde aşağıdaki gibi bir grafik elde ederiz. Zaman soldan sağa doğru akar ve her katman bir çağrı yığınına benzer. console.time işlevini bir console.time
içine yerleştirmek daha fazla ölçüm yapılmasına olanak tanır. Üstteki grafik optimizasyon öncesi, alttaki ise optimizasyon sonrasıdır. Üstteki grafikte gösterildiği gibi, ön optimizasyon sırasında 0-5 arasındaki her bir oluşturma işlemi için updateMatrix
(kelimenin sonu kesilmiş olsa da) çağrıldı. Bu işlem yalnızca nesnelerin konumu veya yönü değiştiğinde gerekli olduğundan, işlevi yalnızca bir kez çağrılacak şekilde değiştirdim.
İzleme işleminin kendisi doğal olarak kaynak kullanır. Bu nedenle, console.time
öğesini aşırı derecede eklemek gerçek performanstan önemli ölçüde sapmaya neden olabilir ve optimizasyon için hangi alanların önemli olduğunu belirlemeyi zorlaştırabilir.
Performans ayarlayıcı
İnternetin doğası gereği, oyun büyük olasılıkla çok farklı özelliklere sahip sistemlerde oynanacaktır. Şubat ayının başlarında yayınlanan Oz Büyücüsü filminde, efektleri kare hızındaki dalgalanmalara göre ölçeklendirmek ve böylece sorunsuz oynatmayı sağlamak için IFLAutomaticPerformanceAdjust
adlı bir sınıf kullanılıyor. World Wide Maze, aynı IFLAutomaticPerformanceAdjust
sınıfını temel alır ve oynanabilirliği mümkün olduğunca sorunsuz hale getirmek için efektleri aşağıdaki sırayla ölçeklendirir:
- Kare hızı 45 fps'nin altına düşerse ortam haritaları güncellemeyi durdurur.
- Hâlâ 40 fps'nin altına düşüyorsa oluşturma çözünürlüğü %70'e (yüzey oranının% 50'si) düşürülür.
- FPS hâlâ 40'ın altına düşerse FXAA (kenar yumuşatma) devre dışı bırakılır.
- Hâlâ 30 fps'nin altına düşüyorsa parıltı efektleri kaldırılır.
Bellek sızıntısı
Nesneleri düzgün bir şekilde kaldırmak three.js ile biraz uğraş gerektirir. Ancak bunları tek başına bırakmak bellek sızıntılarına yol açacağından aşağıdaki yöntemi geliştirdim. @renderer
, THREE.WebGLRenderer
değerini gösterir. (three.js'nin en son düzeltmesinde, biraz farklı bir ayırma yöntemi kullanıldığı için bu yöntem muhtemelen olduğu gibi çalışmaz.)
destructObjects: (object) =>
switch true
when object instanceof THREE.Object3D
@destructObjects(child) for child in object.children
object.parent?.remove(object)
object.deallocate()
object.geometry?.deallocate()
@renderer.deallocateObject(object)
object.destruct?(this)
when object instanceof THREE.Material
object.deallocate()
@renderer.deallocateMaterial(object)
when object instanceof THREE.Texture
object.deallocate()
@renderer.deallocateTexture(object)
when object instanceof THREE.EffectComposer
@destructObjects(object.copyPass.material)
object.passes.forEach (pass) =>
@destructObjects(pass.material) if pass.material
@renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
@renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
@renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2
HTML
WebGL uygulamasının en iyi özelliğinin, sayfa düzenini HTML'de tasarlayabilme olduğunu düşünüyorum. Flash veya openFrameworks (OpenGL)'ta puan veya metin ekranları gibi 2D arayüzler oluşturmak biraz can sıkıcı. Flash'ta en azından bir IDE var ancak openFrameworks'a alışkın değilseniz zordur (Cocos2D gibi bir şey kullanmak işleri kolaylaştırabilir). Öte yandan HTML, web siteleri oluştururken olduğu gibi CSS ile tüm ön uç tasarım özelliklerinin hassas bir şekilde kontrol edilmesine olanak tanır. Parçacıkların bir logoda yoğunlaşması gibi karmaşık efektler mümkün olmasa da CSS Dönüşümleri'nin olanakları dahilinde bazı 3D efektler mümkündür. World Wide Maze'in "HEDEF" ve "ZAMAN DOLMUŞ" metin efektleri, CSS geçişinde ölçek kullanılarak animasyonlandırılmıştır (Transit ile uygulanmıştır). (Arka plan gradyanları WebGL kullanır.)
Oyundaki her sayfanın (başlık, SONUÇ, RANKING vb.) kendi HTML dosyası vardır. Bunlar şablon olarak yüklendikten sonra $(document.body).append()
, uygun değerlerle uygun zamanda çağrılır. Fare ve klavye etkinlikleri, ekleme işleminden önce ayarlanamadığı için ekleme işleminden önce el.click (e) -> console.log(e)
denemesi işe yaramadı.
Uluslararasılaştırma (i18n)
HTML'de çalışmak, İngilizce dil sürümünü oluşturmak için de uygundu. Uluslararasılaştırma ihtiyaçlarım için bir web i18n kitaplığı olan i18next'i kullanmayı tercih ettim. Bu kitaplığı herhangi bir değişiklik yapmadan olduğu gibi kullanabildim.
Oyun içi metinlerin düzenlenmesi ve çevrilmesi Google Dokümanlar E-Tablosu'nda yapıldı. i18next için JSON dosyası gerekir. Bu nedenle, e-tabloları TSV biçiminde dışa aktardım ve ardından özel bir dönüştürücüyle dönüştürdüm. Yayınlamadan hemen önce çok sayıda güncelleme yaptığım için Google Dokümanlar e-tablosundan dışa aktarma işlemini otomatikleştirmek işleri çok daha kolaylaştırırdı.
Sayfalar HTML ile oluşturulduğundan Chrome'un otomatik çeviri özelliği de normal şekilde çalışır. Ancak bazen dili doğru şekilde algılayamaz ve tamamen farklı bir dil (ör. Vietnamca) desteklenmediğinden bu özellik şu anda devre dışıdır. (Meta etiketler kullanılarak devre dışı bırakılabilir.)
RequireJS
JavaScript modül sistemim olarak RequireJS'i seçtim. Oyunun 10.000 satırlık kaynak kodu yaklaşık 60 sınıfa (= coffee dosyası) bölünür ve ayrı js dosyalarına derlenir. RequireJS, bu dosyaları bağımlılığa göre uygun sırada yükler.
define ->
class Hoge
hogeMethod: ->
Yukarıda tanımlanan sınıf (hoge.coffee) aşağıdaki gibi kullanılabilir:
define ['hoge'], (Hoge) ->
class Moge
constructor: ->
@hoge = new Hoge()
@hoge.hogeMethod()
Çalışması için hoge.js, moge.js'den önce yüklenmesi gerekir. Ayrıca "hoge", "define" işlevinin ilk bağımsız değişkeni olarak belirlendiğinden hoge.js her zaman önce yüklenir (hoge.js'nin yüklenmesi tamamlandıktan sonra geri çağrılır). Bu mekanizmaya AMD adı verilir ve AMD'yi desteklediği sürece herhangi bir üçüncü taraf kitaplığı aynı tür geri çağırma için kullanılabilir. Bu özelliklere sahip olmayanlar (ör. three.js) bile bağımlılıklar önceden belirtildiği sürece benzer şekilde çalışır.
Bu, AS3'ü içe aktarmaya benzer. Bu nedenle, bu işlem size çok yabancı gelmeyecektir. Daha fazla bağımlı dosyanız varsa bu olası bir çözümdür.
r.js
RequireJS, r.js adlı bir optimizatör içerir. Bu işlem, ana js dosyasını tüm bağımlı js dosyalarıyla birlikte tek bir dosyada toplar ve ardından UglifyJS (veya Closure Compiler) kullanarak dosyayı küçültür. Bu sayede, tarayıcının yüklemesi gereken dosya sayısı ve toplam veri miktarı azalır. World Wide Maze için toplam JavaScript dosya boyutu yaklaşık 2 MB'tır ve r.js optimizasyonuyla yaklaşık 1 MB'a düşürülebilir. Oyun gzip kullanılarak dağıtılabiliyorsa bu boyut 250 KB'ye daha da düşürülebilir. (GAE'de, 1 MB veya daha büyük gzip dosyalarının iletilmesine izin vermeyen bir sorun vardır. Bu nedenle oyun şu anda sıkıştırılmamış haliyle 1 MB düz metin olarak dağıtılmaktadır.)
Sahne oluşturucu
Aşama verileri aşağıdaki şekilde oluşturulur ve tamamen ABD'deki GCE sunucusunda gerçekleştirilir:
- Sahneye dönüştürülecek web sitesinin URL'si WebSocket üzerinden gönderilir.
- PhantomJS bir ekran görüntüsü alır ve div ile img etiketi konumları alınıp JSON biçiminde yayınlanır.
- Özel bir C++ (OpenCV, Boost) programı, 2. adımdaki ekran görüntüsünü ve HTML öğelerinin konumlandırma verilerini temel alarak gereksiz alanları siler, ada oluşturur, adaları köprülerle birbirine bağlar, koruma raylarının ve öğelerin konumlarını hesaplar, hedef noktasını belirler vb. Sonuçlar JSON biçiminde yayınlanır ve tarayıcıya döndürülür.
PhantomJS
PhantomJS, ekran gerektirmeyen bir tarayıcıdır. Web sayfalarını pencere açmadan yükleyebildiğinden otomatik testlerde veya sunucu tarafında ekran görüntüsü almak için kullanılabilir. Tarayıcı motoru, Chrome ve Safari'nin kullandığı WebKit olduğundan düzeni ve JavaScript yürütme sonuçları da standart tarayıcılarla aşağı yukarı aynıdır.
PhantomJS'de, yürütülmesini istediğiniz işlemleri yazmak için JavaScript veya CoffeeScript kullanılır. Ekran görüntüsü almak bu örnekte gösterildiği gibi çok kolaydır. Linux sunucusunda (CentOS) çalıştığım için Japonca'yı görüntülemek için yazı tipleri yüklemem gerekiyordu (M+ FONTS). Yine de yazı tipi oluşturma işlemi Windows veya Mac OS'ten farklı şekilde ele alınır. Bu nedenle, aynı yazı tipi diğer makinelerde farklı görünebilir (ancak fark çok azdır).
img ve div etiketi konumlarının alınması temel olarak standart sayfalardakiyle aynı şekilde yapılır. jQuery de herhangi bir sorun yaşamadan kullanılabilir.
stage_builder
İlk başta, aşama oluşturmak için daha DOM tabanlı bir yaklaşım (Firefox 3D İnceleyici'ye benzer) kullanmayı düşündüm ve PhantomJS'de DOM analizi gibi bir şey denedim. Ancak sonunda bir görüntü işleme yaklaşımına karar verdim. Bu amaçla, OpenCV ve Boost kullanan "stage_builder" adlı bir C++ programı yazdım. Aşağıdaki işlemleri gerçekleştirir:
- Ekran görüntüsünü ve JSON dosyalarını yükler.
- Resimleri ve metinleri "adalara" dönüştürür.
- Adaları birbirine bağlayan köprüler oluşturur.
- Labirent oluşturmak için gereksiz köprüleri kaldırır.
- Büyük boyutlu öğeler yerleştirir.
- Küçük eşyaları yerleştirir.
- Koruma bariyerleri yerleştirir.
- Konumlandırma verilerini JSON biçiminde döndürür.
Her adım aşağıda ayrıntılı olarak açıklanmıştır.
Ekran görüntüsünü ve JSON dosyalarını yükleme
Ekran görüntülerini yüklemek için normal cv::imread
kullanılır. JSON dosyaları için birkaç kitaplığı test ettim ancak picojson ile çalışmak en kolayı gibi görünüyordu.
Resimleri ve metinleri "adalara" dönüştürme
Yukarıda, aid-dcc.com'un Haberler bölümünün ekran görüntüsü verilmiştir (gerçek boyutu görüntülemek için tıklayın). Resimler ve metin öğeleri adalara dönüştürülmelidir. Bu bölümleri ayırmak için beyaz arka plan rengini (yani ekran görüntüsünde en yaygın rengi) silmemiz gerekir. İşlem tamamlandıktan sonra aşağıdaki gibi görünür:
Beyaz bölümler, potansiyel adalar.
Metin çok ince ve keskin olduğu için cv::dilate
, cv::GaussianBlur
ve cv::threshold
ile kalınlaştıracağız. Resim içeriği de eksik olduğundan bu alanları, PhantomJS'den alınan img etiketi verilerine göre beyazla dolduracağız. Elde edilen resim şu şekilde görünür:
Metin artık uygun kümeler oluşturuyor ve her resim uygun bir ada.
Adaları birbirine bağlayan köprüler oluşturma
Adalar hazır olduğunda köprülerle birbirine bağlanır. Her ada, solda, sağda, yukarıda ve aşağıda bitişik adalar arar, ardından en yakın adanın en yakın noktasına bir köprü bağlar. Bu işlem sonucunda aşağıdaki gibi bir görünüm elde edilir:
Labirent oluşturmak için gereksiz köprüleri kaldırma
Tüm köprüleri tutmak, sahnede gezinmeyi çok kolaylaştıracağından labirent oluşturmak için bazı köprülerin kaldırılması gerekir. Başlangıç noktası olarak bir ada (ör. sol üstteki ada) seçilir ve bu adaya bağlanan bir köprü dışındaki tüm köprüler (rastgele seçilir) silinir. Ardından, kalan köprüyle bağlı bir sonraki ada için de aynı işlem yapılır. Yol çıkmaza ulaştığında veya daha önce ziyaret edilen bir adaya geri döndüğünde, yeni bir adaya erişmeye olanak tanıyan bir noktaya geri döner. Tüm adalar bu şekilde işlendikten sonra labirent tamamlanır.
Büyük öğeleri yerleştirme
Her adaya, boyutlarına bağlı olarak bir veya daha fazla büyük öğe yerleştirilir. Bu öğeler, adaların kenarlarından en uzak noktalardan seçilir. Çok net olmasa da bu noktalar aşağıda kırmızıyla gösterilmiştir:
Bu olası noktalardan sol üstteki başlangıç noktası (kırmızı daire), sağ alttaki hedef (yeşil daire) olarak ayarlanır ve geri kalan noktalardan en fazla altı tanesi büyük öğe yerleşimi için (mor daire) seçilir.
Küçük öğeler yerleştirme
Ada kenarlarından belirli mesafelerde çizgiler boyunca uygun sayıda küçük öğe yerleştirilir. Yukarıdaki resimde (aid-dcc.com'dan alınmamıştır), projelendirilen yerleşim çizgileri gri renkte, adanın kenarlarından düzenli aralıklarla yerleştirilmiş ve kaydırılmış olarak gösterilmektedir. Kırmızı noktalar, küçük öğelerin yerleştirildiği yeri gösterir. Bu resim, geliştirmenin ortasındaki bir sürümden alındığı için öğeler düz çizgiler halinde düzenlenmiştir ancak nihai sürümde öğeler gri çizgilerin her iki tarafına da biraz daha düzensiz bir şekilde dağıtılmıştır.
Koruma bariyeri yerleştirme
Korkuluk, temel olarak adaların dış sınırlarına yerleştirilir ancak erişime izin vermek için köprülerde kesilmelidir. Boost Geometri kitaplığı, ada sınırı verilerinin bir köprünün iki tarafındaki çizgilerle kesiştiği noktaları belirleme gibi geometrik hesaplamaları basitleştirerek bu konuda faydalı oldu.
Adaların ana hatlarını gösteren yeşil çizgiler koruyucu raylardır. Bu resimde görmek zor olabilir ancak köprülerin olduğu yerde yeşil çizgiler yoktur. Bu, JSON olarak oluşturulması gereken tüm nesnelerin dahil edildiği, hata ayıklama için kullanılan nihai resimdir. Açık mavi noktalar küçük öğeleri, gri noktalar ise yeniden başlatma noktalarını gösterir. Top okyanusa düştüğünde oyun, en yakın yeniden başlatma noktasından devam eder. Yeniden başlatma noktaları, küçük öğelerle aşağı yukarı aynı şekilde, adanın kenarından belirli bir mesafede düzenli aralıklarla düzenlenir.
Konumlandırma verilerini JSON biçiminde yayınlama
Çıkış için de picojson kullandım. Verileri standart çıkışa yazar ve bu veriler daha sonra arayan (Node.js) tarafından alınır.
Mac'te Linux'ta çalışacak bir C++ programı oluşturma
Oyun Mac'te geliştirildi ve Linux'ta dağıtıldı. Ancak OpenCV ve Boost her iki işletim sisteminde de mevcut olduğundan derleme ortamı oluşturulduktan sonra geliştirmenin kendisi zor olmadı. Mac'te derlemede hata ayıklama yapmak için Xcode'daki Komut Satırı Araçları'nı kullandım, ardından derlemenin Linux'da derlenebilmesi için automake/autoconf kullanarak bir yapılandırma dosyası oluşturdum. Ardından, çalıştırılabilir dosyayı oluşturmak için Linux'da "configure && make"i kullanmam yeterli oldu. Derleyici sürümü farklılıkları nedeniyle Linux'a özgü bazı hatalarla karşılaştım ancak gdb'yi kullanarak bunları nispeten kolayca çözebildim.
Sonuç
Bu tür bir oyun, Flash veya Unity ile oluşturulabilir. Bu da birçok avantaj sağlar. Ancak bu sürüm için eklenti gerekmez ve HTML5 + CSS3'ün düzen özellikleri son derece güçlüdür. Her görev için doğru araçlara sahip olmak kesinlikle önemlidir. Tamamen HTML5 ile oluşturulmuş bir oyun için oyunun ne kadar iyi sonuç verdiğine ben de şaşırdım. Oyunda hâlâ eksiklikler olsa da gelecekte nasıl gelişeceğini görmek için sabırsızlanıyorum.