دليلك الكامل حول ألعاب HTML5

دانيال إكس. Moore
دانيال إكس: مور

مقدمة

إذًا، هل تريد إنشاء لعبة باستخدام لوحة الرسم وHTML5؟ تابع هذا البرنامج التعليمي وستكون في طريقك في أي وقت من الأوقات.

يفترض البرنامج التعليمي مستوى متوسط على الأقل من المعرفة بلغة JavaScript.

يمكنك أولاً تشغيل اللعبة أو الانتقال مباشرةً إلى المقالة وعرض رمز المصدر للّعبة.

إنشاء اللوحة

من أجل رسم الأشياء، سنحتاج إلى إنشاء لوحة. نظرًا لأن هذا دليل لا دموع، فسنستخدم jQuery.

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

حلقة الألعاب

ومن أجل محاكاة مظهر اللعب السلس والمستمر، نريد تحديث اللعبة وإعادة رسم الشاشة بشكل أسرع مما يدركه العقل والعين.

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

في الوقت الحالي، يمكننا ترك طرق التحديث والرسم فارغة. المهم الذي يجب معرفته هو أنّ "setInterval()" يعتني بالاتصال بهم بشكل دوري.

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

مرحبًا بالعالم

والآن بعد إجراء حلقة ألعاب، لنحدّث طريقة الرسم لرسم بعض النصوص على الشاشة.

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

هذا أمر رائع بالنسبة إلى النصوص الثابتة، ولكن نظرًا لأن لدينا حلقة ألعاب تم إعدادها بالفعل، فمن المفترض أن يكون بإمكاننا جعلها تتحرك بسهولة تامة.

var textX = 50;
var textY = 50;

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

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

الآن جرّب ذلك. إذا كنت تتابعه، فمن المفترض أن تتحرك، ولكنها تغادر أيضًا المرات السابقة التي رسمتها على الشاشة. توقف لحظة لتخمن لماذا قد يكون ذلك هو الحال. وذلك لأنّنا لا نمسح الشاشة. لذا دعونا نضيف بعض التعليمات البرمجية لمحو الشاشة إلى طريقة الرسم.

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

الآن بعد أن حصلت على بعض النصوص المتحركة على الشاشة، فأنت في منتصف الطريق للحصول على لعبة حقيقية. ما عليك سوى تقوية عناصر التحكم وتحسين أسلوب اللعب وتحسين الرسومات. حسنًا، ربما 1/7 من وقت الحصول على لعبة حقيقية، ولكن الخبر السار هو أن هناك المزيد إلى البرنامج التعليمي.

إنشاء المشغّل

أنشئ كائنًا للاحتفاظ ببيانات اللاعب وتحمّل مسؤولية أمور مثل الرسم. ننشئ هنا كائن لاعب باستخدام كائن بسيط حرفي للاحتفاظ بجميع المعلومات.

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

نستخدم مستطيلاً ملونًا بسيطًا لتمثيل المشغل في الوقت الحالي. عندما نرسم اللعبة، نمحو اللوحة ونرسم اللاعب.

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

عناصر تحكم لوحة المفاتيح

استخدام مفاتيح التشغيل السريع في jQuery

يجعل المكوّن الإضافي jQuery Hotkeys معالجة المفتاح في المتصفحات أكثر سهولة. بدلاً من البكاء بخصوص مشاكل keyCode وcharCode التي لا يمكن فك ترميزها على مستوى المتصفحات، يمكننا ربط الأحداث على النحو التالي:

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

عدم الحاجة إلى القلق بشأن تفاصيل المفاتيح التي تحتوي على الرموز المكسبة الكبيرة. نريد فقط أن نعرف أشياء مثل "عندما يضغط المشغل على الزر لأعلى، تنفيذ إجراء ما". تسمح مفاتيح التشغيل السريع في jQuery بذلك بشكل جيد.

حركة اللاعب

تعتمد طريقة تعامل JavaScript مع أحداث لوحة المفاتيح على الأحداث بالكامل. هذا يعني أنه لا يوجد استعلام مضمن للتحقق مما إذا كان المفتاح معطلاً، لذلك سيتعين علينا استخدام استعلامنا الخاص.

قد تطرح على نفسك السؤال التالي، "لماذا لا نستخدم مجرد طريقة التعامل مع الأحداث في التعامل مع المفاتيح؟" يعود السبب في ذلك إلى أنّ معدّل تكرار لوحة المفاتيح يختلف من نظام إلى آخر وليس بتوقيت حلقة الألعاب. وبالتالي، قد يختلف أسلوب اللعب بشكل كبير من نظام إلى آخر. لتوفير تجربة متّسقة، من المهم أن يتم دمج ميزة رصد أحداث لوحة المفاتيح بإحكام في حلقة الألعاب.

