728x90 AdSpace

أخر الأخبار

عمل تطبيقي Mario Sokoban

عمل تطبيقي Mario Sokoban
المكتبة SDL تقدّم، مثلما رأينا، عددا كبيراً من الدوال الجاهزة للاستعمال. يمكن ألا نستطيع التعوّد عليها في البداية لقلّة التطبيق . هذا العمل التطبيقي الأول في هذا الجزء من الدورة سيعطيكم فرصة التطبيق و اختبار أشياء لم تسنح لكم فرصة تجريبها !
أعتقد أنه بإمكانكم التخمين، فهذه المرة التطبيق لن يكون عبارة عن كونسول و إنما سيتحتوي على واجهة رسومية.. ماذا سيكون موضوع هذا العمل التطبيقي ؟ لعبة السوكوبان!
قد لا يعني لكم هذا العنوان شيئاً، لكن هي لعبة ذكاء كلاسيكية تنصّ على دفع صناديق لوضعها في أماكن محددة في متاهة .
بخصوص السوكوبان :
الكلمة Sokoban هي كلمة يابانية تعني " صاحب محلّ  Magasinier  "  . و هي عبارة عن لعبة ذكاء تم اختراعها في الثمانينات بواسطة Hiroyuki Imabayashi. و قد مثّلت برمجة هذه اللعبة تحدّيا كبيراُ في ذلك الزمن .
الهدف من اللعبة :
المبدأ بسيط : تقومون بتحريك شخصية في متاهة. يجدر بالشخصية ان تقوم بدفع صناديق إلى مواقع محددة . لا يمكن للاعب أن يدفع صندوقين في آن واحد.
حتى و إن كان المبدأ مفهوماً و بسيطاً، فهذا لا يعني أن اللعبة في حدّ ذاتها سهلة! إذ انه يجب عليكم تكسير رؤوسكم بالتفكير لحلّ اللغز أحياناً . الصورة الموالية تُريكم كيف تبدو اللعبة التي سنقوم ببرمجتها :
Le jeu Mario Sokoban que nous allons réaliser
لماذا اخترتُ هذه اللعبة بالذات ؟
لأنها لعبة شعبية، جيدة لأن تكون موضوع برمجي و يمكننا إنشاؤها بواسطة ما تعلّمناه من الدروس السابقة .
يجب هنا أن نكون منظّمين، إذ أن الصعوبة لا تكمُن في برمجة اللعبة في حدّ ذاتها لكن في ما إن نظّمنا العمل . و لهذا فسنقوم بتقسيم البرنامج إلى عدّة ملفات c. بطريقة ذكيّة و نحاول إنشاء الدوال المناسبة .
من أجل هذا الأمر، قررت تغيير طريقة العمل، لن أقدّم لكم توجيهات و أنتم تقومون بالعمل لوحدكم ... بالعكس، سأريكم كيف نقوم ببناء المشروع كلّه من الألف إلى الياء .
ماذا لو كنتُ أريد التدرّب لوحدي ؟
حسناً إذا فلتنطلق لوحدك، هذا أمر جيد! ستحتاج ربّما وقتاً أكثر ، لقد استغرقت شخصيا يوماً كاملاً لبرمجة اللعبة، هذا ليس بالوقت الكثير ربّما لأنه جرت العادة أن أقوم بالبرمجة و أن أتحاشى الوقوع في بعض الأفخاخ المشهورة.
إعلموا بأنه توجد الكثير من الطرق التي يمكن بها برمجة هذه اللعبة، سأعطيكم طريقتي في برمجتها ( ليست أحسن طريقة و لكن بالتأكيد ليست أسوء واحدة ).
سننتهي من هذا التطبيق بقائمة من الاقتراحات لتحسين اللعبة، كما أنين سأعطيكم الرابط لتحميل اللعبة و الكود المصدري الخاص بها .
أنصحكم مجدداً أن تحاولوا ببرمجة اللعبة لوحدكم، حتى لو استغرقتم 3 أو 4 أيام ، المهم أن تقوموا بالتطبيق و التعوّد على البرمجة .
 كـُراس الحمولات :
كراس الحمولات هو عبارة عن ملف نكتب فيه كل ما يجب على البرنامج أن يستطيع فعله . هذا ما أقترحه :
  • يجب أن يتمكن اللاعب من التحرّك في المتاهة .
  • لا يمكنه أن يدفع صندوقين معاُ.
  • تكون الجولة مربوحة إذا تواجدت كلّ الصناديق في الأماكن المخصصة لها ( سيتم حفظ كلّ مستويات اللعبة في ملف، ليكن مثلا niveaux.lvl ) .
  • يجب أن يتم دمج ( مـُنشئ éditeur ) في البرنامج ليتمكن أي شخص كان من صنع مراحل خاصة به ( هذا ليس أمراً ضرورياً كنه يعتبر إضافة مميزة )
هذا كافٍ لنعمل كثيراً ... يجب أن تعرفوا أنه هناك أشياء لا يجيد البرنامج القيام بها، و يجب ذِكرُ هذا الأمر أيضاً :
  • برنامجنا قادر على التحكّم في مرحلة واحدة في المرّة الواحدة . إن أردتم أن تكون اللعبة عبارة عن تتالي جولات، فما عليكم سوى برمجة ذلك بأنفسكم في نهاية هذا العمل التطبيقي .
  • البرنامج لا يقوم بحساب الوقت المٌستغرق في كلّ جولة ( نحن لا نجيد فعل ذلك بعد ) و لا يمكنه حساب النقاط .
