Pengalaman Hobbit 2014

Menambahkan gameplay WebRTC ke Hobbit Experience

Daniel Isaksson
Daniel Isaksson

Tepat waktunya untuk film Hobbit baru “The Hobbit: The Battle of the Five Armies”, kami telah berupaya memperluas Eksperimen Chrome tahun lalu, A Journey through Middle-earth dengan sejumlah konten baru. Fokus utama kali ini adalah memperluas penggunaan WebGL karena lebih banyak browser dan perangkat dapat melihat konten serta berfungsi dengan kemampuan WebRTC di Chrome dan Firefox. Kami memiliki tiga tujuan dengan eksperimen tahun ini:

  • Gameplay P2P menggunakan WebRTC dan WebGL di Chrome untuk Android
  • Membuat game multiplayer yang mudah dimainkan dan berdasarkan input sentuh
  • Menghosting di Google Cloud Platform

Menentukan game

Logika game ini dibangun di atas penyiapan berbasis petak dengan pasukan yang bergerak di atas papan game. Hal ini memudahkan kami untuk mencoba gameplay di atas kertas saat kami menentukan aturannya. Penggunaan konfigurasi berbasis grid juga akan membantu mendeteksi tabrakan dalam game untuk mempertahankan performa yang baik karena kamu hanya perlu memeriksa tabrakan dengan objek di petak yang sama atau berdekatan. Sejak awal, kami tahu bahwa kami ingin memfokuskan game baru seputar pertempuran antara empat pasukan utama Middle-earth, Manusia, Kurcaci, Peri, dan Orc. Game juga harus cukup santai untuk dimainkan dalam Eksperimen Chrome dan tidak memiliki terlalu banyak interaksi untuk dipelajari. Kami memulai dengan menentukan lima Battlegrounds di peta Middle-earth yang berfungsi sebagai ruang game tempat banyak pemain dapat bersaing dalam pertempuran antaranggota. Menampilkan beberapa pemain dalam ruang pada layar seluler, dan memungkinkan pengguna memilih siapa yang akan ditantang merupakan tantangan tersendiri. Untuk membuat interaksi dan pemandangan menjadi lebih mudah, kami memutuskan untuk hanya memiliki satu tombol untuk menantang dan menerima dan hanya menggunakan ruangan untuk menampilkan acara dan siapa raja bukit saat ini. Arahan ini juga menyelesaikan beberapa masalah di sisi pencarian lawan dan memungkinkan kami mencocokkan kandidat terbaik untuk pertempuran. Dalam eksperimen Chrome sebelumnya, Cube Slam, kami mengetahui bahwa perlu banyak upaya untuk menangani latensi dalam game multi-player jika hasil game mengandalkannya. Anda harus terus-menerus membuat asumsi tentang di mana keadaan lawan, di mana lawan berpikir bahwa Anda berada dan menyinkronkannya dengan animasi di perangkat yang berbeda. Artikel ini menjelaskan tantangan tersebut secara lebih mendetail. Untuk memudahkan, kami membuat game ini berbasis giliran.

Logika game ini dibangun di atas penyiapan berbasis petak dengan pasukan yang bergerak di atas papan game. Hal ini memudahkan kami untuk mencoba gameplay di atas kertas saat kami menentukan aturannya. Penggunaan konfigurasi berbasis petak juga akan membantu mendeteksi tabrakan dalam game untuk mempertahankan performa yang baik karena Anda hanya perlu memeriksa tabrakan dengan objek di ubin yang sama atau berdekatan.

Bagian dari game

Untuk membuat game multi-player ini, ada beberapa bagian penting yang harus kami bangun:

  • API pengelolaan pemain sisi server menangani statistik pengguna, pencocokan, sesi, dan game.
  • Server untuk membantu membangun koneksi di antara para pemain.
  • API untuk menangani sinyal AppEngine Channels API yang digunakan untuk terhubung dan berkomunikasi dengan semua pemain di ruang game.
  • Game engine JavaScript yang menangani sinkronisasi status dan pesan RTC antara dua pemutar/peer.
  • Tampilan game WebGL.

