Örnek Olay - Inside World Wide Labirent

World Wide Maze, hedef noktalarına ulaşmak amacıyla web sitelerinden oluşturulan 3D labirentlerde yuvarlak bir topun arasında gezinmek için akıllı telefonunuzu kullandığınız bir oyundur.

Dünya Çapında Labirent

Oyunda bol miktarda HTML5 özelliği kullanılıyor. Örneğin, DeviceOrientation etkinliği akıllı telefondan eğme verilerini alır ve bu veriler WebSocket üzerinden PC'ye gönderilir. Burada, oyuncular WebGL ve Web Workers tarafından oluşturulan 3D alanlarda ilerlerler.

Bu makalede bu özelliklerin nasıl kullanıldığını, genel geliştirme sürecini ve optimizasyonun önemli noktalarını tam olarak açıklayacağım.

DeviceOrientation

DeviceOrientation etkinliği (örnek), akıllı telefondan yatırma verilerini almak için kullanılır. DeviceOrientation etkinliğiyle addEventListener kullanıldığında, düzenli aralıklarla DeviceOrientationEvent nesnesine sahip bir geri çağırma bağımsız değişken olarak çağrılır. Aralıkların kendisi kullanılan cihaza göre değişir. Örneğin, iOS + Chrome ve iOS + Safari'de geri çağırma işlevi saniyenin yaklaşık 1/20'sinde bir, Android 4 ve Chrome'da ise saniyenin yaklaşık 1/10'unda bir çağrılır.

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

DeviceOrientationEvent nesnesi, her bir X, Y ve Z ekseni için derece cinsinden (radyan değil) yatırma verilerini içerir (HTML5Rocks hakkında daha fazla bilgi edinin). Bununla birlikte, döndürülen değerler kullanılan cihaz ve tarayıcı kombinasyonuna göre de değişir. Gerçek döndürülen değerlerin aralıkları aşağıdaki tabloda belirtilmiştir:

Cihazın yönü.

En üstte maviyle vurgulanmış değerler W3C spesifikasyonlarında tanımlanmıştır. Yeşille vurgulananlar bu spesifikasyonlara uygundur, ancak kırmızı sapmayla vurgulananlar bu spesifikasyonlara uymaktadır. Şaşırtıcı bir şekilde, yalnızca Android-Firefox kombinasyonu özelliklerle eşleşen değerler döndürdü. Yine de, uygulama söz konusu olduğunda, sık gerçekleşen değerlere yer vermek daha mantıklıdır. Bu nedenle World Wide Maze, iOS döndürme değerlerini standart olarak kullanır ve Android cihazlar için buna göre ayarlama yapar.

if android and event.gamma > 180 then event.gamma -= 360

Ancak bu cihaz hâlâ Nexus 10'u desteklememektedir. Nexus 10 diğer Android cihazlarla aynı değer aralığını döndürse de, beta ve gama değerlerini tersine çeviren bir hata bulunmaktadır. Bu, ayrı olarak ele alınmaktadır. (Varsayılan olarak yatay yönde yapılmış olabilir mi?)

Burada da gösterildiği gibi, fiziksel cihazları içeren API'lerin özellikleri ayarlanmış olsa bile döndürülen değerlerin bu özelliklerle eşleşeceğine dair bir garanti yoktur. Bu nedenle, reklamlarınızı potansiyel tüm cihazlarda test etmek çok önemlidir. Bu, aynı zamanda, geçici çözümler oluşturmayı gerektiren beklenmedik değerlerin girilebileceği anlamına da gelir. World Wide Maze, eğiticinin 1. adımında ilk kez oynayan oyunculardan cihazlarını kalibre etmelerini ister, ancak beklenmedik eğim değerleri alırsa sıfır konumuna doğru şekilde kalibre edilmez. Dolayısıyla, dahili bir zaman sınırı vardır ve bu süre sınırı 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ş sunucusuyla (akıllı telefondan sunucudan bilgisayara) bağlanırlar. Bunun nedeni, WebSocket'in tarayıcıları doğrudan birbirine bağlama yeteneğine sahip olmamasıdır. (WebRTC veri kanallarının kullanılması, eşler arası bağlantıya olanak tanır ve geçiş sunucusuna olan ihtiyacı ortadan kaldırır. Ancak, uygulama anında bu yöntem yalnızca Chrome Canary ve Firefox Nightly ile kullanılabilmektedir.)

Bağlantının zaman aşımına uğraması veya bağlantısının kesilmesi durumunda yeniden bağlanma özellikleri içeren Socket.IO (v0.9.11) adlı bir kitaplık kullanarak uygulamayı seçtim. Bu NodeJS + Socket.IO kombinasyonu birkaç WebSocket uygulama testinde en iyi sunucu tarafı performansı gösterdiği için bunu NodeJS ile birlikte kullandım.

Sayılarla eşleme

  1. Bilgisayarınız sunucuya bağlanır.
  2. Sunucu PC'nize rastgele oluşturulmuş bir sayı verir ve bu sayı ile PC kombinasyonunu hatırlar.
  3. Mobil cihazınızdan bir numara belirtin ve sunucuya bağlanın.
  4. Belirtilen numara, bağlı bir PC'de belirtilen numarayla aynıysa mobil cihazınız bu bilgisayarla eşlenir.
  5. Belirlenmiş bir PC yoksa hata oluşur.
  6. Mobil cihazınızdan gelen veriler, eşlendiği PC'ye gönderilir. Bunun tersi de geçerlidir.