على أي حال فكلّ الأشياء التي نريد القيام بها ( خاصة مـُنشِئ المراحل l'éditeur de niveaux ) تأخذ منا وقتاً لابأس به .
سأعطيكم في نهاية العمل التطبيقي، جملة التحسينات التي تُمكن إضافتها إلى اللعبة، و ليست هذه كلمات في الهواء، سأعطيكم اللعبة الكاملة بعد التحسينات التي سأقترحها عليكم، لكنني لن اعطيكم الكود المصدري الخاص بها لأنني أريدكم أن تعملوا بأنفسكم و تتدرّبوا، لا أن تأخذوا كلّ شيء على طبق من فضّة، ليس هذا هدفنا!
إسترجاع الصور اللازمة في اللعبة :
في معظم الألعاب ثنائية الأبعاد، مهما كان نوعها، نسمّي الصور التي تشكّل اللعبة sprites . في حالتنا، اخترت الشخصية ماريو لتكون اللاعب الرئيسي في اللعبة ( من هنا جاء اسم اللعبة Mario Sokoban ) . بما أن الشخصية ماريو شخصية لها شعبية كبيرة في عالم الألعاب 2D، لن نحتاج الـsprite الخاصة بماريو فقط و إنما أيضا الخاصة بالجدران و الصناديق، الأماكن المستهدفة... الخ ، توجد العديد من المواقع التي تقترح هذه الملصاقات، و هذا ما سنحتاج إليه :
 الملصق
الشرح
Image utilisateurImage utilisateur
 جدار
 Image utilisateurImage utilisateur
 صندوق
 Image utilisateurImage utilisateur
 صندوق متموضع فوق منطقة مستهدفة
 Image utilisateurImage utilisateur
 منطقة مستهدفة ( أين يجب وضع الصناديق )
 Image utilisateurImage utilisateur
 بطل اللعبة ( ماريو ) باتجاه الأسفل
 Image utilisateurImage utilisateur

 بطل اللعبة باتجاه اليمين
 Image utilisateurImage utilisateur

 بطل اللعبة باتجاه اليسار
 Image utilisateurImage utilisateur

 بطل اللعبة باتجاه الأعلى
الأسهل هو أن تقوموا بتحميل الحزمة التي أعددتها لكم :
كان من الممكن أن أستعمل ملصقاً واحداً خاصاً باللاعب. وجعله موجّهاً إلى الأسفل فقط، لكن إضافة امكانية سلك الإتجاهات الأربعة تضيف القليل من الواقعية. و هذا يشكّل تحدّيا آخر لنا !
بإمكاني أيضاً استعمال صورة أخرى لتكون عبارة عن الواجهة الأساسية للعبة حين تبدأ، لقد أرفقت لكم الصورة بالحزمة، لاحظوا الصورة التالية :
L'écran d'accueil du programme
ستلاحظون بأن الصور تأخذ صيغ مختلفة. توجد منها ماهو GIF، ماهو PNG و حتى ماهو JPEG. و لهذا فنحن بحاجة إلى استعمال المكتبة  SDL_Image .
فكّروا في جعل مشروعكم يتحّكم في الـSDL و الـSDL_Image . إذا نسيتم كيف تفعلون ذلك، فراجعوا الدروس السابقة. إذا لم تقوموا بتخصيص المشروع بشكل صحيح، سيشير المُترجم بأن الدوال التي تستعملونها ( مثل IMG_Load ) لا توجد!
الدالة الرئيسية و الثوابت :
في كلّ مرة نبدأ بتحقيق مشروع مهمّ، من الواجب أن نقوم بتنظيم العمل في البداية . بشكل عام، أبدأ في إنشاء ملف ثوابت constantes.h إضافة إلى ملف main.c يحتوي الدالة الرئيسية ( فقط الدالة الرئيسية ) . هذه ليست قاعدة لكنها طريقتي الخاصة في العمل ، و لكلّ شخص طريقته الخاصة .
ملفّات المشروع المختلفة :
أقترح أن نقوم بإنشاء ملفات المشروع كلّه الآن، حتى و إن كانت فارغة لحدّ الآن. هاهي الملفات التي أنشئها إذا :
  • constantes.h : تعريف الثوابت الشاملة global الخاصة بكل البرنامج .
  • main.c : الملف الذي يحتيو الدالة الرئيسية main .
  • jeu.c : دالة تقوم بالتحكّم في جزء من اللعبة Sokoban .
  • jeu.h : نماذج الدوال الخاصة بالملف jeu.c . 
  • editeur.c : ملف يحتيو الدول التي تتحكم في مـُنشئ المراحل l'éditeur de niveaux .
  • editeur.h : نماذج الدوال الخاصة بالملف editeur.c .
 سنبدأ بإنشاء ملف الثوابت .
الثوابت : constantes.h
هذا محتوى الملف constantes.h :

#ifndef DEF_CONSTANTES
#define DEF_CONSTANTES
#define TAILLE_BLOC 34 // Taille d'un bloc (carré) en pixels
#define NB_BLOCS_LARGEUR 12
#define NB_BLOCS_HAUTEUR 12
#define LARGEUR_FENETRE TAILLE_BLOC * NB_BLOCS_LARGEUR
#define HAUTEUR_FENETRE TAILLE_BLOC * NB_BLOCS_HAUTEUR
    enum {HAUT, BAS, GAUCHE, DROITE};
    enum {VIDE, MUR, CAISSE, OBJECTIF, MARIO, CAISSE_OK};
#endif
ستلاحظون الكثير من النقاط المهمّة في هذا الملف الصغير :
  • يبدأ الملف بتعليق رأسي. أنصحكم بوضع تعليق مماثل في كلّ ملفاتكم ( مهما كانت بصيغة c. أو h. ) . بشكل عام، التعليق الرأسي يحتوي : اسم الملف، اسم الكاتب ( المبرمج )، مهمّة الملف ( أي الدوال بحدّ ذاتها ) ... يمكننا أيضاً إضافة تاريخ كتابة الملف و تاريخ آخر تعديل فيه، هذا يسمح لكم بإيجاد المعلومات بسرعة حينما تحتاجون إليها و خاصة حينما يتعلّق الأمر بمشاريع كبيرة .
  •  الملف محمّي ضد التضمينات غير المنتهية. لأنين استعملت التقنية التي تعلّمناها في نهاية الدرس حول المعالج القبلي préprocesseur. هنا تأثير هذه التقنية ليس مهمّا لكن جرت العادة أن أستعملها في كلّ ملفاتي ذات الصيغة h. بدون استثناء .
  • أخيراً، قلب الملف. ستجدون لائحة من المعرّفات define. قمت بتحديد حجم كتلة بالبيكسل ( كل الملصقات sprites هي عبارة عن مربّعات ذات حجم 34 بيكسل ) . أحدد بأن حجم النافذة يساوي 12*12 كتلة كعُرض . و بهذا أقوم بحساب أبعاد النافذة بعملية ضرب الثوابت بسيطة . ما أقوم به هنا ليس ضرورياً، لكنه يعود إلينا بالفائدة . إذا أردت لاحقاً مراجعة حجم اللعبة، يكفي أن أقوم بتعديل هذا الملف و إعادة ترجمة المشروع فيتعامل مع القيم الجديدة دون أية مشاكل.
  • أخيراً، قمت بتعريف ثوابت عن طريق تعدادات énumérations غير معرّفة، الأمر مختلف قليلاً عمّا تعلّمناه في درس "أنشئ أنماط خاصة بك" . هنا أنا لست أقوم بتعريف نمط خاص بي بل أقوم فقط بتعريف ثوابت. هذا يسبه للمعرّفات باختلاف بسيط : الحاسوب هو من يقوم بتوزيع رقم لكلّ قيمة ( بدءاً بالصفر ). و بهذا يكن لدينا : HAUT = 0، BAS = 1، GAUCHE = 2 ... الخ هذا ما سيسمح للكود بان يكون مفهوماً لاحقاً، سترون ذلك .
باختصار، لقد استعملت :
  • معرّفات define  حينما أريد ان اعطي قيمة محددة لثابتة ( مثلاُ 34 بيكسل ) .
  • تعدادات énumérations حينما تكون قيمة الثابتة لا تهمّني. هنا، لا يهمني ما إن كانت القيمة المُرفقة بالعنصر HAUT هي 0 أو 150 أو مهما كانت . ما يهمّني هو أن يكون هذا العنصر مختلف عن BAS و GAUCHE و DROITE .
تضمين تعريفات الثوابت :
المبدأ ينص على تضمين ملف الثوابت في كلّ الملفات ذات الصيغة c. . و بهذا أستطيع استعمال الثوابت في أي مكان من الكود المصدري الخاص بالمشروع، يعني أنه عليّ أن أكتب السطر التالي في كل رأسية للملفات ذات الصيغة c.

#include "constantes.h"
الدالة ائرئيسية main.c :
الدالة الرئيسية الخاصة بالبرنامج سهلة جداً ، فهي تقوم بإظهار واجهة اللعبة ثم توجيه اللعبة إلى القِسم المناسب .


#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include "constantes.h"
#include "jeu.h"
#include "editeur.h"
int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *menu = NULL;
    SDL_Rect positionMenu;
    SDL_Event event;
    int continuer = 1;
    SDL_Init(SDL_INIT_VIDEO);
    SDL_WM_SetIcon(IMG_Load("caisse.jpg"), NULL); // L'icône doit être chargée avant SDL_SetVideoMode
    ecran = SDL_SetVideoMode(LARGEUR_FENETRE, HAUTEUR_FENETRE, 32,SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Mario Sokoban", NULL);
    menu = IMG_Load("menu.jpg");
    positionMenu.x = 0;
    positionMenu.y = 0;
    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
            case SDL_KEYDOWN:
                switch(event.key.keysym.sym)
                {
                    case SDLK_ESCAPE: // Veut arrêter le jeu
                        continuer = 0;
                        break;
                    case SDLK_KP1: // Demande à jouer
                        jouer(ecran);
                        break;
                    case SDLK_KP2: // Demande l'éditeur de niveaux
                        editeur(ecran);
                        break;
                }
                break;
        }
        // Effacement de l'écran
        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0,0));
        SDL_BlitSurface(menu, NULL, ecran, &positionMenu);
        SDL_Flip(ecran);
    }
    SDL_FreeSurface(menu);
    SDL_Quit();
    return EXIT_SUCCESS;
}
الدالة الرئيسية تتكلف بتهيئة الـSDL، إعطاء عنوان للنافذة إضافة إلى منحها أيقونة. في نهاية الدالة، يتم استدعاء الدالة SDL_Quit()  لتوقيف الـSDL بشكل سليم . الدالة الرئيسية تقوم بإظهار قائمة يتم تحميلها بواسطة الدالة IMG_Load من المكتبة SDL_Image .

menu = IMG_Load("menu.jpg");
تلاحظون أنه لكي اعطي أبعاداً للنافذة، أستعمل الثابتتين LARGEUR_FENETRE و HAUTEUR_FENETRE المعرّفتين في الملف constantes.h.
حلقة الأحداث :
الحلقة غير المنتهية تتحكم في الأحداث التالية :
  • تقوم بتوقيف البرنامج SDL_QUIT : إذا قمنا بطلب غلق البرنامج ( النقر على العلامة X أعلى يمين النافذة ) يعني أننا سنعطي القيمة 0 للمتغيرة continuer  و تتوقف الحلقة، باختصار، هذا أمر كلاسيكي.
  • الضغط على الزر Echap : إغلاق البرنامج ( مثل SDL_QUIT ) .
  • الضغط على الزر 1 من لوحة الأرقام : إنطلقا تشغيل اللعبة ( استدعاء الدالة jouer  ).
  • الضغط على الزر 2 من لوحة الأرقام : إنطلاق تشغيل مُنشئ المراحل ( استدعاء الدالة editeur )
