Örnek Olay - Onslaught! Arena

Geoff Blair
Geoff Blair
Matt Hackett
Matt Hackett

Giriş

Haziran 2010'da, yerel yayıncı "dergi" Boing Boing'in bir oyun geliştirme yarışması düzenlendiğini fark ettik. Bunu JavaScript ve <canvas> ile hızlı, basit bir oyun geliştirmek için çok iyi bir bahane olarak gördük ve işe koyulduk. Yarışmadan sonra da hâlâ pek çok fikir vardı ve başladığımız işi bitirmek istiyorduk. İşte sonucun örnek olayını, Onslaught! Arena'yı seçin.

Retro, pikselleştirilmiş görünüm

chiptune'a dayalı bir oyun geliştirmek için yarışma ortamı göz önüne alındığında oyunumuzun retro bir Nintendo Entertainment System oyunu gibi görünmesini ve hissi uyandırması önemliydi. Çoğu oyunda bu şart yoktur, ancak öğe oluşturma kolaylığı ve nostaljik oyunculara doğal çekici gelmesi nedeniyle hâlâ yaygın bir sanatsal stildir (özellikle bağımsız geliştiriciler arasında).

Saldırıya uğratıldı! Arena piksel boyutları
Piksel boyutunu artırmak grafik tasarım çalışmalarını azaltabilir.

Bu imgelerin ne kadar küçük olduğu göz önünde bulundurulduğunda piksellerimizi iki katına çıkarmaya karar verdik. Diğer bir deyişle, 16x16 imge artık 32x32 piksel olacaktır ve bu böyle devam edecektir. En başından beri, işin büyük kısmını tarayıcıya yapmak yerine öğe oluşturma işini ikiye katlıyorduk. Bu, uygulanması daha kolaydı, ancak görünümle ilgili bazı avantajları da vardı.

Aşağıda, dikkate aldığımız bir senaryo verilmiştir:

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

Bu yöntem, öğe oluşturma tarafında iki katına çıkmak yerine 1x1 sprite görsellerden oluşur. Ardından CSS alanı devralıp tuvalin kendisini yeniden boyutlandırır. Karşılaştırmalarımız, bu yöntemin daha büyük (iki katlanmış) resimleri oluşturmaktan yaklaşık iki kat daha hızlı olabileceğini göstermiştir. Ancak ne yazık ki CSS yeniden boyutlandırma özelliği, kenar yumuşatma içerir. Bu da önlemenin bir yolunu bulamadığımız bir durumdur.

Tuval yeniden boyutlandırma seçenekleri
Sol: Photoshop'ta piksel mükemmelliğindeki öğeler ikiye katlandı. Sağ: CSS'nin yeniden boyutlandırması bulanık bir efekt ekledi.

Pikseller tek tek çok önemli olduğu için bu, bizim için bir dönüm noktası oldu. Ancak tuvalinizi yeniden boyutlandırmanız gerekiyorsa ve projeniz için kenar yumuşatma işlemi uygunsa performansla ilgili nedenlerden dolayı bu yaklaşımı değerlendirebilirsiniz.

Tuvalle ilgili eğlenceli ipuçları

Yeni özelliğin <canvas> olduğunu hepimiz biliyoruz ama bazen geliştiriciler hâlâ DOM'yi kullanmayı öneriyor. Hangisini kullanacağınız konusunda kararsızsanız <canvas> ürününün bize nasıl zaman ve enerji tasarrufu sağladığına dair bir örneği aşağıda bulabilirsiniz.

Onslaught! Arena'yı tıklayın. Kırmızı renkte yanıp söner ve kısa süreliğine bir "acı" animasyonu görüntülenir. Oluşturmamız gereken grafik sayısını sınırlandırmak için düşmanlarımız sadece aşağı yönlü "sıkıntıları" gösteriyor. Bu, oyun içi kabul edilebilir görünüyor ve çok fazla zaman tasarrufu sağladı. Ancak patron canavarları için, acı çerçevesi için sola veya yukarıya doğru aniden aşağı bakan büyük bir imgenin (64x64 piksel veya daha fazla pikselde) olduğunu görmek rahatsız ediciydi.

Sekiz yönün her birinde her bir patron için bir sorunlu çerçeve çizmek kesin bir çözüm olabilir, ancak bu çok zaman alırdı. <canvas> sayesinde koddaki şu sorunu çözmeyi başardık:

Onslaught&#39;ta hasar gören oyuncu! Arena
context.globalCompositeOperation kullanarak ilginç efektler oluşturulabilir.

Önce canavarı gizli bir "tampon" <canvas> çizeriz, üzeri kırmızıyla kaplar ve ardından sonucu tekrar ekrana getiririz. Bu kod aşağıdaki gibi görünür:

// 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);

Oyun Döngüsü

