Studi Kasus - Inside World Wide Maze

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

World Wide Maze

Game ini menampilkan penggunaan fitur HTML5 yang berlimpah. 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 Workers.

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

DeviceOrientation

Peristiwa DeviceOrientation (contoh) digunakan untuk mengambil data kemiringan dari smartphone. Jika addEventListener digunakan dengan peristiwa DeviceOrientation, callback dengan objek DeviceOrientationEvent akan dipanggil sebagai argumen secara berkala. Interval itu sendiri bervariasi dengan perangkat yang digunakan. Misalnya, di iOS + Chrome dan iOS + Safari, callback dipanggil sekitar setiap 1/20 detik, sedangkan di Android 4 + Chrome, callback dipanggil sekitar setiap 1/10 detik.

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

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

Orientasi perangkat.

Nilai di bagian atas yang ditandai dengan warna biru adalah nilai yang ditentukan dalam spesifikasi W3C. Nilai yang ditandai dengan warna hijau cocok dengan spesifikasi ini, sedangkan nilai yang ditandai dengan warna merah menyimpang. Anehnya, hanya kombinasi Android-Firefox yang menampilkan nilai yang cocok dengan spesifikasi. Namun, dalam hal penerapan, sebaiknya akomodasi nilai yang sering terjadi. Oleh karena itu, World Wide Maze menggunakan nilai return iOS sebagai standar dan menyesuaikan untuk perangkat Android.

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

Namun, Nexus 10 masih tidak didukung. Meskipun Nexus 10 menampilkan rentang nilai yang sama dengan perangkat Android lainnya, ada bug yang membalikkan nilai beta dan gamma. Hal ini sedang ditangani secara terpisah. (Mungkin defaultnya adalah orientasi lanskap?)

Seperti yang ditunjukkan, meskipun API yang melibatkan perangkat fisik telah menetapkan spesifikasi, tidak ada jaminan bahwa nilai yang ditampilkan akan cocok dengan spesifikasi tersebut. Oleh karena itu, mengujinya di semua perangkat calon sangatlah penting. Hal ini juga berarti bahwa nilai yang tidak terduga dapat dimasukkan, yang memerlukan pembuatan solusi. World Wide Maze meminta pemain pertama kali untuk melakukan kalibrasi perangkat sebagai langkah 1 tutorialnya, tetapi tidak akan melakukan kalibrasi ke posisi nol dengan benar jika menerima nilai kemiringan yang tidak terduga. Oleh karena itu, fitur ini memiliki batas waktu internal dan meminta pemain untuk beralih ke kontrol keyboard jika tidak dapat dikalibrasi dalam batas waktu tersebut.

WebSocket

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

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

Penyambungan berdasarkan 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 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. Jika tidak ada PC yang ditetapkan, akan terjadi error.
  6. Saat data masuk dari perangkat seluler Anda, data tersebut akan dikirim ke PC yang disambungkan, dan sebaliknya.

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

Sinkronisasi Tab

Fitur Sinkronisasi Tab khusus Chrome memudahkan proses penyambungan. Dengan fitur 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 akan memeriksa URL halaman yang terbuka, dan jika nomor ditambahkan, perangkat akan langsung terhubung. Dengan demikian, Anda tidak perlu memasukkan angka secara manual atau memindai kode QR dengan kamera.

Latensi

Karena server relay berada di Amerika Serikat, mengaksesnya dari Jepang akan menyebabkan penundaan sekitar 200 md sebelum data kemiringan smartphone mencapai PC. Waktu respons jelas lambat dibandingkan dengan waktu respons lingkungan lokal yang digunakan selama pengembangan, tetapi menyisipkan sesuatu seperti filter low-pass (saya menggunakan EMA) meningkatkannya ke tingkat yang tidak mengganggu. (Dalam praktiknya, filter low-pass juga diperlukan untuk tujuan presentasi; nilai yang ditampilkan dari sensor kemiringan menyertakan sejumlah besar derau, dan menerapkan nilai tersebut ke layar akan menghasilkan banyak guncangan.) Hal ini tidak berfungsi dengan lompatan, yang jelas lambat, tetapi tidak ada yang dapat dilakukan untuk mengatasinya.

Karena saya sudah mengantisipasi masalah latensi sejak awal, saya mempertimbangkan untuk menyiapkan server relay di seluruh dunia sehingga klien dapat terhubung ke server terdekat yang tersedia (sehingga meminimalkan latensi). Namun, saya akhirnya menggunakan Google Compute Engine (GCE), yang saat itu hanya ada di Amerika Serikat, sehingga hal ini tidak dapat dilakukan.