كما ترون فالأمور تجري بسهولة تامة . إذا ضغطنا على الزر 1، يتم تشغيل اللعبة، ما إن تنتهي اللعبة، تنتهي الدالة jouer و نرجع للـmain من أجل القيام بدورة أخرى للحلقة . الحلقة تستمر في الاشتغال مادمنا لم نطلب توقيف البرنامج .
بفضل هذا التنظيم البسيط، يمكننا التحكم في الدالة الرئيسية و ترك الدوال الأخرى ( مثل jouer و editeur ) تهتم بالتحكم في مختلف أجزاء اللعبة .
اللعبة :
فلنهجم على المرحلة الأكثر أهمية من اللعبة، الدالة jouer !
هذه هي الدالة الأكثر أهمية في البرنامج، كونوا متيقّضين لأن هذه الدالة هي ما يجدر بكم فهمه. لأنكم ستجدون بأن مٌنشئ المراحل ليس بالصعوبة التي تتخيّلونها .
الخاصيات التي نبعثها للدالة :
الدالة jouer تحتاج إلى خاصية واحدة : المساحة ecran. بالفعل، تم فتح النافذة في الدالة الرئيسية، و لكي تستطيع الدالة jouer  أن ترسم على النافذة، يجب أن تقوم باسترجاع المؤشّر نحو المساحة ecran  !
لو تقرؤون مجدداً محتوى الدالة الرئيسية، ستجدون بأنني قمت باستدعاء الدالة jouer و ذلك بإعطائها المؤشّر الذي تحتاجه :

jouer(ecran);
نموذج الدالة الذي يمكنكم وضعه في الملف jeu.h هو التالي :

void jouer(SDL_Surface* ecran);
الدالة لا تقوم بإرجاع أي شيء ( و من هذا نستنتج الـvoid ) . يمكننا أن نجعلها تُرجع قيمة منطقية تشير إلى ما كنّا قد ربحنا الجولة أم لا .
تعريف المتغيرات :
تحتاج هذه الدالة إلى كثير من المتغيرات. لم أفكّر في كلّ المتغيرات التي أحتاجها في الوهلة الأولى . هناك من أضفتها لاحقاً و أنا أكتب الكود .
متغيّرات من أنماط معرّفة في الـSDL :
لكي نبدأ، هاهي كلّ المتغيرا من أنماط سبق تعريفها في الـSDL و التي نحن بحاجة إليها :


SDL_Surface *mario[4] = {NULL}; // 4 surfaces pour 4 directions de
mario
SDL_Surface *mur = NULL, *caisse = NULL, *caisseOK = NULL, *objectif= NULL, *marioActuel = NULL;
SDL_Rect position, positionJoueur;
SDL_Event event;
لقد قمتُ بإنشاء جدول من نمط SDL_Surface يسمّى mario. و هو جدول من أربع خانات يقوم بتخزين ماريو في كلّ من الاتجاهات الأربعة ( واحد للأسفل، الأعلى، اليمين و اليسار ) .
توجد بعد ذلك العديد من المساحات الموافقة لكلّ من الملصقات التي قمتم بتحميلها أعلاه : جدار، صندوق، صندوقOK ( أي صندوق في المكان المناسب ) و المنطقة المستهدفة .
بماذا ينفعنا marioActuel ؟
هو عبارة عن مؤشّر نحو مساحة. و هو مؤشّر يؤشّر نحو المساحة الموافقة لماريو المتّجه نحو الإتجاه الحالي. أي أنه عبارة عن marioActuel  ( ماريو الحالي ) نقوم بلصقه في الشاشة. إذا رأيتم في أسفل الدالة jouer ستجدون :



SDL_BlitSurface(marioActuel, NULL, ecran, &position);
لا نقوم إذا بلصق عنصر من الجدول mario، بل المؤشّر marioActuel .
و بلصق marioActuel، يعني أننا سنلصق إما ماريو نحو الأسفل، أو نحو الأعلى ... الخ . المؤشّر marioActuel يؤشّر نحو إحدى خانات الجدول mario.
ماذا بعد غير هذا ؟
متغيرة position  من نمط SDL_Rect سنستعين بها من أجل تعريف وضعية العناصر التي سنقوم بلصقها ( سنحتاج إليها من أجل كلّ الملصقات، و لا داعي لانشاء SDL_Rect  من أجل كلّ مساحة ! ) .
المتغيرة positionJoueur مختلفة : إنها تشير إلى أية خانة من النافذة يوجد اللاعب . أخيراً، المتغيرة event تهتم بتحليل الأحداث .
متغيّرات أكثر " كلاسيكية " :
حان الوقت لكي أعرّف متغيرة خاصة نوعاً ما من نمط int ( عدد طبيعي ) :




int continuer = 1, objectifsRestants = 0, i = 0, j = 0;
int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};
continuer و objectifsRestants  هي متغيرات منطقية . i و j هي متغيرات مُساعِدة ستساعدنا في قراءة الجدول carte.
هنا تبدأ الأمور الهامّة، لقد قمت فعلياً بإنشاء جدول ذو بُعدين. لم أكلّمكم عن هذا النمط من الجداول من قبل، لكنه الوقت المناسب لتتعلّموا ما يعنيه . ليس الأمر صعباً، سترون ذلك بأنفسكم .





int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};
هو عبارةعن جدول أعداد طبيعية (int) يختلف في كونه يأخذ إشارتين من [ ].
إذا تتذكرون جيداً الملف constantes.h ، فـNB_BLOCS_LARGEUR  و NB_BLOCS_HAUTEUR  هما ثابتتين تأخذ كلتاهما القيمة 12 .






int carte[12][12] = {0};
لكن، ماذا يعني هذا؟
هذا يعني أنه من أجل كلّ "خانة" من carte توجد 12 خانة داخلية . و بهذا تكون لدينا المتغيرات التالية :







carte[0][0]
carte[0][1]
carte[0][2]
carte[0][3]
carte[0][4]
carte[0][5]
carte[0][6]
carte[0][7]
carte[0][8]
carte[0][9]
carte[0][10]
carte[0][11]
carte[1][0]
carte[1][1]
carte[1][2]
carte[1][3]
carte[1][4]
carte[1][5]
carte[1][6]
carte[1][7]
carte[1][8]
carte[1][9]
carte[1][10]
...
carte[11][2]
carte[11][3]
carte[11][4]
carte[11][5]
carte[11][6]
carte[11][7]
carte[11][8]
carte[11][9]
carte[11][10]
carte[11][11]
إذا هو جدول من 12*12=144 خانة !
كلّ من هذه الخانات تمثّل خانة في خارطة اللعبة . الصورة التالية تعطيكم فكرة كيف قُمنا بتمثيل الخارطة  :


و بهذا فالخانة في أعلى اليسار مخزّنة في  carte[0][0] .
الخانة أعلى اليمين مخزنة في carte[0][11] .
الخانة أسفل اليمين ( آخر خانة ) مخزنة في carte[11][11] .
حسب قيمة الخانة ( و التي هي عدد طبيعي )، نعرف أي خانة من النافذة تحتوي جداراً، أو صندوقاً، أو منطقة مستهدفة ... الخ و هنا بالضبط سنستفيد من تعريف التعداد enumeration السابق!






  enum {VIDE, MUR, CAISSE, OBJECTIF, MARIO, CAISSE_OK};
إذا كانت قيمة الخانة تساوي "فارغ VIDE " أي "0" سنعرف بأن هذه المنطقة من الشاشة يجب أن تبقى بيضاء . إذا كانت تساوي " جدار MUR " أي "1"، فسنعرف أنه يجب أن نقوم بلصق صورة جدار .. الخ
تهيئات Initialisations :
تحميل المساحات :
و الآن، بما أننا قمنا بشرح كل متغيرات الدالة jouer ، يمكننا البدء في القيام ببعض التهيئات :






 // Chargement des sprites (décors, personnage...)
mur = IMG_Load("mur.jpg");
caisse = IMG_Load("caisse.jpg");
caisseOK = IMG_Load("caisse_ok.jpg");
objectif = IMG_Load("objectif.png");

mario[BAS] = IMG_Load("mario_bas.gif");
mario[GAUCHE] = IMG_Load("mario_gauche.gif");
mario[HAUT] = IMG_Load("mario_haut.gif");
mario[DROITE] = IMG_Load("mario_droite.gif");
لا يوجد شيء صعب : نقوم بتحميل الكلّ بواسطة IMG_Load .  إن كانت هناك حالة استثنائية، فهي تحميل ماريو. إذ أننا نقوم بتحميل ماريو في كلّ من الإتّجاهات الأربعة في الجدول mario باستعمال الثوابت : HAUT، BAS، GAUCHE، DROITE .
كوننا استعملنا هنا ثوابت فسيصبح الكود أكثر وضوحاً - كما يبدو - . كان بإمكاننا استعمال mario[0]، لكن من الأفضل و من الأكثر وضوحاً أن نستعمل mario[HAUT] مثلاً!







marioActuel = mario[BAS]; // Mario sera dirigé vers le bas au départ
وجدت أنه من المنطقي أكثر أن أبدأ المرحلة فيما يكون ماريو موجّها نحو الأسفل ( أي نحونا ) ، كان بامكانكم أن تكتبوا مثلاً :








