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

مقدمة

هل تريد إنشاء لعبة باستخدام Canvas و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 في معالجة أحداث لوحة المفاتيح على الأحداث بالكامل. ويعني ذلك أنّه لا يتوفّر طلب بحث مُدمَج للتحقّق مما إذا كان المفتاح غير متاح، لذا علينا استخدام طلب البحث الخاص بنا.

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

الخبر السار هو أنّني أدرجت ملفًا برمجيًا لتغليف JavaScript يتألف من 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. اللاعب => السفن المعادية

لننشئ طريقة للتعامل مع عمليات الاصطدام التي يمكننا استدعاؤها من طريقة update.

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، مثل الصور، ولكن بفضل صيغة sound.js السحرية التي لا تتطلّب أيّ جهد، يمكن استخدام الصوت بسهولة فائقة.

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

function Enemy(I) {
  ...

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

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

Farewell

إليك مرة أخرى الإصدار التجريبي الكامل من اللعبة. يمكنك أيضًا تنزيل رمز المصدر بتنسيق zip.

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

المراجع