Pengalaman Hobbit 2014

Menambahkan gameplay WebRTC ke Hobbit Experience

Daniel Isaksson
Daniel Isaksson

Menjelang rilis film Hobbit baru “The Hobbit: The Battle of the Five Armies”, kami telah berupaya memperluas Eksperimen Chrome tahun lalu, Perjalanan melalui Middle-earth dengan beberapa konten baru. Fokus utamanya kali ini adalah memperluas penggunaan WebGL karena lebih banyak browser dan perangkat yang dapat melihat konten dan berfungsi dengan kemampuan WebRTC di Chrome dan Firefox. Kami memiliki tiga sasaran pada eksperimen tahun ini:

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

Menentukan game

Logika game ini dibuat 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 aturan. Menggunakan penyiapan berbasis petak juga membantu deteksi tabrakan dalam game untuk mempertahankan performa yang baik karena Anda hanya perlu memeriksa tabrakan dengan objek di ubin yang sama atau di ubin yang berdekatan. Sejak awal, kami tahu bahwa kami ingin memfokuskan game baru ini pada pertempuran antara empat kekuatan utama di Middle-earth, yaitu Manusia, Kurcaci, Elf, dan Orc. Game ini juga harus cukup kasual untuk dimainkan dalam Eksperimen Chrome dan tidak memiliki terlalu banyak interaksi untuk dipelajari. Kami memulai dengan menentukan lima Battleground di peta Middle-earth yang berfungsi sebagai ruang game tempat beberapa pemain dapat bersaing dalam pertempuran peer-to-peer. Menampilkan beberapa pemain di ruangan pada layar seluler, dan memungkinkan pengguna memilih siapa yang akan ditantang adalah tantangan tersendiri. Untuk mempermudah interaksi dan tampilan, kami memutuskan untuk hanya memiliki satu tombol untuk menantang dan menerima, serta hanya menggunakan ruangan untuk menampilkan peristiwa dan siapa yang saat ini menjadi raja bukit. Perintah ini juga menyelesaikan beberapa masalah di sisi pencarian lawan dan memungkinkan kami mencocokkan kandidat terbaik untuk bertempur. Dalam eksperimen Chrome sebelumnya, Cube Slam, kami mempelajari bahwa perlu banyak pekerjaan untuk menangani latensi dalam game multipemain jika hasil game bergantung padanya. Anda harus terus-menerus membuat asumsi tentang di mana status lawan berada, yang mana lawan berpikir bahwa Anda berada, dan menyinkronkannya dengan animasi di perangkat lain. Artikel ini menjelaskan tantangan ini secara lebih mendetail. Untuk memudahkannya, kami membuat game ini berbasis giliran.

Logika game dibuat berdasarkan penyiapan berbasis petak dengan pasukan yang bergerak di papan game. Hal ini memudahkan kami untuk mencoba gameplay di atas kertas saat kami menentukan aturan. Menggunakan penyiapan berbasis petak juga membantu deteksi tabrakan dalam game untuk mempertahankan performa yang baik, karena kamu cukup memeriksa tabrakan dengan objek di ubin yang sama atau berdekatan.

Bagian game

Untuk membuat game multipemain ini, ada beberapa bagian utama yang harus kita buat:

  • API pengelolaan pemain sisi server menangani statistik pengguna, pencocokan, sesi, dan game.
  • Server untuk membantu membangun koneksi di antara pemain.
  • API untuk menangani sinyal AppEngine Channels API yang digunakan untuk terhubung dan berkomunikasi dengan semua pemain di ruang game.
  • Mesin Game 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 untuk memungkinkan pemain baru mencapai posisi teratas papan peringkat dalam waktu yang wajar. Batas ini juga terhubung 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 hubungannya 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 bagus di awal, tetapi kami segera mengalami masalah terkait cara menggunakannya. Kueri dijalankan terhadap versi database "yang di-commit" (Penulisan NDB dijelaskan secara panjang lebar dalam artikel mendalam ini) yang dapat memiliki penundaan beberapa detik. Namun, entity itu sendiri tidak mengalami penundaan tersebut karena merespons langsung dari cache. Mungkin akan lebih mudah dijelaskan dengan beberapa contoh kode:

// 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, kita dapat melihat masalah dengan jelas dan beralih dari kueri untuk menyimpan hubungan dalam daftar yang dipisahkan koma di memcache. Tindakan ini terasa seperti hack, tetapi berhasil dan memcache AppEngine memiliki sistem seperti transaksi untuk kunci yang menggunakan fitur "compare and set" yang sangat baik sehingga sekarang pengujian lulus lagi.

Sayangnya, memcache tidak selalu sempurna, tetapi memiliki beberapa batasan, yang paling penting adalah ukuran nilai 1 MB (tidak boleh memiliki terlalu banyak ruangan yang terkait dengan medan pertempuran) dan masa berlaku kunci, atau seperti yang dijelaskan dalam dokumen:

Kami memang mempertimbangkan untuk menggunakan penyimpanan nilai kunci lain yang bagus, Redis. Namun, pada saat itu, menyiapkan cluster yang skalabel agak menakutkan dan karena kami lebih suka berfokus pada pembuatan pengalaman daripada mengelola server, kami tidak memilih jalur tersebut. Di sisi lain, Google Cloud Platform baru-baru ini merilis fitur Click-to-deploy sederhana, dengan salah satu opsinya adalah Cluster Redis sehingga akan menjadi opsi yang sangat menarik.