Pengelolaan pemain

Untuk mendukung sejumlah besar pemain, kami menggunakan banyak ruang game paralel per Battleground. Alasan utama untuk membatasi jumlah pemain per ruang game adalah agar pemain baru dapat mencapai puncak papan peringkat dalam waktu yang wajar. Batas ini juga terkait dengan ukuran objek JSON yang menjelaskan ruang game yang dikirim melalui Channel API yang memiliki batas 32 kb. Kita harus menyimpan pemain, ruangan, skor, sesi, dan hubungan mereka dalam game. Untuk melakukannya, pertama-tama kita menggunakan NDB untuk entity dan menggunakan antarmuka kueri untuk menangani hubungan. NDB adalah antarmuka ke Google Cloud Datastore. Penggunaan NDB sangat baik di awal, tetapi kami segera mengalami masalah dengan cara kami harus menggunakannya. Kueri dijalankan terhadap versi database "committed" (NDB Writes dijelaskan secara mendetail dalam artikel mendalam ini) yang dapat mengalami penundaan selama beberapa detik. Tetapi entitas itu sendiri tidak mengalami penundaan karena mereka merespons langsung dari cache. Mungkin akan sedikit lebih mudah dijelaskan dengan beberapa kode contoh:

// example code to explain our issue with eventual consistency
def join_room(player_id, room_id):
    room = Room.get_by_id(room_id)
    
    player = Player.get_by_id(player_id)
    player.room = room.key
    player.put()
    
    // the player Entity is updated directly in the cache
    // so calling this will return the room key as expected
    player.room // = Key(Room, room_id)

    // Fetch all the players with room set to 'room.key'
    players_in_room = Player.query(Player.room == room.key).fetch()
    // = [] (an empty list of players)
    // even though the saved player above may be expected to be in the
    // list it may not be there because the query api is being run against the 
    // "committed" version and may still be empty for a few seconds

    return {
        room: room,
        players: players_in_room,
    }

Setelah menambahkan pengujian unit, kami dapat melihat masalah dengan jelas dan kami beralih dari kueri untuk mempertahankan hubungan dalam daftar yang dipisahkan koma di memcache. Hal ini terasa seperti sedikit peretasan, tetapi berhasil dan memcache AppEngine memiliki sistem seperti transaksi untuk kunci yang menggunakan fitur “bandingkan dan tetapkan” yang sangat baik sehingga kini pengujian lulus lagi.

Sayangnya, memcache tidak semua pelangi dan unicorn tetapi memiliki beberapa batasan. Yang paling penting adalah ukuran nilai 1 MB (tidak dapat memiliki terlalu banyak ruang yang terkait dengan medan perang) dan masa berlaku kunci, atau seperti yang dijelaskan dalam dokumen:

Kami mempertimbangkan untuk menggunakan toko nilai kunci hebat lainnya, Redis. Namun, pada saat menyiapkan cluster yang skalabel agak sulit dan karena kami lebih berfokus pada membangun pengalaman daripada memelihara server, kami tidak mengambil jalur tersebut. Di sisi lain, Google Cloud Platform baru-baru ini merilis fitur sederhana Click-to-deploy, dengan salah satu opsinya adalah Cluster Redis, jadi fitur tersebut akan menjadi opsi yang sangat menarik.

Terakhir, kami menemukan Google Cloud SQL dan memindahkan relasi ke MySQL. Prosesnya sangat melelahkan, tetapi akhirnya berhasil dengan baik. Update kini sepenuhnya bersifat atomik dan pengujian tetap lulus. Skor pengoptimalan juga membuat penerapan pencocokan dan penjagaan skor jauh lebih andal.

Seiring waktu, lebih banyak data secara perlahan dipindahkan dari NDB dan memcache ke SQL, tetapi secara umum entity pemain, medan perang, dan ruang masih disimpan di NDB sementara sesi dan hubungan di antara semuanya disimpan di SQL.

Kami juga harus melacak siapa yang bermain dan memasangkan pemain satu sama lain menggunakan mekanisme pencocokan yang mempertimbangkan tingkat keterampilan dan pengalaman pemain. Kami mendasarkan pencocokan berdasarkan library open source Glicko2.