Oyun geliştirme ile web geliştirme arasında bazı önemli farklar vardır. Web yığınında, etkinlik işleyiciler aracılığıyla gerçekleşen etkinliklere tepki vermek yaygın bir durumdur. Bu nedenle, başlatma kodunun giriş etkinliklerini dinlemekten başka işlevi yoktur. Bir oyunun mantığı farklıdır çünkü sürekli kendisini güncellemek gerekir. Örneğin, bir oyuncu hiç hareket etmemişse bu, goblinlerin onu yakalamasına engel olmamalıdır.

Oyun döngüsüne bir örnek:

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

setInterval(main, 1);

İlk önemli fark, handleInput işlevinin gerçekte hemen bir şey yapmamasıdır. Bir kullanıcı tipik bir web uygulamasında bir tuşa basarsa istenen işlemi hemen gerçekleştirmek mantıklıdır. Ancak bir oyunda işlemlerin doğru şekilde akması için işlemlerin kronolojik sırada gerçekleşmesi gerekir.

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];
  }
};

Artık girdiyi biliyoruz ve diğer oyun kurallarına uyacağını bilerek bu girdiyi update işlevinde değerlendirebiliriz.

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();
  }
};

Son olarak, her şey hesaplandıktan sonra ekranı yeniden çizmenin zamanı gelmiş demektir! DOM alanında, tarayıcı bu kaldırma işlemini gerçekleştirir. Ancak <canvas> kullanırken bir şey meydana geldiğinde (genellikle her karede) manuel olarak yeniden çizim yapmanız gerekir.

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
  );
};

Zamana Dayalı Modelleme

Zamana dayalı modelleme, son kare güncellemesinden bu yana geçen süre miktarına göre imgelerin taşınması kavramıdır. Bu teknik, oyununuzun mümkün olduğunca hızlı çalışmasını sağlarken sprite görsellerin tutarlı hızlarda hareket etmesini sağlar.

Zamana dayalı modellemeyi kullanmak için son kare çizildikten sonra geçen süreyi yakalamamız gerekir. Bunu takip etmek için oyun döngümüzün update() işlevini genişletmemiz gerekir.

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

};

Artık geçen zamana sahip olduğumuza göre, belirli bir imgenin her kareyi ne kadar uzağa taşıması gerektiğini hesaplayabiliriz. Öncelikle, bir imge nesnesi üzerinde birkaç şeyi takip etmemiz gerekir: Mevcut konum, hız ve yön.

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;
};

Bu değişkenleri göz önünde bulundurarak, zamana dayalı modellemeyi kullanarak yukarıdaki imge sınıfının bir örneğini şu şekilde taşıyacağız:

// 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);

direction.x ve direction.y değerlerinin normalleştirilmesi, yani her zaman -1 ile 1 arasında olması gerektiğini unutmayın.

Denetimler

Kontroller muhtemelen geliştirme sırasında en büyük engel olmuştur. Onslaught! Arena'yı seçin. İlk demo yalnızca klavyeyi destekliyordu. Oyuncular ana karakteri ok tuşlarıyla ekranda hareket ettirdi ve boşluk çubuğuyla baktığı yöne doğru ateş etti. Biraz sezgisel ve anlaşılması kolay olmakla birlikte, bu oyun daha zor seviyelerde neredeyse oynanamaz hale getirdi. Oyuncuya her an düzinelerce düşman ve mermi uçtığından, herhangi bir yönde ateş ederken kötü adamlar arasında hareket etmek şart.

Türündeki benzer oyunlarla karşılaştırma yapmak amacıyla, karakterin saldırılarını nişan almak için kullanacağı hedefleme retikülünü kontrol etmek için fare desteği ekledik. Karakter yine de klavyeyle hareket ettirilebilirdi, ancak bu değişiklikten sonra aynı anda 360 derecelik herhangi bir tam yönde de etkinleşebilir. Deneyimli oyuncular bu özelliği çok beğendi ancak bu özellik, dokunmatik yüzey kullanıcılarında talihsiz bir yan etki yarattı.

Saldırıya uğratıldı! Alan kontrolleri iletişim kutusu (kullanımdan kaldırıldı)
Onslaught'ta eski kontroller veya "nasıl oynanır" mesajı Arena.

Dokunmatik yüzey kullanıcılarına kolaylık sağlamak için geri ok tuşu kontrollerini, bu kez de basılmış yönlerde etkinleşmeye izin verecek şekilde kullanıma sunduk. Her tür oyuncuya hizmet verdiğimizi düşünürken, farkında olmadan oyunumuzu çok fazla karmaşık hale getiriyorduk. Büyük ölçüde göz ardı edilen eğitim modellerine rağmen, daha sonra bazı oyuncuların saldırı için isteğe bağlı fare (veya klavye!) kontrollerinden haberdar olmadığını duyduk.

Saldırıya uğratıldı! Arena kontrolleri eğitimi
Oyuncular eğitim yer paylaşımını çoğunlukla göz ardı eder; oyun oynayıp eğlenmeyi tercih ederler!