İlk bağlantıyı mobil cihazınızdan da yapabilirsiniz. Bu durumda cihazlar tersine çevrilir.

Sekme Senkronizasyonu

Chrome'a özgü Sekme Senkronizasyonu özelliği eşleme işlemini daha da kolaylaştırır. Bu özellik sayesinde, bilgisayarda açık olan sayfalar mobil cihazlarda da kolayca açılabilir (tam tersi de geçerlidir). PC, sunucu tarafından verilen bağlantı numarasını alır ve history.replaceState kullanarak 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 numara eklenirse hemen bağlanmaya başlar. Bu sayede, numaraları manuel olarak girme veya QR kodlarını bir kamera ile tarama ihtiyacı ortadan kalkar.

Gecikme

Geçiş sunucusu ABD’de bulunduğundan, bu sunucuya Japonya’dan erişmek akıllı telefonun yatırma verileri PC’ye ulaşmadan önce yaklaşık 200 ms’lik bir gecikmeye neden olur. Yanıt süreleri, geliştirme sırasında kullanılan yerel ortamdaki yanıt sürelerine kıyasla açık bir şekilde uzundu. Ancak, düşük geçişli filtre (EMA kullandım) gibi bir şey eklemek bunu göze batmayan seviyelere yükseltti. (Pratikte, sunum amacıyla da bir alçak geçiş filtresi gerekiyordu; eğim sensöründen gelen dönüş değerlerinde önemli miktarda gürültü vardı ve bu değerlerin ekrana uygulanması da çok fazla sarsıntıya neden oluyordu.) Bu, açıkça yavaş ilerleyen atlamalarda işe yaramadı ancak bunu çözmek için hiçbir şey yapılamazdı.

Başından beri gecikme sorunları olmasını beklediğimden, istemcilerin mevcut en yakın bağlantıya geçebilmesi (ve böylece gecikmeyi en aza indirebilmesi) için dünyanın dört bir yanında geçiş sunucuları ayarlamayı düşündüm. Ancak sonunda yalnızca ABD'de bulunan Google Compute Engine'i (GCE) kullanmaya karar verdim. Dolayısıyla bu mümkün değildi.

Nagle Algoritması problemi

Nagle algoritması, genellikle TCP düzeyinde arabelleğe alma yoluyla verimli iletişim için işletim sistemlerine dahil edilir, ancak bu algoritma etkinken gerçek zamanlı olarak veri gönderemediğimi anladım. (Özellikle TCP gecikmeli onayı ile birleştirildiğinde. Gecikmeli ACK olmasa bile, sunucunun deniz aşırında olması gibi faktörler nedeniyle ACK bir dereceye kadar geciktiğinde aynı sorun meydana gelir.)

Nagle gecikme sorunu, Android için Chrome'da, Nagle'ı devre dışı bırakmaya ilişkin TCP_NODELAY seçeneğini içeren WebSocket'de oluşmadı ancak iOS için Chrome'da kullanılan ve bu seçeneğin etkinleştirilmediği WebKit WebSocket'te meydana geldi. (Aynı WebKit'i kullanan Safari'de de bu sorun yaşanmıştır. Sorun, Google aracılığıyla Apple'a bildirildi ve WebKit'in geliştirme sürümünde çözülmüştür.

Bu sorun ortaya çıktığında, her 100 ms'de bir gönderilen eğme verileri yalnızca 500 ms'de bir PC'ye ulaşan parçalara birleştirilir. Oyun bu koşullarda çalışamaz, böylece sunucu tarafının kısa aralıklarla (yaklaşık 50 ms'de bir) veri göndermesini sağlayarak bu gecikmeyi önler. Kısa aralıklarla ACK almanın Nagle algoritmasını aptalca veri göndermenin uygun olduğuna inandırdığına inanıyorum.

Nagle algoritması 1

Yukarıdaki grafik, alınan gerçek verilerin aralıklarını gösterir. Paketler arasındaki zaman aralıklarını belirtir. Yeşil renk çı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, santral sunucusu olan bir iPhone kullandım. Hem çıkış hem de giriş yaklaşık 100 ms civarında ve çalışması sorunsuz.

Nagle algoritması 2

Buna karşılık, bu grafik ABD'de sunucu kullanmanın sonuçlarını gösterir. Yeşil çıkış aralıkları 100 ms'de sabit kalırken, giriş aralıkları 0 ms ile en yüksek 500 ms arasında dalgalanarak PC'nin parçalar halinde veri aldığını gösterir.

ALT_TEXT_HERE

Son olarak bu grafik, sunucunun yer tutucu verileri göndermesini sağlayarak gecikmeyi önlemenin sonuçlarını gösterir. Japonca sunucu kullanımı kadar iyi performans göstermese de, giriş aralıklarının yaklaşık 100 ms ile nispeten sabit olduğu açıktır.

Hata mı?