Karena ini adalah game multi-pemain, kami ingin memberi tahu pemain lain di ruangan tentang peristiwa seperti “siapa yang masuk atau keluar”, “siapa yang menang atau kalah”, dan apakah ada tantangan untuk diterima. Untuk menangani hal ini, kami menyediakan kemampuan untuk menerima notifikasi ke Player Management API.

Menyiapkan WebRTC

Ketika dua pemain dicocokkan untuk bertarung, layanan pemberian sinyal digunakan untuk membuat dua pemain yang cocok saling berbicara satu sama lain dan membantu memulai koneksi rekan.

Ada beberapa library pihak ketiga yang dapat Anda gunakan untuk layanan sinyal, yang juga menyederhanakan penyiapan WebRTC. Beberapa opsinya adalah PeerJS, SimpleWebRTC, dan PubNub WebRTC SDK. PubNub menggunakan solusi server yang dihosting dan untuk project ini, kami ingin menghostingnya di Google Cloud Platform. Dua library lainnya menggunakan server node.js yang seharusnya kami instal di Google Compute Engine, tetapi kami juga harus memastikan library tersebut dapat menangani ribuan pengguna serentak, sesuatu yang sudah kami tahu bisa dilakukan oleh Channel API.

Salah satu keuntungan utama menggunakan Google Cloud Platform dalam hal ini adalah penskalaan. Penskalaan resource yang dibutuhkan untuk project AppEngine mudah ditangani melalui Google Developers Console dan tidak diperlukan upaya tambahan untuk menskalakan layanan sinyal saat menggunakan Channels API.

Ada beberapa kekhawatiran tentang latensi dan seberapa tangguhnya Channels API, tetapi kami sebelumnya telah menggunakannya untuk project CubeSlam dan terbukti berfungsi bagi jutaan pengguna dalam project itu sehingga kami memutuskan untuk menggunakannya lagi.

Karena kami tidak memilih menggunakan library pihak ketiga untuk membantu WebRTC, kami harus membuatnya sendiri. Untungnya kami bisa menggunakan kembali banyak hal yang kami lakukan untuk proyek CubeSlam. Setelah kedua pemain bergabung, sesi disetel ke “aktif”, lalu kedua pemain akan menggunakan ID sesi aktif tersebut untuk memulai koneksi peer-to-peer melalui Channel API. Setelah itu semua komunikasi antara dua pemutar akan ditangani melalui RTCDataChannel.

Kita juga memerlukan server STUN dan TURN untuk membantu membangun koneksi dan mengatasi NAT dan {i>firewall<i}. Baca lebih lanjut tentang penyiapan WebRTC di artikel HTML5 Rocks WebRTC di dunia nyata: STUN, TURN, dan sinyal.

Jumlah server TURN yang digunakan juga harus dapat diskalakan tergantung pada traffic. Untuk menangani hal ini, kami menguji Google Deployment Manager. Dengan begitu, kami dapat men-deploy resource secara dinamis di Google Compute Engine dan menginstal server TURN menggunakan template. Fitur ini masih dalam versi alfa, tetapi untuk tujuan kami, fitur ini berfungsi dengan baik. Untuk server TURN, kami menggunakan coturn, yang merupakan implementasi STUN/TURN yang sangat cepat, efisien, dan tampaknya dapat diandalkan.

Channel API

Channel API digunakan untuk mengirim semua komunikasi ke dan dari ruang game di sisi klien. Player Management API kami menggunakan Channel API untuk notifikasinya tentang acara game.

Ada beberapa hambatan saat menggunakan Channels API. Salah satu contohnya adalah karena pesan bisa datang tidak berurutan, kita harus menggabungkan semua pesan dalam sebuah objek dan mengurutkannya. Berikut beberapa contoh kode tentang cara kerjanya:

var que = [];  // [seq, packet...]
var seq = 0;
var rcv = -1;

function send(message) {
  var packet = JSON.stringify({
    seq: seq++,
    msg: message
  });
  channel.send(packet);
}

