تصميم لعبة الثعبان بإستخدام جافاسكربت

برمجة لعبة الثعبان بإستخدام جافاسكربت JavaScript

لاحظ ستحتاج لفتح الكود فى Codepen عن طريق الضغط على Codepen فى الأسفل لمعاينة اللعبة او يمكنك تحميل الملفات مباشرتًا هنا

See the Pen
Snake game using JavaScript for Motwr.com
by amrelarabi (@amrelarabi)
on CodePen.0

سنقوم فى هذا الدرس ببرمجة لعبة الثعبان بشكل بسيط عن طريق لغة جافاسكربت و HTML و CSS.

قبل البدء دعونا نتعرف على بعض المصطلحات المهمة فى هذه المشروع.

مفهوم Canvas

وهو عنصر يسمح بالرسم الجرافيكى فى HTML تم اصداره فى HTML5.
يتم انشاءه عن طريق <canvas> يمكنك تخيله على انه لوحة فارغة تقوم بالرسم عليها عن طريق الجافاسكربت.

لاحظ التالى
فى الحقيقة هذا المعنى الحرفى لكملة Canvas فى اللغة العربية وتعنى اللوحة).

لنبدء فى شرح الكود

 <div class="game">
        <canvas id="gameCanvas">
            <p>عذراً، لا يدعم متصفحك الـ canvas</p>
        </canvas>
        <div class="game-control">
           <button class="button" id="resetButton">البدء</button>
            <button class="button" id="startPause">ايقاف</button>

            <span class="score">النقاط: <span id="score">0</span></span>
        </div>
        <script src="snake.js"></script>
    </div>

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

لاحظ التالى
كل عنصر من العناصر <canvas> و <button> و <span id=”score”> لها ID او معرف وهو ضرورى من أجل تحديد العنصر والتعامل معه فى ملف الجافاسكربت كما سترى لاحقًا.

ملف CSS يحتوى على بعض التنسيقات لن نتطرق إليها لان الأهم هنا هو ملف الجافاسكربت وسنشرح بالتفصيل كما يلى.

الجزء الأول ملف JavaScript هو تعريف بعض المتغيرات فى اللعبة كالتالى:

const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const resetButton = document.getElementById("resetButton");
const scoreHolder = document.getElementById("score");
const startPause = document.getElementById("startPause");
canvas.width = 800;
canvas.height = 600;
let snake = [];
let snakeLength = 10;
let speed = 1;
let score = 0;
let loopId;
let cellSize = 10;
let food = {
    x: 0,
    y: 0
};
let direction = "right";

بعض المتغيرات هى عبارة عن إختيار عنصر HTML عن طريق المعرف او ID على سبيل المثال السطر الأول.

const canvas = document.getElementById("gameCanvas");

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

من السطور المهمة ايضًا فى الجزء السابق هو السطر

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

ويحدد هذا السطر طريقة التعامل مع Canvas فعلى سبيل المثال فى حالتنا هذه نقوم بإستدعاء الدالة عن طريق 2d مما يعنى التعامل مع canvas على أساس محورى السينى والصادى اي بعدين فقط لن نتطرق لباقى طرق التعامل ولكن الجدير بالذكر أنه يمكن التعامل معه على أساس ثلاثى الأبعاد 3D.
يتم تخزين القيمة هذه فى المتغير ctx للتعامل معه لاحقًا والرسم عليه.

باقى الكود هو عبارة عن مجموعة من الدوال المعرف سنقوم بشرح كل واحدة منهم على حدة ثم فى الأخير كيفية إستخدامها ولكن قبل ذلك نود شرح مفهوم مهم جدًا وهو Game Loop.

و Game loop هو عبارة دالة تكرر بإستمرار مع بدء اللعبة وهى المسئولة عن تشغيل اللعبة ولا تتوقف تكرار هذه الحلقة إلا مع توقف اللعبة.

ننتقل إلى الدوال المعرف فى باقى الملف.

function createSnake() {
  snake[0] = {
    x: canvas.width / 2,
    y: canvas.height / 2
  };
}

