Studi Kasus - Serangan! Arena

Pengantar

Pada bulan Juni 2010, kami menyadari bahwa penerbit lokal "zine" Boing Boing mengadakan kompetisi pengembangan game. Kami melihatnya sebagai alasan yang sangat tepat untuk membuat game yang cepat dan sederhana di JavaScript dan <canvas>, jadi kami mulai bekerja. Setelah berkompetisi, kami masih memiliki banyak ide dan ingin menyelesaikan apa yang telah kami mulai. Berikut studi kasus hasilnya, sebuah game kecil bernama Onslaught! Arena.

Tampilan retro dengan piksel

Penting bagi kami untuk menjaga tampilan dan nuansa game seperti game Nintendo Entertainment System retro, mengingat premis kontes untuk mengembangkan game berdasarkan chiptune. Sebagian besar game tidak memiliki persyaratan ini, tetapi tetap merupakan gaya artistik umum (terutama di kalangan developer indie) karena kemudahan pembuatan aset dan daya tariknya yang alami bagi gamer yang masih nostalgia.

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

Mengingat seberapa kecil sprite ini, kami memutuskan untuk menggandakan piksel, yang berarti bahwa sprite 16x16 sekarang akan menjadi 32x32 piksel dan seterusnya. Sejak awal, kami telah menggandakan hal-hal terkait pembuatan aset, bukan membuat browser melakukan bagian pekerjaan yang sulit. Fitur ini lebih mudah diimplementasikan, tetapi juga memiliki beberapa kelebihan 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 menggandakannya pada sisi pembuatan aset. Dari sana, CSS akan mengambil alih dan mengubah ukuran kanvas itu sendiri. Tolok ukur kami mengungkapkan bahwa metode ini dapat kira-kira dua kali lebih cepat daripada gambar yang lebih besar (digandakan), tetapi sayangnya pengubahan ukuran CSS mencakup anti-aliasing, sesuatu yang tidak dapat kami temukan cara untuk mencegahnya.

Opsi pengubahan ukuran kanvas
Kiri: aset dengan piksel sempurna dibuat dua kali lipat di Photoshop. Kanan: Pengubahan ukuran CSS menambahkan efek buram.

Ini adalah pemecah kesepakatan untuk game karena setiap piksel sangat penting, tetapi jika Anda perlu mengubah ukuran kanvas dan anti-aliasing sesuai untuk project, Anda dapat mempertimbangkan pendekatan ini karena alasan performa.

Trik seru kanvas

Kita semua tahu bahwa <canvas> adalah fitur baru yang populer, tetapi terkadang developer masih merekomendasikan penggunaan DOM. Jika Anda bingung mengenai mana yang harus digunakan, berikut adalah contoh bagaimana <canvas> menghemat banyak waktu dan energi.

Saat musuh diserang serangan gencar! Arena, warnanya berkedip merah dan menampilkan animasi "pain" secara singkat. Untuk membatasi jumlah grafis yang harus dibuat, kita hanya menampilkan musuh dalam "sakit" ke arah bawah. Ini terlihat dalam game yang dapat diterima dan menghemat banyak waktu pembuatan sprite. Namun, bagi monster bos, sangat mengagetkan saat melihat sprite besar (dengan ukuran 64x64 piksel atau lebih) bergeser dari kiri atau ke atas secara tiba-tiba menghadap ke bawah untuk mendapatkan frame nyeri.

Solusi yang jelas adalah dengan membuat masalah untuk setiap atasan di kedelapan arah masing-masing, tetapi cara ini akan sangat memakan waktu. Berkat <canvas>, kami dapat menyelesaikan masalah ini dalam kode:

Penangkapan mengalami kerusakan dalam Serangan gencar! Arena
Efek menarik dapat dibuat menggunakan context.globalCompositeOperation.

Pertama, kita menggambar monster ke "buffer" <canvas> tersembunyi, menempatkannya 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 penting dengan pengembangan web. Dalam stack web, biasanya bereaksi terhadap peristiwa yang terjadi melalui pemroses peristiwa. Jadi, kode inisialisasi mungkin tidak melakukan apa pun selain memproses peristiwa input. Logika game berbeda, karena game harus terus diupdate. Jika, misalnya, pemain belum bergerak, seharusnya tidak menghentikan goblin menangkapnya!

Berikut adalah contoh game loop:

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

setInterval(main, 1);