function recv(packet) {
  var data = JSON.parse(packet);

  if (data.seq &lt;= rcv) {
    // ignoring message, older or already received
  } else if (data.seq > rcv + 1) {
    // message from the future. queue it up.
    que.push(data.seq, packet);
  } else {
    // message in order! update the rcv index and emit the message
    rcv = data.seq;
    emit('message', data.message);

    // and now that we have updated the `rcv` index we 
    // will check the que for any other we can send
    setTimeout(flush, 10);
  }
}

function flush() {
  for (var i=0; i&lt;que.length; i++) {
    var seq = que[i];
    var packet = que[i+1];
    if (data.seq == rcv + 1) {
      recv(packet);
      return; // wait for next flush
    }
  }
}

Kami juga ingin menjaga berbagai API situs tetap modular dan terpisah dari hosting situs serta memulai dengan menggunakan modul bawaan GAE. Sayangnya, setelah membuat semuanya berfungsi di developer, kami menyadari bahwa Channel API tidak berfungsi sama sekali dengan modul dalam produksi. Namun, kami beralih menggunakan instance GAE terpisah dan mengalami masalah CORS yang memaksa kami untuk menggunakan postMessage bridge iframe.

Mesin game

Untuk membuat game engine sedinamis mungkin, kami mem-build aplikasi frontend menggunakan pendekatan entity-component-system (ECS). Ketika kami memulai pengembangan, {i>wireframe<i} dan spesifikasi fungsional belum ditetapkan, sehingga sangat membantu untuk dapat menambahkan fitur dan logika seiring berjalannya pengembangan. Misalnya, prototipe pertama menggunakan sistem render kanvas sederhana untuk menampilkan entity dalam petak. Beberapa iterasi kemudian, sistem untuk tabrakan ditambahkan, dan satu lagi untuk pemain yang dikontrol AI. Di tengah project, kita bisa beralih ke sistem 3d-renderer tanpa mengubah kode lainnya. Ketika bagian-bagian jaringan aktif dan berjalan, ai-system dapat dimodifikasi untuk menggunakan perintah jarak jauh.

Jadi, logika dasar multiplayer adalah mengirim konfigurasi perintah tindakan ke pembanding lain melalui DataChannels dan membiarkan simulasi bertindak seolah-olah itu adalah pemain AI. Selain itu, ada logika untuk memutuskan belokan mana, jika pemain menekan tombol pass/serangan, mengantrekan perintah jika pemain masuk saat pemain masih melihat animasi sebelumnya, dll.

Jika hanya dua pengguna yang bergantian, kedua rekan bisa berbagi tanggung jawab untuk memberikan giliran kepada lawan ketika mereka selesai, tetapi ada pemain ketiga yang terlibat. Sistem AI menjadi lebih berguna lagi (bukan hanya untuk pengujian), saat kami perlu menambahkan musuh seperti spider dan troll. Untuk membuatnya sesuai dengan aliran berbasis giliran, itu harus dihasilkan dan dieksekusi persis sama di kedua sisi. Hal ini dapat diselesaikan dengan mengizinkan satu pembanding mengontrol sistem giliran dan mengirim status saat ini ke peer jarak jauh. Kemudian ketika spider berputar, {i>turn manager<i} membiarkan sistem membuat perintah yang dikirimkan ke pengguna jarak jauh. Karena game-engine hanya bertindak berdasarkan perintah dan entity-id:s, game akan disimulasikan dengan cara yang sama di kedua sisi. Semua unit juga dapat memiliki komponen AI yang memungkinkan pengujian otomatis yang mudah.

Solusi yang optimal adalah memiliki perender kanvas yang lebih sederhana di awal pengembangan sambil berfokus pada logika game. Namun, kesenangan nyata dimulai ketika versi 3d diterapkan dan adegan menjadi lebih hidup dengan lingkungan dan animasi. Kami menggunakan three.js sebagai mesin 3d, dan mudah untuk mencapai status pemutaran karena arsitekturnya.

Posisi mouse dikirim lebih sering ke pengguna jarak jauh dan petunjuk halus cahaya 3d tentang posisi kursor saat ini.