Android 4'teki (ICS) varsayılan tarayıcının bir WebSocket API'si olmasına rağmen bağlantı kurulamaz. Bu da Socket.IO connect_failed etkinliğine neden olur. Dahili olarak zaman aşımına uğrar ve sunucu tarafı da bağlantıyı doğrulayamaz. (Bunu tek başına WebSocket ile test etmedim, bu nedenle bir Socket.IO sorunu olabilir.)

Geçiş sunucularını ölçeklendirme

Geçiş sunucusunun rolü o kadar karmaşık olmadığından, aynı PC ve mobil cihazın her zaman aynı sunucuya bağlı olmasını sağladığınız sürece, sunucu sayısını artırmak ve artırmak zor olmamalıdır.

Fizik

Oyun içi top hareketi (yokuş aşağı yuvarlanma, yere çarpma, duvarlara çarpma, öğe toplama vb.), 3D bir fizik simülatörüyle yapılır. Ammo.js'yi (yaygın olarak kullanılan Bullet fizik motorunun Emscripten ile JavaScript'e bağlantı noktası) "Web Çalışanı" olarak Physijs ile birlikte kullandım.

Web İşçileri

Web Workers, JavaScript'i ayrı iş parçacıklarında çalıştırmaya yönelik bir API'dir. Web İşçisi olarak başlatılan JavaScript, orijinalinden farklı bir iş parçacığı şeklinde çalışır. Böylece, sayfayı duyarlı tutarken ağır görevler gerçekleştirilebilir. Physijs, normalde yoğun olan 3D fizik motorunun sorunsuz bir şekilde çalışmasına yardımcı olmak için Web İşçilerini verimli bir şekilde kullanır. World Wide Maze, fizik motorunu ve WebGL görüntü oluşturmayı tamamen farklı kare hızlarında yönetir. Bu yüzden, düşük özellikli bir makinede yoğun WebGL görüntüleme yükü nedeniyle kare hızı düşse bile fizik motorunun kendisi 60 kare/saniyeyi daha fazla ya da daha iyi koruyacak ve oyun kontrollerini engellemeyecektir.

FPS

Bu resimde, Lenovo G570 cihazında elde edilen kare hızları gösterilmektedir. Üstteki kutu WebGL (görüntü oluşturma) kare hızını, alttaki kutu ise fizik motoru için kare hızını gösterir. GPU, entegre bir Intel HD Graphics 3000 çipi olduğu için görüntü oluşturma kare hızı beklenen 60 fps'ye ulaşamadı. Ancak fizik motoru beklenen kare hızına ulaştığı için oynanabilirlik deneyimi, yüksek teknik özelliklere sahip bir makinenin performansından pek farklı değil.

Etkin Web İşçileri içeren ileti dizilerinde konsol nesnesi bulunmadığından, hata ayıklama günlükleri oluşturmak için verilerin postMessage aracılığıyla ana ileti dizisine gönderilmesi gerekir. console4Worker kullanılması, Çalışan konumunda bir konsol nesnesinin eşdeğerini oluşturarak hata ayıklama sürecini önemli ölçüde kolaylaştırır.

Hizmet çalışanları

Chrome'un son sürümleri, Web Workers'ı başlatırken ayrılma noktaları belirlemenize olanak tanır. Bu özellik, hata ayıklama için de kullanışlıdır. Bu, Geliştirici Araçları'ndaki "Çalışanlar" panelinde bulunabilir.

Performans

Poligon sayısı yüksek olan aşamalar bazen 100.000 poligonu aşsa da tamamen Physijs.ConcaveMesh (Madde işaretinde btBvhTriangleMeshShape) olarak oluşturulduklarında bile performansta herhangi bir düşüş görülmedi.

Başlangıçta, çarpışma algılaması gerektiren nesnelerin sayısı arttıkça kare hızı düştü, ancak Physijs'deki gereksiz işlemlerin ortadan kaldırılması performansı artırdı. Bu iyileştirme, orijinal Physijs'in bir çatalında yapılmıştır.

Hayalet nesneler

Çarpışma algılama özelliği olan ancak çarpışma üzerinde etkisi olmayan ve dolayısıyla diğer nesnelere etkisi olmayan nesnelere Madde işaretinde "hayalet nesneler" denir. Physijs, hayalet nesneleri resmi olarak desteklemese de Physijs.Mesh oluşturduktan sonra bayraklarla kurcalayarak hayalet nesneleri oluşturmak mümkündür. World Wide Labirent, öğelerin ve hedef noktalarının çarpışma tespiti için hayalet nesneleri kullanır.

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

collision_flags için 1 değeri CF_STATIC_OBJECT, 4 değeri CF_NO_CONTACT_RESPONSE'dir. Daha fazla bilgi için Bullet forumu, Stack Overflow veya Bullet dokümanlarında arama yapmayı deneyin. Physijs, Ammo.js için bir sarmalayıcı olduğundan Ammo.js temel olarak Bullet ile aynı olduğundan, Madde işareti içinde yapılabilecek çoğu şey Physijs'de de yapılabilir.

Firefox 18 sorunu

Firefox'un sürüm 17'den 18'e güncellemesi, Web İşçilerinin veri alışverişi yapma şeklini değiştirdi ve bunun sonucunda Physijs çalışmayı durdurdu. Bu sorun, GitHub'da bildirildi ve birkaç gün sonra çözüldü. Bu açık kaynak verimliliği beni etkilemiş olsa da olay, World Wide Maze'in birkaç farklı açık kaynak çerçeveden nasıl oluştuğunu da hatırlattı. Bu makaleyi bir tür geri bildirim sağlamak umuduyla yazıyorum.

