Programming Basics SQL HTML CSS JavaScript Python C++ Java JavaFX Swing Problem Solving English English Conversations Computer Fundamentals Learn Typing

Swingطريقة إنشاء لعبة الأفعى Snake 2D

في هذا الدرس ستتعلم طريقة إنشاء لعبة ( Snake 2D ) إحترافية بإستخدام إطار الـ Swing.

java swing 2d snake game source code تحميل كود لعبة الأفعى في جافا


مميزات اللعبة

  • أعلى مجموع يصل إليه اللاعب يبقى مخزناً حتى إذا تم إغلاق اللعبة.
  • يمكن إيقاف و متابعة اللعبة بالنقر على زر المسافة الفارغة Space.
  • إذا لمست الأفعى الحائط لا يخسر اللاعب, لأن الأفعى ستظهر من الجهة المقابلة.
  • يمكن تعديل كود اللعبة بكل سهولة لأنه غير معقد.


بناء اللعبة

في هذا المشروع قمنا بوضع ملفات الجافا بداخل مجلد إسمه snake_2d.
و قمنا بوضع الصور بداخل مجلد إسمه images كما في الصورة التالية.

⇓ تحميل اللعبة ⇓ تحميل المشروع كاملاً ⇓ تحميل مجلد الصور فقط



كود اللعبة

Position.java
// snake_2d موجود بداخل المجلد Position.java هنا ذكرنا أن الملف
package snake_2d;
// قمنا بإنشاء هذا الكلاس لجعل أي كائن منه يمثل موقع دائرة من الدوائر الموجودة في الأفعى
public class Position {
// إذاً كل دائرة في الأفعى ستكون موجودة في نفس الوقت على خط طول و خط عرض محددين
int x, y;
// قمنا بتجهيز هذا الكونستركتور لتحديد مكان وجود أي دائرة في الأفعى لحظة إنشاء الكائن منه
public Position(int x, int y) {
this.x = x;
this.y = y;
}
}
// snake_2d موجود بداخل المجلد Position.java هنا ذكرنا أن الملف package snake_2d; // قمنا بإنشاء هذا الكلاس لجعل أي كائن منه يمثل موقع دائرة من الدوائر الموجودة في الأفعى public class Position { // إذاً كل دائرة في الأفعى ستكون موجودة في نفس الوقت على خط طول و خط عرض محددين int x, y; // قمنا بتجهيز هذا الكونستركتور لتحديد مكان وجود أي دائرة في الأفعى لحظة إنشاء الكائن منه public Position(int x, int y) { this.x = x; this.y = y; } }

