Studi Kasus - Inside World Wide Maze

World Wide Maze adalah game yang mengharuskan Anda menggunakan smartphone untuk menavigasi bola yang bergulir melalui labirin 3D yang dibuat dari situs untuk mencoba mencapai titik sasaran.

Labirin Luas Dunia

Game ini menggunakan banyak fitur HTML5. Misalnya, peristiwa DeviceOrientation mengambil data kemiringan dari smartphone, yang kemudian dikirim ke PC melalui WebSocket, tempat pemain menemukan jalan melalui ruang 3D yang dibuat oleh WebGL dan Web Worker.

Dalam artikel ini, saya akan menjelaskan dengan tepat cara fitur ini digunakan, keseluruhan proses pengembangan, dan poin-poin penting untuk pengoptimalan.

DeviceOrientation

Peristiwa DeviceOrientation (contoh) digunakan untuk mengambil data kemiringan dari smartphone. Saat addEventListener digunakan dengan peristiwa DeviceOrientation, callback dengan objek DeviceOrientationEvent dipanggil sebagai argumen pada interval yang teratur. Intervalnya sendiri bervariasi sesuai dengan perangkat yang digunakan. Misalnya, di iOS + Chrome dan iOS + Safari, callback dipanggil kira-kira setiap 1/20 detik, sedangkan di Android 4 + Chrome dipanggil sekitar 1/10 detik.

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

Objek DeviceOrientationEvent berisi data kemiringan untuk masing-masing sumbu X, Y, dan Z dalam derajat (bukan radian) (Baca selengkapnya tentang HTML5Rocks). Namun, nilai yang ditampilkan juga bervariasi sesuai kombinasi perangkat dan browser yang digunakan. Rentang nilai pengembalian sebenarnya tercantum dalam tabel di bawah:

Orientasi perangkat.

Nilai pada bagian atas yang disorot dengan warna biru adalah nilai yang didefinisikan dalam spesifikasi W3C. Yang disorot dengan warna hijau sesuai dengan spesifikasi ini, sementara yang disorot dengan warna merah menyimpang. Anehnya, hanya kombinasi Android-Firefox yang menampilkan nilai yang sesuai dengan spesifikasi. Meskipun demikian, lebih masuk akal untuk mengakomodasi nilai-nilai yang sering terjadi. Oleh karena itu, World Wide Maze menggunakan nilai return iOS sebagai standar dan melakukan penyesuaian untuk perangkat Android sebagaimana mestinya.

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

Namun, perangkat ini masih tidak mendukung Nexus 10. Meskipun Nexus 10 menampilkan rentang nilai yang sama dengan perangkat Android lainnya, ada bug yang membalikkan nilai beta dan gamma. Hal ini akan ditangani secara terpisah. (Mungkin secara default disetel ke orientasi lanskap?)

Seperti yang ditunjukkan, meskipun API yang melibatkan perangkat fisik telah menetapkan spesifikasi, tidak ada jaminan bahwa nilai yang ditampilkan akan sesuai dengan spesifikasi tersebut. Oleh karena itu, sangatlah penting untuk mengujinya di semua calon perangkat. Hal ini juga berarti bahwa nilai yang tidak diharapkan mungkin dimasukkan, sehingga memerlukan pembuatan solusi. World Wide Maze meminta pemain pemula untuk mengkalibrasi perangkat mereka sebagai langkah 1 dalam tutorial, tetapi tidak akan melakukan kalibrasi ke posisi nol dengan tepat jika menerima nilai kemiringan yang tidak terduga. Oleh karena itu, aplikasi memiliki batas waktu internal dan meminta pemutar untuk beralih ke kontrol keyboard jika kalibrasi tidak dapat dilakukan dalam batas waktu tersebut.

WebSocket

Di World Wide Maze, smartphone dan PC Anda terhubung melalui WebSocket. Lebih tepatnya, perangkat terhubung melalui server relai di antara perangkat tersebut, yaitu ponsel cerdas ke server ke PC. Ini karena WebSocket tidak memiliki kemampuan untuk menghubungkan browser satu sama lain secara langsung. (Menggunakan saluran data WebRTC memungkinkan konektivitas peer-to-peer dan menghilangkan kebutuhan akan server relai, tetapi pada saat penerapan, metode ini hanya dapat digunakan dengan Chrome Canary dan Firefox Nightly.)

Saya memilih untuk mengimplementasikan menggunakan library yang disebut Socket.IO (v0.9.11), yang menyertakan fitur untuk menghubungkan kembali jika waktu tunggu koneksi atau pemutusan koneksi. Saya menggunakannya bersama dengan NodeJS, karena kombinasi NodeJS + Socket.IO ini menunjukkan kinerja sisi server terbaik dalam beberapa pengujian implementasi WebSocket.

