HTML5 oyunları hakkında göz atın

Daniel X. Moore
Daniel X. Moore

Giriş

Tuval ve HTML5 kullanarak oyun oluşturmak mı istiyorsunuz? Bu eğitimle kısa sürede yola çıkabilirsiniz.

Bu eğitimde, JavaScript hakkında en azından orta düzeyde bilgi sahibi olduğunuz varsayılmaktadır.

Önce oyunu oynayabilir veya doğrudan makaleye gidip oyunun kaynak kodunu görüntüleyebilirsiniz.

Tuvali oluşturma

Bir şeyler çizebilmek için bir kanvas oluşturmamız gerekir. Bu bir "zor değil" kılavuzu olduğu için jQuery kullanacağız.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

Oyun döngüsü

Sorunsuz ve kesintisiz bir oyun deneyimi sunmak için oyunu güncellemek ve ekranı insan aklının ve gözünün algılayabileceğinden biraz daha hızlı yeniden çizmek isteriz.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

Şimdilik güncelleme ve çizim yöntemlerini boş bırakabiliriz. Önemli olan, setInterval()'ün bu kullanıcıları düzenli olarak araması gerektiğidir.

function update() { ... }
function draw() { ... }

Hello world

Oyun döngümüzü oluşturduğumuza göre, ekrana metin çizmek için çizim yöntemimizi güncelleyelim.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

Hareketsiz metinler için bu oldukça iyi bir yöntem. Ancak zaten ayarlanmış bir oyun döngümüzün olması nedeniyle metni kolayca hareket ettirebiliriz.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Şimdi bunu deneyin. Bu işlemi takip ediyorsanız şekil hareket ediyor olmalı ancak ekranda çizildiği önceki zamanları da bırakıyor olmalıdır. Bunun neden olabileceğini bir dakikanızı ayırıp tahmin edin. Bunun nedeni, ekranı temizlemememizdir. Şimdi, draw yöntemine ekranı temizleme kodu ekleyelim.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Ekranınızda hareket eden metinler var. Artık gerçek bir oyuna sahip olma yolunda yarı yoldasınız. Kontrolleri sıkılaştırın, oynanabilirliği iyileştirin, grafikleri düzeltin. Tamam, gerçek bir oyuna sahip olma yolunda belki de 7/1'i tamamladınız. Ancak iyi haber şu ki, eğitimde çok daha fazlası var.

Oyuncu oluşturma

Oyuncu verilerini saklayacak ve çizim gibi işlemlerden sorumlu olacak bir nesne oluşturun. Burada, tüm bilgileri saklamak için basit bir nesne değişmezi kullanarak bir oyuncu nesnesi oluşturuyoruz.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

Şu anda oyuncuyu temsil etmek için basit bir renkli dikdörtgen kullanıyoruz. Oyunu çizerken tuvali temizleyip oyuncuyu çizeriz.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Klavye denetimleri

jQuery kısayol tuşlarını kullanma

jQuery Hotkeys eklentisi, tarayıcılar arasında anahtar kullanımını çok daha kolay hale getirir. Anlaşılması zor tarayıcılar arası keyCode ve charCode sorunları için ağlamak yerine etkinlikleri aşağıdaki gibi bağlayabiliriz:

$(document).bind("keydown", "left", function() { ... });

Hangi anahtarların hangi kodlara sahip olduğuyla ilgili ayrıntılar hakkında endişelenmenize gerek kalmaması büyük bir avantajdır. "Oyuncu yukarı düğmesine bastığında bir şey yap" gibi şeyler söylemek istiyoruz. jQuery Hotkeys bunu güzel bir şekilde sağlıyor.

Oyuncu hareketi

JavaScript'in klavye etkinliklerini işleme şekli tamamen olaya dayalı. Bu, bir anahtarın kapalı olup olmadığını kontrol etmek için yerleşik bir sorgu olmadığı anlamına gelir. Bu nedenle, kendi sorgumuzu kullanmamız gerekir.

"Anahtarları işlemek için neden etkinlik odaklı bir yöntem kullanmıyoruz?" diye sorabilirsiniz. Bunun nedeni, klavye tekrar hızının sistemlere göre değişiyor olması ve oyun döngüsünün zamanlamasına bağlı olmamasıdır. Bu nedenle, oyun oynama deneyimi sistemden sisteme büyük ölçüde değişiklik gösterebilir. Tutarlı bir deneyim oluşturmak için klavye etkinliği algılamanın oyun döngüsüne sıkı bir şekilde entegre edilmesi önemlidir.