الثعبان فى اللعبة هو عبارة عن مجموعة من المربعات المتتالية لتشكل شكل الثعبان وهذه المربعات يتم تمثيلها فى مصفوفة Array كما رائينا فى تعرف المتغير snake فهو عبارة عن مصفوفة.
فى الدالة السابقة نقوم بشكل ابتدائى بتحديد رأس الثعبان وهو العناصر 0 تمامًا فى منتصف Canvas, حيث المحورى السينى (الافقى ) X له يساوى نصف العرض والصادى او الرأسي يساوى نصف طول Canvas.

function drawSnake() {
  for (let i = 0; i < snake.length; i++) {
    ctx.fillStyle = "green";
    ctx.fillRect(snake[i].x - cellSize/2, snake[i].y - cellSize/2, cellSize, cellSize);
  }
}

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

نقوم بتعريفها كالتالى المعامل الاول (المحور الافقى) يساوي snake[i].x – cellSize/2 لاننا نريد المربع ان يتم توسيط المربع على المحور السينى.
فعلى سبيل المثال إذا كان عرض المربع هو 20 بكسل و موقع هذا المربع على المحور السينى هو snake[i].x = 50 فهذه الحالة الجزء الحافة اليسرى من المربع سيتم رسمها على النقط 50 كما يوضح الشكل التالى

مثال لتوضيح رسم جزء من الثعبان
مثال توضيحى

ولكن نحن نريد توسيط المربع فبالتالى نقوم بطرح نصف قيمة طول الخلية (cellSize).
وبالمثل على المحور الصادى (الرأسي)

  function createFood() {
    food.x = Math.floor(Math.random() * (canvas.width - 10) + 10);
    food.y = Math.floor(Math.random() * (canvas.height - 10) + 10);
  }

يقوم هذا الجزء بتوليد المربع الأحمر والذى يمثل الطعام فى أماكن عشوائية على الCanvas على المحور الافقى والرأسي ولنأخذ المحور X – الافقى كمثال (السطر الاول)

  • يولد رقمًا عشوائيًا بين 0 و 1 باستخدام ()Math.random.
  • ضرب الرقم العشوائي في عرض اللوحة (canvas.width).
  • تطرح 10 للتأكد من أن مربع الطعام لا يظهر قريبًا جدًا من الحافة اليسرى لCanvas.
  • تقريب لأسفل إلى أقرب عدد صحيح باستخدام Math.floor () والتقريب لاسفل لضمان ان العدد صحيح على سبيل المثال إذا كان خارج العملية 250,8 نقوم بالتقريب للعدد الصحيح السابق وهو 250.

وبالمثل فى المحور الصادى Y فى السطر الثانى.

function drawFood() {
ctx.fillStyle = "red";
ctx.fillRect(food.x - cellSize/2, food.y - cellSize/2, cellSize, cellSize);
}

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

document.addEventListener("keydown", changeDirection);

function changeDirection(e) {
  if (e.keyCode == 37 && direction != "right") {
    direction = "left";
  } else if (e.keyCode == 38 && direction != "down") {
    direction = "up";
  } else if (e.keyCode == 39 && direction != "left") {
    direction = "right";
  } else if (e.keyCode == 40 && direction != "up") {
    direction = "down";
  }
}