Avrupalı taraftarlar için de şanslıyız ancak onlardan tipik QWERTY klavyelere sahip olmamaları ve yön hareketleri için WASD tuşlarını kullanamama gibi hayal kırıklığı yaşandığını öğrendik. Solak oyuncular da benzer şikayetlerini belirttiler.

Gerçekleştirdiğimiz bu karmaşık kontrol şemasıyla, mobil cihazlarda oyun oynama sorunu da bir arada. Gerçekten de en çok aldığımız isteklerden biri Onslaught! Arena; Android, iPad ve diğer dokunmatik cihazlarda (klavyenin bulunmadığı) kullanılabilir. HTML5'in güçlü yanlarından biri taşınabilirliğidir. Bu yüzden, oyunu bu cihazlara taşımak kesinlikle mümkün. Tek yapmanız gereken birçok sorunu (en önemlisi kontroller ve performans) çözmektir.

Bu sorunların üstesinden gelmek için, yalnızca fare (veya dokunma) etkileşimi kullanan tek girişli bir oyun yöntemiyle oynamaya başladık. Oyuncular ekranı tıklar veya ekrana dokunur, ana karakter basıldığı yere doğru yürür ve en yakındaki kötü adama otomatik olarak saldırır. Bu kod aşağıdaki gibi görünür:

// 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);
}

Düşmanlara nişan alma zorunluluğu ek unsurunu ortadan kaldırmak bazı durumlarda oyunu kolaylaştırabilir, ancak oyuncu için işleri basitleştirmenin birçok avantajı olduğunu düşünüyoruz. Karakterleri hedeflemek için tehlikeli düşmanların yakınına yerleştirmek gibi başka stratejiler de ortaya çıkıyor. Dokunmatik cihazları destekleme özelliği paha biçilmez bir niteliğe sahip.

Ses

Onslaught! Arena, HTML5'in <audio> etiketiydi. Muhtemelen en kötü yanı gecikmedir: Neredeyse tüm tarayıcılarda .play() çağrısı ile sesin gerçekten çalınması arasında bir gecikme vardır. Bu durum, özellikle de bizimki gibi yüksek tempolu bir oyunla oynarken oyuncuların deneyimini mahvedebilir.

Diğer sorunlar arasında "ilerleme" etkinliğinin etkinleşmemesi yer alır ve bu, oyunun yükleme akışının süresiz olarak takılmasına neden olabilir. Bu nedenlerle, "fall-up" dediğimiz yöntemi benimsedik. Burada, Flash’ın yüklenemediği durumlarda HTML5 Ses’e geçeriz. Bu kod aşağıdaki gibi görünür:

/*
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;
  }
};

Ayrıca, bir oyunun Mozilla Firefox gibi MP3 dosyalarını oynatmayan tarayıcıları desteklemesi de önemli olabilir. Bu durumda, destek algılanabilir ve aşağıdaki gibi bir kodla Ogg Vorbis gibi bir moda geçirilebilir:

/*
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
    }
  }
}

Veri tasarrufu

Rekor sayı olmadan arcade tarzı nişancı oyunu oynayamazsınız! Oyun verilerimizin bir kısmının kalıcı olması gerektiğini biliyorduk ve kurabiye gibi eski tip bir şey kullanabilsek de, yeni HTML5 teknolojilerini keşfetmek istedik. Yerel depolama, Oturum depolama alanı ve Web SQL Veritabanları gibi seçenekler için kesinlikle hiçbir sorun yoktur.

ALT_TEXT_HERE
Rekorlar ve her düşmanı yendikten sonra oyundaki konumunuz kaydedilir.

Yeni, harika ve kullanımı kolay olduğu için localStorage kullanmaya karar verdik. Basit oyunlarımızın ihtiyacı olan temel anahtar/değer çiftlerini kaydetmeyi destekler. Aşağıda, bunu nasıl kullanacağınıza ilişkin basit bir örnek verilmiştir:

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

Dikkat edilmesi gereken bazı “önemli noktalar” vardır. Neyi iletirseniz iletin, değerler dize olarak depolanır ve bu durum bazı beklenmedik sonuçlara yol açabilir:

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"}

Özet

HTML5 ile çalışmak harikadır. Çoğu uygulama, grafiklerden oyun durumunu kaydetmeye kadar bir oyun geliştiricinin ihtiyaç duyduğu her şeyi halleder. Gittikçe büyüyen bazı zorluklar (<audio> etiket sorunları gibi) olsa da tarayıcı geliştiricileri hızlı bir şekilde hareket ediyor ve zaten iyi olan işlerle birlikte HTML5 tabanlı oyunlar için gelecek parlak gözüküyor.

Saldırıya uğratıldı! Gizli HTML5 logosu bulunan alan
Onslaught oynarken "html5" yazarak bir HTML5 kalkanı alabilirsiniz! Arena.