الخبر السار هو أنني قمت بتضمين برنامج تضمين JS من 16 سطرًا سيجعل الاستعلام عن الحدث متاحًا. وهو يُدعى key_status.js، ويمكنك الاستعلام عن حالة المفتاح في أي وقت عن طريق التحقق من keydown.left وما إلى ذلك.

والآن بعد أن أصبح بإمكاننا معرفة ما إذا كانت المفاتيح معطلة، يمكننا استخدام طريقة التحديث البسيطة هذه لتحريك المشغل.

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

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

انطلق وجربها.

قد تلاحظ إمكانية إبعاد المشغّل عن الشاشة. لنثبّت موضع اللاعب للحفاظ عليه ضمن الحدود. بالإضافة إلى ذلك، يبدو أن المشغّل بطيئًا بعض الشيء، لذا هيا نزيد السرعة أيضًا.

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

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

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

ستكون إضافة المزيد من الإدخالات بالسهولة نفسها، لذلك دعنا نضيف نوعًا من القذائف.

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

إضافة المزيد من عناصر اللعبة

قذائف

لنقم الآن بإضافة القذائف بشكل حقيقي. أولاً، نحتاج إلى مجموعة لتخزينها جميعًا في:

var playerBullets = [];

بعد ذلك، نحتاج إلى دالة إنشائية لإنشاء مثيلات نقطية.

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

عندما يطلق اللاعب، يجب أن ننشئ مثيلاً نقطيًا ونضيفه إلى مجموعة الرموز النقطية.

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

نحتاج الآن إلى إضافة تحديث الرموز النقطية إلى دالة خطوة التحديث. لمنع مجموعة الرموز النقطية من الامتلاء إلى أجل غير مسمى، تتم تصفية قائمة الرموز النقطية لتضمين الرموز النقطية النشطة فقط. هذا يتيح لنا أيضًا إزالة الرصاصات التي اصطدمت بعدوٍ ما.

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

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

الخطوة الأخيرة هي رسم الرموز النقطية:

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

الأعداء

حان الوقت الآن لإضافة الأعداء بنفس الطريقة التي أضفنا بها الرصاص.

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

تحميل الصور ورسمها

من الرائع مشاهدة كل تلك الصناديق التي تطير حولها، ولكن سيكون الحصول على صور لها أكثر برودة. عادةً ما يكون تحميل الصور ورسمها على اللوحة تجربة دمعة. ولمنع هذا الألم والبؤس، يمكننا استخدام فئة بسيطة للخدمات.

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

  ...
}

رصد الحوادث

تظهر لدينا كل هذه العروض على الشاشة، لكنها لا تتفاعل مع بعضها البعض. لكي يتسنى لكل شيء معرفة وقت حدوث انفجار، سنحتاج إلى إضافة نوع من أنواع كشف الاصطدامات.

لنستخدم خوارزمية بسيطة مستطيلة للكشف عن التصادم:

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

هناك تصادمان نريد التحقق منهما:

  1. الألعاب النقطية => سفن العدو
  2. اللاعب => سفن العدو

لنُنشئ طريقة للتعامل مع التصادمات التي يمكننا استدعاءها من طريقة التحديث.

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

نحتاج الآن إلى إضافة طرق الانفجار إلى اللاعب والأعداء. سيؤدي ذلك إلى الإبلاغ عن هذه العناصر لإزالتها وإضافة تأثير انفجار.

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

صوت

ولختام التجربة، سنضيف بعض المؤثرات الصوتية الرائعة. قد تكون الأصوات، مثل الصور، صعبة إلى حد ما عند استخدامها في HTML5، ولكن بفضل معادلتنا السحرية الخالية من الدماغ، يمكن جعل الصوت بسيطًا للغاية.

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

function Enemy(I) {
  ...

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

على الرغم من أنّ واجهة برمجة التطبيقات أصبحت خالية من المدموع الآن، إنّ إضافة الأصوات هي حاليًا أسرع طريقة لتعطُّل تطبيقك. من الشائع أن تتعطل الأصوات أو تزيلها علامة تبويب المتصفح بأكملها، لذا جهِّز مناديلك.

الوداع

مرة أخرى، إليك العرض التوضيحي الكامل لللعبة التي تعمل بشكل كامل. يمكنك تنزيل رمز المصدر كملف zip أيضًا.

حسنًا، أتمنى أن تكون قد استمتعت بتعلم أساسيات إنشاء لعبة بسيطة بJavaScript وHTML5. ومن خلال البرمجة على المستوى المناسب من التجريد، يمكننا عزل أنفسنا عن الأجزاء الأكثر صعوبة في واجهات برمجة التطبيقات، بالإضافة إلى المرونة في مواجهة أي تغييرات مستقبلية.

المراجع