هذا الجزء هو المسئول عن تغير إتجاه الثعبان عن طريق الضغط على الأسهم فى لوحة المفتايح (لاحظ انه تم من قبل تحديد إتجاه مبدئى للثعبان direction لجهة اليمين.

document.addEventListener("keydown", changeDirection);

هذا السطر يقوم بتتبع الضغط على زر ويقوم بتنفيذ الدالة changeDirection.

  if (e.keyCode == 37 && direction != "right") {
    direction = "left";
  } else if (e.keyCode == 38 && direction != "down") {
    direction = "up";
  } else if (e.keyCode == 39 && direction != "left") {
    direction = "right";
  } else if (e.keyCode == 40 && direction != "up") {
    direction = "down";
  }

هذه بعض الشروط لتحديد ماهو الزر الذى قمنا بالضغط عليه وتغير متغير الإتجاه بناء على ذلك.

لاحظ التالى
كل زر فى لوحة المفاتيح يتم تعريفه عن طريق رقم معين KeyCode وفى المثال السابق أستخدمنا الأرقام المحددة للأسهم فى لوحة المفاتيح يمكنك معرفه كود اي زر عن طريق هذه الأداة فى الرابط https://www.toptal.com/developers/keycode

function moveSnake() {
  let headX = snake[0].x;
  let headY = snake[0].y;

  if (direction == "right") {
    headX += speed;
  } else if (direction == "left") {
    headX -= speed;
  } else if (direction == "up") {
    headY -= speed;
  } else if (direction == "down") {
    headY += speed;
  }

  let newHead = {
    x: headX,
    y: headY
  };

  snake.unshift(newHead);

  if (snake.length > snakeLength) {
    snake.pop();
  }
}

هذا الدالة هى الدالة المسئولة عن تحريك الثعبان كالتالى :

هذه هي دالة “moveSnake()” المحدثة التي تستخدم متغير “speed” للتحكم في سرعة حركة الثعبان.

  • عندما يكون الإتجاه إلى اليمين نقوم بزيادة موضع رأس الثعبان الافقى X بمقدار معين (speed متغير قمنا بتحديد فى أول الكود).
  • عندما يكون الإتجاه إلى اليسار نقوم بإنقاص الموضوع الافقى X لرأس الثعبان بنفس المقدار (المتغير speed).
  • عندما يكون الإتجاه إلى أعلى نقوم بإنقاص موضع رأس الثعبان الرأسي بمقدار (المتغير speed).
  • عندما يكون الإتجاه إلى أسفل نقوم بزيادة موضع رأس الثعبان الرأسي بمقدار (المتغير speed).
  • يتم إضافة موضع الرأس الجديد إلى بداية مصفوفة “snake” باستخدام “snake.unshift(newHead)”، مما يحرك الثعبان للأمام خطوة واحدة.
  • بينما يتم إزالة العنصر الأخير في مصفوفة “snake” باستخدام “snake.pop()” إذا تجاوزت طولها “snakeLength”، مما يضمن أن الثعبان لا ينمو إلى ما لا نهاية (نتخلص من المربعات التالى تجاوزت الطول الفعلى).
function checkCollisions() {
    // Check if snake toutch the walls
    if (snake[0].x < cellSize || snake[0].x > canvas.width ||
        snake[0].y < cellSize || snake[0].y > canvas.height) {
      // Game over
      alert("انتهت اللعبة واحرزت  " + score);
      resetGame();
      return;
    }
  
    // Check if snake hits its own body
    for (let i = 1; i < snake.length; i++) {
      if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) {
        // Game over
        alert("انتهت اللعبة واحرزت  " + score);
        resetGame();
        return;
      }
    }
  
    // Check if snake eats the food
    if (snake[0].x >= food.x - 10 && snake[0].x <= food.x + 10 &&
        snake[0].y >= food.y - 10 && snake[0].y <= food.y + 10) {
      // Increase the snake's length and generate a new food pellet
      snakeLength+=10;
      createFood();
      score++;
      scoreHolder.innerHTML = score;
    }
  }

هذه الدالة هى المسئولة عن التعامل مع التصادمات (وتعنى تصادم الثعبان ام بالحائط او بالطعام او بنفسه) ويتم التعامل مع التصادمات كالتالى:

مثال على التصادمات مثلًا الجزء الاول التحقق من تصادم الثعبان مع الحائط.

// التحقق مما إذا كان الثعبان يلمس الحوائط
if (snake[0].x < 0 || snake[0].x > canvas.width ||
    snake[0].y < 0 || snake[0].y > canvas.height) {
  // اللعبة انتهت
  alert("انتهت اللعبة واحرزت  " + score);
  resetGame();
  return;
}

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

// التحقق مما إذا كان الثعبان يضرب جسمه الخاص
for (let i = 1; i < snake.length; i++) {
  if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) {
    // اللعبة انتهت
    alert("انتهت اللعبة واحرزت  " + score);
    resetGame();
    return;
  }
}

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

// التحقق مما إذا كان الثعبان يأكل الطعام
if (snake[0].x >= food.x - 10 && snake[0].x <= food.x + 10 &&
    snake[0].y >= food.y - 10 && snake[0].y <= food.y + 10) {
  // زيادة طول الثعبان وإنشاء حبيبات طعام جديدة
  snakeLength+=10;
  createFood();
  score++;
  scoreHolder.innerHTML = score;
}