asm.js

Bu durum doğrudan World Wide Maze'i ilgilendirmiyor olsa da Ammo.js, Mozilla'nın kısa süre önce duyurduğu asm.js'yi zaten desteklemektedir (asm.js'nin esasen Emscripten tarafından oluşturulan JavaScript'i hızlandırmak için oluşturulması ve Emscripten'ın yaratıcısının aynı zamanda Ammo.js'nin de yaratıcısı olması nedeniyle bu şaşırtıcı değildir). Chrome, asm.js'yi de destekliyorsa fizik motorunun bilgi işlem yükü önemli ölçüde azalır. Firefox Gecelik ile test edildiğinde hız fark edilecek şekilde daha yüksekti. C/C++'ta daha fazla hız gerektiren bölümler yazmak ve ardından bunları Emscripten kullanarak JavaScript'e taşımak en iyi seçenek olabilir mi?

WebGL

WebGL uygulaması için en aktif şekilde geliştirilen kitaplık olan three.js'yi (r53) kullandım. Geliştirmenin sonraki aşamalarında düzeltme 57 zaten yayınlanmış olsa da API'da büyük değişiklikler yapılmıştı ve bu yüzden orijinal düzeltmede kaldım.

Parlaklık efekti

Topun merkezine ve öğelere eklenen ışıltı efekti, "Kawase Method MGF" adlı basit bir sürüm kullanılarak uygulanır. Ancak Kawase yöntemi tüm parlak alanların çiçeklenmesini sağlarken World Wide Maze, parlaması gereken alanlar için ayrı oluşturma hedefleri oluşturuyor. Bunun nedeni, sahne dokuları için bir web sitesinin ekran görüntüsünün kullanılması ve sadece tüm parlak alanların çıkarılması, örneğin, beyaz bir arka plana sahip olması halinde web sitesinin tamamının parlamasına yol açmasıdır. Ayrıca HDR kalitesindeki her şeyi işlemeyi de düşünmüştüm. Ancak uygulamalar oldukça karmaşık olacağından bu sefer yapmamaya karar verdim.

Glow

Sol üstte, parlak alanların ayrı olarak oluşturulduğu ve ardından bulanıklaştırma uygulandığı ilk geçiş gösterilmektedir. Sağ altta, resim boyutunun% 50 azaltılıp bulanıklaştırma uygulandığı ikinci geçiş gösterilmektedir. Sağ üstte, resmin tekrar% 50 azaltılıp bulanıklaştırıldığı üçüncü geçiş gösterilmektedir. Ardından, sol altta gösterilen son birleşik resmi oluşturmak için üçü yerleştirildi. Bulanıklaştırma için üç.js'de yer alan VerticalBlurShader ve HorizontalBlurShader öğelerini kullandım; dolayısıyla, daha fazla optimizasyon yapabilirsiniz.

Yansıtıcı top

Top üzerindeki yansıma, üç.js'den bir örneğe dayanır. Tüm yönler topun konumundan elde edilir ve çevre haritaları olarak kullanılır. Ortam haritalarının top her hareket ettiğinde güncellenmesi gerekir. Ancak 60 kare/saniyede güncelleme yoğun olduğu için haritalar her üç karede bir güncellenir. Sonuç, her kareyi güncellemek kadar mükemmel olmasa da özellikle belirtilmediği sürece aradaki fark neredeyse algılanamaz.

Gölgelendirici, gölgelendirici, gölgelendirici...

WebGL, tüm oluşturma işlemleri için gölgelendiriciler (köşe gölgelendiriciler, parça gölgelendiriciler) gerektirir. üç.js'deki gölgelendiriciler zaten çok çeşitli efektlere izin verse de, daha ayrıntılı gölgelendirme ve optimizasyon için kendinizin yazılması kaçınılmazdır. World Wide Maze, fizik motoruyla CPU'yu meşgul ettiğinden, CPU işlemenin (JavaScript aracılığıyla) daha kolay olduğu durumlarda bile mümkün olduğunca çok gölgelendirme dilinde (GLSL) yazarak GPU'dan yararlanmaya çalıştım. Okyanus dalgası etkileri, hedef noktalarındaki havai fişekler ve top göründüğünde kullanılan örgü etkisi gibi, doğal olarak gölgelendiricilere dayanır.

Gölgelendirici topları

Yukarıdaki bilgiler, top göründüğünde kullanılan örgü etkisi testlerinden alınmıştır. Soldaki, 320 poligondan oluşan oyun içinde kullanılan öğedir. Ortadaki yaklaşık 5000 poligon ve sağdaki 300.000 poligon kullanılmıştır. Bu kadar çok sayıda poligon bulunsa bile gölgelendiricilerle işleme, 30 fps'lik sabit bir kare hızı sağlayabilir.

Gölgelendirici örgü

Sahne boyunca dağılmış küçük öğelerin tümü tek bir ağa entegre edilmiştir ve bireysel hareket, poligon uçlarının her birini hareket ettiren gölgelendiricilere dayanır. Bu, performansın çok sayıda nesne mevcut olduğunda etkilenip etkilenmeyeceğini anlamaya yönelik bir testtir. Burada yaklaşık 20.000 poligondan oluşan yaklaşık 5.000 nesne yerleştirilmiştir. Performansta hiçbir düşüş görülmedi.