Perbedaan penting pertama adalah fungsi handleInput tidak benar-benar melakukan apa pun secara langsung. Jika pengguna menekan tombol di aplikasi web biasa, sebaiknya segera lakukan tindakan yang diinginkan. Namun, dalam game, segala sesuatunya harus terjadi dalam urutan kronologis agar dapat 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 telah mengetahui input tersebut dan dapat mempertimbangkannya dalam fungsi update karena 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 selesai menghitung semuanya, saatnya menggambar ulang layar. Dalam DOM-land, browser menangani peningkatan heaning ini. Namun, saat menggunakan <canvas>, Anda perlu menggambar ulang secara manual setiap kali sesuatu terjadi (yang biasanya terjadi di 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 gerakan sprite berdasarkan jumlah waktu yang berlalu sejak update frame terakhir. Teknik ini memungkinkan game Anda berjalan secepat mungkin sambil memastikan sprite bergerak dengan kecepatan yang konsisten.

Untuk menggunakan pemodelan berbasis waktu, kita perlu merekam waktu yang berlalu sejak frame terakhir digambar. Kita perlu meningkatkan fungsi update() game loop 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 berlalu, kita dapat menghitung seberapa jauh sprite harus menggerakkan 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 adalah cara kita memindahkan instance dari 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 dalam pengembangan Onslaught! Arena. Demo pertama hanya mendukung keyboard; pemain memindahkan karakter utama di sekitar layar dengan tombol panah dan menembak ke arah hadapnya dengan tombol spasi. Meskipun agak intuitif dan mudah dipahami, hal ini membuat game ini hampir tidak dapat dimainkan di level yang lebih sulit. Dengan banyaknya musuh dan proyektil yang terbang ke pemain pada waktu tertentu, sangat penting untuk dapat menembak orang-orang jahat sambil menembaki pemain 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 digerakkan dengan keyboard, tetapi setelah perubahan ini, dia dapat menembak secara bersamaan ke arah 360 derajat penuh. Pemain hardcore menyukai fitur ini, tetapi memiliki efek samping yang tidak menyenangkan, yaitu membuat pengguna trackpad yang frustrasi.

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

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

Serangan gencar! Tutorial kontrol arena
Sebagian besar pemain mengabaikan overlay tutorial; mereka lebih suka bermain dan bersenang-senang.

Kami juga beruntung memiliki beberapa penggemar Eropa, tapi kami pernah mendengar kekesalan dari mereka karena mereka mungkin tidak memiliki keyboard QWERTY biasa dan tidak dapat menggunakan tombol WASD untuk gerakan terarah. Pemain tangan kiri telah menyampaikan keluhan yang serupa.

Dengan skema kontrol kompleks yang telah kita implementasikan ini, ada juga masalah pemutaran di perangkat seluler. Benar-benar salah satu permintaan yang paling umum adalah membuat Onslaught! Arena tersedia di Android, iPad, dan perangkat sentuh lainnya (jika tidak ada keyboard). Salah satu kekuatan inti HTML5 adalah portabilitasnya, sehingga menghadirkan game ke perangkat ini pasti dapat dilakukan. Kami 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 ke lokasi yang ditekan, secara otomatis menyerang penjahat terdekat. Kodenya akan 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 karena harus membidik musuh dapat membuat game menjadi lebih mudah dalam beberapa situasi, tetapi kami merasa bahwa menyederhanakan segalanya bagi pemain memiliki banyak keuntungan. Strategi lainnya juga diterapkan, seperti memosisikan karakter di dekat musuh berbahaya untuk menargetkan mereka, dan kemampuan untuk mendukung perangkat sentuh sangat berharga.

Audio

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

Masalah lainnya meliputi peristiwa "progres" yang gagal diaktifkan, yang dapat menyebabkan alur pemuatan game berhenti berfungsi tanpa batas. Karena alasan ini, kami mengadopsi metode yang kami sebut metode "fall-forward", dan jika Flash gagal dimuat, kita akan beralih ke Audio HTML5. Kodenya akan 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 game harus 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

Kamu tidak mungkin melakukan tembak-menembak bergaya arcade tanpa skor yang tinggi! Kami tahu bahwa kami memerlukan sebagian data game kami untuk bertahan, dan meskipun kami dapat menggunakan sesuatu yang lama seperti cookie, kami ingin menggali teknologi HTML5 baru yang menyenangkan. Opsi yang tersedia adalah Penyimpanan lokal, Penyimpanan Sesi, dan Database Web SQL, tentu saja tetap tersedia.

ALT_TEXT_HERE
Skor tertinggi akan disimpan, serta posisimu di game setelah mengalahkan setiap bos.

Kami memutuskan untuk menggunakan localStorage karena baru, mengagumkan, dan mudah digunakan. API ini mendukung penyimpanan key-value pair dasar yang diperlukan oleh game sederhana. 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 "gotcha" yang harus diperhatikan. Apa pun yang Anda teruskan, nilai disimpan sebagai string, yang dapat menyebabkan beberapa hasil yang tidak diharapkan:

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 luar biasa untuk digunakan. Sebagian besar implementasi menangani semua yang dibutuhkan developer game, mulai dari grafis hingga menyimpan status game. Meskipun ada beberapa masalah yang semakin besar (seperti masalah tag <audio>), developer browser bergerak cepat dan dengan hal-hal yang sudah sehebat mereka, masa depan tampak cerah untuk game yang dibangun di HTML5.

Serangan gencar! Arena dengan logo HTML5 tersembunyi
Kamu bisa mendapatkan perisai HTML5 dengan mengetik "html5" saat memainkan serangan gencar! Arena.