Masalah Algoritma Nagle

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

Masalah latensi Nagle tidak terjadi dengan WebSocket di Chrome untuk Android, yang menyertakan opsi TCP_NODELAY untuk menonaktifkan Nagle, tetapi terjadi dengan WebSocket WebKit 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 diatasi dalam versi pengembangan WebKit.

Saat masalah ini terjadi, data kemiringan yang dikirim setiap 100 md digabungkan menjadi beberapa bagian yang hanya mencapai PC setiap 500 md. Game tidak dapat berfungsi dalam kondisi ini, sehingga menghindari latensi ini dengan meminta sisi server mengirim data dalam interval singkat (setiap 50 md atau lebih). Saya yakin bahwa menerima ACK dalam interval singkat akan mengelabui algoritma Nagle sehingga algoritma tersebut berpikir bahwa data dapat dikirim.

Algoritma Nagle 1

Grafik di atas menunjukkan interval data sebenarnya yang diterima. Ini menunjukkan interval waktu antar-paket; hijau mewakili interval output dan merah mewakili interval input. Minimumnya adalah 54 md, maksimumnya adalah 158 md, dan tengahnya mendekati 100 md. Di sini, saya menggunakan iPhone dengan server relay yang berada di Jepang. Output dan input sekitar 100 md, dan operasi berjalan lancar.

Algoritma Nagle 2

Sebaliknya, grafik ini menunjukkan hasil penggunaan server di Amerika Serikat. Meskipun interval output hijau tetap stabil pada 100 md, interval input berfluktuasi antara minimum 0 md dan maksimum 500 md, yang menunjukkan bahwa PC menerima data dalam potongan.

ALT_TEXT_HERE

Terakhir, grafik ini menunjukkan hasil dari menghindari latensi dengan meminta server mengirim data placeholder. Meskipun performanya tidak sebaik menggunakan server Jepang, jelas bahwa interval input tetap relatif stabil di sekitar 100 md.

Bug?

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

Menskalakan server relay

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

Fisika

Semua gerakan bola dalam game (bergulir menuruni bukit, bertabrakan dengan tanah, bertabrakan dengan dinding, mengumpulkan item, dll.) dilakukan dengan simulator fisika 3D. Saya menggunakan Ammo.js—port dari mesin fisika Bullet yang banyak digunakan ke JavaScript menggunakan Emscripten—bersama dengan Physijs untuk menggunakannya sebagai "Web Worker".

Web Worker

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

FPS

Gambar ini menunjukkan kecepatan frame yang dihasilkan di Lenovo G570. Kotak atas menunjukkan kecepatan frame untuk WebGL (rendering gambar), dan kotak bawah menunjukkan kecepatan frame untuk mesin fisika. GPU adalah 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. Menggunakan console4Worker akan membuat objek konsol yang setara di Worker, sehingga proses proses debug menjadi jauh lebih mudah.

Pekerja layanan

Chrome versi terbaru memungkinkan Anda menetapkan titik henti sementara saat meluncurkan Web Worker, yang juga berguna untuk proses debug. Hal ini dapat ditemukan di panel "Workers" di Developer Tools.

Performa

Tahap dengan jumlah poligon tinggi terkadang melebihi 100.000 poligon, tetapi performa tidak terlalu terpengaruh meskipun poligon tersebut dihasilkan sepenuhnya sebagai Physijs.ConcaveMesh (btBvhTriangleMeshShape di Bullet).

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

Objek ghost

Objek yang memiliki deteksi tabrakan, tetapi tidak memiliki dampak tabrakan sehingga tidak memengaruhi objek lain disebut "objek hantu" di Bullet. Meskipun Physijs tidak mendukung objek ghost secara resmi, Anda dapat membuatnya di sana dengan mengutak-atik flag setelah membuat Physijs.Mesh. World Wide Maze menggunakan objek ghost untuk deteksi 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 Bullet, Stack Overflow, atau dokumentasi Bullet untuk mengetahui 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