poly2tri

Aşamalar, sunucudan alınan özet 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, üç.js tarafından kötü bir şekilde uygulanır ve genellikle başarısız olur. Bu nedenle, ben de poly2tri adlı farklı bir üçgenleme kitaplığını entegre etmeye karar verdim. Görünüşe göre, üç.js geçmişte açık bir şekilde aynı şeyi denemişti. Dolayısıyla, sadece bir bölümünü yorumlayarak çalışmasını sağladım. Sonuç olarak hata sayısı önemli ölçüde azaldı ve çok daha fazla oynanabilir aşama sağlandı. Zaman zaman ortaya çıkan hatalar devam ediyor ve bazı nedenlerle poly2tri hataları uyarı vererek işliyor. Ben de bunun yerine istisnalar atacak şekilde değişiklik yaptım.

poly2tri

Yukarıda mavi dış çizginin nasıl üçgenlendiği ve kırmızı çokgenlerin nasıl oluşturulduğu gösterilmiştir.

Anisotropik filtreleme

Standart izotropik MIP eşlemesi, görüntüleri hem yatay hem de dikey eksenlerde küçülttüğünden, poligonları eğik açılardan görüntülemek, World Wide Labirent aşamalarının en ucundaki dokuların yatay olarak uzun, düşük çözünürlüklü dokular gibi görünmesini sağlar. Bu Wikipedia sayfasında sağ üstteki görselde bunun iyi bir örneği görülmektedir. Pratikte, daha fazla yatay çözünürlük gerekir. WebGL (OpenGL), anisotropik filtreleme adı verilen bir yöntem kullanarak bunu çözer. üç.js'de, THREE.Texture.anisotropy için 1'den büyük bir değer ayarlanması anisotropik filtrelemeyi etkinleştirir. Ancak bu özellik bir uzantıdır ve tüm GPU'lar tarafından desteklenmeyebilir.

Optimize etme

Bu WebGL en iyi uygulamalarından da bahsedildiği gibi, WebGL (OpenGL) performansını iyileştirmenin en önemli yolu, çizim çağrılarını en aza indirmektir. World Wide Labirent'in ilk geliştirmesi sırasında tüm oyun içi adalar, köprüler ve gözetleme korkulukları ayrı nesnelerdi. Bu durum bazen 2.000'den fazla çekiliş çağrısına neden olarak karmaşık aşamaların kontrolsüz olmasına yol açtı. Ancak aynı türdeki nesnelerin hepsini bir ağa yerleştirdiğimde çağrıların sayısı elliye kadar düşmüştü ve performansı önemli ölçüde yükseltmişti.

Daha fazla optimizasyon için Chrome izleme özelliğini kullandım. Chrome'un Geliştirici Araçları'nda yer alan 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 1/1000'e kadar) tam olarak bildirir. İzlemenin nasıl kullanılacağıyla ilgili ayrıntılar için bu makaleye göz atın.

Optimizasyon

Yukarıda, topun yansıması için ortam haritaları oluşturulmasından elde edilen iz sonuçları verilmiştir. trip.js'de, görünüşte alakalı olan konumlara console.time ve console.timeEnd değerlerini eklemek bize aşağıdakine benzer bir grafik sağlar. Zaman soldan sağa doğru akıyor ve her katman bir çağrı yığını gibidir. console.time'ı console.time içine yerleştirmek daha ayrıntılı ölçümlere olanak tanır. Üstteki grafik optimizasyon öncesi, alt grafik ise optimizasyon sonrasıdır. Üstteki grafikte görüldüğü gibi, optimizasyon öncesi oluşturma sırasında 0-5 arasındaki her bir oluşturma için updateMatrix (kelime kısaltılmış olsa da) çağrılmıştır. Ancak, bu işlem yalnızca nesnelerin konumu veya yönü değiştirildiğinde gerekli olduğundan, yalnızca bir kez çağrılacak şekilde değiştirdim.

İzleme sürecinin kendisi doğal olarak kaynakları kullanır. Bu nedenle, console.time öğesini aşırı şekilde eklemek gerçek performanstan önemli bir sapmaya neden olarak optimizasyon için gerekli alanların tespit edilmesini zorlaştırabilir.

Performans ayarlayıcı