marioActuel = mario[DROITE];
ستُلاحظون بأن ماريو سيكون موجّهاً نحو اليمين في بداية اللعبة .
تحميل الخارطة :
الآن، يجدر بنا ملئ الجدول ثنائي الأبعاد carte. لحدّ الآن، الجدول لا يحتوي إلا أصفاراً .
يجب أن نقرأ المستوى المخزّن في الملف niveaux.lvl :









// Chargement du niveau
if (!chargerNiveau(carte))
   exit(EXIT_FAILURE); // On arrête le jeu si on n'a pas pu charger le niveau
لقد اخترت التحكّم في تحميل chargement ( و تسجيل enregistrement ) المستويات بواسطة دوال متواجدة بالملف fichiers.c . هنا ، نستدعي إذا الدالة chargerNiveau . سنقوم بدراستها بالتفصيل لاحقا ( هي ليست معقدة كثيراً على أي حال ). كل ما يهمنا هنا هو معرفة أنه تم تحميل المستوى في الجدول carte . إذا لم يتم تحميل المستوى ( لأن ملف niveaux.lvl لا يوجد )، ستُرجع الدالة القيمة faux . في الحالة المعاكسة تُرجع vrai .
نقوم إذا باختبار نتيجة التحميل بواسطة شرط . إذا كانت النتيجة سلبية ( من هنا استعملت إشارة التعجّب لأعبّر عن ضدّ الشرط ) يتوقف كلّ شيء : سنستدعي الدالة exit . في الحالة المضادّة، كلّ شيء يعمل بشكل جيّد إذاً و يمكننا المواصلة .
نحن نتوفّر الآن على جدول carte  يصف محتوى كلّ خانة : جدار، فراغ، صندوق ...
البحث عن وضعية الإنطلاق لماريو :
يجب الآن أن نعطي قيمة ابتدائية للمتغيرة positionJoueur. 
هذه المتغيرة من نمط SDL_Rect خاصّة قليلاً. لن نستعين بها لتخزين الإحداثيات بالبيكسل و إنما بتخظينها بدلالة الـ"خانات" في الخارطة . و بهذا فإن كانت لدينا :
positionJoueur.x == 11 positionJoueur.y == 11
فهذا يعني أن اللاعب متواجد في آخر خانة في أسفل يمين الخارطة ... يمكنكم الرجوع إلى الصورة السابقة لتتوضح لكم الأمور أكثر .
سنقوم بالتقدّم داخل الجدول carte و ذلك باستعمال حلقتين، نستعمل المتغيرة i للتقدّم في الجدول عمودياً ، و نستعمل المتغيرة j للتقدّم فيه أفقياً :










// Recherche de la position de Mario au départ
for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
   {
       for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
           {
                if (carte[i][j] == MARIO) // Si Mario se trouve à cette position
               {
                   positionJoueur.x = i;
                   positionJoueur.y = j;
                   carte[i][j] = VIDE;
                }
           }
}
في كلّ خانة، نختبر ما إن كانت هذه الأخيرة تحتوي MARIO ( أي نقطة انطلاق اللاعب في الخارطة ) . إذا كانت كذلك، نقوم بتخزين الإحداثيات الحالية ( المتواجدة في i و j ) في المتغيرة positionJoueur .
نمسح أيضاً الخانة و ذلك بإعطائها القيمة VIDE لكي يتم اعتبارها كخانة فارغة لاحقاً .
تفعيل تكرار الضغط على الأزرار
آخر شيء، أمر سهل جداً : سنقوم بتفعيل تكرار الضغط على الأزرار لكي نستطيع التحرّك في الخارطة و ذلك بترك الزر مضغوطاً .











// Activation de la répétition des touches
SDL_EnableKeyRepeat(100, 100);
الحلقة الرئيسية :
حسناً، لقد قُمنا بتهيئة كلّ شيء، يمكننا الآن العمل على الحلقة الرئيسية . إنها حلقة كلاسيكية تعمل بنفس المخطط الذي تعمل به الحلقات التي رأيناها لحدّ الآن. هي فقط كبيرة قليلاً . فلنرى عن قرب الـswitch الذي يختبر الحدَث :











switch(event.type)
{
    case SDL_QUIT:
        continuer = 0;
        break;
    case SDL_KEYDOWN:
        switch(event.key.keysym.sym)
        {
            case SDLK_ESCAPE:
                continuer = 0;
                break;
            case SDLK_UP:
                marioActuel = mario[HAUT];
                deplacerJoueur(carte, &positionJoueur, HAUT);
                break;
            case SDLK_DOWN:
                marioActuel = mario[BAS];
                deplacerJoueur(carte, &positionJoueur, BAS);
                break;
            case SDLK_RIGHT:
                marioActuel = mario[DROITE];
                deplacerJoueur(carte, &positionJoueur, DROITE);
                break;
            case SDLK_LEFT:
                marioActuel = mario[GAUCHE];
                deplacerJoueur(carte, &positionJoueur, GAUCHE);
                break;
        }
        break;
}
إذا ضغطنا على الزر Echap، فستنتهي اللعبة و نرجع للقائمة الرئيسية .
كما ترون، لا توجد العديد من الأحداث لنتحكّم بها: سنختبر فقط ما إن ضغط اللاعب على الأزرار "أعلى"، "أسفل"،"يمين" أو "يسار"، على حسب الزر المضغوط نغيّر اتجاه ماريو. و هنا تتدخّل المتغيرة marioActuel ! إذا ضغطنا السهم الموجه نحو الأعلى إذاً:











marioActuel = mario[HAUT];
إذا ضغطنا على السهم الموجّه نحو الأسفل فإذاً :











marioActuel = mario[BAS];
marioActuel  يؤشّر إذا نحو المساحة التي تمثّل ماريو في الوضعية الحالية. و بهذا فإنه بلصق marioActuel قبل قليل، تأكّدنا بأننا قمنا بلصق ماريو في الإتجاه الصحيح .
الآن، شيء مهمّ جداً : نستدعي الدالة deplacerJoueur. هذه الدالة ستقوم بتحريك اللاعب في الخارطة إن كان له الحق في فعل ذلك .
  •  مثلاً، لا يمكننا أن نُحرك ماريو إلى الأعلى لو كان متواجداً أصلاً في الحافة العلوية للنافذة .
  • لا يمكننا أيضاً أن نحرّكه للاعلى لو كان فوقه جداراً .
  • لا يمكننا أن نحرّكه للأعلى لو كان فوقه صندوقين .
  • على العكس، يمكننا تحريكه للاعلى لو تواجد صندوق واحد فوقه .
  • لكن احذروا، لا يمكننا تحريكه للاعلى لو تواجد صندوق واحد فوقه و كان هذا الصندوق متواجد أصلاً في الحافة العلوية للنافذة!
يا إلاهي، ماهذا السوق؟
هذا ما نسمّيه التحّكم في الاصطدامات la gestion des collisions. و لكي أضمن لكم، نحن نقوم بالتعامل مع الاصطدامات البسيطة بما أن اللاعب يتحرّك خانة بخانة و في أربع اتجاهات فقط . في لعبة ثنائية الأبعاد أين يتحرّك اللاعب في كلّ الاتجاهات بيكسل ببيكسل، يكون التحكّم في الاصطدامات أمر صعب. لكن هناك ماهو أسوء : الألعاب ثلاثية الأبعاد . التحكم في الاصطدامات في لعبة ثلاثية الأبعاد يُعدّ كابوساً بالنسبة للمبرمجين. لحسن الحظ، توجد مكتبات للتحكّم في الاصطدامات في العوالم ثلاثية الأبعاد و التي تقوم بالكثير من العمل في مكاننا : لنرجع للدالة deplacerJoueur  و لنركّز . نقوم بإعطائها ثلاثة خاصيات :
  • الخارطة : لكي تستطيع قراءتها و أيضاً التعديل عليها إذا قمنا بتحريك صندوق مثلاُ .
  • وضعية اللاعب: هنا أيضاً، يجب على الدالة قراءة و " ربما " تعديل وضعية اللاعب .
  •  الإتجاه الذي نطلب من اللاعب التوجّه إليه : نستعمل هنا أيضاً الثوابت : HAUT، BAS، GAUCHE، DROITE من أجل فهم الكود بشكل أفضل .
سندرس الدالة deplacerJoueur لاحقاً. كان بإمكاني وضع كلّ الاختبارات داخل الـswitch، لكن سيصبح بذلك كبيراً و ستصعب علينا قراءته. و من هنا نستنتج الفائدة من تقسيم الكود إلى عدّة دوال .
لقد انتهينا من الـswitch : في هذه الوضعية من البرنامج، قد تكون الخارطة قد تغيّرت و كذا وضعية اللاعب. مهما كان، لقد حان وقت اللصق !
سنبدؤ بمسح الشاشة و ذلك بإعطائها لون خلفية أبيض .











