Studi Kasus - Serangan! Arena

Pengantar

Pada Juni 2010, kami mengetahui bahwa "zine" publikasi lokal Boing Boing mengadakan kompetisi pengembangan game. Kami melihat ini sebagai alasan yang sangat bagus untuk membuat game yang cepat dan sederhana dalam JavaScript dan <canvas>, jadi kami mulai bekerja. Setelah kompetisi, kami masih memiliki banyak ide dan ingin menyelesaikan apa yang telah kami mulai. Berikut adalah studi kasus hasilnya, sebuah game kecil bernama Onslaught! Arena.

Tampilan retro yang pixelated

Game kami harus terlihat dan terasa seperti game Nintendo Entertainment System retro, mengingat premis kontes untuk mengembangkan game berdasarkan chiptune. Sebagian besar game tidak memiliki persyaratan ini, tetapi ini masih merupakan gaya artistik yang umum (terutama di kalangan developer indie) karena kemudahan pembuatan aset dan daya tarik alami bagi gamer yang bernuansa nostalgia.

Serangan! Ukuran piksel arena
Meningkatkan ukuran piksel dapat mengurangi pekerjaan desain grafis.

Mengingat ukuran sprite ini yang kecil, kami memutuskan untuk menggandakan piksel, yang berarti bahwa sprite 16x16 kini menjadi 32x32 piksel dan seterusnya. Sejak awal, kami telah melakukan duplikasi pada sisi pembuatan aset, bukan membuat browser melakukan pekerjaan berat. Hal ini lebih mudah diterapkan, tetapi juga memiliki beberapa keuntungan tampilan yang pasti.

Berikut adalah skenario yang kami pertimbangkan:

<style>
canvas {
  width: 640px;
  height: 320px;
}
</style>
<canvas width="320" height="240">
  Sorry, your browser is not supported.
</canvas>

Metode ini akan terdiri dari sprite 1x1, bukan menggandakan sprite di sisi pembuatan aset. Dari sana, CSS akan mengambil alih dan mengubah ukuran kanvas itu sendiri. Benchmark kami mengungkapkan bahwa metode ini dapat dua kali lebih cepat daripada merender gambar yang lebih besar (dua kali lipat), tetapi sayangnya perubahan ukuran CSS menyertakan anti-aliasing, sesuatu yang tidak dapat kami temukan cara untuk mencegahnya.

Opsi pengubahan ukuran kanvas
Kiri: aset yang sempurna pikselnya digandakan di Photoshop. Kanan: Perubahan ukuran CSS menambahkan efek buram.

Hal ini menjadi masalah besar bagi game kita karena setiap piksel sangat penting, tetapi jika Anda perlu mengubah ukuran kanvas dan anti-aliasing sesuai untuk project Anda, Anda dapat mempertimbangkan pendekatan ini karena alasan performa.

Trik kanvas yang menyenangkan

Kita semua tahu bahwa <canvas> adalah hal baru yang sedang populer, tetapi terkadang developer masih merekomendasikan penggunaan DOM. Jika Anda ragu-ragu tentang mana yang akan digunakan, berikut contoh bagaimana <canvas> menghemat banyak waktu dan energi.

Saat musuh terkena serangan di Onslaught! Arena, lampu akan berkedip merah dan secara singkat menampilkan animasi "nyeri". Untuk membatasi jumlah grafik yang harus kita buat, kita hanya menampilkan musuh yang "sakit" dalam arah yang menghadap ke bawah. Tampilan ini terlihat dapat diterima dalam game dan menghemat banyak waktu pembuatan sprite. Namun, untuk monster bos, akan terasa aneh jika melihat sprite besar (64x64 piksel atau lebih) beralih dari menghadap ke kiri atau ke atas menjadi tiba-tiba menghadap ke bawah untuk frame rasa sakit.

Solusi yang jelas adalah menggambar frame rasa sakit untuk setiap bos di setiap delapan arah, tetapi hal ini akan sangat memakan waktu. Berkat <canvas>, kita dapat menyelesaikan masalah ini dalam kode:

Beholder menerima kerusakan di Onslaught! Arena
Efek yang menarik dapat dibuat menggunakan context.globalCompositeOperation.

Pertama, kita menggambar monster ke "buffer" <canvas> yang tersembunyi, menutupinya dengan warna merah, lalu merender hasilnya kembali ke layar. Kodenya akan terlihat seperti ini:

// Get the "buffer" canvas (that isn't visible to the user)
var bufferCanvas = document.getElementById("buffer");
var buffer = bufferCanvas.getContext("2d");