Update Firefox dari versi 17 ke 18 mengubah cara Web Worker bertukar data, dan Physijs berhenti berfungsi sebagai hasilnya. Masalah dilaporkan di GitHub dan diselesaikan setelah beberapa hari. Meskipun efisiensi open source ini membuat saya terkesan, insiden ini 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 hal ini tidak secara langsung berkaitan dengan World Wide Maze, Ammo.js sudah mendukung asm.js yang baru-baru ini diumumkan oleh Mozilla (tidak mengherankan karena asm.js pada dasarnya dibuat untuk mempercepat JavaScript yang dihasilkan oleh Emscripten, dan pembuat Emscripten juga merupakan pembuat 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 sebaiknya tulis bagian yang memerlukan lebih banyak kecepatan di C/C++, lalu port ke JavaScript menggunakan Emscripten?

WebGL

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

Efek cahaya

Efek cahaya yang ditambahkan ke inti bola dan ke item diterapkan menggunakan versi sederhana dari yang disebut "Kawase Method MGF". Namun, meskipun Metode Kawase membuat semua area terang mekar, World Wide Maze membuat target render terpisah untuk area yang perlu bersinar. Hal ini karena screenshot situs harus digunakan untuk tekstur panggung, dan hanya mengekstrak semua area terang akan menyebabkan seluruh situs bersinar jika, misalnya, situs memiliki latar belakang putih. Saya juga mempertimbangkan untuk memproses semuanya dalam HDR, tetapi kali ini saya memutuskan untuk tidak melakukannya karena penerapannya akan menjadi cukup rumit.

Kilau

Kiri atas menunjukkan proses pertama, tempat area cahaya dirender secara terpisah, lalu pemburaman diterapkan. Kanan bawah menunjukkan proses kedua, saat ukuran gambar dikurangi sebesar 50%, lalu pemburaman diterapkan. Kanan atas menunjukkan proses ketiga, saat gambar kembali dikurangi sebesar 50%, lalu diburamkan. Ketiganya kemudian ditempatkan untuk membuat gambar komposit 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

Pantulan 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 pembaruan pada 60 fps sangat intensif, peta lingkungan diperbarui setiap tiga frame. Hasilnya tidak selancar memperbarui setiap frame, tetapi perbedaannya hampir tidak terlihat kecuali jika ditunjukkan.

Shader, shader, shader…

WebGL memerlukan shader (vertex shader, fragment shader) untuk semua rendering. Meskipun shader yang disertakan dalam three.js sudah memungkinkan berbagai efek, Anda tidak dapat menghindari penulisan shader sendiri untuk mendapatkan bayangan dan pengoptimalan yang lebih rumit. Karena World Wide Maze membuat CPU sibuk dengan mesin fisikanya, saya mencoba memanfaatkan GPU dengan menulis sebanyak mungkin dalam bahasa shading (GLSL), meskipun pemrosesan CPU (melalui JavaScript) akan lebih mudah. Efek gelombang laut mengandalkan shader, seperti halnya kembang api di titik sasaran dan efek mesh yang digunakan saat bola muncul.

Bola shader

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

Mesh shader

Item kecil yang tersebar di seluruh panggung semuanya terintegrasi ke dalam satu mesh, dan setiap gerakan bergantung pada shader yang memindahkan setiap ujung poligon. Ini adalah hasil dari pengujian untuk melihat apakah performa akan terpengaruh dengan adanya banyak objek. Sekitar 5.000 objek ditata di sini, yang terdiri dari sekitar 20.000 poligon. Performa tidak terpengaruh sama sekali.

poly2tri

Tahap terbentuk berdasarkan informasi garis batas yang diterima dari server, lalu dipoligonkan 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 yang berbeda yang disebut poly2tri sendiri. Ternyata, three.js telah mencoba hal yang sama sebelumnya, jadi saya membuatnya berfungsi hanya dengan mengomentari sebagiannya. Akibatnya, error berkurang secara signifikan, sehingga memungkinkan lebih banyak tahap yang dapat dimainkan. Error sesekali tetap ada, dan karena alasan tertentu, poly2tri menangani error dengan mengeluarkan pemberitahuan, jadi saya mengubahnya untuk menampilkan pengecualian.

poly2tri

Gambar di atas menunjukkan cara garis batas biru ditriangulasikan dan poligon merah dihasilkan.

Penyaringan anisotropik

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

Optimalkan

Seperti yang juga disebutkan dalam artikel 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 pagar pembatas dalam game adalah objek terpisah. Hal ini terkadang menghasilkan lebih dari 2.000 panggilan gambar, sehingga membuat tahap yang kompleks menjadi tidak praktis. Namun, setelah saya memaketkan jenis objek yang sama ke dalam satu mesh, panggilan gambar turun menjadi sekitar lima puluh, sehingga meningkatkan performa secara signifikan.

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

Pengoptimalan

Gambar di atas adalah hasil rekaman aktivitas dari pembuatan peta lingkungan untuk refleksi 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 lapisan adalah seperti tumpukan panggilan. Menempatkan console.time dalam console.time memungkinkan pengukuran lebih lanjut. Grafik atas adalah pra-pengoptimalan dan grafik bawah adalah pasca-pengoptimalan. Seperti yang ditunjukkan grafik atas, updateMatrix (meskipun kata terpotong) dipanggil untuk setiap render 0-5 selama pra-pengoptimalan. Namun, saya mengubahnya agar hanya dipanggil sekali karena proses ini hanya diperlukan saat objek mengubah posisi atau orientasi.

Proses pelacakan itu sendiri tentu saja menggunakan resource, sehingga menyisipkan console.time secara berlebihan dapat menyebabkan penyimpangan yang signifikan dari performa sebenarnya, sehingga sulit untuk menentukan area pengoptimalan.

Pengatur performa

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

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

Kebocoran memori

Menghapus objek dengan rapi agak merepotkan dengan three.js. Namun, membiarkannya akan menyebabkan kebocoran memori, jadi saya merancang metode di bawah ini. @renderer mengacu pada THREE.WebGLRenderer. (Revisi terbaru three.js menggunakan metode penghapusan alokasi yang sedikit berbeda, sehingga mungkin tidak akan berfungsi dengan semestinya.)

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 tentang aplikasi WebGL adalah kemampuan untuk mendesain tata letak halaman dalam HTML. Membuat 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 dapat mempermudahnya). Di sisi lain, HTML memungkinkan kontrol yang akurat atas semua aspek desain frontend dengan CSS, seperti saat membuat situs. Meskipun efek kompleks seperti partikel yang mengembun menjadi logo tidak mungkin, 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 ragam latar belakang menggunakan WebGL.)

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