// Effacement de l'écran
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
و الآن، نقوم بالتقدّم في الجدول ذو البعدين carte لكي نعرف أي عنصر سنقوم بلصقه و في أي منطقة من الشاشة . سنستعمل حلقتين كما رأينا سابقاً للتقدّم في الـ144 خانة من الجدول ( المصفوفة ).











// Placement des objets à l'écran
objectifsRestants = 0;
for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
    for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
    {
        position.x = i * TAILLE_BLOC;
        position.y = j * TAILLE_BLOC;
        switch(carte[i][j])
        {
            case MUR:
                SDL_BlitSurface(mur, NULL, ecran, &position);
                break;
            case CAISSE:
                SDL_BlitSurface(caisse, NULL, ecran, &position);
                break;
            case CAISSE_OK:
                SDL_BlitSurface(caisseOK, NULL, ecran, &position);
                break;
            case OBJECTIF:
                SDL_BlitSurface(objectif, NULL, ecran, &position);
                objectifsRestants = 1;
                break;
        }
    }
}
من اجل كل خانة، نحضّر المتغيرة position ( من نمط SDL_Rect ) لكي نضع العنصر الحالي في الوضعية المناسبة من الشاشة.
العملية بسيطة :











position.x = i * TAILLE_BLOC;
position.y = j * TAILLE_BLOC;
يكفي ضرب  i بـ TAILLE_BLOC لكي نعرف قيمة position.x. و بهذا فإن كنا نتواجد في الخانة الثالثة، أي أن i =2 ( لا تنسوا أن i يبدء من الصفر ! ). نقوم إذاً بالعملية 2*34=68 . إذا نقوم بلصق الصورة 68 بيكسل نحو اليمين في المساحة ecran .
نقوم بنفس الشيء بالنسبة للترتيبة y .
بعد ذلك، نطبّق switch على الخانة التي نقوم بتحليلها من الخارطة . هنا أيضاً، استعمال الثوابت يعتبر شيئاً عملياً و يسمح بقراءة مُثلى للكود. نختبر إذا ما إن كانت الخانة تساوي MUR، في هذه الحالة نقوم بلصق جدار . نفس الشيء بالنسبة للصناديق و المناطق المُستهدفة .
اختبار الربح :
 تلاحظون أن قبل استعمال الحلقتين المتداخلتين، نعطي القيمة الإبتدائية 0 للمتغيرة المنطقية objectifsRestants . هذه المتغيرة المنطقية تأخذ القيمة 1 ما إن نقوم بكشف منطقة مُستهدفة على الخارطة. حينما لا تتبقّى أية منطقة مستهدفة، أي أن كل الصناديق متواجدة فوق هذه المناطق ( لم تتبقّ سوى صناديق CAISSE_OK ). يكفي أن نختبر ما إن كانت المتغيرة المنطقية تحمل القيمة faux ، أي انه لم تتبقّ أية منطقة مستهدفة . في هذه الحالة، نُعطي القيمة 0 للمتغيرة continuer من أجل توقيف الجولة . 











// Si on n'a trouvé aucun objectif sur la carte, c'est qu'on a gagné
if (!objectifsRestants)
    continuer = 0;
اللاعب :
لم يتبقّ سوى لصق اللاعب :











// On place le joueur à la bonne position
position.x = positionJoueur.x * TAILLE_BLOC;
position.y = positionJoueur.y * TAILLE_BLOC;
SDL_BlitSurface(marioActuel, NULL, ecran, &position);
نحسُب وضعيته ( بالبيكسل هذه المرة ) و ذلك بالقيام بعملية ضرب بين positionJoueur و TAILLE_BLOC. بعد ذلك، نقوم بلصق اللاعب في الوضعية المناسبة.
الإظهار :
لقد قُمنا بكلّ شيء، يكفي أن نُظهر الشاشة للمستعمل :











SDL_Flip(ecran);
نهاية الدالة : تحرير
بعد الحلقة الرئيسية، يجدر بنا القيم بتحرير الذاكرة التي حجزناها للمُلصقات sprites التي حمّلناها على الشاشة .
نقوم بتعطيل تكرار الضغط على الأزرار و ذلك بإعطاء القيمة 0 للدالة SDL_EnableKeyRepeat :











// Désactivation de la répétition des touches (remise à 0)
SDL_EnableKeyRepeat(0, 0);
// Libération des surfaces chargées
SDL_FreeSurface(mur);
SDL_FreeSurface(caisse);
SDL_FreeSurface(caisseOK);
SDL_FreeSurface(objectif);
for (i = 0 ; i < 4 ; i++)
    SDL_FreeSurface(mario[i]);
الدالة deplacerJoueur :
هذه الدالة متواجدة أيضاً في الملف jeu.c .
هي دالة ... حساسة من ناحية كتابتها. و ربّما هي الدالة الأكثر صعوبة حينما نريد برمجة لعبة السوكوبان .
تذكير : الدالة deplacerJoueur تختبر ما إن كان لدينا الحق في تحريك اللاعب في الإتجاه المطلوب، تقوم بتحديث وضعية اللاعب positionJoueur و أيضاً بتحديث الخارطة إذا تم تحريك الصندوق. هذا نموذج الدالة :











void deplacerJoueur(int carte[][NB_BLOCS_HAUTEUR], SDL_Rect *pos,int direction);
هذا النموذج خاص قليلاً. تلاحظون أنني أبعث الجدول  carte و أحدد الحجم الخاص بالبُعد الثاني (NB_BLOCS_HAUTEUR).
لماذا هذا ؟
الإجابة معقّدة قليلاً لكي أقوم بتطويرها في هذا الدرس. لكي نبسّط الأمور، لغة الـC لا تتكهّن بأننا نتحدّث عن جدول ثنائي الأبعاد و أنه يجب أن نعطي على الأقل حجم البُعد الثاني لكي تشتغل الأمور .
 إذا، حينما تبعثون جدولاً ذو بُعدين إلى دالة، يجب ان تحددّوا حجم البُعد الثاني للجدول. هذا ماهو عليه الأمور في نموذج الدالة. هكذا تعمل الأمور، إن الأمر ضروري :
أمر آخر : تلاحظون أن positionJoueur تُسمى pos في هذه الدالة. لقد اخترت اختصار الاسم لكي تسهل كتابته بما أننا سنحتاج إلى كتابته عدّة مرات.
فلنبدأ باختبار الإتجاه الذي نريد التوجّه إليه و ذلك باستعمال switch ضخم :