İnternet'in doğası nedeniyle, oyun büyük olasılıkla çok farklı spesifikasyonlara sahip sistemlerde oynanır. Şubat ayının başlarında yayınlanan Find Your Way to Oz (Oz'a Giden Yolu Bulun) kanalı, oynatmanın sorunsuz olmasını sağlamak için efektleri kare hızındaki dalgalanmalara göre ölçeklendirmek üzere IFLAutomaticPerformanceAdjust adlı bir sınıf kullanıyor. World Wide Maze, aynı IFLAutomaticPerformanceAdjust sınıfını temel alır ve oynanabilirliği olabildiğince akıcı hale getirmek için efektleri aşağıdaki sırayla ölçeklendirir:

  1. Kare hızı 45 fps'nin altına düşerse ortam haritalarının güncellenmesi durdurulur.
  2. Görüntü çözünürlüğü yine 40 fps'nin altına düşerse oluşturma çözünürlüğü %70'e (yüzey oranının% 50'si) düşürülür.
  3. Yine de 40 fps'nin altına düşerse FXAA (tam yumuşatma) kaldırılır.
  4. Video hâlâ 30 fps'nin altına düşerse parlaklık efektleri ortadan kaldırılır.

Bellek sızıntısı

Nesneleri düzgün bir şekilde ortadan kaldırmak, üç.js ile bir tür zorluk teşkil eder. Ancak bunları olduğu gibi bırakmak kesinlikle bellek sızıntısına yol açacağından aşağıdaki yöntemi tasarladım. @renderer, THREE.WebGLRenderer anlamına gelir. (3.js'nin en son revizyonunda, biraz farklı bir anlaşma konumlandırma yöntemi kullanıldığından, bu yöntem büyük olasılıkla şu anki haliyle işe yaramayacaktır.)

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

Şahsen WebGL uygulamasının en iyi yanı, HTML olarak sayfa düzeni tasarlayabilme yeteneğidir. Flash veya openFrameworks'te (OpenGL) puan ya da metin görüntüleri gibi 2D arayüzler oluşturmak biraz zahmetli. Flash'ın en azından bir IDE'si vardır, ancak buna alışkın değilseniz openFrameworks kullanmak zordur (Cocos2D gibi bir şey kullanmak bunu kolaylaştırabilir). Diğer yandan HTML, tıpkı web sitesi oluştururken olduğu gibi CSS ile tüm ön uç tasarımı özelliklerinin hassas bir şekilde denetlenmesini sağlar. Parçacıkların logoya dönüşmesi gibi karmaşık efektler imkansız olsa da CSS Dönüşümleri'nin yetenekleri dahilinde bazı 3D efektler kullanılabilir. World Wide Labirent'in "GOAL" ve "TIME IS UP" metin efektleri CSS Geçişinde ölçek kullanılarak canlandırılır (Transit ile uygulanır). (Arka plan derecelendirmeleri elbette WebGL'yi kullanır.)

Oyundaki her sayfanın (başlık, RESULT, SIRALAMA vb.) kendi HTML dosyası vardır ve bu dosyalar şablon olarak yüklendikten sonra, $(document.body).append() uygun zamanda uygun değerlerle çağrılır. Bir kesinti, fare ve klavye etkinliklerinin eklemeden önce ayarlanamamasıydı. Bu yüzden, eklemeden önce el.click (e) -> console.log(e) denemesi işe yaramadı.

Uluslararasılaştırma (i18n)

İngilizce sürüm oluşturmak için HTML'de çalışmak da kolay oldu. Uluslararasılaştırma ihtiyaçlarım için bir web i18n kitaplığı olan i18next'i kullanmayı seçtim, bu kitaplığı herhangi bir değişiklik yapmadan olduğu gibi kullanabiliyorum.

Oyun içi metinlerin düzenleme ve çevirisi Google Dokümanlar E-tablosunda yapıldı. i18next, JSON dosyaları gerektirdiğinden, e-tabloları TSV'ye aktardım ve daha sonra, özel bir dönüştürücüyle dönüştürdüm. Yayınlamadan hemen önce birçok güncelleme yaptım. Bu nedenle dışa aktarma işlemini Google Dokümanlar E-tablosundan otomatik hale getirmek işleri çok daha kolay hale getirecekti.

Sayfalar HTML ile oluşturulduğu için Chrome'un otomatik çeviri özelliği de normal bir şekilde çalışır. Ancak bazen dili doğru bir şekilde algılayamamakta, bunun yerine tamamen farklı bir dil (ör. Vietnamca) dolayısıyla bu özellik şu anda devre dışıdır. (Meta etiketler kullanılarak devre dışı bırakılabilir.)

RequireJS

JavaScript modül sistemim olarak RequireJS'yi seçtim. Oyunun 10.000 satırlık kaynak kodu yaklaşık 60 sınıfa ayrılmıştır (= kahve dosyaları) ve ayrı js dosyalarında derlenir. RequireJS, bu dosyaları bağımlılığa göre uygun sırayla yükler.

define ->
  class Hoge
    hogeMethod: ->

Yukarıda tanımlanan sınıf (hoge.coffee) aşağıdaki şekilde kullanılabilir:

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

Çalışması için hoge.js'nin moge.js'den önce yüklenmesi gerekir. Ayrıca "hoge", "define" ifadesinin ilk bağımsız değişkeni olarak belirtildiği için her zaman ilk önce hoge.js yüklenir (hoge.js yüklendikten sonra geri çağrılır). Bu mekanizmaya AMD adı verilir ve AMD'yi desteklediği sürece aynı geri çağırma türü için herhangi bir üçüncü taraf kitaplığı kullanılabilir. Bağımlılıklar önceden belirtildiği sürece, kullanmayanlar da (ör.three.js) benzer bir performans gösterecektir.

Bu, AS3'ün içe aktarılmasına benzer; dolayısıyla bu kadar da tuhaf görünmemelidir. Daha fazla bağımlı dosyanız varsa bu olası bir çözümdür.

r.js

RequireJS, r.js adlı bir optimize edici içerir. Bu işlem, ana js'yi tüm bağımlı js dosyalarıyla tek bir grupta toplar, ardından UglifyJS (veya Closure Compiler) kullanarak küçültür. Böylece, 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 optimizasyonu ile yaklaşık 1 MB'a düşürülebilir. Oyun gzip kullanılarak dağıtılabiliyorsa bu boyut daha da düşürülerek 250 KB olur. (GAE'de, 1 MB veya daha büyük gzip dosyalarının aktarılmasına izin vermeyen bir sorun vardır. Bu nedenle, oyun şu anda sıkıştırılmamış halde 1 MB'lık düz metin olarak dağıtılmıştır.)

Sahne oluşturucu

Aşama verileri aşağıdaki gibi oluşturulur ve tamamen ABD'deki GCE sunucusunda gerçekleştirilir:

  1. Aşamaya dönüştürülecek web sitesinin URL'si WebSocket aracılığıyla gönderilir.
  2. PhantomJS ekran görüntüsünü alır ve div ve img etiket konumları alınıp JSON biçiminde alınır.
  3. 2. adımdaki ekran görüntüsüne ve HTML öğelerinin konumlandırma verilerine göre, özel bir C++ (OpenCV, Boost) programı gereksiz alanları siler, adalar oluşturur, adaları köprülerle bağlar, koruma demiryolu ve öğe konumlarını hesaplar, hedef noktasını ayarlar vb. Sonuçlar JSON biçiminde çıkartılı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ükleyebilir, böylece otomatik testlerde kullanılabilir veya sunucu tarafında ekran görüntüleri yakalamak için kullanılabilir. Tarayıcı motoru, Chrome ve Safari tarafından kullanılanla aynı olan WebKit'tir. Bu nedenle, tarayıcının düzeni ve JavaScript yürütme sonuçları da standart tarayıcılarınkiyle hemen hemen aynıdır.

PhantomJS'de, yürütülmesini istediğiniz işlemleri yazmak için JavaScript veya CoffeeScript kullanılır. Bu örnekte de gösterildiği gibi ekran görüntüsü yakalamak çok kolaydır. Bir Linux sunucusunda (CentOS) çalışıyordum, Japonca görüntülemek için yazı tipleri yüklemem gerekti (M+ FONTS). O zaman da yazı tipi oluşturma işlemi Windows veya Mac OS'ta olduğundan farklı şekilde gerçekleştirilir. Dolayısıyla 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 sayfalarda olduğu gibi gerçekleştirilir. jQuery herhangi bir sorun olmadan da kullanılabilir.

stage_builder

Başlangıçta, aşama oluşturmak için daha DOM tabanlı bir yaklaşım kullanmayı düşündüm (Firefox 3D İnceleyici'ye benzer) ve PhantomJS'de DOM analizine benzer bir şey denedim. Ama sonunda bir resim işleme yaklaşımına karar verdim. Bu amaçla, OpenCV ve Boost kullanan "stage_builder" adlı bir C++ programı yazdım. Şunları gerçekleştirir:

  1. Ekran görüntüsünü ve JSON dosyalarını yükler.
  2. Resimleri ve metni "adalara" dönüştürür.
  3. Adaları birbirine bağlamak için köprüler oluşturur.
  4. Labirent oluşturmak için gereksiz köprüleri ortadan kaldırır.
  5. Büyük boyutlu öğeler yerleştirir.
  6. Küçük öğeler yerleştirir.
  7. Koruma korkulukları yerleştirir.
  8. Konumlandırma verilerini JSON biçiminde verir.

Her bir adım aşağıda ayrıntılı olarak açıklanmıştır.

Ekran görüntüsü ve JSON dosyaları yükleniyor

Ekran görüntülerini yüklemek için normal cv::imread kullanılır. JSON dosyaları için birkaç kitaplığı test ettim, ancak çalışması en kolay olanın picojson olduğunu düşündüm.

Resimleri ve metni "adalara" dönüştürme

Aşama oluşturma

Yukarıdaki, aid-dcc.com sitesinin Haberler bölümünün bir ekran görüntüsüdür (gerçek boyutu görmek için tıklayın). Resimler ve metin öğeleri adalara dönüştürülmelidir. Bu bölümleri izole etmek için beyaz arka plan rengini, diğer bir deyişle ekran görüntüsünde en sık kullanılan rengi silmemiz gerekir. Bu işlem tamamlandıktan sonra şöyle görünür:

Aşama oluşturma

Beyaz bölümler potansiyel adalardır.

Metin çok ince ve keskin olduğundan cv::dilate, cv::GaussianBlur ve cv::threshold ile kalınlaştıracağız. Resim içeriği de eksik olduğundan, bu alanları PhantomJS'deki img etiketi veri çıktısına göre beyaz renkle dolduracağız. Elde edilen görüntü aşağıdaki gibi görünür:

Aşama oluşturma

Metin artık uygun kümeler oluşturuyor ve her resim düzgün bir ada.

Adaları birbirine bağlayacak köprüler inşa etmek

Adalar hazır olduğunda köprülerle birbirine bağlanır. Her ada sol, sağ, yukarı ve aşağı bitişik adaları arar ve ardından bir köprüyü en yakın adanın en yakın noktasına bağlar. Bu da aşağıdakine benzer bir sonuç verir:

Aşama oluşturma

Labirent oluşturmak için gereksiz köprüleri ortadan kaldırın

Tüm köprüleri tutmak sahnede gezinmeyi çok kolaylaştıracağından labirent oluşturmak için bazılarının ortadan kaldırılması gerekiyor. Başlangıç noktası olarak bir ada (ör. sol üstteki ada) seçilir ve bu adaya bağlanan bir köprü dışında tümü (rastgele seçilir) silinir. Aynı şey kalan köprüyle bağlanan bir sonraki ada için de yapılır. Yol çıkmaza ulaştığında veya daha önce ziyaret edilen bir adaya geri döndüğünde yeni bir adaya giriş imkanı sunan bir noktaya geri dönüyor. Tüm adalar bu şekilde işlendikten sonra labirent tamamlanır.

Aşama oluşturma

Büyük boyutlu öğeleri yerleştirme

Boyutlarına bağlı olarak her bir adaya, adaların kenarlarından en uzaktaki noktalardan seçim yaparak bir veya daha fazla büyük öğe yerleştirilir. Çok açık olmasa da, bu noktalar aşağıda kırmızı renkle gösterilmektedir:

Aşama oluşturma

Bu olası tüm noktalardan, sol üstteki konum başlangıç noktası (kırmızı daire), sağ alttaki konum hedef (yeşil daire) olarak ayarlanır ve büyük öğe yerleşimi (mor daire) için bunlardan en fazla altı tanesi seçilir.

Küçük öğeler yerleştirme

Aşama oluşturma

Ada kenarlarından belirli mesafelerde çizgiler boyunca uygun sayıda küçük öğe yerleştirilir. Yukarıdaki resimde (aid-dcc.com adresinden değil), öngörülen yerleşim çizgileri gri renkte ve adanın kenarlarından belirli aralıklarla yerleştirilmiş olarak gösterilmektedir. Kırmızı noktalar küçük öğelerin yerleştirildiği yerleri gösterir. Bu resim, geliştirmenin ortasındaki bir sürümden geldiği için öğeler düz çizgilerle yerleştirilmiştir, ancak son sürümde gri çizgilerin her iki tarafına da öğeler biraz daha düzensiz bir şekilde dağılır.

Korkuluk yerleştirme

Barbekü rayları temelde adaların dış sınırlarına yerleştirilmiştir, ancak ulaşıma olanak tanımak için köprülerde kesilmeleri gerekir. Boost Geometri kitaplığı bu konuda fayda sağladı ve ada sınırı verilerinin köprünün her iki tarafındaki çizgilerle nerede kesiştiğini belirlemek gibi geometrik hesaplamaları basitleştirdi.

Aşama oluşturma

Adaları çevreleyen yeşil çizgiler, gözetleme raylarıdır. Bu resimde görmek zor olabilir, ancak köprülerin olduğu yerlerde yeşil çizgiler yoktur. Bu, hata ayıklama için kullanılan son görüntüdür; burada, JSON çıkışının yapılması gereken tüm nesneler dahil edilmiştir. Açık mavi noktalar küçük öğeler, gri noktalar ise önerilen yeniden başlatma noktalarıdır. Top okyanusa düştüğünde oyun en yakın yeniden başlatma noktasından devam ettirilir. Yeniden başlatma noktaları, küçük öğelerle aynı şekilde, adanın kenarından belirli bir mesafede düzenli aralıklarla düzenlenir.

Konumlandırma verilerinin JSON biçiminde çıkışı

Çıkış için de Picojson kullandım. Verileri standart çıkışa yazar ve bu çıkış, daha sonra çağrıyı yapan tarafından (Node.js) alınır.

Linux'ta çalıştırılacak bir Mac'te C++ programı oluşturma

Oyun bir Mac üzerinde geliştirilip Linux'ta dağıtıldı ancak her iki işletim sisteminde de OpenCV ve Boost mevcut olduğundan derleme ortamı kurulduktan sonra geliştirmenin kendisi zor değildi. Mac'teki derlemede hata ayıklamak için Xcode'da Komut Satırı Araçları'nı kullandım. Ardından, derlemenin Linux'ta derlenebilmesi için automake/autoconf kullanarak bir yapılandırma dosyası oluşturdum. Sonrasında, yürütülebilir dosyayı oluşturmak için Linux'ta "yapılandır && yap" komutunu kullanmam gerekti. Derleyici sürümü farklılıkları nedeniyle Linux'a özgü bazı hatalarla karşılaştım ancak bunları gdb'yi kullanarak nispeten kolay bir şekilde çözebildim.

Sonuç

Böyle bir oyun, Flash veya Unity ile geliştirilebilir ve bunun sayısız avantaj sağlar. Ancak, bu sürümde eklenti gerekmemektedir ve HTML5 + CSS3'ün düzen özelliklerinin son derece güçlü olduğu kanıtlanmıştır. Her görev için doğru araçlara sahip olmak kesinlikle önemlidir. Oyunun tamamen HTML5'te yapılmış bir oyunda bu kadar iyi sonuç vermesi beni kişisel olarak şaşırttı. Oyunda hâlâ birçok alanda eksiklikler olsa da gelecekte nasıl gelişeceğini görmek için sabırsızlanıyorum.