Menyambungkan dengan angka

  1. PC Anda terhubung ke server.
  2. Server memberikan angka yang dibuat secara acak ke PC Anda dan mengingat kombinasi angka dan PC.
  3. Dari perangkat seluler, tentukan nomor telepon dan hubungkan ke server.
  4. Jika nomor yang ditentukan sama dengan nomor dari PC yang terhubung, perangkat seluler Anda akan disambungkan dengan PC tersebut.
  5. Error akan terjadi jika tidak ada PC yang ditentukan.
  6. Saat masuk dari perangkat seluler, data dikirim ke PC yang tersambung dengan data tersebut, dan sebaliknya.

Anda juga dapat membuat koneksi awal dari perangkat seluler. Dalam hal ini, perangkat hanya akan dibalik.

Sinkronisasi Tab

Fitur Sinkronisasi Tab khusus Chrome membuat proses penyambungan lebih mudah. Dengan layanan ini, halaman yang terbuka di PC dapat dibuka di perangkat seluler dengan mudah (dan sebaliknya). PC mengambil nomor koneksi yang dikeluarkan oleh server dan menambahkannya ke URL halaman menggunakan history.replaceState.

history.replaceState(null, null, '/maze/' + connectionNumber)

Jika Sinkronisasi Tab diaktifkan, URL akan disinkronkan setelah beberapa detik dan halaman yang sama dapat dibuka di perangkat seluler. Perangkat seluler memeriksa URL halaman yang terbuka, dan jika angka ditambahkan, nomor akan langsung terhubung. Dengan fitur ini, Anda tidak perlu memasukkan nomor secara manual atau memindai kode QR dengan kamera.

Latensi

Karena server relai berlokasi di AS, mengaksesnya dari Jepang akan menghasilkan penundaan sekitar 200 md sebelum data kemiringan smartphone mencapai PC. Waktu respons jelas lambat dibandingkan dengan lingkungan lokal yang digunakan selama pengembangan, tetapi memasukkan sesuatu seperti filter low-pass (saya menggunakan EMA) membuatnya lebih ringan ke tingkat yang tidak mengganggu. (Dalam praktiknya, filter low-pass juga diperlukan untuk tujuan presentasi; nilai yang dikembalikan dari sensor kemiringan mencakup sejumlah besar noise, dan menerapkan nilai-nilai tersebut ke layar sebagaimana mengakibatkan banyak guncangan.) Cara ini tidak berhasil untuk lompatan, yang jelas-jelas lambat, tetapi tidak ada yang dapat dilakukan untuk mengatasi masalah ini.

Karena saya memperkirakan adanya masalah latensi sejak awal, saya mempertimbangkan untuk menyiapkan server relai di seluruh dunia agar klien dapat terhubung ke jaringan terdekat yang tersedia (sehingga meminimalkan latensi). Namun, saya akhirnya menggunakan Google Compute Engine (GCE), yang hanya ada di AS pada saat itu, jadi hal ini tidak mungkin dilakukan.

Soal Algoritma Nagle

Algoritma Nagle biasanya disertakan ke dalam sistem operasi untuk komunikasi yang efisien dengan buffering pada tingkat TCP, tetapi saya mendapati bahwa saya tidak dapat mengirim data secara real time saat algoritma ini diaktifkan. (Khususnya jika digabungkan dengan TCP tertunda konfirmasi. Meskipun tidak ada ACK yang tertunda, masalah yang sama terjadi jika ACK tertunda hingga tingkat tertentu karena faktor seperti server yang berlokasi di luar negeri.)