يقوم هذا الجزء بالتحقق من تلامس الثعبان مع الطعام بطريقة مشابهة للأكواد السابقة.
يتم استخدام قيم -10 و +10 فى الجزء السابق لتحديد نطاق من 20 بكسل حول مربع الطعام.
يتحقق الشرط مما إذا كان رأس الثعبان ضمن هذا المجال ، مما يعني أن رأس الثعبان قريب بدرجة كافية من مربعات الطعام ليتم اعتباره قد أكله.
هذه طريقة بسيطة للسماح ببعض هامش الخطأ وجعل اللعبة أكثر واقعية.

function clearCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  }

يتم استدعاء هذه الدالة في بداية كل إطار Frame لمسح Canvas قبل رسم حالة اللعبة المحدثة (انتقال الثعبان – أكل الطعام – التصادم).

  function gameLoop() {
    clearCanvas();
    moveSnake();
    drawSnake();
    checkCollisions();
    drawFood();
    loopId = requestAnimationFrame(gameLoop);
  }

هذه الدالة تلعب دور المايسترو فى اللعبة فهى تنفذ مع كل Frame فى اللعبة.
تقوم بالنداء على الدوال السابق تعريفها كالتالى:

  • تمسح Canvas من الحالة السابقة.
  • تقوم بتحريك الثعبان.
  • تقوم برسم الثعبان مرة أخرى.
  • تتحقق من تصادمات الثعبان (التصادم بنفسه اوبالحائط او اكل الطعام).
  • رسم الطعام فى مكان عشوائى.
  • اخر سطر مهم جدًا لدوران الحلقة مرة اخرى عن طريق الدالة requestAnimationFrame ونقوم مرة اخرى بتمرير loop وتعود الدالة هذه ب ID لهذا الحلقة (تستخدم لإيقاف اللعبة).
لاحظ التالى
تخبر الدالة window.requestAnimationFrame () المتصفح أنك ترغب في أداء رسم متحرك وتطلب أن يستدعي المتصفح وظيفة محددة لتحديث رسم متحرك قبل إعادة الرسم التالية. وهذا الطريقة أفضل فى الأداء من الطرق الأخرى.

  function resetGame() {
    snake = [];
    snakeLength = 10;
    createSnake();
    createFood();
    score = 0;
    snake[0].x = canvas.width / 2;
    snake[0].y = canvas.height / 2;
    clearCanvas();
    cancelAnimationFrame(loopId);
    gameLoop();
  }

هذا الدالة تقوم بإعادة المتغيرات للعبة إلى حالتها الأول.

window.onload = function() {
    createSnake();
    createFood();
};  
resetButton.addEventListener("click", function(){
    // html text
    resetButton.innerHTML = "البدء من جديد";
    resetGame();
} );
startPause.addEventListener("click", function() {
    if (startPause.innerHTML == "استئناف") {
      startPause.innerHTML = "ايقاف";
      gameLoop();
    } else {
      startPause.innerHTML = "استئناف";
      cancelAnimationFrame(loopId);
    }
  }
);

هذا الجزء هو المسئول عن رسم اللوحة اول مرة وتعريف الأزرار لبداية اللعبة وتوقفيها عند الحاجة.

cancelAnimationFrame(loopId);

هذا السطر على عكس requestAnimationFrame التى تقوم ببدء الحلقة تقوم هذا الدالة بإلغاء هذه الحلقة التكرارية.

بالطبع اللعبة تحتاج إلى مميزات أخرى هنالك بعض الأفكار التى يمكنك تطبيقها بنفسك كالتالى:

  • زيادة السرعة عند كل 5 نقط على سبيل المثال 5 و 10 و 15 والخ.
  • وضع عوائق أخرى عشوائيه فى داخل اللعبة غير الجدران الخارجية.
  • وضع رسومات للثعبان و الطعام.

إذا كان لديك تحسينات او اي استفسارات او اي شئ تريد مشاركته معنا لا تتردد فى اختبارك فى التعليقات.

اشترك فى القائمة البريدية

عن الكاتب

شارك على وسائل التواصل

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

خمسة × 2 =