switch(direction)
{
    case HAUT:
   
فلننطلق في رحلة من الاختبارات المجنونة !
يجب الآن أن نكتب الاختبارات الخاصة بكلّ حالة ممكنة و لنحاول ألا ننسى أية واحدة . هكذا تقوم الخطة التي اعتمدها : أختبر كل الحالات الممكنة للإصطدامات حالة بحالة، و ما إن أكشف عن إصطدام ( أي أن اللاعب غير متمكّن من التحرّك ) أضع الأمر break لاخرج من الـswitch، و بهذا أمنع التحرّك .
هذا مثال عن كلّ حالات الإصطدام المتواجدة للاعب يريد التحرّك نحو الأعلى :
  • اللاعب متواجد أصلاُ في أقصى علوّ الخارطة.
  •  يوجد جدار فوق اللاعب .
  • يوجد صندوقين معاً فوق اللاعب، و كونه غير قادر على دفع صندوقين ( تذكّروا ) لا يمكنه التحرك .
  • يوجد صندوق فوق اللاعب و الصندوق متواجد في الحافة العلوية للخارطة .
إذا لم يوجد أي مشكل من هذا النوع، يمكننا تحريك اللاعب. سأريكم الاختبارات اللازمة من أجل التحرّك نحو الأعلى. من أجل الحالات الأخرى، يكفي تعديل الكود قليلا.











if (pos->y - 1 < 0) // Si le joueur dépasse l'écran, on arrête
    break;
نبدأ بالتحقق ما إن كان اللاعب متواجداً اعلى النافذة. بالفعل، لو نحاول أن نستدعي الخانة carte[5][-1] مثلاً، سيتوقف البرنامج بشكل خاطئ  !
نبدأ إذا بالتأكد من أننا لن نقوم بتعطيل سير البرنامج . ثم :











if (carte[pos->x][pos->y - 1] == MUR) // S'il y a un mur, on arrête
    break;
هنا أيضاً، الأمر بسيط. نتحقق من عدم وجود جدار فوق اللاعب. إذا كان هناك واحد، نتوقّف (break) . بعد ذلك :











// Si on veut pousser une caisse, il faut vérifier qu'il n'y a pas de mur derrière (ou une autre caisse, ou la limite du monde)
if ((carte[pos->x][pos->y - 1] == CAISSE || carte[pos->x][pos->y -1] == CAISSE_OK) &&
    (pos->y - 2 < 0 || carte[pos->x][pos->y - 2] == MUR || carte[pos->x][pos->y - 2] == CAISSE || carte[pos->x][pos->y - 2] == CAISSE_OK))
    break;
هذا الإختبار الضخم يمكن ترجمته كالتالي :""" إذا كان هناك صندوق فوق اللاعب ( أو صندوق caisse_ok، أي أن الصندوق في الوضعية المناسبة ) و إذا كان فوق هذا الصندوق يوجد إما الفراغ ( سيتعطل البرنامج لأننا في أقصى الأعلى )، إما صندوقاً آخر، إما صندوق caisse_ok : إذاً لا يمكننا التحرّك : خروج break """ .
إذا تمكنّا من عبور هذا الإختبار فنحن قادرون على التحرّك، أوووف !
نستدعي اولاً دالة تقوم بتحريك الصندوق إن كنا بحاجة إلى ذلك :











// Si on arrive là, c'est qu'on peut déplacer le joueur !
// On vérifie d'abord s'il y a une caisse à déplacer
deplacerCaisse(&carte[pos->x][pos->y - 1], &carte[pos->x][pos->y -2]);
تحريك الصناديق :
قررت التحكم في تحرّك الصناديق باستعمال دالة أخرى لأنه يبقى نسف الكود من أجل الإتجاهات الأربعة. يجب فقط أن نتأكّد بأننا قادرون على التحرّك ( هذا ما كنتُ بصدد شرحه ) . سنبعث للدالة خاصيتين : محتوى الخانة التي نريد الذهاب إليها و محتوى الخانة التي تليها .











void deplacerCaisse(int *premiereCase, int *secondeCase)
{
    if (*premiereCase == CAISSE || *premiereCase == CAISSE_OK)
    {
        if (*secondeCase == OBJECTIF)
            *secondeCase = CAISSE_OK;
        else
            *secondeCase = CAISSE;
        if (*premiereCase == CAISSE_OK)
            *premiereCase = OBJECTIF;
        else
            *premiereCase = VIDE;
    }
}
هذه الدالة تقوم بتحديث الخارطة و هي تأخذ كخاصية مؤشّرات نحو الخانات المعنيّةن سأترككم لتقرؤوها، فهي سهلة للفهم. لا يجب أن ننسى أننا إذا حرّكنا  صندوق CAISSE_OK، يجب تعويض المكان الذي كان به بمنطقة مُستهدفة. و إلا، إذا كان صندوق عادي، سنعوّض مكانه بالـ"فراغ" .
تحريك اللاعب :
نعود للدالة deplacerJoueur .
نحن هنا في الحالة الصحيحة، سنقوم بتحريك اللاعب . كيف نفعل ذلك؟ هذا أمر سهل .











pos->y--; // On peut enfin faire monter le joueur (oufff !)
يكفي أن ننقِص من الترتيبة لأن اللاعب يريد الصعود للأعلى .
تلخيص :
كملخّص، هاهي كلّ الاختبارات اللازمة من أجل الصعود إلى الأعلى :











switch(direction)
{
    case HAUT:
        if (pos->y - 1 < 0) // Si le joueur dépasse l'écran, on arrête
            break;
        if (carte[pos->x][pos->y - 1] == MUR) // S'il y a un mur, on arrête
            break;
        // Si on veut pousser une caisse, il faut vérifier qu'il n'y a pas de mur derrière (ou une autre caisse, ou la limite du monde)
        if ((carte[pos->x][pos->y - 1] == CAISSE || carte[pos->x][pos->y - 1] == CAISSE_OK) &&
            (pos->y - 2 < 0 || carte[pos->x][pos->y - 2] == MUR || carte[pos->x][pos->y - 2] == CAISSE || carte[pos->x][pos->y - 2] == CAISSE_OK))
            break;
      
        // Si on arrive là, c'est qu'on peut déplacer le joueur !
        // On vérifie d'abord s'il y a une caisse à déplacer
        deplacerCaisse(&carte[pos->x][pos->y - 1], &carte[pos->x][pos->y - 2]);
        pos->y--; // On peut enfin faire monter le joueur (oufff !)
        break;
سأترك لكم عناء نقل الكود و تعديله من أجل الحالات الأخرى .
ها قد انتهينا من كتابة كود اللعبة !
حسناً، قريباً : بقي لنا أن نرى دالة التحميل و حِفظ المستويات .
سنرى بعد ذلك كيف نقوم بكتابة كود مٌنشئ الخرائط !
تحميل و حِفظ المستويات :
الملف fichiers.c يحتوي دالتين :
  • chargerNiveau .
  • sauvegarderNiveau. 
فلنبدأ بتحميل المستوى .
تحميل المستوى chargerNiveau  :
هذه الدالة تأخذ خاصية : الخارطة .هنا أيضاً، يجب تحديد حجم البُعد الثاني للجدول لأننا نتكلم عن جدول ذو بعدين (مصفوفة).
الدالة تُرجع متغيرة منطقية : "صحيح" إذا تم التحميل بنجاح، "خطأ" إذا حدث هناك خطأ . النموذج إذا هو :











int chargerNiveau(int niveau[][NB_BLOCS_HAUTEUR]);
فلنرى بداية الدالة :











FILE* fichier = NULL;
char ligneFichier[NB_BLOCS_LARGEUR * NB_BLOCS_HAUTEUR + 1] = {0};
int i = 0, j = 0;
fichier = fopen("niveaux.lvl", "r");
if (fichier == NULL)
    return 0;
نقوم بإنشاء جدول للخزين المؤقّت للنتيجة الخاصة بتحميل المستوى . نفتح الملف بأسلوب "قراءة فقط" أي ("r"). نوقف الدالة و ذلك بإرجاع القيمة 0 " خطأ " إذا فشلت عملية فتح الملف . عملية كلاسيكية .
الملف niveaux.lvl يحتوي على سطر و الذي هو عبارة عن تتالي أرقام . كل رقم يمثّل خانة من المستوى، مثلا :











11111001111111111400000111110001100103310101101100000200121110 [...]
سنقرؤ إذا هذا السطر باستعمال fgets :











fgets(ligneFichier, NB_BLOCS_LARGEUR * NB_BLOCS_HAUTEUR + 1, fichier);
سنقوم بتحليل محتوى ligneFichier، نحن نعرف أن أول 12 حرف تمثل السطر الأول، الـ12 حرف الموالية تمثل السطر الموالي ... إلى آخره :











for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
    for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
    {
        switch (ligneFichier[(i * NB_BLOCS_LARGEUR) + j])
        {
            case '0':
                niveau[j][i] = 0;
                break;
            case '1':
                niveau[j][i] = 1;
                break;
            case '2':
                niveau[j][i] = 2;
                break;
            case '3':
                niveau[j][i] = 3;
                break;
            case '4':
                niveau[j][i] = 4;
                break;
        }
    }
}
 بواسطة عملية حسابية بسيطة، نأخذ الحرف الذي يهمّنا في ligneFichier و نحلل قيمته. إنها "أحرف" مخزّنة في الملف . ما أريد أن أقوله بهذا هو أن '0' مخزّن كحرف '0' أسكي ASCII و أن قيمته ليست الصِفر .
لنحلل الملف، يجب الاختبار بالحالة  '0' و ليس الحالة 0  ! احذروا من الخلط بين الأحرف و الأرقام !
يقوم الـswitch بالتحويل : '0' => 0 ، '1' => 1 ... الخ . يقوم بوضع الكلّ في الجدول carte . الخارطة تسمّى niveau في هذه الدالة لكن هذا لا يغيّر أي شيء.
ما إن يتم هذا، يمكننا غلق الملف بإرجاع القيمة 1 لنقول أن كلّ شيء تمّ على مايُرام . 











fclose(fichier);
return 1;
أخيراً، تحميل المستوى في الملف لم يكن معقداً . الفخّ الوحيد الذي وُجب تجنّبه هو التفكير في تحويل القيمة أسكي '0' إلى الرقم 0 ( نفس الشيء بالنسبة لـ1، 2، 3، 4 )
 حِفظ المستوى sauvegarderNiveau :
هذه الدالة أسهل :











int sauvegarderNiveau(int niveau[][NB_BLOCS_HAUTEUR])
{
    FILE* fichier = NULL;
    int i = 0, j = 0;
    fichier = fopen("niveaux.lvl", "w");
    if (fichier == NULL)
        return 0;
    for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
    {
        for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
        {
            fprintf(fichier, "%d", niveau[j][i]);
        }
    }
    fclose(fichier);
    return 1;
}
استعمل الدالة fprintf من أجل " ترجمة " أرقام الجدول إلى حروف أسكي ASCII. كانت هنا الصعوبة الوحيدة ( ل ايجب كتابة 0 و إنما '0' ) .
مُنـشئ المستويات :
هذا الأخير سهلة كتابته مما انتم تتخيلون .. بالمناسبة فهي تقنية تسمح بالرفع من عمر لعبتنا، فلما نتجاهلها ؟
هكذا تسري الأمور :
  •  نستعمل الفأرة لوضع القوالب التي نريدها في النافذة .
  • النقر باليمين يسمح بمسح القالب الذي تتواجد فوقه الفأرة  .
  • النقر باليسار يسمح بوضع الشيء على الخارطة . خذا الشيء يكون مخزّناً : بشكل أوّلي، نقوم بوضع الجدران بالنقر بيسار الفأرة. يمكننا تغيير الشيء اليذ نريد وضعه في الخارطة و ذلك بالضغط على الأزرار المتواجدة في لوحة الأرقام :
    1. جدار .
    2. صندوق .
    3. منطقة مُستهدفة.
    4. مكان انطلاق ماريو .
  •  بالضغط على S يتم حفظ المستوى .
  • يمكننا الرجوع إلى القائمة الرئيسية بالضغط على Echap ( أو Escape بالإنجليزي )
  
Édition d'un niveau avec l'éditeur

التهيئات Initialisations :
 بشكل عام، تشبه هذه الدالة، الدالة الخاصة باللعب. ولذلك فقط بدأت في كتابتها باستعمال "نسخ-لصق" لدالة اللعب، و بعد ذلك قمتُ بنزع ما لا أحتاجُه و أضفت خواص جديدة. هذه كانت البداية :











void editeur(SDL_Surface* ecran)
{
    SDL_Surface *mur = NULL, *caisse = NULL, *objectif = NULL,*mario = NULL;
    SDL_Rect position;
    SDL_Event event;
  
    int continuer = 1, clicGaucheEnCours = 0, clicDroitEnCours = 0;
    int objetActuel = MUR, i = 0, j = 0;
    int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};
  
    // Chargement des objets et du niveau
    mur = IMG_Load("mur.jpg");
    caisse = IMG_Load("caisse.jpg");
    objectif = IMG_Load("objectif.png");
    mario = IMG_Load("mario_bas.gif");
  
    if (!chargerNiveau(carte))
        exit(EXIT_FAILURE);
هنا تجدون تعريف المتغيرات و التهيئات اللازمة .
تلاحظون أنني لا أقوم بتحميل إلا ماريو ( المتّجه نحو الأسفل ). في الواقع، لن نقوم بتوجيه ماريو للأسفل و إنما نحتاج إلى ملصق يمثّل وضعية الإنطلاق الخاصة به.
المتغيرة objetActuel تحفظ الشيء الذي يختاره حالياً المُستعمل. بشكل مبدئي، هذا الشيء هو "جدار"، أي أننا في البداية إذا نقرنا باليسار سنقوم بوضع جدار، و يمكن تغيير هذا بواسطة المستعمل و ذلك بالضغط على 1،2،3 أو 4.
المتغيرات المنطقية clicGaucheEnCours و clicDroitEnCours كما تشير أسماؤها، تسمح بحفظ ما إن كان هناك نقر ياليمين حالياً ( أي أن زر الفأرة مضغوط ). سأشرح لكم المبدأ لاحقاً ... على أي حال، هذه التقنية تسمح لنا بإضافة أشياء إلى الخارطة بترك زر الفأرة مضغوطاً، و إلا فسنكون مجبرين على الضغط على الزر عدة مرات من أجل وضع نفس الشيء عدّة مرات في الخارطة، و هذا أمر مُتعب.
أخيراً، يتم تحميل الخارطة المحفوطة حالياً في الملف niveaux.lvl .
التحكّم في الأحداث :
هذه المرة سيكون علينا التحكم في كثير من الأحداث المختلفة. هيا بنأ، واحداً واحداً .

SDL_QUIT :











case SDL_QUIT:
    continuer = 0;
    break;
إذا ضغطنا على الزر X، تتوقف الحلقة و نعود إلى القائمة الرئيسية. و ليكن في علمكم أن هذا الشيء يغر مريح بالنسبة للاعب، فهو يريد الخروج من اللعبة و ليس الرجوع إلى القائمة الرئيسية. يجب أن نجد حلاً لتوقيف البرنامج و ذلك بإرجاع قيمة خاصّة للدالة الرئيسية مثلاُ. سأترككم لتجدوا حلاً بأنفسكم؟
SDL_MOUSEBUTTONDOWN :











case SDL_MOUSEBUTTONDOWN:
    if (event.button.button == SDL_BUTTON_LEFT)
    {
        // On met l'objet actuellement choisi (mur, caisse...) à l'endroit du clic
        carte[event.button.x / TAILLE_BLOC][event.button.y / TAILLE_BLOC] = objetActuel;
        clicGaucheEnCours = 1; // On retient qu'un bouton est enfoncé
    }
    else if (event.button.button == SDL_BUTTON_RIGHT) // Clic droit pour effacer
    {
        carte[event.button.x / TAILLE_BLOC][event.button.y /TAILLE_BLOC] = VIDE;
        clicDroitEnCours = 1;
    }
    break;
نبدؤ باختبار الزر المضغوط ( نرى ما إن كان ضغط يساري أو يميني ) .
  • إذا كان ضغط يساري، نقوم بوضع الشيء الحالي objetActuel عل ىالخارطة في الموضع الذي تشير إليه الفأرة.
  • إذا كان ضغط يميني، نمسح مايوجد في الموضع الحالي للفأرة ( نضع VIDE كما سبق و قلتُ لكم )
كيف نعرف في أي " خانة " من الخارطة نحن متواجدون؟
نعرف ذلك عن طريق عملية حسابية صغيرة. يكفي أن نأخذ إحداثيات الفأرة ( event.button.x مثلاُ ) و نقسم هذه القيمة على حجم كتلة TAILLE_BLOC.
هذه قسمة لأعداد طبيعية. و بما أن قسمة الأعداد الطبيعية في لغة السي تُعطي عدداً طبيعياً فنتحصّل بالتأكيد على قيمة توافق خانة من الخارطة.
مثلاً، لأو أنني في البيكسل الـ75 من الخارطة ( على محور الفواصل x )، أقسم هذا العدد على TAILLE_BLOC و التي تساوي هنا 34 فيكون لدينا : 75/34=2 . لا تنسوا هنا أننا نتجاهل باقي القسمة و نقوم بحفظ الجزء الطبيعي فقط لأننا نتكلم عن قسمة أعداد طبيعية.
نحن نعلم إذا أننا نتواجد في الخانة رقم 2 ( أي الخانة الثالثة لأن الجدول يبدؤ من الصفر، لا تنسوا ذلك ) . مثال آخر : لو أنني في البيكسل العاشر ( أي أنني قريبة من الحافة )، ستكون لدينا العملية الحسابية التالية :10/34=0 أي أننا في الخانة رقم 0  !
بفضل هذه العملية الحسابية البسيطة يمكننا أن نعرف في أي خانة من الخارطة نحن متواجدون .












carte[event.button.x / TAILLE_BLOC][event.button.y / TAILLE_BLOC] = objetActuel;
شيء آخر مهم : إعطاء القيمة 1 للمتغيرة المنطقية clicGaucheEnCours  ( أو clicDroit حسب الحالة ) يسمح لنا بمعرفة، خلال حدث MOUSEMOTION، ما إن كان زر الفأرة مضغوطاً خلال الإنتقال.
 SDL_MOUSEBUTTONUP :











case SDL_MOUSEBUTTONUP: // On désactive le booléen qui disait qu'un bouton était enfoncé
    if (event.button.button == SDL_BUTTON_LEFT)
        clicGaucheEnCours = 0;
    else if (event.button.button == SDL_BUTTON_RIGHT)
        clicDroitEnCours = 0;
    break;
الحدث MOUSEBUTTONUP يقوم ببساطة بإعطاء القيمة 0 للمتغيرة المنطقية. نحن نعرف بأن النقر انتهى و بهذا لا يوجد أي " نقر حالي " بالفأرة .
 SDL_MOUSEMOTION :











case SDL_MOUSEMOTION:
    if (clicGaucheEnCours) // Si on déplace la souris et que le bouton gauche de la souris est enfoncé
    {
        carte[event.motion.x / TAILLE_BLOC][event.motion.y /TAILLE_BLOC] = objetActuel;
    }
    else if (clicDroitEnCours) // Pareil pour le bouton droit de la souris
    {
        carte[event.motion.x / TAILLE_BLOC][event.motion.y / TAILLE_BLOC] = VIDE;
    }
    break;
هنا يمكن لنا رؤية أهمية المتغيرات المنطقية. نختبر حينما نقوم بحريك الفأرة ما إن كان هناك نقر حالي . إذا كانت هذه هي الحالة، نضع على الخارطة شيئاً ما ( أو الفراغ إذا كان نقر باليمين ) .
هذا يسمحُ لنا بوضع شيء واحد لعدة مرات دون الحاجة إلى إلى النقر في كلّ مرة من أجل كلّ تكرار للشيء، يكفي إذاً أن نُبقي زر الفأرة مضغوطاً بينما نسحبُ هذه الأخيرة. 
 الأمر واضح : في كلّ مرة نحرّك فيها الفأرة ( يكون ذلك ببيكسل واحد )، نختبر ما إن كانت المتغيرات المنطقية مفعّلة. إذا كان الأمر كذلك، نقوم بوضع شيء على الخارطة. و إلاً، لا نقوم بأي شيء.
ملخّص : سألخّص التقنية لأنها ستكون مفيدة من أجل برامج أخرى .
تسمح هذه التقنية بمعرفة ما إن كان زر الفأرة مضغوطاً بينما يتم تحريك هذه الأخيرة. يمكننا أن نستفيد من هذا الأمر لبرمجة " تحريك و زلق glisser-déplacer ".
  1. خلال حدث MOUSEBUTTONDOWN : نعطي القيمة 1 للمتغيرة المنطقية clicEnCours .
  2. خلال حدث MOUSEMOTION : نختبر ما إن كانت المتغيرة المنطقية clicEnCours تساوي " صحيح " . إذا كان الأمر كذلك فسنعرف أننا نقوم بما نسميه تحريك و زلق glisser-déplacer ".
  3.  خلال حدث MOUSEBUTTONUP : نعيد القيمة 0 للمتغيرة المنطقية clicEnCours لأن النقر قد انتهى ( تحرير زر الفأرة )
SDL_KEYDOWN :
تسمح أزرار لوحة المفاتيح بتحميل و حفظ المستوى و أيضاً بتغيير الحالة الحالية للشيء المُختار من أجل النقر اليساري بالفأرة .











case SDL_KEYDOWN:
    switch(event.key.keysym.sym)
    {
        case SDLK_ESCAPE:
            continuer = 0;
            break;
        case SDLK_s:
            sauvegarderNiveau(carte);
            break;
        case SDLK_c:
            chargerNiveau(carte);
            break;
        case SDLK_KP1:
            objetActuel = MUR;
            break;
        case SDLK_KP2:
            objetActuel = CAISSE;
            break;
        case SDLK_KP3:
            objetActuel = OBJECTIF;
            break;
        case SDLK_KP4:
            objetActuel = MARIO;
            break;
    }
    break;
هذا الكود سهلٌ للغاية. نقوم بتغيير الشيء إذا تم الضغط على الأرقام في اللوحة ، نقوم بحفظ المستوى إذا تم الضغط على S و نقوم بتحميل آخر مستوى تم حفظه بالنقر على ِC .
حان وقت اللصق :
ها نحن ذا : لقد أتتممنا كلّ الأحداث .
الآن، لم يتبقّ لنا سوى لصق كل عناصر الخارطة بمساعدة حلقتين متداخلتين، الكود التالي يشبه الكود الذي استعملناه في دالة اللعب و لهذا فلن أعيد شرحه هنا :










// Effacement de l'écran
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
// Placement des objets à l'écran
for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
    for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
    {
        position.x = i * TAILLE_BLOC;
        position.y = j * TAILLE_BLOC;
        switch(carte[i][j])
        {
            case MUR:
                SDL_BlitSurface(mur, NULL, ecran, &position);
                break;
            case CAISSE:
                SDL_BlitSurface(caisse, NULL, ecran, &position);
                break;
            case OBJECTIF:
                SDL_BlitSurface(objectif, NULL, ecran, &position);
                break;
            case MARIO:
                SDL_BlitSurface(mario, NULL, ecran, &position);
                break;
        }
    }
}
// Mise à jour de l'écran
SDL_Flip(ecran);
لا يجب أن ننسى بعد الانتهاء من الحلقة الرئيسية أن نحرر الذاكرة بالشكل اللازم ( باستعمال  SDL_FreeSurface ) :










