اذهب إلى المحتوى

بناء تطبيق الكرات المرتدة الملونة باستخدام الكائنات في جافا سكريبت -الجزء الأول


ابراهيم الخضور

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

ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على:

  1. أساسيات HTML.

  2. أساسيات عمل CSS

  3. أساسيات جافا سكريبت كما شرحناها في سلسلة المقالات السابقة.

  4. أساسيات البرمجة كائنية التوجه في جافا سكريبت OOJS كما شرحناها في مقال أساسيات البرمجة كائنية التوجه في جافا سكريبت، ومقال كائنات Prototype في جافا سكريبت.

تطبيق الكرات المرتدة في جافا سكريبت

سنكتب في هذا التطبيق شيفرة برنامج تجريبي بعنوان "الكرات المرتدة Bouncing Balls" لاستعراض مدى فائدة الكائنات في كتابة برامج جافا سكريبت. سترتد الكرات في تطبيقنا عن حواف الشاشة، وتغير ألوانها عندما تتلامس أو تصدم مع بعضها البعض، وسيبدو شكل التطبيق عندما ينتهي مشابهًا للصورة التالية:

01 bouncing balls

يستخدم التطبيق الواجهة البرمجية Canvas لرسم الكرات على الشاشة، والواجهة لتحريك المشهد بأكمله. لا حاجة لأي معرفة مسبقة بالتعامل مع هذه الواجهات البرمجية، على أمل أن يدفعك العمل خلال فترة إنجاز التطبيق إلى استكشاف هاتين الواجهتين أكثر. ستستخدم أيضًا بعض الكائنات اﻷنيقة، ونستعرض تقنيات جميلة مثل جعل كرة ترتد عن جدار، والتحقق من تصادم كرتين (وتعرف باكتشاف التصادم collision detection).

نقطة الانطلاق

لتبدأ العمل، انسخ الملفات index.html و style.css و main.js إلى حاسوبك، وتتضمن هذه الملفات:

  1. ملف HTML بسيط يضم العنصر <h1> والعنصر <canvas> لرسم الكرات وعناصر لتطبيق تنسيقات CSS وشيفرة جافا سكريبت على صفحة HTML.
  2. بعض التنسيقات البسيطة جدًا لتنسيق وضبط موقع العنصر، والتخلص من حواشي الصفحة وأشرطة التمرير لتظهر بمظهر أنيق.
  3. ملف يضم بعض شيفرة جافا سكريبت ﻹعداد العنصر <canvas> وتحضير دالة عامة سنستخدمها في التطبيق.

يبدو القسم اﻷول من الشيفرة كالتالي:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const width = (canvas.width = window.innerWidth);
const height = (canvas.height = window.innerHeight);

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

نُعد تاليًا الثابتين width و height اللذين يمثلان اتساع وارتفاع لوحة الرسم (أي قيمتي الخاصيتين canvas.width وcanvas.height) ليكونا مساويين لارتفاع واتساع نافذة عرض المتصفح (المنطقة التي تُعرض عليها صفحة الويب) ونحصل عليهما من التعليمتين Window.innerWidth و Window.innerHeight. لاحظ كيف ربطنا عدة عمليات إسناد معًا لتسريع عملية ضبط قيم المتغيرات، وهذا أمر صحيح تمامًا.

لدينا بعد ذلك دالتين مساعدتين:

function random(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function randomRGB() {
  return `rgb(${random(0, 255)},${random(0, 255)},${random(0, 255)})`;
}

الأولى هي الدالة ()random التي تأخذ وسيطين وتعيد عددًا عشوائيًا بين قيمتي الوسيطين، والثانية هي ()randomRGB وتولّد لونًا عشوائيًا على شكل نص ناتج عن تنفيذ الدالة ()rgb.

نمذجة كرة في تطبيقنا

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

لنبدأ بإضافة تعريف الصنف التالي في نهاية الشيفرة الموجودة:

class Ball {
  constructor(x, y, velX, velY, color, size) {
    this.x = x;
    this.y = y;
    this.velX = velX;
    this.velY = velY;
    this.color = color;
    this.size = size;
  }
}

يضم الصنف حتى اللحظة دالة بانية فقط، مهمتها تهيئة الخاصيات التي تحتاجها كل كرة حتى تعمل كما هو مطلوب في التطبيق:

  • x و y هما الإحداثيين اﻷفقي والعمودي للنقطة التي تنطلق منها الكرة على الشاشة. يمكن أن تبدأ النقاط من النقطة (0,0) وهي أعلى ويسار نافذة العرض، حتى النقطة التي تمثل أقصى اتساع وأقصى ارتفاع لنافذة عرض المتصفح وهي أدنى ويمين نافذة العرض.
  • السرعتين اﻷفقية velX والعمودية velY: إذ تُعطى لكل كرة سرعتين أفقية وعمودية. وما نفعله في الواقع هو إضافة هاتين القيمتين بانتظام إلى اﻹحداثيين x و y عند تحريك الكرة، وذلك لدفعها بهذا المقدار في كل إطار للحركة.
  • اللون color: وهو اللون الذي تأخذه الكرة.
  • القياس size: وهو قياس الكرة، ويُمثَّل بنصف قطرها.

تمثل القيم السابقة خاصيات الكائن، لكن ماذا عن التوابع؟ فنحن نريد للكرات أن تسلك سلوكًا ما في التطبيق!

رسم الكرة

أضف بداية التابع ()draw إلى الصنف Ball:

draw() {
  ctx.beginPath();
  ctx.fillStyle = this.color;
  ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
  ctx.fill();
}

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

  • نستخدم أولًا التابع ()beginPath لنوضح أننا نريد رسم شكل ما على اللوحة.

  • نستخدم تاليًا التابع fillStyle لنحدد لون الشكل، وضبطناه على قيمة الخاصية color للكرة.

  • نستخدم بعدها التابع ()arc لرسم قوس على الصفحة، وللتابع المعاملات التالية:

  • x و y: وهما إحداثيا مركز القوس، ونضبطهما من قبل الخاصيتين x و y للكرة.

  • نصف قطر القوس: ونضبطه على قيم الخاصية size للكرة.

  • يحدد آخر معاملين زاوية بداية ونهاية القوس على الدائرة (مقدرين بالراديان). وفي حالتنا كانت قيمة البداية 0 والنهاية 2*PI التي تعادل 360 درجة. وهكذا سنتمكن من رسم كامل الدائرة، ولو كانت النهاية PI سنحصل على نصف دائرة (180 درجة).

  • نستخدم أخيرًا التابع ()fill والذي يقتضي مبدئيًا إنهاء رسم المسار الذي بدأه التابع ()beginPath ومن ثم ملئ المساحة الناتجة باللون الذي خصصناه باستخدام التابع fillStyle.

بإمكانك اختبار الكائن اﻵن:

  1. احفظ التغييرات التي أجريتها على الشيفرة وأعد تحميل ملف HTMl.

  2. افتح طرفية جافا سكريبت في المتصفح وأعد تحميل الصفحة كي تأخذ لوحة الرسم أبعاد نافذة عرض المتصفح الجديدة والتي تكون أصغر من اﻷصلية لأنها تُعرض مع الطرفية.

  3. اكتب الشيفرة التالية ﻹنشاء نسخة عن الكائن Ball:

const testBall = new Ball(50, 100, 4, 4, "blue", 10);
  1. جرب استدعاء عناصر الكائن:
testBall.x;
testBall.size;
testBall.color;
testBall.draw();

عندما تكتب التعليمات السابقة، ستشاهد الكرة وقد رسمت نفسها على اللوحة.

تحديث بيانات الكرة

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

update() {
  if ((this.x + this.size) >= width) {
    this.velX = -(this.velX);
  }

  if ((this.x - this.size) <= 0) {
    this.velX = -(this.velX);
  }

  if ((this.y + this.size) >= height) {
    this.velY = -(this.velY);
  }

  if ((this.y - this.size) <= 0) {
    this.velY = -(this.velY);
  }

  this.x += this.velX;
  this.y += this.velY;
}

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

نتحقق في أربع حالات إذا ما كان:

  • اﻹحداثي x أكبر من اتساع لوحة الرسم، ويعني ذلك تجاوز الكرة إلى أقصى يمين اللوحة.
  • اﻹحداثي x أصغر من 0، ويعني ذلك أن تجاوز الكرة إلى أقصى يسار اللوحة.
  • اﻹحداثي y أكبر من ارتفاع لوحة الرسم، ويعني ذلك تجاوز الكرة إلى أسفل لوحة الرسم.
  • اﻹحداثي y أصغر من 0، ويعني ذلك تجاوز الكرة إلى أعلى اللوحة.

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

يضيف السطرين اﻷخيرن قيمتي velX و velY إلى قيمتي x و yعلى الترتيب كي تتحرك الكرة كل مرة نستدعي فيها هذا التابع.

ما فعلناه كافٍ حتى اللحظة، لننتقل الآن إلى مرحلة تحريك الرسوم.

تحريك الكرة

سنبدأ اﻵن إضافة كرات إلى لوحة الرسم وتحريكها. ونحتاج بداية إلى إنشاء مكان نُخزن فيه كل الكرات ثم نطلقها، والشيفرة التالية ستؤدي المطلوب:

const balls = [];

while (balls.length < 25) {
  const size = random(10, 20);
  const ball = new Ball(
    // تُرسم الكرات دائمًا في موقع يبعد على اﻷقل مقدار اتساع كرة
    // عن حافة لوحة الرسم لتفادي أية أخطاء
    random(0 + size, width - size),
    random(0 + size, height - size),
    random(-7, 7),
    random(-7, 7),
    randomRGB(),
    size,
  );

  balls.push(ball);
}

تُنشئ الحلقة while نسخة جديدة من الكائن باستخدام قيم عشوائية تولّدها الدالتان المساعدتان ()random و ()randomRGB ثم نستخدم التابع ()push لدفع الكرة الناتجة إلى نهاية مصفوفة الكرات، طالما أن عدد الكرات في المصفوفة أقل من 25 كرة. بإمكانك تغيير عدد الكرات اﻷعظمي في المصفوفة من التعليمة balls.length < 25 وذلك وفقًا لقدرة المعالجة التي يتمتع بها حاسوبك ومتصفحك، فاختيار عدة آلاف من الكرات قد تبطئ حركة الرسوم.

أضف تاليًا الشيفرة التالية إلى نهاية الشيفرة الموجودة:

function loop() {
  ctx.fillStyle = "rgb(0 0 0 / 25%)";
  ctx.fillRect(0, 0, width, height);

  for (const ball of balls) {
    ball.draw();
    ball.update();
  }

  requestAnimationFrame(loop);
}

تتضمن معظم البرامج التي تحرّك الرسوم نوعًا من الحلقات، وذلك لتحديث المعلومات في البرنامج وتصيير نتيجة الحسابات وعرضها في كل إطار من إطارات المشهد. وهذا هو أساس معظم اﻷلعاب والبرامج المشابهة. وفي تطبيقنا تنفّذ الدالة ()loop ما يلي:

  • يضبط لون خلفية لوحة الرسم على لون أسود وتتكون نصف شفافة، ومن ثم ترسم مربعًا من نفس اللون ليغطي أبعاد اللوحة باستخدام التابع ()fillRectangle (معاملاته هي إحداثيات نقطة بداية المربع واتساعه وارتفاعه). أما وظيفية هذا المربع فهو تغطية الإطار السابق قبل رسم الإطار التالي. فإن لم نفعل ذلك سنرى أفاعي طويلة تلتف ضمن اللوحة بدلًا من كرات تتحرك. أما سبب اللون نصف الشفاف للخلفية ( 25% / 0 0 0)rgb، فهو السماح للإطار السابق أن يظهر بشكل طفيف خلف اﻹطار الحالي لتعطي أثرًا خلف الكرة المتحركة. وإن جعلت قيمة الشفافية 1 بدلًا من 0.25، فلن ترى هذا اﻷثر. حاول أن تغير هذه القيمة وراقب ما يحدث.
  • تتنقل بين جميع الكرات في مصفوفة الكرات balls، وينفّذ الدالتين ()draw و ()update لرسم كل كرة على الشاشة، ثم تنفّذ التحديثات اللازمة للموقع والسرعة في الوقت المناسب للإطار التالي.
  • تشغّل نفسها مجددًا باستخدام التابع ()requestAnimationFrame الذي يُنفَّذ تكرارًا، وتُمرر له الدالة نفسها. ينفّذ التابع الدالة عدة مرات بالثانية لتكوين رسوميات متحركة سلسلة. وهذه العملة تُنفَّذ عادة بطريقة تعاودية recursively، أي تستدعي الدالة نفسها في كل مرة تُنفَّذ فيها، وهكذا تُنفَّذ مرارًا وتكرارًا.

أضف أخيرًا السطر التالي إلى أسفل الشيفرة، إذ عليك استدعاء الدالة مرة حتى تبدأ عملية تحريك الرسوم:

loop();

هذا هو المطلوب بدايةً، جرّب حفظ التغييرات التي أجريتها واختبر الكرات المرتدة.

إضافة آلية لالتقاط التصادم بين الكرات

سنضيف اﻵن آلية لاكتشاف التصادم بين الكرات في تطبيقنا، كي تعرف كراتنا أنها تصطدم ببعضها.

أضف بداية التابع التالي لى الصنف Ball:

collisionDetect() {
  for (const ball of balls) {
    if (this !== ball) {
      const dx = this.x - ball.x;
      const dy = this.y - ball.y;
      const distance = Math.sqrt(dx * dx + dy * dy);

      if (distance < this.size + ball.size) {
        ball.color = this.color = randomRGB();
      }
    }
  }
}

في التابع السابق بعض التعقيد، لذا لا تقلق إن لم تفهم تمامًا طريقة عمله، وإليك شرحه:

  • نتحقق من أجل كل كرة إذا ما اصطدمت بالكرة الحالية. ولتنفيذ اﻷمر نبدأ حلقة for...of جديدة للتنقل بين جميع كرات المصفوفة []balls.

  • نستخدم مباشرة في الحلقة for العبارة if للتحقق من أن الكرة التي وصلنا إليها هي نفسها الكرة الحالية التي نتفحصها. ولأننا لا نريد أن نتحقق إن اصطدمت كرة بنفسها، نتحقق أن الكرة الحالية (وهي الكرة التي استدعت التابع ()collisonDetect) هي نفسها الكرة التي وصلنا إليها في هذا التكرار من تكرارات الحلقة. نستخدم بعد ذلك عامل النفي ! في عبارة التحقق this !== ball كي لا تُنفَّذ الشيفرة داخل if إلا إن لم تكن الكرة هي نفسها.

  • نستخدم بعد ذلك خوارزمية شائعة للتحقق من التصادم بين كرتين، وذلك عن طريق التحقق من تداخل مساحتي الكرتين.

  • إن اكتُشف التصادم، تُنفَّذ الشيفرة ضمن عبارة if الثانية. وفي هذه الحالة نغيّر فقط لوني الكرتين المتصادمتين عشوائيًا. بإمكاننا أيضًا تعقيد التطبيق بجعل الكرات ترتد عن بعضها كما يجري اﻷمر في الواقع، لكن ذلك صعب التنفيذ. ولمحاكاة هذه العمليات الفيزيائية، يميل المطورّون إلى استخدام مكتبات جاهزة مثل Physics و mattre و Phaser وغيرها.

لا بد أيضًا من استدعاء هذه الدالة من أجل كل إطار من إطارات المشهد المتحرك. عدّل الدالة لتستدعي التابع ()ball.collisionDetect بعد التابع ()ball.update:

function loop() {
  ctx.fillStyle = "rgb(0 0 0 / 25%)";
  ctx.fillRect(0, 0, width, height);

  for (const ball of balls) {
    ball.draw();
    ball.update();
    ball.collisionDetect();
  }

  requestAnimationFrame(loop);
}

احفظ التغييرات وأعد تحميل التطبيق التجريبي، وسترى كيف تغيّر الكرات لونها عندما تتصادم.

ملاحظة: إن واجهت صعوبة في تنفيذ هذا التطبيق، تستطيع مقارنة ما فعلته مع النسخة الجاهزة من الشيفرة، ويمكنك أيضًا تجريب النسخة العاملة.

الخلاصة

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

ترجمة -وبتصرف- للمقال: Object building practice

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...