Masalah latensi Nagle tidak terjadi pada WebSocket di Chrome untuk Android, yang menyertakan opsi TCP_NODELAY untuk menonaktifkan Nagle, tetapi masalah terjadi pada WebKit WebSocket yang digunakan di Chrome untuk iOS, yang tidak mengaktifkan opsi ini. (Safari, yang menggunakan WebKit yang sama, juga mengalami masalah ini. Masalah ini dilaporkan ke Apple melalui Google dan tampaknya telah diselesaikan dalam versi pengembangan WebKit.

Ketika masalah ini terjadi, kemiringan data yang dikirim setiap 100 md digabungkan menjadi potongan yang hanya mencapai PC setiap 500 md. Game tidak dapat berfungsi dalam kondisi tersebut, jadi menghindari latensi ini dengan meminta sisi server mengirimkan data dengan interval pendek (setiap 50 md atau lebih). Saya yakin bahwa menerima ACK dalam interval pendek akan mengecoh algoritma Nagle sehingga menganggap bahwa data boleh dikirim.

Algoritma nagle 1

Grafik di atas menggambarkan interval data aktual yang diterima. Ini menunjukkan interval waktu antar paket; warna hijau mewakili interval output dan merah mewakili interval input. Minimum 54 ms, maksimum 158 ms, dan tengah adalah 100 ms. Di sini saya menggunakan iPhone dengan server relai yang berlokasi di Jepang. Output dan input berjarak sekitar 100 md, dan operasi berjalan lancar.

Algoritma nagle 2

Sebaliknya, grafik ini menunjukkan hasil penggunaan server di AS. Sementara interval output hijau stabil pada 100 md, interval input berfluktuasi antara posisi terendah 0 md dan tertinggi 500 md, yang menunjukkan bahwa PC menerima data dalam potongan.

ALT_TEXT_HERE

Terakhir, grafik ini menunjukkan hasil menghindari latensi dengan meminta server mengirimkan data placeholder. Meskipun tidak berperforma sebaik penggunaan server Jepang, jelas bahwa interval input tetap relatif stabil di sekitar 100 milidetik.

Serangga?

Meskipun browser default di Android 4 (ICS) memiliki WebSocket API, browser tersebut tidak dapat terhubung, sehingga menghasilkan peristiwa Socket.IO connect_failed. Waktu habis secara internal, dan sisi server juga tidak dapat memverifikasi koneksi. (Saya belum mengujinya dengan WebSocket saja, jadi ini bisa menjadi masalah Socket.IO.)

Menskalakan server relai

Karena peran server relai tidak terlalu rumit, peningkatan skala dan jumlah server seharusnya tidak sulit selama Anda memastikan bahwa PC dan perangkat seluler yang sama selalu terhubung ke server yang sama.

Fisika

Gerakan bola dalam game (berguling menurun, bertabrakan dengan tanah, bertabrakan dengan dinding, mengumpulkan item, dll.) semuanya dilakukan dengan simulator fisika 3D. Saya menggunakan Ammo.js—port mesin fisika Bullet yang banyak digunakan ke JavaScript menggunakan Emscripten—beserta Physijs untuk menggunakannya sebagai "Web Worker".

Web Worker

Web Workers adalah API untuk menjalankan JavaScript di thread terpisah. JavaScript yang diluncurkan sebagai Web Worker berjalan sebagai thread yang terpisah dari yang awalnya menyebutnya, sehingga tugas-tugas berat dapat dilakukan sambil menjaga halaman tetap responsif. Physijs menggunakan Web Worker secara efisien untuk membantu mesin fisika 3D yang biasanya intensif berjalan lancar. World Wide Maze menangani mesin fisika dan rendering gambar WebGL dengan kecepatan frame yang benar-benar berbeda, sehingga meskipun kecepatan frame turun pada mesin berspesifikasi rendah karena beban rendering WebGL yang berat, mesin fisika itu sendiri akan kurang lebih mempertahankan 60 fps dan tidak mengganggu kontrol game.

FPS

Gambar ini menunjukkan kecepatan frame yang dihasilkan pada Lenovo G570. Kotak atas menunjukkan kecepatan frame untuk WebGL (rendering gambar), dan kotak yang lebih rendah menampilkan kecepatan frame untuk mesin fisika. GPU merupakan chip Intel HD Graphics 3000 terintegrasi, sehingga kecepatan frame rendering gambar tidak mencapai 60 fps yang diharapkan. Namun, karena mesin fisika mencapai kecepatan frame yang diharapkan, gameplay-nya tidak jauh berbeda dengan performa pada mesin dengan spesifikasi tinggi.

Karena thread dengan Web Worker aktif tidak memiliki objek konsol, data harus dikirim ke thread utama melalui postMessage untuk menghasilkan log proses debug. Penggunaan console4Worker akan membuat objek konsol yang setara di Worker, sehingga proses debug menjadi jauh lebih mudah.

Service worker

Chrome versi terbaru memungkinkan Anda menyetel titik henti sementara saat meluncurkan Web Workers, yang juga berguna untuk proses debug. Ini dapat ditemukan di panel "Pekerja" pada Developer Tools.

Performa

Tahapan dengan jumlah poligon yang tinggi terkadang melebihi 100.000 poligon, tetapi performa tidak terlalu terpengaruh bahkan saat dibuat sepenuhnya sebagai Physijs.ConcaveMesh (btBvhTriangleMeshShape dalam Bullet).

Awalnya, kecepatan frame menurun karena jumlah objek yang memerlukan deteksi tabrakan meningkat, tetapi menghilangkan pemrosesan yang tidak perlu di Physijs telah meningkatkan performa. Peningkatan ini dilakukan pada fork Physijs asli.

Objek hantu

Objek yang memiliki deteksi tabrakan, tetapi tidak berdampak pada tabrakan, sehingga tidak berpengaruh pada objek lain disebut "objek hantu" di Bullet. Meskipun Physijs tidak secara resmi mendukung objek ghost, Anda dapat membuatnya di sana dengan mengotak-atik flag setelah membuat Physijs.Mesh. World Wide Maze menggunakan benda hantu untuk mendeteksi tabrakan item dan titik sasaran.

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

Untuk collision_flags, 1 adalah CF_STATIC_OBJECT, dan 4 adalah CF_NO_CONTACT_RESPONSE. Coba telusuri forum, Stack Overflow, atau dokumentasi Bullet untuk informasi selengkapnya. Karena Physijs adalah wrapper untuk Ammo.js dan Ammo.js pada dasarnya identik dengan Bullet, sebagian besar hal yang dapat dilakukan di Bullet juga dapat dilakukan di Physijs.

Masalah Firefox 18

Pembaruan Firefox dari versi 17 ke 18 mengubah cara Web Worker bertukar data, dan akibatnya Physijs berhenti berfungsi. Masalah ini dilaporkan di GitHub dan diatasi setelah beberapa hari. Meskipun efisiensi open source ini membuat saya terkesan, insiden tersebut juga mengingatkan saya bahwa World Wide Maze terdiri dari beberapa framework open source yang berbeda. Saya menulis artikel ini dengan harapan dapat memberikan semacam masukan.

asm.js

Meskipun ini tidak menyangkut World Wide Maze secara langsung, Ammo.js sudah mendukung asm.js Mozilla yang baru-baru ini diumumkan (tidak mengherankan karena asm.js pada dasarnya dibuat untuk mempercepat JavaScript yang dihasilkan oleh Emscripten, dan kreator Emscripten juga merupakan kreator Ammo.js). Jika Chrome juga mendukung asm.js, beban komputasi mesin fisika akan berkurang secara signifikan. Kecepatan terasa lebih cepat saat diuji dengan Firefox Nightly. Mungkin yang terbaik adalah menulis bagian yang memerlukan kecepatan lebih tinggi pada C/C++ lalu mem-portnya ke JavaScript menggunakan Emscripten?

WebGL

Untuk implementasi WebGL, saya menggunakan library yang paling aktif dikembangkan, three.js (r53). Meskipun revisi 57 sudah dirilis pada tahap pengembangan terakhir, perubahan besar telah dilakukan pada API, jadi saya berhenti dengan revisi asli untuk rilis.

Efek glow

Efek glow yang ditambahkan ke inti bola dan ke item diterapkan menggunakan versi sederhana dari apa yang disebut "Kawase Method MGF". Namun, sementara Metode Kawase membuat semua area yang terang menjadi jelas, World Wide Maze membuat target render terpisah untuk area yang perlu menyala. Ini karena screenshot situs harus digunakan untuk tekstur tahap, dan hanya dengan mengekstrak semua area terang akan membuat seluruh situs bercahaya jika, misalnya, memiliki latar belakang putih. Saya juga mempertimbangkan untuk memproses semuanya dalam HDR, tetapi saya memutuskan untuk tidak memprosesnya kali ini karena penerapan menjadi cukup rumit.

Kilau

Kiri atas menunjukkan penerusan pertama, dengan area glow dirender secara terpisah lalu blur diterapkan. Kanan bawah menunjukkan tahap kedua, dengan ukuran gambar diperkecil 50% lalu diterapkan blur. Kanan atas menunjukkan tahap ketiga, dengan gambar dikurangi lagi sebesar 50% lalu diburamkan. Ketiganya kemudian di-overlay untuk membuat gambar gabungan akhir yang ditampilkan di kiri bawah. Untuk pemburaman, saya menggunakan VerticalBlurShader dan HorizontalBlurShader, yang disertakan dalam Three.js, sehingga masih ada ruang untuk pengoptimalan lebih lanjut.

Bola reflektif

Refleksi pada bola didasarkan pada contoh dari Three.js. Semua arah dirender dari posisi bola dan digunakan sebagai peta lingkungan. Peta lingkungan perlu diperbarui setiap kali bola bergerak, tetapi karena memperbarui pada 60 fps intensif, peta diperbarui setiap tiga frame. Hasilnya tidak semulus memperbarui setiap frame, tetapi perbedaannya hampir tidak terlihat kecuali ditunjukkan.

Shader, shader, shader...

WebGL memerlukan shader (shader vertex, shader fragmen) untuk semua rendering. Meskipun shader yang disertakan dalam Three.js sudah memungkinkan berbagai efek, menulis sendiri tidak dapat dihindari untuk bayangan dan pengoptimalan yang lebih rumit. Karena World Wide Maze membuat CPU tetap sibuk dengan mesin fisiknya, saya mencoba menggunakan GPU dengan menulis sebanyak mungkin dalam bahasa shading (GLSL), meskipun pemrosesan CPU (melalui JavaScript) akan lebih mudah. Efek gelombang laut bergantung pada shader, secara alami, seperti halnya kembang api pada titik sasaran dan efek mesh yang digunakan saat bola muncul.

Bola shader

Hal di atas berasal dari pengujian efek mesh yang digunakan saat bola muncul. Ikon di sebelah kiri adalah yang digunakan dalam game, terdiri dari 320 poligon. Yang di tengah menggunakan sekitar 5.000 poligon, dan yang di sebelah kanan menggunakan sekitar 300.000 poligon. Meskipun poligon sebanyak ini, pemrosesan dengan shader dapat mempertahankan kecepatan frame tetap sebesar 30 fps.

Mesh shader

Item-item kecil yang tersebar di seluruh bidang terintegrasi dalam satu mesh, dan setiap gerakan bergantung pada shader yang memindahkan setiap ujung poligon. Ini adalah pengujian untuk melihat apakah performa akan menurun jika terdapat banyak objek. Sekitar 5.000 objek diletakkan di sini, terdiri dari sekitar 20.000 poligon. Kinerja tidak menurun sama sekali.

poly2tri

Tahapan dibentuk berdasarkan informasi garis batas yang diterima dari server, kemudian dipoligon oleh JavaScript. Triangulasi, bagian penting dari proses ini, diterapkan dengan buruk oleh Three.js dan biasanya gagal. Oleh karena itu, saya memutuskan untuk mengintegrasikan library triangulasi lain yang disebut poly2tri sendiri. Ternyata, tiga.js terbukti pernah mencoba hal yang sama, jadi saya membuatnya berfungsi cukup dengan mengomentari sebagian. Hasilnya, error menurun secara signifikan, sehingga memungkinkan lebih banyak tahapan yang dapat diputar. Error sesekali tetap ada, dan karena alasan tertentu poly2tri menangani error dengan mengeluarkan pemberitahuan, jadi saya mengubahnya untuk menampilkan pengecualian.

poly2tri

Di atas menunjukkan cara garis luar biru ditriangulasi dan poligon merah dihasilkan.

Penyaringan anisotropik

Karena pemetaan MIP isotropik standar menurunkan ukuran gambar pada sumbu horizontal dan vertikal, melihat poligon dari sudut miring akan membuat tekstur di ujung jauh tahapan World Wide Maze terlihat seperti tekstur memanjang secara horizontal dengan resolusi rendah. Gambar di kanan atas halaman Wikipedia ini menunjukkan contoh yang bagus tentang hal ini. Dalam praktiknya, diperlukan resolusi yang lebih horizontal, yang diselesaikan oleh WebGL (OpenGL) menggunakan metode yang disebut penyaringan anisotropik. Di Three.js, menyetel nilai yang lebih besar dari 1 untuk THREE.Texture.anisotropy akan mengaktifkan pemfilteran anisotropik. Namun, fitur ini merupakan ekstensi dan mungkin tidak didukung oleh semua GPU.

Pengoptimalan

Seperti yang juga disebutkan dalam praktik terbaik WebGL ini, cara paling penting untuk meningkatkan performa WebGL (OpenGL) adalah dengan meminimalkan panggilan gambar. Selama pengembangan awal World Wide Maze, semua pulau, jembatan, dan rel penjaga dalam game adalah objek yang terpisah. Hal ini terkadang menghasilkan lebih dari 2.000 panggilan gambar, sehingga membuat stage yang kompleks menjadi sulit. Namun, setelah saya mengemas jenis objek yang sama semuanya ke dalam satu mesh, panggilan gambar turun menjadi lima puluh atau lebih, yang meningkatkan performa secara signifikan.

Saya menggunakan fitur pelacakan Chrome untuk pengoptimalan lebih lanjut. Profiler yang disertakan dalam Developer Tools Chrome dapat menentukan waktu pemrosesan metode secara keseluruhan hingga batas tertentu, tetapi perekaman dapat memberi tahu Anda dengan tepat berapa lama waktu yang dibutuhkan setiap bagian, hingga 1/1000 detik. Lihat artikel ini untuk mengetahui detail tentang cara menggunakan perekaman aktivitas.

Pengoptimalan

Di atas adalah hasil rekaman aktivitas dari pembuatan peta lingkungan untuk pantulan bola. Menyisipkan console.time dan console.timeEnd ke lokasi yang tampaknya relevan di Three.js akan memberi kita grafik yang terlihat seperti ini. Waktu mengalir dari kiri ke kanan, dan setiap {i>layer<i} adalah sesuatu seperti stack panggilan. Menyusun bertingkat console.time dalam console.time memungkinkan pengukuran lebih lanjut. Grafik atas adalah pra-pengoptimalan dan bagian bawah adalah pengoptimalan pasca-pengoptimalan. Seperti yang ditunjukkan pada grafik di atas, updateMatrix (meskipun kata terpotong) dipanggil untuk setiap render 0-5 selama pra-pengoptimalan. Saya mengubahnya sehingga hanya dipanggil sekali, karena proses ini hanya diperlukan saat objek berubah posisi atau orientasi.

Proses pelacakan itu sendiri menghabiskan resource secara alami, sehingga menyisipkan console.time secara berlebihan dapat menyebabkan penyimpangan signifikan dari performa sebenarnya, sehingga sulit untuk menentukan area yang akan dioptimalkan.

Penyesuai performa

Karena sifat dari Internet, game kemungkinan akan dimainkan di sistem dengan spesifikasi yang sangat beragam. Find Your Way to Oz, yang dirilis pada awal Februari, menggunakan class yang disebut IFLAutomaticPerformanceAdjust untuk mengurangi efek sesuai dengan fluktuasi kecepatan frame, sehingga membantu memastikan pemutaran yang lancar. World Wide Maze dibuat di class IFLAutomaticPerformanceAdjust yang sama dan mengurangi efek dalam urutan berikut untuk membuat gameplay selancar mungkin:

  1. Jika kecepatan frame turun di bawah 45 fps, peta lingkungan akan berhenti diupdate.
  2. Jika masih turun di bawah 40 fps, resolusi rendering akan dikurangi menjadi 70% (50% dari rasio permukaan).
  3. Jika masih turun di bawah 40 fps, FXAA (anti-aliasing) akan dihapus.
  4. Jika masih turun di bawah 30 fps, efek glow akan dihilangkan.

Kebocoran memori

Menghilangkan objek dengan rapi agak merepotkan dengan Three.js. Namun membiarkannya saja jelas akan menyebabkan kebocoran memori, jadi saya merancang metode di bawah ini. @renderer mengacu pada THREE.WebGLRenderer. (Revisi terbaru Three.js menggunakan metode dealokasi yang sedikit berbeda, jadi metode ini mungkin tidak akan berfungsi sebagaimana mestinya.)

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

Secara pribadi, saya pikir hal terbaik dari aplikasi WebGL adalah kemampuan untuk mendesain tata letak laman dalam HTML. Membangun antarmuka 2D seperti skor atau tampilan teks di Flash atau openFrameworks (OpenGL) agak merepotkan. Flash setidaknya memiliki IDE, tetapi openFrameworks sulit jika Anda tidak terbiasa (menggunakan sesuatu seperti Cocos2D mungkin membuatnya lebih mudah). Di sisi lain, HTML memungkinkan kontrol yang tepat atas semua aspek desain frontend dengan CSS, sama seperti saat membuat situs. Meskipun efek kompleks seperti partikel yang memadat menjadi logo tidak mungkin dilakukan, beberapa efek 3D dalam kemampuan Transformasi CSS dapat dilakukan. Efek teks "GOAL" dan "TIME IS UP" World Wide Maze dianimasikan menggunakan skala dalam Transisi CSS (diterapkan dengan Transit). (Jelas gradasi latar belakang menggunakan WebGL.)

Setiap halaman di game (judul, HASIL, PERINGKAT, dll.) memiliki file HTML sendiri, dan setelah dimuat sebagai template, $(document.body).append() dipanggil dengan nilai yang sesuai pada waktu yang tepat. Salah satu kendalanya adalah peristiwa mouse dan keyboard tidak dapat ditetapkan sebelum menambahkan, sehingga mencoba el.click (e) -> console.log(e) sebelum menambahkan tidak berhasil.

Internasionalisasi (i18n)

Bekerja dalam HTML juga nyaman untuk membuat versi bahasa Inggris. Saya memilih untuk menggunakan i18next, library i18n web, untuk kebutuhan internasionalisasi saya, yang dapat saya gunakan sebagaimana adanya tanpa modifikasi.

Pengeditan dan terjemahan teks dalam game dilakukan di Spreadsheet Google Dokumen. Karena i18next memerlukan file JSON, saya mengekspor spreadsheet ke TSV, lalu mengonversinya dengan pengonversi kustom. Saya membuat banyak pembaruan sebelum rilis, jadi mengotomatiskan proses ekspor dari Spreadsheet Google Dokumen akan membuat segalanya lebih mudah.

Fitur terjemahan otomatis Chrome juga berfungsi seperti biasa karena halaman dibuat dengan HTML. Namun, terkadang gagal mendeteksi bahasa dengan benar, alih-alih salah mengenalinya sebagai bahasa yang sama sekali berbeda (misalnya, Vietnam), jadi fitur ini saat ini dinonaktifkan. (Dapat dinonaktifkan menggunakan tag meta.)

RequireJS

Saya memilih RequireJS sebagai sistem modul JavaScript saya. 10.000 baris kode sumber permainan dibagi menjadi sekitar 60 kelas (= file kopi) dan dikompilasi menjadi file js individual. RequireJS memuat masing-masing file dalam urutan yang tepat berdasarkan dependensi.

define ->
  class Hoge
    hogeMethod: ->

Class yang ditentukan di atas (hoge.coffee) dapat digunakan sebagai berikut:

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

Agar berfungsi, hoge.js harus dimuat sebelum moge.js, dan karena "hoge" ditetapkan sebagai argumen pertama untuk "define", hoge.js selalu dimuat terlebih dahulu (dipanggil kembali setelah hoge.js selesai dimuat). Mekanisme ini disebut AMD, dan library pihak ketiga mana pun dapat digunakan untuk jenis callback yang sama selama mendukung AMD. Bahkan yang tidak memiliki performa yang sama (misalnya, Three.js) akan memiliki performa yang sama selama ependensi ditentukan di awal.

Hal ini mirip dengan mengimpor AS3, jadi seharusnya tidak terlalu aneh. Jika pada akhirnya Anda memiliki file yang lebih bergantung, ini adalah solusi yang memungkinkan.

r.js

RequireJS menyertakan pengoptimal yang disebut r.js. Ini menggabungkan js utama dengan semua file js dependen menjadi satu, lalu meminifikasinya menggunakan UglifyJS (atau Closure Compiler). Hal ini mengurangi jumlah file dan jumlah total data yang perlu dimuat browser. Total ukuran file JavaScript untuk World Wide Maze adalah sekitar 2 MB dan dapat dikurangi menjadi sekitar 1 MB dengan optimasi r.js. Jika game dapat didistribusikan menggunakan gzip, ukuran ini akan dikurangi menjadi 250 KB. (GAE memiliki masalah yang tidak memungkinkan transmisi file gzip berukuran 1 MB atau lebih besar, sehingga saat ini game didistribusikan tanpa dikompresi sebagai 1 MB teks biasa.)

Stage builder

Data tahap dibuat sebagai berikut, yang dilakukan sepenuhnya di server GCE di AS:

  1. URL situs yang akan dikonversi menjadi stage dikirim melalui WebSocket.
  2. PhantomJS mengambil screenshot, lalu posisi tag div dan img diambil dan dihasilkan dalam format JSON.
  3. Berdasarkan tangkapan layar dari langkah 2 dan data pemosisian elemen HTML, program C++ (OpenCV, Boost) khusus menghapus area yang tidak perlu, menghasilkan pulau, menghubungkan pulau dengan jembatan, menghitung posisi pagar pengaman dan item, menetapkan titik sasaran, dll. Hasilnya berupa output dalam format JSON dan dikembalikan ke browser.

PhantomJS

PhantomJS adalah browser yang tidak membutuhkan layar. Layanan ini dapat memuat halaman web tanpa membuka jendela, sehingga dapat digunakan dalam pengujian otomatis atau untuk mengambil screenshot di sisi server. Mesin browsernya adalah WebKit, yang sama dengan yang digunakan oleh Chrome dan Safari, sehingga tata letak dan hasil eksekusi JavaScript-nya juga kurang lebih sama dengan browser standar.

Dengan PhantomJS, JavaScript atau CoffeeScript digunakan untuk menulis proses yang ingin Anda jalankan. Mengambil screenshot sangat mudah, seperti yang ditunjukkan dalam contoh ini. Saya mengerjakan server Linux (CentOS), jadi saya perlu menginstal font untuk menampilkan bahasa Jepang (M+ FONTS). Meski begitu, rendering font ditangani secara berbeda dibandingkan di Windows atau Mac OS, sehingga font yang sama dapat terlihat berbeda di komputer lain (meskipun perbedaannya minimal).

Pengambilan posisi tag img dan div pada dasarnya ditangani dengan cara yang sama seperti di halaman standar. jQuery juga dapat digunakan tanpa masalah.

stage_builder

Awalnya saya mempertimbangkan untuk menggunakan pendekatan yang lebih berbasis DOM untuk membuat tahapan (mirip dengan Firefox 3D Inspector) dan mencoba sesuatu seperti analisis DOM di PhantomJS. Namun pada akhirnya, saya memilih pendekatan pemrosesan gambar. Untuk tujuan ini saya menulis program C++ yang menggunakan OpenCV dan Boost yang disebut "stage_builder". Fungsi ini akan melakukan hal berikut:

  1. Memuat screenshot dan file JSON.
  2. Mengonversi gambar dan teks menjadi "pulau".
  3. Membuat jembatan untuk menghubungkan pulau.
  4. Menghilangkan jembatan yang tidak diperlukan untuk membuat labirin.
  5. Menempatkan item berukuran besar.
  6. Menempatkan item-item kecil.
  7. Pagar pengaman tempat.
  8. Menghasilkan data posisi dalam format JSON.

Setiap langkah dijelaskan di bawah ini.

Memuat screenshot dan file JSON

cv::imread yang biasa digunakan untuk memuat screenshot. Saya telah menguji beberapa library untuk file JSON, tetapi tampaknya picojson adalah yang paling mudah digunakan.

Mengonversi gambar dan teks menjadi "pulau"

Build panggung

Gambar di atas adalah screenshot bagian Berita dari aid-dcc.com (klik untuk melihat ukuran sebenarnya). Gambar dan elemen teks harus dikonversi menjadi pulau. Untuk mengisolasi bagian ini, kita harus menghapus warna latar belakang putih—dengan kata lain warna yang paling umum di screenshot. Tampilannya akan terlihat seperti berikut setelah prosesnya selesai:

Build panggung

Bagian putih adalah pulau potensial.

Teks terlalu halus dan tajam, jadi kita akan menebalnya dengan cv::dilate, cv::GaussianBlur, dan cv::threshold. Konten gambar juga tidak ada, jadi kita akan mengisi area tersebut dengan warna putih, berdasarkan output data tag img dari PhantomJS. Gambar yang dihasilkan akan terlihat seperti ini:

Build panggung

Teks sekarang membentuk rumpun-rumpun yang sesuai, dan setiap gambar adalah pulau yang sesuai.

Membuat jembatan untuk menghubungkan pulau

Setelah siap, pulau akan dihubungkan dengan jembatan. Setiap pulau mencari pulau yang berdekatan di kiri, kanan, atas, dan bawah, lalu menghubungkan jembatan ke titik terdekat dari pulau terdekat, sehingga menghasilkan sesuatu seperti ini:

Build panggung

Menghilangkan jembatan yang tidak diperlukan untuk membuat labirin

Menyimpan semua jembatan akan membuat panggung terlalu mudah dinavigasi, sehingga beberapa jembatan harus dihilangkan untuk membuat labirin. Satu pulau (mis., pulau di kiri atas) dipilih sebagai titik awal, dan semua kecuali satu jembatan (dipilih secara acak) yang terhubung ke pulau tersebut akan dihapus. Kemudian hal yang sama dilakukan untuk pulau berikutnya yang dihubungkan oleh jembatan yang tersisa. Setelah jalan buntu mencapai jalan buntu atau mengarah kembali ke pulau yang pernah dikunjungi, jalan tersebut akan kembali ke titik yang memungkinkan akses ke pulau baru. Labirin selesai setelah semua pulau diproses dengan cara ini.

Build panggung

Meletakkan barang yang besar

Satu atau beberapa benda besar ditempatkan di setiap pulau tergantung dimensinya, yang dipilih dari titik terjauh dari tepi pulau. Meskipun tidak terlalu jelas, poin-poin ini ditunjukkan dengan warna merah di bawah ini:

Build panggung

Dari semua kemungkinan titik ini, titik di kiri atas ditetapkan sebagai titik awal (lingkaran merah), titik di kanan bawah ditetapkan sebagai sasaran (lingkaran hijau), dan maksimal enam titik lainnya dipilih untuk penempatan item besar (lingkaran ungu).

Meletakkan barang-barang kecil

Build panggung

Sejumlah item kecil yang sesuai ditempatkan di sepanjang garis pada jarak yang ditentukan dari tepi pulau. Gambar di atas (bukan dari aid-dcc.com) menunjukkan garis penempatan yang diproyeksikan dalam warna abu-abu, offset, dan ditempatkan secara berkala dari tepi pulau. Titik merah menunjukkan lokasi item kecil. Karena gambar ini berasal dari versi pertengahan pengembangan, item diletakkan dalam garis lurus, tetapi versi terakhir menyebarkan item sedikit lebih tidak teratur ke kedua sisi garis abu-abu.

Menempatkan pagar pengaman

Rel pengaman pada dasarnya ditempatkan di sepanjang batas-batas luar pulau tetapi harus diputus di jembatan untuk memungkinkan akses. Library Geometri Boost terbukti bermanfaat untuk hal ini, dengan menyederhanakan penghitungan geometri seperti menentukan tempat data batas pulau yang berpotongan dengan garis di kedua sisi jembatan.

Build panggung

Garis hijau yang menguraikan pulau-pulau tersebut merupakan pagar pengaman. Mungkin sulit untuk melihat gambar ini, tetapi tidak ada garis hijau di tempat jembatan berada. Ini adalah image akhir yang digunakan untuk proses debug, tempat semua objek yang perlu menjadi output ke JSON disertakan. Titik biru muda adalah item kecil, dan titik abu-abu diusulkan sebagai titik memulai ulang. Saat bola jatuh ke laut, permainan dilanjutkan dari titik memulai ulang terdekat. Titik mulai ulang disusun kurang lebih seperti item kecil, dengan interval teratur pada jarak yang ditentukan dari tepi pulau.

Menghasilkan data pemosisian dalam format JSON

Saya juga menggunakan {i>picojson<i} untuk {i>output<i}. Fungsi ini menulis data ke output standar, yang kemudian diterima oleh pemanggil (Node.js).

Membuat program C++ di Mac untuk dijalankan di Linux

Game ini dikembangkan di Mac dan di-deploy di Linux, tetapi karena OpenCV dan Boost ada untuk kedua sistem operasi, pengembangan itu sendiri tidak sulit setelah lingkungan kompilasi didirikan. Saya menggunakan Alat Baris Perintah di Xcode untuk men-debug build di Mac, lalu membuat file konfigurasi menggunakan automake/autoconf sehingga build dapat dikompilasi di Linux. Kemudian saya perlu menggunakan "{i>configure && make<i}" di Linux untuk membuat file yang dapat dieksekusi. Saya mengalami beberapa bug khusus Linux karena perbedaan versi compiler, tetapi mampu menyelesaikannya dengan relatif mudah menggunakan PendingIntent.

Kesimpulan

Game seperti ini dapat dibuat dengan Flash atau Unity, yang akan memberikan banyak keuntungan. Namun, versi ini tidak memerlukan plugin, dan fitur tata letak HTML5 + CSS3 terbukti sangat kuat. Sangat penting untuk memiliki alat yang tepat untuk setiap tugas. Saya pribadi terkejut melihat betapa bagusnya game yang dibuat sepenuhnya dalam HTML5, dan meskipun masih kurang di banyak area, saya menantikan perkembangannya di masa mendatang.