SDL_FreeSurface(mur);
SDL_FreeSurface(caisse);
SDL_FreeSurface(objectif);
SDL_FreeSurface(mario);
حسناً، إنتهينا من التنظيف .
ملخّص و تحسينات :
حسناً لقد انتهينا من كلّ شيء و حان وقت التلخيص !
هل تشاركونني الرأي بأن أحسن تلخيص للدرس هو الكود المصدري الكامل للعبة مع التعلقيات المفصّلة ؟
بسبب عدم رغبتي في إعادة إعطاءكم الكود الكامل هنا، أفضّل أن تقوموا بتحميله و بتحميل الملف المصدري ( التنفيذي ) المٌترجم للويندوز .
الملف zip. يحتوي :
  • الملف التنفيذي للويندوز ( إذا كنتم تعملون بنظام تشغيل آخر، تكفي إعادة الترجمة ) .
  • الملفات DLL الخاصة بالـSDL و الـSDL_Image .
  • كل الصور التي تحتاجونها في البرنامج ( هي نفسها التي قمتم بتحميلها في الحزمة sprites أعلاه )
  • الملفات المصدري الكاملة الخاصة بالبرنامج .
  • الملف cbp. الخاص بمشروع Code::Blocks. إذا أردتم فتح المشروع باستعمال بيئة تطويرية أخرى، قوموا بإنشاء مشروع SDL، أضيفوا إليه يدوياً كل الملفات h. و c. . ليس الأمر صعباً، سترون .