GameDrawer.java
// snake_2d موجود بداخل المجلد GameDrawer.java هنا ذكرنا أن الملف
package snake_2d;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.LinkedList;
import java.util.Random;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;
// أي أصبح هذا الكلاس يمثل حاوية ,JPanel هنا قمنا بإنشاء كلاس يرث من الكلاس
// حتى نجعل المستخدم قادر على تحريك الأفعى من الكيبورد KeyListener و جعلناه يطبق الإنترفيس
// حتى نستطيع رسم الأفعى من جديد كلما تحركت ActionListener و جعلناه يطبق الإنترفيس
public class GameDrawer extends JPanel implements KeyListener, ActionListener{
// قمنا بإنشاء هاتين المصفوفتين لتخزين مكان وجود كل دائرة من الأفعى كل لحظة, أي لتحديد المكان المحجوز لعرض دوائر الأفعى
// ( بما أن عدد الدوائر الأقصى بالطول هو 22 و بالعرض هو أيضاً 22, فهذا يعني أنه يمكن تخزين 22×22 دائرة ( أي 484
final int[] boardX = new int[484];
final int[] boardY = new int[484];
// سنستخدم هذه المصفوفة لتخزين مكان وجود كل دائرة في الأفعى حتى نعرف الموقع الذي لا يجب أن نظهر فيه الدائرة الحمراء
// ملاحظة: سبب إستخدام هذا المصفوفة لتخزين موقع كل دائرة في الأفعى من جديد هو فقط لجعل اللعبة لا تعلق, أي لتحسين أداء اللعبة
LinkedList<Position> snake = new LinkedList();
// سنستخدم هذه المتغيرات لتحديد الإتجاه الذي ستتجه إليه الأفعى
boolean left = false;
boolean right = false;
boolean up = false;
boolean down = false;
// سنستخدم هذه الكائنات لرسم إتجاه وجه الأفعى
ImageIcon lookToRightImage = new ImageIcon(this.getClass().getResource("/images/face-look-right.jpg"));
ImageIcon lookToLeftImage = new ImageIcon(this.getClass().getResource("/images/face-look-left.jpg"));
ImageIcon lookToUpImage = new ImageIcon(this.getClass().getResource("/images/face-look-up.jpg"));
ImageIcon lookToDownImage = new ImageIcon(this.getClass().getResource("/images/face-look-down.jpg"));
// سنستخدم هذا الكائن في كل مرة لرسم جسد الأفعى
ImageIcon snakeBodyImage = new ImageIcon(this.getClass().getResource("/images/body.png"));
// سنستخدم هذا الكائن في كل مرة لرسم طعام الأفعى
ImageIcon fruitImage = new ImageIcon(this.getClass().getResource("/images/fruit.png"));
// سنستخدم هذا المتغير لتخزين عدد الدوائر التي تشكل الأفعى
int lengthOfSnake = 3;
// سنستخدم هاتين المصفوفتين لتحديد الأماكن التي يمكن أن يظهر فيها الطعام
int[] fruitXPos = {20, 40, 60, 80, 100, 120, 140, 160, 200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 460};
int[] fruitYPos = {20, 40, 60, 80, 100, 120, 140, 160, 200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 460};
// سنستخدم هذا الكائن لجعل الأفعى تستمر بالحركة .Thread و الذي يشبه الـ Timer هنا قمنا بإنشاء كائن من الكلاس
Timer speedTimer;
// سنستخدم هذا المتغير لتحديد أنه كل 0.1 ثانية سستتحرك الأفعى
int delay = 100;
// سنستخدم هذا المتغير لمعرفة إذا كانت الأفعى تتحرك أم لا
int moves = 0;
// سنستخدم هذه المتغيرات لتحديد المجموع الذي يحققه اللاعب أثناء اللعب
int totalScore = 0;
int fruitEaten = 0;
int scoreReverseCounter = 99;
// في حال كان اللاعب قد حقق مجموع عالي من قبل, سيتم إظهاره كأفضل مجموع وصل إليه
int bestScore = readBestScorefromTheFile();
// لتوليد أماكن ظهور طعام الأفعى بشكل عشوائي random سنستخدم الكائن
Random random = new Random();
// هنا قمنا بتحديد مكان أول طعام سيظهر في اللعبة قبل أن يبدأ المستخدم باللعب, و جعلناه يظهر تحت الأفعى
int xPos = random.nextInt(22);
int yPos = 5+random.nextInt(17);
// سنستخدم هذا المتغير لمعرفة ما إذا كان المستخدم قد خسر أم لا
boolean isGameOver = false;
// هنا تحديد حجم اللعبة و تهيئتها لإستشعار الأزرار التي ينقر عليها المستخدم لحظة إنشاء كائن من هذا الكلاس
// لجعل اللعبة تتحرك بسرعة 0.1 بالثانية speedTimer و قمنا تهيئة اللعبة لإستشعار الأزرار التي ينقر عليها المستخدم و تهيئة الكائن
public GameDrawer()
{
setPreferredSize(new Dimension(750, 500));
addKeyListener(this);
setFocusable(true);
setFocusTraversalKeysEnabled(false);
speedTimer = new Timer(delay, this);
}
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
}
@Override
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
}
// (لتحديد كيف سيتم رسم و تلوين كل شيء في الحاوية (أي في اللعبة paint() للدالة Override هنا فعلنا
// ملاحظة هذه الدالة تستدعى بشكل تلقائي عند رسم أي شيء في الحاوية
@Override
public void paint(Graphics g)
{
// هنا قمنا بتحديد مكان وجود الأفعى في كل مرة يقوم المستخدم ببدأ اللعبة من جديد
if(moves == 0)
{
boardX[2] = 40;
boardX[1] = 60;
boardX[0] = 80;
boardY[2] = 100;
boardY[1] = 100;
boardY[0] = 100;
scoreReverseCounter = 99;
speedTimer.start();
}
// هنا قمنا بجعل المجموع الحالي الذي وصل إليه المستخدم يظهر كأعلى مجموع وصل إليه في حال تخطى المجموع القديم
if(totalScore > bestScore)
bestScore = totalScore;
// هنا قمن بإنشاء مربع أسود يمثل لون خلفية اللعبة
g.setColor(Color.BLACK);
g.fillRect(0, 0, 750, 500);
// هنا قمنا برسم المربعات التي تشكل الحدود التي لا تستطيع الأفعى عبورها باللون الرمادي
// حجم كل مربع 13 بيكسل و المسافة بينهما 5 بيكسل
g.setColor(Color.DARK_GRAY);
for(int i=6; i<=482; i+=17)
for(int j=6; j<=482; j+=17)
g.fillRect(i, j, 13, 13);
// هنا فمنا بإنشاء مربع أسود كبير فوق المربعات التي تشكل حدود اللعبة لتظهر و كأنها فارغة من الداخل
g.setColor(Color.BLACK);
g.fillRect(20, 20, 460, 460);
// هنا قمنا بكتابة إسم اللعبة و تلوينه بالأزرق
g.setColor(Color.CYAN);
g.setFont(new Font("Arial", Font.BOLD, 26));
g.drawString("Snake 2D", 565, 35);
// هنا قمنا بكتابة إسم اللعبة و تلوينه بالأزرق
g.setFont(new Font("Arial", Font.PLAIN, 13));
g.drawString("+ "+scoreReverseCounter, 510, 222);
// هنا جعلنا أي شيء سنقوم بكتابته يظهر باللون الرمادي
g.setColor(Color.LIGHT_GRAY);
// هنا قمنا بطباعة أنه تم تطوير اللعبة بواسطة موقعنا
g.setFont(new Font("Arial", Font.PLAIN, 15));
g.drawString("Developed by harmash.com", 530, 60);
// لقياس حجم الأرفام الظاهرة في المربعات الثلاث بالبيكسل حتى نستطيع عرضهم في الوسط FontMetrics هنا قمنا بإنشاء الكائن
FontMetrics fm = g.getFontMetrics();
// هنا جعلنا أي شيء سنقوم بكتابته يظهر بنوع و حجم هذا الخط
g.setFont(new Font("Arial", Font.PLAIN, 18));
//و المربع الذي تحته و الرقم الذي بداخله Best Score هنا قمنا برسم النص
g.drawString("Best Score", 576, 110);
g.drawRect(550, 120, 140, 30);
g.drawString(bestScore+"", 550+(142-fm.stringWidth(bestScore+""))/2, 142);
//و المربع الذي تحته و الرقم الذي بداخله Total Score هنا قمنا برسم النص
g.drawString("Total Score", 573, 190);
g.drawRect(550, 200, 140, 30);
g.drawString(totalScore+"", 550+(142-fm.stringWidth(totalScore+""))/2, 222);
//و المربع الذي تحته و الرقم الذي بداخله Fruit Eaten هنا قمنا برسم النص
g.drawString("Fruit Eaten", 575, 270);
g.drawRect(550, 280, 140, 30);
g.drawString(fruitEaten+"", 550+(142-fm.stringWidth(fruitEaten+""))/2, 302);
// Controls هنا قمنا برسم النص
g.setFont(new Font("Arial", Font.BOLD, 16));
g.drawString("Controls", 550, 360);
// Controls هنا قمنا برسم النصوص الظاهرة تحت النص
g.setFont(new Font("Arial", Font.PLAIN, 14));
g.drawString("Pause / Start : Space", 550, 385);
g.drawString("lookTo Up : Arrow Up", 550, 410);
g.drawString("lookTo Down : Arrow Down", 550, 435);
g.drawString("lookTo Left : Arrow Left", 550, 460);
g.drawString("lookTo Right : Arrow Right", 550, 485);
// هنا جعلنا الأفعى تنظر ناحية اليمين قبل أن يبدأ اللاعب بتحريكها
lookToRightImage.paintIcon(this, g, boardX[0], boardY[0]);
// هنا قمنا بمسح مكان وجود الأفعى السابق لأننا سنقوم بتخزين المكان الجديد كلما تحركت
snake.clear();
// هنا قمنا بإنشاء حلقة ترسم كامل الدوائر التي تشكل الأفعى كل 0.1 ثانية
for(int i=0; i<lengthOfSnake; i++)
{
if(i==0 && left)
lookToLeftImage.paintIcon(this, g, boardX[i], boardY[i]);
else if(i==0 && right)
lookToRightImage.paintIcon(this, g, boardX[i], boardY[i]);
else if(i==0 && up)
lookToUpImage.paintIcon(this, g, boardX[i], boardY[i]);
else if(i==0 && down)
lookToDownImage.paintIcon(this, g, boardX[i], boardY[i]);
else if(i!=0)
snakeBodyImage.paintIcon(this, g, boardX[i], boardY[i]);
// snake هنا قمنا بتخزين الموقع الحالي لكل دائرة في الأفعى في الكائن
snake.add(new Position(boardX[i], boardY[i]));
}
// تقل بشكل تدريجي و أدنى قيمة ممكن أن تصل إليها هي 10 scoreReverseCounter هنا جعلنا قيمة العداد
if(scoreReverseCounter != 10)
scoreReverseCounter--;
// هنا قمنا بإنشاء هذه الحلقة للتأكد إذا كان رأس الأفعى قد لامس أي جزء من جسدها
for(int i=1; i<lengthOfSnake; i++)
{
// إذاً عندما يلمس رأس الأفعى جسدها سيتم جعل أول دائرة موجودة خلف الرأس تمثل رأس الأفعى حتى لا يظهر رأسها فوق جسدها
if(boardX[i] == boardX[0] && boardY[i] == boardY[0])
{
if(right)
lookToRightImage.paintIcon(this, g, boardX[1], boardY[1]);
else if(left)
lookToLeftImage.paintIcon(this, g, boardX[1], boardY[1]);
else if(up)
lookToUpImage.paintIcon(this, g, boardX[1], boardY[1]);
else if(down)
lookToDownImage.paintIcon(this, g, boardX[1], boardY[1]);
// للإشارة إلى أن اللاعب قد خسر true تساوي isGameOver بعدها سيتم جعل قيمة الـ
// Space و بالتالي يمكنه أن يبدأ من جديد بالنقر على زر المسافة الفارغة
isGameOver = true;
// يتوقف و بالتالي ستتوقف الأفعى تماماً عن الحركة speedTimer بعدها سيتم جعل الـ
speedTimer.stop();
// Game Over بعدها سيتم إظهار النص
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, 50));
g.drawString("Game Over", 110, 220);
// تحته Press Space To Restart و سيتم إظهار النص
g.setFont(new Font("Arial", Font.BOLD, 20));
g.drawString("Press Space To Restart", 130, 260);
// في الأخير سيتم إستدعاء هذه الدالة لحفظ أكبر مجموع وصل إليه اللاعب
writeBestScoreInTheFile();
}
}
// إذا لمس رأس الأفعى الطعام سيتم إخفاء الطعام و زيادة مجموع اللاعب
if((fruitXPos[xPos] == boardX[0]) && fruitYPos[yPos] == boardY[0])
{
totalScore += scoreReverseCounter;
scoreReverseCounter = 99;
fruitEaten++;
lengthOfSnake++;
}
// هنا في كل مرة سيتم ضمان أن لا يظهر الطعام فوق الأفعى
for(int i=0; i<snake.size(); i++)
{
// في حال ظهر الطعام فوق جسد الأفعى سيتم خلق مكان عشوائي آخر لوضعها فيه
if(snake.get(i).x == fruitXPos[xPos] && snake.get(i).y == fruitYPos[yPos]) {
xPos = random.nextInt(22);
yPos = random.nextInt(22);
}
}
// في الأخير سيتم عرض الطعام بعيداً عن جسد الأفعى
fruitImage.paintIcon(this, g, fruitXPos[xPos], fruitYPos[yPos]);
// قمنا باستدعاء هذه الدالة للتخلص من أي مصادر لا حاجة لها في البرنامج و يمكنك إزالتها لأنها لن تؤثر هنا
g.dispose();
}
// لتحديد الإتجاه الذي ستتحرك فيه النافذة keyPressed() للدالة Override هنا فعلنا
@Override
public void keyPressed(KeyEvent e) {
// Space هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر المسافة الفارغة
if(e.getKeyCode() == KeyEvent.VK_SPACE)
{
// سيتم إيقاف اللعبة بشكل مؤقت إذا كانت اللعبة شغالة
if(speedTimer.isRunning() && isGameOver == false)
speedTimer.stop();
// سيتم إعادة اللعبة للعمل إذا كان قد تم إيقافها سابقاً
else if(!speedTimer.isRunning() && isGameOver == false)
speedTimer.start();
// سيتم بدأ اللعبة من جديد في حال كان قد تم إيقاف اللعبة لأن اللاعب قد خسر
else if(!speedTimer.isRunning() && isGameOver == true)
{
isGameOver = false;
speedTimer.start();
moves = 0;
totalScore = 0;
fruitEaten = 0;
lengthOfSnake = 3;
right = true;
left = false;
xPos = random.nextInt(22);
yPos = 5+random.nextInt(17);
}
}
// هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم الأيمن
else if(e.getKeyCode() == KeyEvent.VK_RIGHT)
{
// إذا لم تكن الأفعى تسير في الإتجاه الأيسر سيتم توجيهها نحو الإتجاه الأيمن
moves++;
right = true;
if(!left)
right = true;
else
{
right = false;
left = true;
}
up = false;
down = false;
}
// هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم الأيسر
else if(e.getKeyCode() == KeyEvent.VK_LEFT)
{
// إذا لم تكن الأفعى تسير في الإتجاه الأيمن سيتم توجيهها نحو الإتجاه الأيسر
moves++;
left = true;
if(!right)
left = true;
else
{
left = false;
right = true;
}
up = false;
down = false;
}
// هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم المتجه للأعلى
else if(e.getKeyCode() == KeyEvent.VK_UP)
{
// إذا لم تكن الأفعى تسير في اتجاه الأسفل سيتم توجيهها نحو الأعلى
moves++;
up = true;
if(!down)
up = true;
else
{
up = false;
down = true;
}
left = false;
right = false;
}
// هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم المتجه للأسفل
else if(e.getKeyCode() == KeyEvent.VK_DOWN)
{
// إذا لم تكن الأفعى تسير في اتجاه الأعلى سيتم توجيهها نحو الأسفل
moves++;
down = true;
if(!up)
down = true;
else
{
up = true;
down = false;
}
left = false;
right = false;
}
}
// هنا قمنا بتحديد المكان الذي ستظهر فيه الأفعى لحظة تحركها
// و لاحظ أنه يوجد شرط متعلق بكل إتجاه تسلكه الأفعى حالياً
// هذه الشروط الموضوعة تجعل الأفعى تظهر في الجهة المقابلة لها عندما تلمس الحائط
@Override
public void actionPerformed(ActionEvent e) {
if(right)
{
for(int i = lengthOfSnake-1; i>=0; i--)
boardY[i+1] = boardY[i];
for(int i = lengthOfSnake; i>=0; i--)
{
if(i==0)
boardX[i] = boardX[i] + 20;
else
boardX[i] = boardX[i-1];
if(boardX[i] > 460)
boardX[i] = 20;
}
}
else if(left)
{
for(int i = lengthOfSnake-1; i>=0; i--)
boardY[i+1] = boardY[i];
for(int i = lengthOfSnake; i>=0; i--)
{
if(i==0)
boardX[i] = boardX[i] - 20;
else
boardX[i] = boardX[i-1];
if(boardX[i] < 20)
boardX[i] = 460;
}
}
else if(up)
{
for(int i = lengthOfSnake-1; i>=0; i--)
boardX[i+1] = boardX[i];
for(int i = lengthOfSnake; i>=0; i--)
{
if(i==0)
boardY[i] = boardY[i] - 20;
else
boardY[i] = boardY[i-1];
if(boardY[i] < 20)
boardY[i] = 460;
}
}
else if(down)
{
for(int i = lengthOfSnake-1; i>=0; i--)
boardX[i+1] = boardX[i];
for(int i = lengthOfSnake; i>=0; i--)
{
if(i==0)
boardY[i] = boardY[i] + 20;
else
boardY[i] = boardY[i-1];
if(boardY[i] > 460)
boardY[i] = 20;
}
}
repaint();
}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
// هذه الدالة تحفظ أعلى مجموع وصل إليه اللاعب في ملف خارجي بجانب ملف اللعبة
private void writeBestScoreInTheFile()
{
if(totalScore >= bestScore)
{
try {
FileOutputStream fos = new FileOutputStream("./snake-game-best-score.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
osw.write(bestScore+"");
osw.flush();
osw.close();
}
catch(IOException e) {
}
}
}
// هذه الدالة تقرأ أعلى مجموع وصل إليه اللاعب من الملف الخارجي الموجود بجانب ملف اللعبة
// في حال كان لا يوجد ملف خارجي ستقوم بإنشائه و وضع القيمة 0 فيه كقيمة أولية و هذا ما سيحدث عندما يقوم اللاعب بتشغيل اللعبة أول مرة
private int readBestScorefromTheFile()
{
try {
InputStreamReader isr = new InputStreamReader( new FileInputStream("./snake-game-best-score.txt"), "UTF-8" );
BufferedReader br = new BufferedReader(isr);
String str = "";
int c;
while( (c = br.read()) != -1){
if(Character.isDigit(c))
str += (char)c;
}
if(str.equals(""))
str = "0";
br.close();
return Integer.parseInt(str);
}
catch(IOException e) {
}
return 0;
}
}
// snake_2d موجود بداخل المجلد GameDrawer.java هنا ذكرنا أن الملف package snake_2d; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.LinkedList; import java.util.Random; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.Timer; // أي أصبح هذا الكلاس يمثل حاوية ,JPanel هنا قمنا بإنشاء كلاس يرث من الكلاس // حتى نجعل المستخدم قادر على تحريك الأفعى من الكيبورد KeyListener و جعلناه يطبق الإنترفيس // حتى نستطيع رسم الأفعى من جديد كلما تحركت ActionListener و جعلناه يطبق الإنترفيس public class GameDrawer extends JPanel implements KeyListener, ActionListener{ // قمنا بإنشاء هاتين المصفوفتين لتخزين مكان وجود كل دائرة من الأفعى كل لحظة, أي لتحديد المكان المحجوز لعرض دوائر الأفعى // ( بما أن عدد الدوائر الأقصى بالطول هو 22 و بالعرض هو أيضاً 22, فهذا يعني أنه يمكن تخزين 22×22 دائرة ( أي 484 final int[] boardX = new int[484]; final int[] boardY = new int[484]; // سنستخدم هذه المصفوفة لتخزين مكان وجود كل دائرة في الأفعى حتى نعرف الموقع الذي لا يجب أن نظهر فيه الدائرة الحمراء // ملاحظة: سبب إستخدام هذا المصفوفة لتخزين موقع كل دائرة في الأفعى من جديد هو فقط لجعل اللعبة لا تعلق, أي لتحسين أداء اللعبة LinkedList<Position> snake = new LinkedList(); // سنستخدم هذه المتغيرات لتحديد الإتجاه الذي ستتجه إليه الأفعى boolean left = false; boolean right = false; boolean up = false; boolean down = false; // سنستخدم هذه الكائنات لرسم إتجاه وجه الأفعى ImageIcon lookToRightImage = new ImageIcon(this.getClass().getResource("/images/face-look-right.jpg")); ImageIcon lookToLeftImage = new ImageIcon(this.getClass().getResource("/images/face-look-left.jpg")); ImageIcon lookToUpImage = new ImageIcon(this.getClass().getResource("/images/face-look-up.jpg")); ImageIcon lookToDownImage = new ImageIcon(this.getClass().getResource("/images/face-look-down.jpg")); // سنستخدم هذا الكائن في كل مرة لرسم جسد الأفعى ImageIcon snakeBodyImage = new ImageIcon(this.getClass().getResource("/images/body.png")); // سنستخدم هذا الكائن في كل مرة لرسم طعام الأفعى ImageIcon fruitImage = new ImageIcon(this.getClass().getResource("/images/fruit.png")); // سنستخدم هذا المتغير لتخزين عدد الدوائر التي تشكل الأفعى int lengthOfSnake = 3; // سنستخدم هاتين المصفوفتين لتحديد الأماكن التي يمكن أن يظهر فيها الطعام int[] fruitXPos = {20, 40, 60, 80, 100, 120, 140, 160, 200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 460}; int[] fruitYPos = {20, 40, 60, 80, 100, 120, 140, 160, 200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 460}; // سنستخدم هذا الكائن لجعل الأفعى تستمر بالحركة .Thread و الذي يشبه الـ Timer هنا قمنا بإنشاء كائن من الكلاس Timer speedTimer; // سنستخدم هذا المتغير لتحديد أنه كل 0.1 ثانية سستتحرك الأفعى int delay = 100; // سنستخدم هذا المتغير لمعرفة إذا كانت الأفعى تتحرك أم لا int moves = 0; // سنستخدم هذه المتغيرات لتحديد المجموع الذي يحققه اللاعب أثناء اللعب int totalScore = 0; int fruitEaten = 0; int scoreReverseCounter = 99; // في حال كان اللاعب قد حقق مجموع عالي من قبل, سيتم إظهاره كأفضل مجموع وصل إليه int bestScore = readBestScorefromTheFile(); // لتوليد أماكن ظهور طعام الأفعى بشكل عشوائي random سنستخدم الكائن Random random = new Random(); // هنا قمنا بتحديد مكان أول طعام سيظهر في اللعبة قبل أن يبدأ المستخدم باللعب, و جعلناه يظهر تحت الأفعى int xPos = random.nextInt(22); int yPos = 5+random.nextInt(17); // سنستخدم هذا المتغير لمعرفة ما إذا كان المستخدم قد خسر أم لا boolean isGameOver = false; // هنا تحديد حجم اللعبة و تهيئتها لإستشعار الأزرار التي ينقر عليها المستخدم لحظة إنشاء كائن من هذا الكلاس // لجعل اللعبة تتحرك بسرعة 0.1 بالثانية speedTimer و قمنا تهيئة اللعبة لإستشعار الأزرار التي ينقر عليها المستخدم و تهيئة الكائن public GameDrawer() { setPreferredSize(new Dimension(750, 500)); addKeyListener(this); setFocusable(true); setFocusTraversalKeysEnabled(false); speedTimer = new Timer(delay, this); } @Override public void setSize(int width, int height) { super.setSize(width, height); } @Override public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); } // (لتحديد كيف سيتم رسم و تلوين كل شيء في الحاوية (أي في اللعبة paint() للدالة Override هنا فعلنا // ملاحظة هذه الدالة تستدعى بشكل تلقائي عند رسم أي شيء في الحاوية @Override public void paint(Graphics g) { // هنا قمنا بتحديد مكان وجود الأفعى في كل مرة يقوم المستخدم ببدأ اللعبة من جديد if(moves == 0) { boardX[2] = 40; boardX[1] = 60; boardX[0] = 80; boardY[2] = 100; boardY[1] = 100; boardY[0] = 100; scoreReverseCounter = 99; speedTimer.start(); } // هنا قمنا بجعل المجموع الحالي الذي وصل إليه المستخدم يظهر كأعلى مجموع وصل إليه في حال تخطى المجموع القديم if(totalScore > bestScore) bestScore = totalScore; // هنا قمن بإنشاء مربع أسود يمثل لون خلفية اللعبة g.setColor(Color.BLACK); g.fillRect(0, 0, 750, 500); // هنا قمنا برسم المربعات التي تشكل الحدود التي لا تستطيع الأفعى عبورها باللون الرمادي // حجم كل مربع 13 بيكسل و المسافة بينهما 5 بيكسل g.setColor(Color.DARK_GRAY); for(int i=6; i<=482; i+=17) for(int j=6; j<=482; j+=17) g.fillRect(i, j, 13, 13); // هنا فمنا بإنشاء مربع أسود كبير فوق المربعات التي تشكل حدود اللعبة لتظهر و كأنها فارغة من الداخل g.setColor(Color.BLACK); g.fillRect(20, 20, 460, 460); // هنا قمنا بكتابة إسم اللعبة و تلوينه بالأزرق g.setColor(Color.CYAN); g.setFont(new Font("Arial", Font.BOLD, 26)); g.drawString("Snake 2D", 565, 35); // هنا قمنا بكتابة إسم اللعبة و تلوينه بالأزرق g.setFont(new Font("Arial", Font.PLAIN, 13)); g.drawString("+ "+scoreReverseCounter, 510, 222); // هنا جعلنا أي شيء سنقوم بكتابته يظهر باللون الرمادي g.setColor(Color.LIGHT_GRAY); // هنا قمنا بطباعة أنه تم تطوير اللعبة بواسطة موقعنا g.setFont(new Font("Arial", Font.PLAIN, 15)); g.drawString("Developed by harmash.com", 530, 60); // لقياس حجم الأرفام الظاهرة في المربعات الثلاث بالبيكسل حتى نستطيع عرضهم في الوسط FontMetrics هنا قمنا بإنشاء الكائن FontMetrics fm = g.getFontMetrics(); // هنا جعلنا أي شيء سنقوم بكتابته يظهر بنوع و حجم هذا الخط g.setFont(new Font("Arial", Font.PLAIN, 18)); //و المربع الذي تحته و الرقم الذي بداخله Best Score هنا قمنا برسم النص g.drawString("Best Score", 576, 110); g.drawRect(550, 120, 140, 30); g.drawString(bestScore+"", 550+(142-fm.stringWidth(bestScore+""))/2, 142); //و المربع الذي تحته و الرقم الذي بداخله Total Score هنا قمنا برسم النص g.drawString("Total Score", 573, 190); g.drawRect(550, 200, 140, 30); g.drawString(totalScore+"", 550+(142-fm.stringWidth(totalScore+""))/2, 222); //و المربع الذي تحته و الرقم الذي بداخله Fruit Eaten هنا قمنا برسم النص g.drawString("Fruit Eaten", 575, 270); g.drawRect(550, 280, 140, 30); g.drawString(fruitEaten+"", 550+(142-fm.stringWidth(fruitEaten+""))/2, 302); // Controls هنا قمنا برسم النص g.setFont(new Font("Arial", Font.BOLD, 16)); g.drawString("Controls", 550, 360); // Controls هنا قمنا برسم النصوص الظاهرة تحت النص g.setFont(new Font("Arial", Font.PLAIN, 14)); g.drawString("Pause / Start : Space", 550, 385); g.drawString("lookTo Up : Arrow Up", 550, 410); g.drawString("lookTo Down : Arrow Down", 550, 435); g.drawString("lookTo Left : Arrow Left", 550, 460); g.drawString("lookTo Right : Arrow Right", 550, 485); // هنا جعلنا الأفعى تنظر ناحية اليمين قبل أن يبدأ اللاعب بتحريكها lookToRightImage.paintIcon(this, g, boardX[0], boardY[0]); // هنا قمنا بمسح مكان وجود الأفعى السابق لأننا سنقوم بتخزين المكان الجديد كلما تحركت snake.clear(); // هنا قمنا بإنشاء حلقة ترسم كامل الدوائر التي تشكل الأفعى كل 0.1 ثانية for(int i=0; i<lengthOfSnake; i++) { if(i==0 && left) lookToLeftImage.paintIcon(this, g, boardX[i], boardY[i]); else if(i==0 && right) lookToRightImage.paintIcon(this, g, boardX[i], boardY[i]); else if(i==0 && up) lookToUpImage.paintIcon(this, g, boardX[i], boardY[i]); else if(i==0 && down) lookToDownImage.paintIcon(this, g, boardX[i], boardY[i]); else if(i!=0) snakeBodyImage.paintIcon(this, g, boardX[i], boardY[i]); // snake هنا قمنا بتخزين الموقع الحالي لكل دائرة في الأفعى في الكائن snake.add(new Position(boardX[i], boardY[i])); } // تقل بشكل تدريجي و أدنى قيمة ممكن أن تصل إليها هي 10 scoreReverseCounter هنا جعلنا قيمة العداد if(scoreReverseCounter != 10) scoreReverseCounter--; // هنا قمنا بإنشاء هذه الحلقة للتأكد إذا كان رأس الأفعى قد لامس أي جزء من جسدها for(int i=1; i<lengthOfSnake; i++) { // إذاً عندما يلمس رأس الأفعى جسدها سيتم جعل أول دائرة موجودة خلف الرأس تمثل رأس الأفعى حتى لا يظهر رأسها فوق جسدها if(boardX[i] == boardX[0] && boardY[i] == boardY[0]) { if(right) lookToRightImage.paintIcon(this, g, boardX[1], boardY[1]); else if(left) lookToLeftImage.paintIcon(this, g, boardX[1], boardY[1]); else if(up) lookToUpImage.paintIcon(this, g, boardX[1], boardY[1]); else if(down) lookToDownImage.paintIcon(this, g, boardX[1], boardY[1]); // للإشارة إلى أن اللاعب قد خسر true تساوي isGameOver بعدها سيتم جعل قيمة الـ // Space و بالتالي يمكنه أن يبدأ من جديد بالنقر على زر المسافة الفارغة isGameOver = true; // يتوقف و بالتالي ستتوقف الأفعى تماماً عن الحركة speedTimer بعدها سيتم جعل الـ speedTimer.stop(); // Game Over بعدها سيتم إظهار النص g.setColor(Color.WHITE); g.setFont(new Font("Arial", Font.BOLD, 50)); g.drawString("Game Over", 110, 220); // تحته Press Space To Restart و سيتم إظهار النص g.setFont(new Font("Arial", Font.BOLD, 20)); g.drawString("Press Space To Restart", 130, 260); // في الأخير سيتم إستدعاء هذه الدالة لحفظ أكبر مجموع وصل إليه اللاعب writeBestScoreInTheFile(); } } // إذا لمس رأس الأفعى الطعام سيتم إخفاء الطعام و زيادة مجموع اللاعب if((fruitXPos[xPos] == boardX[0]) && fruitYPos[yPos] == boardY[0]) { totalScore += scoreReverseCounter; scoreReverseCounter = 99; fruitEaten++; lengthOfSnake++; } // هنا في كل مرة سيتم ضمان أن لا يظهر الطعام فوق الأفعى for(int i=0; i<snake.size(); i++) { // في حال ظهر الطعام فوق جسد الأفعى سيتم خلق مكان عشوائي آخر لوضعها فيه if(snake.get(i).x == fruitXPos[xPos] && snake.get(i).y == fruitYPos[yPos]) { xPos = random.nextInt(22); yPos = random.nextInt(22); } } // في الأخير سيتم عرض الطعام بعيداً عن جسد الأفعى fruitImage.paintIcon(this, g, fruitXPos[xPos], fruitYPos[yPos]); // قمنا باستدعاء هذه الدالة للتخلص من أي مصادر لا حاجة لها في البرنامج و يمكنك إزالتها لأنها لن تؤثر هنا g.dispose(); } // لتحديد الإتجاه الذي ستتحرك فيه النافذة keyPressed() للدالة Override هنا فعلنا @Override public void keyPressed(KeyEvent e) { // Space هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر المسافة الفارغة if(e.getKeyCode() == KeyEvent.VK_SPACE) { // سيتم إيقاف اللعبة بشكل مؤقت إذا كانت اللعبة شغالة if(speedTimer.isRunning() && isGameOver == false) speedTimer.stop(); // سيتم إعادة اللعبة للعمل إذا كان قد تم إيقافها سابقاً else if(!speedTimer.isRunning() && isGameOver == false) speedTimer.start(); // سيتم بدأ اللعبة من جديد في حال كان قد تم إيقاف اللعبة لأن اللاعب قد خسر else if(!speedTimer.isRunning() && isGameOver == true) { isGameOver = false; speedTimer.start(); moves = 0; totalScore = 0; fruitEaten = 0; lengthOfSnake = 3; right = true; left = false; xPos = random.nextInt(22); yPos = 5+random.nextInt(17); } } // هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم الأيمن else if(e.getKeyCode() == KeyEvent.VK_RIGHT) { // إذا لم تكن الأفعى تسير في الإتجاه الأيسر سيتم توجيهها نحو الإتجاه الأيمن moves++; right = true; if(!left) right = true; else { right = false; left = true; } up = false; down = false; } // هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم الأيسر else if(e.getKeyCode() == KeyEvent.VK_LEFT) { // إذا لم تكن الأفعى تسير في الإتجاه الأيمن سيتم توجيهها نحو الإتجاه الأيسر moves++; left = true; if(!right) left = true; else { left = false; right = true; } up = false; down = false; } // هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم المتجه للأعلى else if(e.getKeyCode() == KeyEvent.VK_UP) { // إذا لم تكن الأفعى تسير في اتجاه الأسفل سيتم توجيهها نحو الأعلى moves++; up = true; if(!down) up = true; else { up = false; down = true; } left = false; right = false; } // هنا قمنا بتحديد ما سيحدث إذا قام اللاعب بالنقر على زر السهم المتجه للأسفل else if(e.getKeyCode() == KeyEvent.VK_DOWN) { // إذا لم تكن الأفعى تسير في اتجاه الأعلى سيتم توجيهها نحو الأسفل moves++; down = true; if(!up) down = true; else { up = true; down = false; } left = false; right = false; } } // هنا قمنا بتحديد المكان الذي ستظهر فيه الأفعى لحظة تحركها // و لاحظ أنه يوجد شرط متعلق بكل إتجاه تسلكه الأفعى حالياً // هذه الشروط الموضوعة تجعل الأفعى تظهر في الجهة المقابلة لها عندما تلمس الحائط @Override public void actionPerformed(ActionEvent e) { if(right) { for(int i = lengthOfSnake-1; i>=0; i--) boardY[i+1] = boardY[i]; for(int i = lengthOfSnake; i>=0; i--) { if(i==0) boardX[i] = boardX[i] + 20; else boardX[i] = boardX[i-1]; if(boardX[i] > 460) boardX[i] = 20; } } else if(left) { for(int i = lengthOfSnake-1; i>=0; i--) boardY[i+1] = boardY[i]; for(int i = lengthOfSnake; i>=0; i--) { if(i==0) boardX[i] = boardX[i] - 20; else boardX[i] = boardX[i-1]; if(boardX[i] < 20) boardX[i] = 460; } } else if(up) { for(int i = lengthOfSnake-1; i>=0; i--) boardX[i+1] = boardX[i]; for(int i = lengthOfSnake; i>=0; i--) { if(i==0) boardY[i] = boardY[i] - 20; else boardY[i] = boardY[i-1]; if(boardY[i] < 20) boardY[i] = 460; } } else if(down) { for(int i = lengthOfSnake-1; i>=0; i--) boardX[i+1] = boardX[i]; for(int i = lengthOfSnake; i>=0; i--) { if(i==0) boardY[i] = boardY[i] + 20; else boardY[i] = boardY[i-1]; if(boardY[i] > 460) boardY[i] = 20; } } repaint(); } @Override public void keyReleased(KeyEvent e) { } @Override public void keyTyped(KeyEvent e) { } // هذه الدالة تحفظ أعلى مجموع وصل إليه اللاعب في ملف خارجي بجانب ملف اللعبة private void writeBestScoreInTheFile() { if(totalScore >= bestScore) { try { FileOutputStream fos = new FileOutputStream("./snake-game-best-score.txt"); OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); osw.write(bestScore+""); osw.flush(); osw.close(); } catch(IOException e) { } } } // هذه الدالة تقرأ أعلى مجموع وصل إليه اللاعب من الملف الخارجي الموجود بجانب ملف اللعبة // في حال كان لا يوجد ملف خارجي ستقوم بإنشائه و وضع القيمة 0 فيه كقيمة أولية و هذا ما سيحدث عندما يقوم اللاعب بتشغيل اللعبة أول مرة private int readBestScorefromTheFile() { try { InputStreamReader isr = new InputStreamReader( new FileInputStream("./snake-game-best-score.txt"), "UTF-8" ); BufferedReader br = new BufferedReader(isr); String str = ""; int c; while( (c = br.read()) != -1){ if(Character.isDigit(c)) str += (char)c; } if(str.equals("")) str = "0"; br.close(); return Integer.parseInt(str); } catch(IOException e) { } return 0; } }