Terakhir, kita menemukan Google Cloud SQL dan memindahkan hubungan ke MySQL. Prosesnya sangat merepotkan, tetapi akhirnya berhasil. Update-nya kini sepenuhnya menyeluruh dan pengujiannya masih lulus pengujian. Hal ini juga membuat penerapan pencocokan dan pencatatan skor menjadi jauh lebih andal.

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

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

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

Menyiapkan WebRTC

Saat dua pemain dicocokkan untuk bertarung, layanan sinyal digunakan untuk membuat dua peer yang cocok saling berkomunikasi dan membantu memulai koneksi peer.

Ada beberapa library pihak ketiga yang dapat Anda gunakan untuk layanan sinyal dan juga menyederhanakan penyiapan WebRTC. Beberapa opsi 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 dapat kita instal di Google Compute Engine, tetapi kita juga harus memastikan bahwa library tersebut dapat menangani ribuan pengguna secara serentak, sesuatu yang sudah kita ketahui dapat dilakukan oleh Channel API.

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

Ada beberapa masalah terkait latensi dan seberapa andal Channels API, tetapi sebelumnya kami telah menggunakannya untuk project CubeSlam dan terbukti berfungsi untuk jutaan pengguna dalam project tersebut sehingga kami memutuskan untuk menggunakannya lagi.

Karena kami tidak memilih untuk menggunakan library pihak ketiga untuk membantu WebRTC, kami harus membuat library sendiri. Untungnya, kita dapat menggunakan kembali banyak hal yang telah kita lakukan untuk proyek CubeSlam. Saat kedua pemain telah bergabung ke sesi, sesi akan ditetapkan ke "aktif", dan kedua pemain kemudian akan menggunakan ID sesi aktif tersebut untuk memulai koneksi peer-to-peer melalui Channel API. Setelah itu, semua komunikasi antara kedua pemain akan ditangani melalui RTCDataChannel.

Kita juga memerlukan server STUN dan TURN untuk membantu membuat koneksi dan mengatasi NAT dan firewall. Baca selengkapnya tentang cara menyiapkan WebRTC di artikel HTML5 Rocks WebRTC di dunia nyata: STUN, TURN, dan sinyal.

Jumlah server TURN yang digunakan juga harus dapat diskalakan bergantung pada traffic. Untuk menangani hal ini, kami menguji Pengelola deployment Google. Solusi ini memungkinkan kami menerapkan 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 sempurna. Untuk server TURN, kami menggunakan coturn, yang merupakan implementasi STUN/TURN yang sangat cepat, efisien, dan tampaknya andal.

Channel API

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

Menggunakan Channels API memiliki beberapa kendala. Salah satu contohnya adalah karena pesan dapat datang tanpa urutan, kita harus menggabungkan semua pesan dalam 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 <= 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<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 mempertahankan API yang berbeda dari situs secara modular dan terpisah dari hosting situs serta memulai dengan menggunakan modul yang disertakan dalam GAE. Sayangnya, setelah semuanya berfungsi di pengembangan, kami menyadari bahwa Channel API tidak berfungsi sama sekali dengan modul dalam produksi. Sebagai gantinya, kami beralih menggunakan instance GAE terpisah dan mengalami masalah CORS yang memaksa kami menggunakan bridge postMessage iframe.

Mesin game

Untuk membuat game engine sedinamis mungkin, kami mem-build aplikasi frontend menggunakan pendekatan entity-component-system (ECS). Saat kami memulai pengembangan, wireframe dan spesifikasi fungsional belum ditetapkan, jadi sangat membantu untuk dapat menambahkan fitur dan logika saat pengembangan berlangsung. Misalnya, prototipe pertama menggunakan sistem rendering kanvas sederhana untuk menampilkan entitas dalam petak. Beberapa iterasi kemudian, sistem untuk tabrakan ditambahkan, dan satu lagi untuk pemain yang dikontrol AI. Di tengah project, kita dapat beralih ke sistem 3d-renderer tanpa mengubah kode lainnya. Saat bagian jaringan aktif dan berjalan, sistem AI dapat diubah untuk menggunakan perintah jarak jauh.

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

Jika hanya dua pengguna yang berganti giliran, kedua rekan dapat berbagi tanggung jawab untuk meneruskan giliran kepada lawan setelah mereka selesai, tetapi ada pemain ketiga yang terlibat. Sistem AI kembali berguna (bukan hanya untuk pengujian), saat kami perlu menambahkan musuh seperti spider dan troll. Agar sesuai dengan alur berbasis giliran, alur tersebut harus dibuat dan dijalankan persis sama di kedua sisi. Hal ini diatasi dengan mengizinkan satu rekan mengontrol sistem giliran kerja dan mengirim status saat ini ke peer jarak jauh. Kemudian ketika giliran spider, pengelola giliran memungkinkan ai-system membuat perintah yang dikirimkan ke pengguna jarak jauh. Karena game engine hanya bertindak berdasarkan perintah dan entity-id, game akan disimulasikan dengan cara yang sama di kedua sisi. Semua unit juga bisa memiliki komponen ai yang memungkinkan pengujian otomatis yang mudah.

Akan optimal untuk memiliki perender kanvas yang lebih sederhana di awal pengembangan sambil berfokus pada logika game. Namun, kesenangan yang sebenarnya dimulai saat versi 3D diterapkan dan adegan menjadi hidup dengan lingkungan dan animasi. Kami menggunakan three.js sebagai mesin 3D, dan mudah untuk mencapai status yang dapat dimainkan karena arsitekturnya.

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