Internasionalisasi (i18n)

Bekerja dalam HTML juga memudahkan pembuatan versi bahasa Inggris. Saya memilih untuk menggunakan i18next, library i18n web, untuk kebutuhan internasionalisasi saya, yang dapat saya gunakan apa 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 konverter kustom. Saya melakukan banyak pembaruan tepat sebelum rilis, sehingga mengotomatiskan proses ekspor dari Spreadsheet Google Dokumen akan mempermudah semuanya.

Fitur terjemahan otomatis Chrome juga berfungsi secara normal karena halaman dibuat dengan HTML. Namun, terkadang fitur ini gagal mendeteksi bahasa dengan benar, dan malah salah mengiranya sebagai bahasa yang sama sekali berbeda (misalnya, Vietnam), sehingga fitur ini saat ini dinonaktifkan. (Fitur ini dapat dinonaktifkan menggunakan tag meta.)

RequireJS

Saya memilih RequireJS sebagai sistem modul JavaScript saya. 10.000 baris kode sumber game dibagi menjadi sekitar 60 class (= file coffee) dan dikompilasi menjadi setiap file js. RequireJS memuat setiap file ini dalam urutan yang sesuai 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 "define", maka hoge.js selalu dimuat terlebih dahulu (dipanggil kembali setelah selesai dimuat). Mekanisme ini disebut AMD, dan library pihak ketiga apa pun dapat digunakan untuk jenis callback yang sama selama mendukung AMD. Bahkan yang tidak (misalnya, three.js) akan berperforma serupa selama dependensi ditentukan sebelumnya.

Hal ini mirip dengan mengimpor AS3, jadi seharusnya tidak terlalu aneh. Jika Anda akhirnya memiliki lebih banyak file dependen, ini adalah solusi yang mungkin.

r.js

RequireJS menyertakan pengoptimal yang disebut r.js. Tindakan ini akan 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 pengoptimalan r.js. Jika game dapat didistribusikan menggunakan gzip, ukurannya akan lebih diperkecil menjadi 250 KB. (GAE memiliki masalah yang tidak akan mengizinkan transmisi file gzip berukuran 1 MB atau lebih besar, sehingga game saat ini didistribusikan tanpa dikompresi sebagai teks biasa berukuran 1 MB.)

Pembuat stage

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

  1. URL situs yang akan dikonversi menjadi tahap dikirim melalui WebSocket.
  2. PhantomJS mengambil screenshot, dan posisi tag div dan img diambil serta ditampilkan dalam format JSON.
  3. Berdasarkan screenshot dari langkah 2 dan data pemosisian elemen HTML, program C++ kustom (OpenCV, Boost) menghapus area yang tidak perlu, membuat pulau, menghubungkan pulau dengan jembatan, menghitung posisi item dan pagar pembatas, menetapkan titik tujuan, dll. Hasilnya adalah output dalam format JSON dan ditampilkan ke browser.

PhantomJS

PhantomJS adalah browser yang tidak memerlukan layar. Browser 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 dieksekusi. Mengambil screenshot sangat mudah, seperti yang ditunjukkan dalam contoh ini. Saya bekerja di server Linux (CentOS), jadi saya perlu menginstal font untuk menampilkan bahasa Jepang (M+ FONTS). Meskipun demikian, rendering font ditangani secara berbeda dengan di Windows atau Mac OS, sehingga font yang sama dapat terlihat berbeda di komputer lain (meskipun perbedaannya minimal).