تلاحظون أن المشروع يحتوي بالإضافة إلى الملفات  h. و c.، ملف مصدري ressources.rc. إنه ملف تمكن إضافته للمشروع ( فقط على الويندوز ) و يسمح بإدخال الملفات في الملف التنفيذي . هنا، استعنت به لإدخال أيقونة إلى الملف التنفيذي . و هذا يسمح بإعطاء أيقونة للملف التنفيذي مرئية في الويندوز، أنظروا الصورة التالية :
Intégration d'une icône à l'exécutable

إعترفوا بذلك، صنع أيقونة من أجل البرنامج أمر رائع للغاية، يمكنكم قراءة المزيد عن هذا : إضغط هنا .
حسّنوا اللعبة !
ألا ترون بأن هذا البرنامج غير مثالي و بعيد من أي كون كذلك ؟
هل تريدون أفكاراً للتطوير؟
  • ينقص دليل استعمال، حيث يتم إظهار شاشة قبل انطلاق المرحلة و قبل انطلاق مـُنشئ المستويات. نقوم بشرح الأزرار اللازمة لكي يستعملها اللاعب.
  • في مـُنشئ المستويات، اللاعب لا يعرف أن شيء هو مُختار حالياً و الذي يجدر به أن يتتبع مؤشّر الفأرة. الأمر سهل للتطبيق، هل تتذكرون أننا في الدرس السابق قمنا بجعل Zozor يتبع الفأرة؟
  • يمكن أن نبدأ مستوى ما بوجود بعض الصناديق المتواضعة أساساً فوق المناطق المستهدفة CAISSE_OK . هذا لا يعني أن المستوى سهل، فقد يكون عليكم تحريك الصندوق من مكانه في مرحلة من مراحل الجولة .
  • في مـُنشئ المستويات أيضاً، يجب أن نمنع المستعمل من أن يضع موضِعي إنطلاق لماريو في نفس الخارطة !
  • حينما ننجح في مُستوى، نرجع مباشرة إلى القائمة الرئيسية. هذا أمر فضّ نوعاً ما، ما رأيكم بإظهار رسالة بوسط الشاشة : " هنيئاً، لقد نجحت في المستوى ".
  • أخيراً، سيكون من الجيد أن يتمكن البرنامج من التحكم في عدة مستويات في المرة الواحدة، إذ أنه سيكون علينا بناء رحلة لعب تستمر لـ20 مستوى مثلاُ، سيكون الأمر أصعب قليلاً من ناحية البرمجة، لكن يمكن القيام به. يجب عليكم التعديل في كود اللعبة و أيضاً في كود مـُنشئ المستويات. أنصحكم بأن تضعوا مستوى واحداً في السطر الواحد بالملف niveaux.lvl.
كما وعدتكم  ! لن أعطيكم الكود المصدري الخاص بهذه التحسينات، و لكنّي سأعطيكم مباشرة الملف التنفيذي مُترجماً للويندوز و اللينكس .
اللعبة تحتوي 20 مستوى تختلف صعوبتها من ( سهل جداً إلى ... شديد الصعوبة ). لكي أتمكّن من تحقيق بعض المستويات، احتجت لزيارة موقع شخص مهووس بلعبة السوكوبان sokoban.online.fr .
هاهي اللعبة المحسّنة للويندوز و اللينكس :
أو

  • Blogger Comments
  • Facebook Comments

0 التعليقات:

إرسال تعليق

Item Reviewed: عمل تطبيقي Mario Sokoban Rating: 5 Reviewed By: Mahmoud H.Dahab