// Draw your image on the buffer
buffer.drawImage(image, 0, 0);

// Draw a rectangle over the image using a nice translucent overlay
buffer.save();
buffer.globalCompositeOperation = "source-in";
buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red
buffer.fillRect(0, 0, image.width, image.height);
buffer.restore();

// Copy the buffer onto the visible canvas
document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);

Game Loop

Pengembangan game memiliki beberapa perbedaan yang signifikan dengan pengembangan web. Dalam stack web, biasanya kita bereaksi terhadap peristiwa yang terjadi melalui pemroses peristiwa. Jadi, kode inisialisasi tidak dapat melakukan apa pun selain memproses peristiwa input. Logika game berbeda, karena perlu terus diperbarui. Misalnya, jika pemain belum bergerak, hal itu tidak akan menghentikan goblin untuk menangkapnya.

Berikut adalah contoh loop game:

function main () {
  handleInput();
  update();
  render();
};

setInterval(main, 1);

Perbedaan penting pertama adalah fungsi handleInput tidak langsung melakukan apa pun. Jika pengguna menekan tombol di aplikasi web biasa, sebaiknya segera lakukan tindakan yang diinginkan. Namun, dalam game, semuanya harus terjadi dalam urutan kronologis agar alur berjalan dengan benar.

window.addEventListener("mousedown", function(e) {
  // A mouse click means the players wants to attack.
  // We don't actually do that yet, but instead tell the rest
  // of the program about the request.
  buttonStates[e.button] = true;
}, false);

function handleInput() {
  // Here is where we respond to the click
  if (buttonStates[LEFT_BUTTON]) {
    player.attacking = true;
    delete buttonStates[LEFT_BUTTON];
  }
};

Sekarang kita tahu tentang input dan dapat mempertimbangkannya dalam fungsi update, dengan mengetahui bahwa input tersebut akan mematuhi aturan game lainnya.

function update() {
  // Check for collisions, states, whatever else is needed

  // If after that the player can still attack, do it!
  if (player.attacking && player.canAttack()) {
    player.attack();
  }
};

Terakhir, setelah semuanya dikomputasi, saatnya menggambar ulang layar. Di DOM, browser menangani pengangkatan berat ini. Namun, saat menggunakan <canvas>, Anda perlu menggambar ulang secara manual setiap kali terjadi sesuatu (yang biasanya setiap frame).

function render() {
  // First erase everything, something like:
  context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

  // Draw the player (and whatever else you need)
  context.drawImage(
    player.getImage(),
    player.x, player.y
  );
};

Pemodelan Berbasis Waktu

Pemodelan berbasis waktu adalah konsep sprite bergerak berdasarkan jumlah waktu yang berlalu sejak pembaruan frame terakhir. Teknik ini memungkinkan game Anda berjalan secepat mungkin sekaligus memastikan sprite bergerak dengan kecepatan yang konsisten.

Untuk menggunakan pemodelan berbasis waktu, kita perlu merekam waktu yang telah berlalu sejak frame terakhir digambar. Kita harus meningkatkan fungsi update() loop game untuk melacaknya.

function update() {

  // NOTE: You'll need to initially seed this.lastUpdate
  // with the current time when your game loop starts
  // this.lastUpdate = Date.now();

  // Calculate elapsed time since last frame
  var now = Date.now();
  var elapsed = (now - this.lastUpdate);
  this.lastUpdate = now;

  // Do stuff with elapsed

};

Setelah memiliki waktu yang berlalu, kita dapat menghitung seberapa jauh sprite tertentu harus memindahkan setiap frame. Pertama, kita harus melacak beberapa hal pada objek sprite: Posisi, kecepatan, dan arah saat ini.

var Sprite = function() {

  // The sprite's position relative to the top left of the game world
  this.position = {x: 0, y: 0};

  // The sprite's direction. A positive x value indicates moving to the right
  this.direction = {x: 1, y: 0};

  // How many pixels the sprite moves per second
  this.speed = 50;
};

Dengan mempertimbangkan variabel ini, berikut cara memindahkan instance class sprite di atas menggunakan pemodelan berbasis waktu:

// Determine how far this sprite will move this frame
var distance = (sprite.speed / 1000) * elapsed;

// Apply the movement distance to the sprite's current position
// taking into account its direction
sprite.position.x += (distance * sprite.direction.x);
sprite.position.y += (distance * sprite.direction.y);

Perhatikan bahwa nilai direction.x dan direction.y harus dinormalisasi yang berarti nilai tersebut harus selalu berada di antara -1 dan 1.