Mengambil 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 tahap (mirip dengan Firefox 3D Inspector) dan mencoba sesuatu seperti analisis DOM di PhantomJS. Namun, pada akhirnya, saya memutuskan untuk menggunakan pendekatan pemrosesan gambar. Untuk tujuan ini, saya menulis program C++ yang menggunakan OpenCV dan Boost yang disebut "stage_builder". Metode ini 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 perlu untuk membuat labirin.
  5. Menempatkan item besar.
  6. Menempatkan item kecil.
  7. Menempatkan pagar pembatas.
  8. Menghasilkan data pemosisian dalam format JSON.

Setiap langkah dijelaskan di bawah ini.

Memuat screenshot dan file JSON

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

Mengonversi gambar dan teks menjadi "pulau"

Build stage

Gambar di atas adalah screenshot bagian Berita di aid-dcc.com (klik untuk melihat ukuran sebenarnya). Elemen gambar dan 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. Berikut tampilannya setelah selesai:

Build stage

Bagian putih adalah pulau potensial.

Teks terlalu halus dan tajam, jadi kita akan mempertebalnya 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 stage

Teks kini membentuk gumpalan yang sesuai, dan setiap gambar adalah pulau yang tepat.

Membuat jembatan untuk menghubungkan pulau-pulau

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

Build stage

Menghapus jembatan yang tidak perlu untuk membuat labirin

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

Build stage

Menempatkan item besar

Satu atau beberapa item besar ditempatkan di setiap pulau, bergantung pada dimensinya, dengan memilih dari titik yang paling jauh dari tepi pulau. Meskipun tidak terlalu jelas, titik-titik ini ditampilkan dalam warna merah di bawah:

Build stage

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

Menempatkan item kecil

Build stage

Jumlah item kecil yang sesuai ditempatkan di sepanjang garis pada jarak yang ditetapkan dari tepi pulau. Gambar di atas (bukan dari aid-dcc.com) menunjukkan garis penempatan yang diproyeksikan dalam warna abu-abu, dioffset, dan ditempatkan secara berkala dari tepi pulau. Titik merah menunjukkan tempat item kecil ditempatkan. Karena gambar ini berasal dari versi tengah pengembangan, item disusun dalam garis lurus, tetapi versi final menyebarkan item sedikit lebih tidak teratur ke kedua sisi garis abu-abu.

Menempatkan pembatasan

Pagar pembatas pada dasarnya ditempatkan di sepanjang batas luar pulau, tetapi harus dipotong di jembatan untuk memungkinkan akses. Library Geometri Boost terbukti berguna untuk hal ini, yang menyederhanakan penghitungan geometris seperti menentukan tempat data batas pulau bersimpangan dengan garis di kedua sisi jembatan.

Build stage

Garis hijau yang menguraikan pulau adalah pagar pembatas. Mungkin sulit untuk dilihat dalam gambar ini, tetapi tidak ada garis hijau di tempat jembatan berada. Ini adalah gambar akhir yang digunakan untuk proses debug, yang menyertakan semua objek yang perlu di-output ke JSON. Titik biru muda adalah item kecil, dan titik abu-abu adalah titik mulai ulang yang diusulkan. Saat bola jatuh ke laut, game akan dilanjutkan dari titik mulai ulang terdekat. Titik mulai ulang diatur kurang lebih sama dengan item kecil, dengan interval reguler pada jarak yang ditetapkan dari tepi pulau.

Menampilkan data pemosisian dalam format JSON

Saya juga menggunakan picojson untuk output. 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 tersebut, pengembangannya sendiri tidak sulit setelah lingkungan kompilasi dibuat. Saya menggunakan Command Line Tools di Xcode untuk men-debug build di Mac, lalu membuat file konfigurasi menggunakan automake/autoconf sehingga build dapat dikompilasi di Linux. Kemudian, saya hanya perlu menggunakan "configure && make" di Linux untuk membuat file yang dapat dieksekusi. Saya mengalami beberapa bug khusus Linux karena perbedaan versi compiler, tetapi dapat menyelesaikannya dengan relatif mudah menggunakan gdb.

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 canggih. Penting untuk memiliki alat yang tepat untuk setiap tugas. Saya pribadi terkejut dengan hasil game yang dibuat sepenuhnya dalam HTML5, dan meskipun masih kurang di banyak area, saya menantikan perkembangannya di masa mendatang.