İyi bir haberim var. Etkinlik sorgulamasını mümkün kılacak 16 satırlık bir JS sarmalayıcı ekledim. Bu dosya key_status.js olarak adlandırılır ve keydown.left vb. kontrol ederek bir anahtarın durumunu istediğiniz zaman sorgulayabilirsiniz.

Tuşların basılı olup olmadığını sorgulayabildiğimize göre, oynatıcıyı taşımak için bu basit güncelleme yöntemini kullanabiliriz.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Denemeye ne dersiniz?

Oynatıcının ekrandan kaldırılabileceğini fark edebilirsiniz. Oyuncunun konumunu, sınırlar içinde tutacak şekilde sınırlayalım. Ayrıca oynatıcı biraz yavaş görünüyor. Hızı da artıralım.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Daha fazla giriş eklemek de aynı derecede kolaydır. Bu nedenle, bir tür mermi ekleyelim.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

Daha fazla oyun nesnesi ekleme

Roketler

Şimdi mermileri gerçek olarak ekleyelim. Öncelikle, hepsini depolayacak bir koleksiyona ihtiyacımız var:

var playerBullets = [];

Ardından, madde örnekleri oluşturmak için bir kurucuya ihtiyacımız var.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

Oyuncu ateş ettiğinde bir mermi örneği oluşturmalı ve bunu mermi koleksiyonuna eklemeliyiz.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

Şimdi, güncelleme adımı işlevine madde işaretlerinin güncellenmesini eklememiz gerekiyor. Listedeki madde işaretlerinin sınırsız bir şekilde dolmasını önlemek için madde işaretleri listesini yalnızca etkin madde işaretlerini içerecek şekilde filtreleriz. Bu sayede, düşmanla çarpışan mermileri de kaldırabiliriz.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

Son adım, ipuçlarını çizmektir:

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Düşmanlar

Şimdi, mermileri eklediğimize benzer şekilde düşmanları ekleme zamanı.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Resim yükleme ve çizme

Tüm bu kutuların uçuşmasını izlemek çok güzel ama bunların resimlerini görmek daha da güzel olur. Kanvas üzerine resim yüklemek ve çizmek genellikle göz yaşartıcı bir deneyimdir. Bu sıkıntıyı önlemek için basit bir yardımcı program sınıfı kullanabiliriz.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Çarpışma algılama

Ekranda uçuşan tüm bu öğeler birbirinden etkileşimde bulunmuyor. Her şeyin ne zaman patlayacağını bildirmek için bir tür çarpışma algılama eklememiz gerekiyor.

Basit bir dikdörtgensel çarpışma algılama algoritması kullanalım:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Kontrol etmek istediğimiz birkaç çakışma var:

  1. Oyuncu Mermileri => Düşman Gemileri
  2. Oyuncu => Düşman Gemileri

Çakışmaları ele alacak ve güncelleme yönteminden çağırabileceğimiz bir yöntem oluşturalım.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

Şimdi, patlama yöntemlerini oyuncuya ve düşmanlara eklememiz gerekiyor. Bu işlem, öğeleri kaldırmak üzere işaretler ve bir patlama ekler.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Ses

Deneyimi tamamlamak için bazı ses efektleri de ekleyeceğiz. Resimler gibi sesleri HTML5'te kullanmak biraz zor olabilir. Ancak göz yaşartmayan sihirli formülümüz sound.js sayesinde sesleri çok kolay kullanabilirsiniz.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

API artık sorunsuz olsa da ses eklemek, uygulamanızı kilitlemeye yol açan en hızlı yöntemdir. Seslerin kesilmesi veya tarayıcı sekmesinin tamamının kapanması gibi durumlarla karşılaşmanız olasıdır. Bu nedenle, yanınıza mendil alın.

Farewell

Tam çalışan oyun demosunu aşağıda bulabilirsiniz. Kaynak kodunu ZIP olarak da indirebilirsiniz.

JavaScript ve HTML5'te basit bir oyun oluşturmanın temellerini öğrenmekten memnuniyet duyduğunuzu umuyoruz. Doğru soyutlama düzeyinde programlayarak kendimizi API'lerin daha zor kısımlarından koruyabilir ve gelecekteki değişikliklere karşı dirençli olabiliriz.

Referanslar