Kontrol

Kontrol mungkin menjadi batu sandungan terbesar saat mengembangkan Onslaught. Arena. Demo pertama hanya mendukung keyboard; pemain memindahkan karakter utama di sekitar layar dengan tombol panah dan menembak ke arah yang dia hadapi dengan tombol spasi. Meskipun cukup intuitif dan mudah dipahami, hal ini membuat game hampir tidak dapat dimainkan di level yang lebih sulit. Dengan puluhan musuh dan proyektil yang terbang ke arah pemain pada waktu tertentu, Anda harus dapat menghindari musuh sambil menembak ke arah mana pun.

Untuk membandingkan dengan game serupa dalam genrenya, kami menambahkan dukungan mouse untuk mengontrol reticle penargetan, yang akan digunakan karakter untuk mengarahkan serangannya. Karakter masih dapat dipindahkan dengan keyboard, tetapi setelah perubahan ini, dia dapat menembak secara bersamaan ke arah 360 derajat penuh. Pemain hardcore memuji fitur ini, tetapi fitur ini memiliki efek samping yang tidak menguntungkan, yaitu membuat pengguna trackpad frustrasi.

Serangan! Modal kontrol Arena (tidak digunakan lagi)
Kontrol lama atau modal "cara bermain" di Onslaught. Arena.

Untuk mengakomodasi pengguna trackpad, kami menghadirkan kembali kontrol tombol panah, kali ini untuk memungkinkan pengaktifan dalam arah yang ditekan. Meskipun kami merasa bahwa kami melayani semua jenis pemain, kami juga tanpa sadar memperkenalkan terlalu banyak kompleksitas ke game kami. Yang mengejutkan, kami kemudian mendengar bahwa beberapa pemain tidak mengetahui kontrol mouse (atau keyboard!) opsional untuk menyerang, meskipun modal tutorial, yang sebagian besar diabaikan.

Serangan! Tutorial kontrol Arena
Pemain sebagian besar mengabaikan overlay tutorial; mereka lebih suka bermain dan bersenang-senang.

Kami juga beruntung memiliki beberapa penggemar di Eropa, tetapi kami mendengar keluhan dari mereka bahwa mereka mungkin tidak memiliki keyboard QWERTY standar dan tidak dapat menggunakan tombol WASD untuk gerakan arah. Pemain kidal telah menyampaikan keluhan yang serupa.

Dengan skema kontrol kompleks yang telah kami terapkan, ada juga masalah pemutaran di perangkat seluler. Memang, salah satu permintaan kami yang paling umum adalah membuat Onslaught! Arena tersedia di Android, iPad, dan perangkat sentuh lainnya (tanpa keyboard). Salah satu kekuatan inti HTML5 adalah portabilitasnya, sehingga game dapat diinstal di perangkat ini, kita hanya harus menyelesaikan banyak masalah (terutama, kontrol dan performa).

Untuk mengatasi banyak masalah ini, kami mulai bermain dengan metode game input tunggal yang hanya melibatkan interaksi mouse (atau sentuh). Pemain mengklik atau menyentuh layar dan karakter utama berjalan menuju lokasi yang ditekan, secara otomatis menyerang penjahat terdekat. Kodenya terlihat seperti ini:

// Find the nearest hostile target (if any) to the player
var player = this.getPlayerObject();
var hostile = this.getNearestHostile(player);
if (hostile !== null) {
  // Found one! Shoot in its direction
  var shoot = hostile.boundingBox().center().subtract(
    player.boundingBox().center()
  ).normalize();
}

// Move towards where the player clicked/touched
var move = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).normalize();
var distance = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).magnitude();

// Prevent jittering if the character is close enough
if (distance < 3) {
  move.zero();
}

// Move the player
if ((move.x !== 0) || (move.y !== 0)) {
  player.setDirection(move);
}

Menghapus faktor tambahan yang mengharuskan pemain membidik musuh dapat membuat game lebih mudah dalam beberapa situasi, tetapi kami merasa bahwa mempermudah pemain memiliki banyak keuntungan. Strategi lain muncul, seperti harus memosisikan karakter dekat dengan musuh berbahaya untuk menargetkannya, dan kemampuan untuk mendukung perangkat sentuh sangatlah berharga.

Audio

Di antara kontrol dan performa, salah satu masalah terbesar kami saat mengembangkan Onslaught! Arena adalah tag <audio> HTML5. Mungkin aspek terburuknya adalah latensi: di hampir semua browser, ada penundaan antara memanggil .play() dan suara yang benar-benar diputar. Hal ini dapat merusak pengalaman gamer, terutama saat bermain dengan game yang cepat seperti game kita.