Main.java
// snake_2d موجود بداخل المجلد Main.java هنا ذكرنا أن الملف
package snake_2d;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
// و بالتالي أصبح إنشاء كائن منه يمثل إنشاء نافذة JFrame يرث من الكلاس Main هنا جعلنا الكلاس
public class Main extends JFrame {
// فقط createAndShowGUI() سيقوم الكونستركتور بإستدعاء الدالة Main عند إنشاء كائن من الكلاس
public Main() {
createAndShowGUI();
}
// هنا نضع كود إنشاء النافذة و محتوياتها
private void createAndShowGUI() {
// هنا قمنا بإنشاء كائن من الحاوية التي تحتوي على اللعبة
GameDrawer gameDrawer = new GameDrawer();
// هنا وضعنا الحاوية التي تحتوي على اللعبة في النافذة حتى تظهر بداخلها
add(gameDrawer);
// هنا قمنا بتحديد بعض خصائص النافذة و جعلناها مرئية
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// التي ستنشئ النافذة createAndShowGUI() و بالتالي سيتم إستدعاء الدالة Main هنا قمنا بإنشاء كائن من الكلاس
new Main();
}
});
}
}
// snake_2d موجود بداخل المجلد Main.java هنا ذكرنا أن الملف package snake_2d; import javax.swing.JFrame; import javax.swing.SwingUtilities; // و بالتالي أصبح إنشاء كائن منه يمثل إنشاء نافذة JFrame يرث من الكلاس Main هنا جعلنا الكلاس public class Main extends JFrame { // فقط createAndShowGUI() سيقوم الكونستركتور بإستدعاء الدالة Main عند إنشاء كائن من الكلاس public Main() { createAndShowGUI(); } // هنا نضع كود إنشاء النافذة و محتوياتها private void createAndShowGUI() { // هنا قمنا بإنشاء كائن من الحاوية التي تحتوي على اللعبة GameDrawer gameDrawer = new GameDrawer(); // هنا وضعنا الحاوية التي تحتوي على اللعبة في النافذة حتى تظهر بداخلها add(gameDrawer); // هنا قمنا بتحديد بعض خصائص النافذة و جعلناها مرئية setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); pack(); setLocationRelativeTo(null); setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { // التي ستنشئ النافذة createAndShowGUI() و بالتالي سيتم إستدعاء الدالة Main هنا قمنا بإنشاء كائن من الكلاس new Main(); } }); } }