كيفية برمجة لعبة Endless Runner باستخدام Arduino IDE
في عالم تطوير الألعاب المدمجة (Embedded Game Development)،
يقدم الأردوينو (Arduino) منصة مثالية للمبتدئين لاستكشاف برمجة
الألعاب على مستوى الأجهزة المادية. إذا كنت تحلم بإنشاء لعبتك الخاصة، ولكنك تفضل
البدء بمشروع عملي وملموس يتفاعل مع العالم الحقيقي، فإن برمجة لعبة
Endless Runner (لعبة الركض اللانهائي) باستخدام Arduino IDE هي
نقطة انطلاق ممتازة. ستتعلم في هذا المقال كيفية بناء لعبة بسيطة ومسببة للإدمان
حيث يتحكم اللاعب بشخصية تتجنب العقبات المتولدة عشوائياً، كل ذلك باستخدام
مكونات إلكترونية بسيطة وشاشة عرض صغيرة. سنغوص في مفاهيم مثل
التحكم بالمدخلات (Input Control)، عرض الرسوميات (Graphics Display)،
توليد العقبات العشوائية (Random Obstacle Generation)، والفيزياء
الأساسية للعبة (Basic Game Physics)، مما يمنحك فهماً قوياً لأساسيات
برمجة الألعاب المصغرة (Mini-Game Programming).
خطوات بناء لعبة Endless Runner على Arduino IDE
لإنشاء لعبة Endless Runner على منصة الأردوينو، سنقوم بتقسيم
المشروع إلى خطوات منطقية، بدءاً من إعداد المكونات وصولاً إلى برمجة منطق اللعبة والتفاعل مع اللاعب.
* الأدوات والمكونات المطلوبة :
- لوحة أردوينو: Arduino Uno أو Nano (أو أي لوحة متوافقة).
- شاشة عرض OLED: شاشة 0.96 بوصة I2C OLED Display (128x64 بكسل).
- زر ضغط (Push Button): واحد للقفز/التفاعل.
- مقاومة (Resistor): 10 كيلو أوم (لزر الضغط).
- لوحة تجارب (Breadboard).
- أسلاك توصيل (Jumper Wires).
- كابل USB: لتوصيل الأردوينو بالكمبيوتر.
1. إعداد بيئة الأردوينو وتوصيل المكونات
قبل البدء في البرمجة، يجب عليك تثبيت Arduino IDE وتوصيل شاشة
OLED وزر الضغط بلوحة الأردوينو بشكل صحيح.
1.1. تثبيت مكتبات Arduino IDE:
- افتح Arduino IDE وانتقل إلى
Sketch > Include Library > Manage Libraries... وابحث
عن المكتبات التالية وقم بتثبيتها:
- Adafruit GFX Library: مكتبة أساسية للرسوميات.
- Adafruit SSD1306: مكتبة خاصة للتحكم بشاشة OLED (SSD1306 Chip).
1.2. توصيل المكونات (Fritzing/Diagram):
* توصيل شاشة OLED (I2C):
- VCC: توصيل إلى 5V في الأردوينو.
- GND: توصيل إلى GND في الأردوينو.
- SDA: توصيل إلى A4 في الأردوينو (بيانات I2C).
- SCL: توصيل إلى A5 في الأردوينو (ساعة I2C).
* توصيل زر الضغط :
طرف واحد من الزر إلى Digital Pin 2 في الأردوينو.
الطرف الآخر من الزر إلى GND في الأردوينو عبر مقاومة 10 كيلو أوم (Pull-Down Resistor).
نفس الطرف المتصل بـ Digital Pin 2 يوصل أيضاً مباشرةً إلى 5V (أو VCC).
* مخطط التوصيل (نص وصفي) :
تخيل أن لوحة الأردوينو هي الأساس. قم بتوصيل أسلاك الطاقة
(الأحمر للـ 5V، الأسود للـ GND) من الأردوينو إلى جانبي لوحة التجارب.
الآن، قم بتوصيل دبابيس شاشة OLED: دبوس VCC إلى 5V، دبوس
GND إلى GND، دبوس SDA إلى دبوس A4، ودبوس SCL إلى دبوس A5.
بالنسبة للزر، ضع الزر على لوحة التجارب. قم بتوصيل أحد طرفي الزر إلى
5V (أو VCC) مباشرةً. الطرف المقابل لذلك الطرف على نفس جانب الزر
(عبر الفجوة المركزية في لوحة التجارب) يوصل إلى دبوس 2 في الأردوينو،
وإلى GND عبر مقاومة 10 كيلو أوم. هذا يضمن أن الدبوس 2 يقرأ "منخفض"
عندما لا يتم الضغط على الزر و"مرتفع" عند الضغط عليه.
2. الكود الأساسي لشاشة OLED والمدخلات
سنبدأ بكود يتحقق من عمل الشاشة وزر الضغط.
مكان الكود: arduino_endless_runner/arduino_endless_runner.ino
(الملف الرئيسي في مشروع الأردوينو)
++C
#include <Wire.h> // مكتبة الاتصال I2C#include <Adafruit_GFX.h> // مكتبة الرسوميات الأساسية#include <Adafruit_SSD1306.h> // مكتبة شاشة OLED
// تعريف أبعاد الشاشة#define SCREEN_WIDTH 128 // عرض الشاشة بالبكسل#define SCREEN_HEIGHT 64 // ارتفاع الشاشة بالبكسل
// تعريف عنوان شاشة OLED (عادة 0x3C أو 0x3D، تحقق من شاشتك)#define OLED_RESET -1 // دبوس إعادة الضبط (عادة لا يستخدم مع I2C)Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// تعريف دبوس زر الضغطconst int JUMP_BUTTON_PIN = 2;
void setup() { Serial.begin(9600); // بدء الاتصال التسلسلي للمراقبة (Debugging)
// تهيئة شاشة OLED if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // عنوان I2C 0x3C Serial.println(F("SSD1306 allocation failed")); for (;;) ; // توقف إذا فشل التهيئة }
display.display(); // مسح الشاشة delay(2000); // انتظار ثانيتين display.clearDisplay(); // مسح الشاشة مرة أخرى
// تهيئة دبوس زر الضغط كمدخل مع مقاومة سحب داخلية (لتبسيط التوصيل إذا لم تستخدم مقاومة خارجية) //pinMode(JUMP_BUTTON_PIN, INPUT_PULLUP); // إذا كان الزر موصولاً بـ GND ويقرأ LOW عند الضغط pinMode(JUMP_BUTTON_PIN, INPUT); // إذا كان الزر موصولاً بـ 5V ويقرأ HIGH عند الضغط (مع مقاومة خارجية)}
void loop() { display.clearDisplay(); // امسح الشاشة في كل حلقة
// قراءة حالة زر الضغط int buttonState = digitalRead(JUMP_BUTTON_PIN);
display.setTextSize(1); // حجم النص display.setTextColor(SSD1306_WHITE); // لون النص أبيض display.setCursor(0, 0); // موضع بداية النص
if (buttonState == HIGH) { // إذا تم الضغط على الزر (يعتمد على توصيلك) display.println("Button Pressed!"); } else { display.println("Button Not Pressed."); }
display.display(); // عرض ما كتبته على الشاشة delay(10); // تأخير قصير لتجنب الوميض السريع}
--
3. منطق اللعبة: الشخصية، الجاذبية، والقفز
الآن سنبدأ في برمجة شخصية اللاعب (مربع بسيط) ومنطق حركته.
مكان الكود: ضمن نفس الملف arduino_endless_runner.ino،
استبدل محتويات loop() وأضف المتغيرات الجديدة.
++C
// ... (الاستيرادات والتعريفات الموجودة في الجزء 2)
// ======= متغيرات اللعبة =======// الشخصية (اللاعب)int playerX = 20; // موضع اللاعب الأفقي (ثابت تقريباً)int playerY = SCREEN_HEIGHT - 10 - 1; // موضع اللاعب العمودي (أعلى الأرض بقليل)int playerWidth = 10; // عرض اللاعبint playerHeight = 10; // ارتفاع اللاعب
// الجاذبية والقفزfloat verticalSpeed = 0; // السرعة العمودية للاعب (لأسفل بفعل الجاذبية، للأعلى عند القفز)const float GRAVITY = 0.5; // قيمة الجاذبيةconst float JUMP_FORCE = -8; // قوة القفز (قيمة سالبة للحركة للأعلى)bool isJumping = false; // لتتبع ما إذا كان اللاعب يقفز
// حالة اللعبةenum GameState { RUNNING, GAMEOVER };GameState currentGameState = RUNNING;
void setup() { // ... (setup() كما في الجزء 2)}
void loop() { // إذا كانت اللعبة تعمل if (currentGameState == RUNNING) { display.clearDisplay(); // امسح الشاشة في كل حلقة
// ======= تحديث الجاذبية والقفز ======= verticalSpeed += GRAVITY; // تطبيق الجاذبية playerY += verticalSpeed; // تحديث موضع اللاعب العمودي
// التأكد من أن اللاعب لا يخترق الأرضية if (playerY >= SCREEN_HEIGHT - playerHeight - 1) { playerY = SCREEN_HEIGHT - playerHeight - 1; // ثبت اللاعب على الأرض verticalSpeed = 0; // أوقف السرعة العمودية isJumping = false; // لم يعد يقفز }
// التحكم بالقفز if (digitalRead(JUMP_BUTTON_PIN) == HIGH && !isJumping) { verticalSpeed = JUMP_FORCE; // تطبيق قوة القفز isJumping = true; // اللاعب يقفز الآن }
// ======= رسم اللاعب ======= display.fillRect(playerX, playerY, playerWidth, playerHeight, SSD1306_WHITE); // رسم مربع اللاعب
// ======= رسم الأرضية ======= display.drawLine(0, SCREEN_HEIGHT - 1, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1, SSD1306_WHITE);
display.display(); // عرض ما كتبته على الشاشة delay(10); // تأخير قصير للتحكم في سرعة اللعبة } // إذا كانت اللعبة قد انتهت (Game Over) else if (currentGameState == GAMEOVER) { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(10, SCREEN_HEIGHT / 2 - 10); display.println("GAME OVER!"); display.setTextSize(1); display.setCursor(0, SCREEN_HEIGHT / 2 + 10); display.println("Press button to restart"); display.display();
// انتظار الضغط على الزر لإعادة التشغيل if (digitalRead(JUMP_BUTTON_PIN) == HIGH) { // إعادة تهيئة اللعبة playerY = SCREEN_HEIGHT - 10 - 1; verticalSpeed = 0; isJumping = false; currentGameState = RUNNING; // يجب أيضاً إعادة تهيئة العقبات والنقاط هنا } }}
--
4. توليد العقبات واكتشاف الاصطدام
سنضيف الآن عقبات تتحرك نحو اللاعب وتوليدها بشكل عشوائي، بالإضافة إلى منطق اكتشاف الاصطدام.
مكان الكود: ضمن نفس الملف arduino_endless_runner.ino.
أضف متغيرات العقبات، كلاس العقبة (اختياري، أو مجرد متغيرات)،
وقم بتعديل loop() ليتضمن منطق العقبات.
++C
// ... (الاستيرادات والتعريفات الموجودة)
// ======= متغيرات اللعبة (إضافة العقبات) =======// ... (متغيرات اللاعب، الجاذبية، القفز كما في الجزء 3)
// العقباتconst int MAX_OBSTACLES = 3; // الحد الأقصى لعدد العقبات التي تظهر على الشاشةint obstacleX[MAX_OBSTACLES]; // موضع العقبة الأفقيint obstacleWidth[MAX_OBSTACLES]; // عرض العقبةint obstacleHeight[MAX_OBSTACLES]; // ارتفاع العقبةbool obstacleActive[MAX_OBSTACLES]; // هل العقبة نشطة؟int obstacleSpeed = 2; // سرعة حركة العقبات
// متى يتم توليد عقبة جديدةlong lastObstacleTime = 0;const long MIN_OBSTACLE_INTERVAL = 1500; // الحد الأدنى للوقت بين عقبتين (بالمللي ثانية)const long MAX_OBSTACLE_INTERVAL = 3000; // الحد الأقصى للوقت بين عقبتين
// النقاطlong score = 0;long highScore = 0; // لحفظ أعلى نتيجة
// ... (enum GameState كما في الجزء 3)
void setup() { // ... (setup() كما في الجزء 2) randomSeed(analogRead(0)); // تهيئة مولد الأرقام العشوائية (باستخدام مدخل A0 غير المتصل) resetGame(); // استدعاء دالة لإعادة تهيئة اللعبة (سنقوم بإنشائها)}
// دالة لإعادة تهيئة اللعبةvoid resetGame() { playerY = SCREEN_HEIGHT - 10 - 1; verticalSpeed = 0; isJumping = false; score = 0; currentGameState = RUNNING;
// إعادة تهيئة العقبات for (int i = 0; i < MAX_OBSTACLES; i++) { obstacleActive[i] = false; } lastObstacleTime = millis(); // إعادة ضبط مؤقت العقبات}
void loop() { if (currentGameState == RUNNING) { display.clearDisplay();
// ======= تحديث الجاذبية والقفز (كما في الجزء 3) ======= verticalSpeed += GRAVITY; playerY += verticalSpeed; if (playerY >= SCREEN_HEIGHT - playerHeight - 1) { playerY = SCREEN_HEIGHT - playerHeight - 1; verticalSpeed = 0; isJumping = false; } if (digitalRead(JUMP_BUTTON_PIN) == HIGH && !isJumping) { verticalSpeed = JUMP_FORCE; isJumping = true; }
// ======= تحديث وتوليد العقبات ======= for (int i = 0; i < MAX_OBSTACLES; i++) { if (obstacleActive[i]) { obstacleX[i] -= obstacleSpeed; // حرك العقبة لليسار
// إذا خرجت العقبة من الشاشة، أوقف تفعيلها if (obstacleX[i] + obstacleWidth[i] < 0) { obstacleActive[i] = false; score++; // زيادة النقاط عند تجاوز العقبة } else { // رسم العقبة display.fillRect(obstacleX[i], SCREEN_HEIGHT - obstacleHeight[i] - 1, obstacleWidth[i], obstacleHeight[i], SSD1306_WHITE);
// ======= اكتشاف الاصطدام (AABB Collision) ======= // إذا تداخل مستطيل اللاعب مع مستطيل العقبة if (playerX < obstacleX[i] + obstacleWidth[i] && playerX + playerWidth > obstacleX[i] && playerY < SCREEN_HEIGHT - obstacleHeight[i] && playerY + playerHeight > SCREEN_HEIGHT - obstacleHeight[i] - 1) { currentGameState = GAMEOVER; // اللعبة انتهت if (score > highScore) { highScore = score; // تحديث أعلى نتيجة } } } } }
// توليد عقبة جديدة إذا حان الوقت وهناك مكان شاغر long currentTime = millis(); if (currentTime - lastObstacleTime > random(MIN_OBSTACLE_INTERVAL, MAX_OBSTACLE_INTERVAL)) { int nextObstacleIndex = -1; for (int i = 0; i < MAX_OBSTACLES; i++) { if (!obstacleActive[i]) { nextObstacleIndex = i; break; } } if (nextObstacleIndex != -1) { obstacleX[nextObstacleIndex] = SCREEN_WIDTH + random(20, 50); // تبدأ من خارج الشاشة obstacleWidth[nextObstacleIndex] = random(10, 25); // عرض عشوائي obstacleHeight[nextObstacleIndex] = random(10, 25); // ارتفاع عشوائي obstacleActive[nextObstacleIndex] = true; lastObstacleTime = currentTime; } }
// ======= رسم اللاعب والأرضية (كما في الجزء 3) ======= display.fillRect(playerX, playerY, playerWidth, playerHeight, SSD1306_WHITE); display.drawLine(0, SCREEN_HEIGHT - 1, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1, SSD1306_WHITE);
// ======= عرض النقاط ======= display.setTextSize(1); display.setCursor(0, 0); display.print("Score: "); display.println(score); display.print("High: "); display.println(highScore);
display.display(); delay(10); } // إذا كانت اللعبة قد انتهت (Game Over) else if (currentGameState == GAMEOVER) { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(10, SCREEN_HEIGHT / 2 - 20); display.println("GAME OVER!"); display.setTextSize(1); display.setCursor(5, SCREEN_HEIGHT / 2 + 5); display.print("Score: "); display.println(score); display.setCursor(5, SCREEN_HEIGHT / 2 + 20); display.println("Press button to restart"); display.display();
if (digitalRead(JUMP_BUTTON_PIN) == HIGH) { resetGame(); // استدعاء دالة إعادة التهيئة } }}
--
5. تحسينات إضافية (اختياري)
يمكنك تطوير اللعبة أكثر بإضافة هذه الميزات:
رسوميات متقدمة: استخدام صور (Bitmaps) بدلاً من المربعات للشخصية والعقبات.
يمكن رسمها باستخدام drawBitmap() في مكتبة Adafruit GFX.
- صوتيات: إضافة مؤثرات صوتية بسيطة باستخدام buzzer أو
مكبر صوت صغير (للقفز، الاصطدام، نهاية اللعبة).
- زيادة الصعوبة تدريجياً: زيادة obstacleSpeed أو تقليل
MIN_OBSTACLE_INTERVAL مع مرور الوقت أو زيادة النقاط.
* شاشة بداية: إضافة شاشة ترحيب قبل بدء اللعبة.
حفظ أعلى نتيجة: استخدام الذاكرة الداخلية لـ الأردوينو (EEPROM)
لحفظ highScore حتى بعد إيقاف تشغيل الجهاز.
الخاتمة: خطوتك الأولى في برمجة الألعاب المدمجة
لقد أكملت للتو مشروع لعبة Endless Runner بسيطة باستخدام Arduino IDE،
مما يمثل خطوة أولى رائعة في عالم برمجة الألعاب المدمجة.
من خلال هذا المشروع، تعلمت كيفية التفاعل مع الأجهزة المادية
(Hardware Interaction) مثل شاشات OLED والأزرار، وتطبيق
مفاهيم برمجية أساسية (Core Programming Concepts) مثل الجاذبية،
القفز، توليد العشوائية (Random Generation)، واكتشاف الاصطدام
(Collision Detection). هذه التجربة لا تعزز فقط مهاراتك في برمجة الأردوينو
(Arduino Programming) والإلكترونيات (Electronics)، بل تفتح لك
أيضاً الباب لاستكشاف مشاريع ألعاب أكثر تعقيداً على المتحكمات الدقيقة.
تذكر أن الإبداع لا حدود له، وأن هذا المشروع هو مجرد نقطة انطلاق لرحلتك
في تطوير الألعاب المادية (Physical Game Development).