Masalah lainnya termasuk peristiwa "progres" yang gagal diaktifkan, yang dapat menyebabkan alur pemuatan game berhenti berfungsi tanpa batas waktu. Oleh karena itu, kami mengadopsi metode yang kami sebut "fall-forward", yaitu jika Flash gagal dimuat, kami akan beralih ke Audio HTML5. Kodenya terlihat seperti ini:

/*
This example uses the SoundManager 2 library by Scott Schiller:
http://www.schillmania.com/projects/soundmanager2/
*/

// Default to sm2 (Flash)
var api = "sm2";

function initAudio (callback) {
  switch (api) {
    case "sm2":
      soundManager.onerror = (function (init) {
        return function () {
          api = "html5";
          init(callback);
        };
      }(arguments.callee));
      break;
    case "html5":
      var audio = document.createElement("audio");

      if (
        audio
        && audio.canPlayType
        && audio.canPlayType("audio/mpeg;")
      ) {
        callback();
      } else {
        // No audio support :(
      }
      break;
  }
};

Mungkin juga penting bagi game untuk mendukung browser yang tidak akan memutar file MP3 (seperti Mozilla Firefox). Jika demikian, dukungan dapat dideteksi dan dialihkan ke sesuatu seperti Ogg Vorbis, dengan kode seperti ini:

/*
Note: you could instead use "new Audio()" here,
but the client will throw an error if it doesn't support Audio,
which makes using "document.createElement" a safer approach.
*/

var audio = document.createElement("audio");

if (audio && audio.canPlayType) {
  if (!audio.canPlayType("audio/mpeg;")) {
    // Here you know you CANNOT use .mp3 files
    if (audio.canPlayType("audio/ogg; codecs=vorbis")) {
      // Here you know you CAN use .ogg files
    }
  }
}

Menyimpan data

Anda tidak dapat memiliki game tembak-tembakan bergaya arcade tanpa skor tinggi. Kami tahu bahwa kita memerlukan beberapa data game untuk dipertahankan, dan meskipun kita dapat menggunakan sesuatu yang sudah lama seperti cookie, kita ingin mempelajari teknologi HTML5 baru yang menyenangkan. Tentu saja ada banyak opsi, termasuk Penyimpanan lokal, Penyimpanan sesi, dan Database SQL Web.

ALT_TEXT_HERE
Skor tertinggi akan disimpan, begitu juga posisi Anda dalam game setelah mengalahkan setiap bos.

Kami memutuskan untuk menggunakan localStorage karena baru, keren, dan mudah digunakan. Class ini mendukung penyimpanan pasangan kunci/nilai dasar yang diperlukan game sederhana kita. Berikut adalah contoh sederhana cara menggunakannya:

if (typeof localStorage == "object") {
  localStorage.setItem("foo", "bar");
  localStorage.getItem("foo"); // Value is "bar"
  localStorage.removeItem("foo");
  localStorage.getItem("foo"); // Value is now null
}

Ada beberapa "masalah" yang perlu diperhatikan. Apa pun yang Anda teruskan, nilai akan disimpan sebagai string, yang dapat menyebabkan beberapa hasil yang tidak terduga:

localStorage.setItem("foo", false);
typeof localStorage.getItem("foo"); // Value is "false" (a string literal)
if (localStorage.getItem("foo")) {
  // It's true!
}

// Don't pass objects into setItem
localStorage.setItem("bar", {"key": "value"});
localStorage.getItem("bar"); // Value is "[object Object]" (a string literal)

// JSON stringify and parse when dealing with localStorage
localStorage.setItem("json", JSON.stringify({"key": "value"}));
typeof localStorage.getItem("json"); // string
JSON.parse(localStorage.getItem("json")); // {"key": "value"}

Ringkasan

HTML5 sangat mudah digunakan. Sebagian besar implementasi menangani semua yang diperlukan developer game, mulai dari grafis hingga menyimpan status game. Meskipun ada beberapa masalah pertumbuhan (seperti masalah tag <audio>), developer browser bergerak dengan cepat dan dengan hal-hal yang sudah bagus, masa depan terlihat cerah untuk game yang dibuat di HTML5.

Serangan! Arena dengan logo HTML5 tersembunyi
Anda bisa mendapatkan perisai HTML5 dengan mengetik "html5" saat bermain Onslaught. Arena.