Créons un jeu de plateformes de A à Z !


Tutoriel présenté par : Jérémie F. Bellanger
Dernière mise à jour : 11 novembre 2010
Difficulté :




7. Créons un level editor pour éditer nos maps


    Ce chapitre va être sans doute un peu plus long que le précédent, car il va nous falloir gérer et rajouter pas mal de nouveaux éléments pour créer un level editor fonctionnel. Cependant, rassurez-vous, ce sera globalement très simple à comprendre (ce sera plus compliqué plus tard quand nous aborderons les collisions, la gestion des monstres, etc.) 



Résultat à la fin de ce chapitre : un level editor basique  !



A. Préparons un nouveau projet pour notre level editor

    Pour notre level editor, nous allons réutiliser le moteur que nous avons créé jusqu'à présent, mais dans un autre projet code::blocks car nous voulons garder notre moteur tel quel pour notre futur jeu. Nous allons donc en faire une copie  !

    Dans l'explorateur de fichiers, faites donc un copier-coller de votre dossier Aron et renommez-le :  Level editor. (Vous pouvez aussi regrouper vos deux dossiers dans un même dossier nommé Aron project pour les retrouver plus facilement 
).

    Sélectionnez ensuite le fichier Aron.cbp et renommez-le en : Aron Editor (vous pouvez aussi supprimer les fichiers Aron.depend, Aron.layout et Aron.exe dans les dossiers bin/debug et release car ils ne seront plus utiles).

    Démarrez le fichier Aron Editor.cbp et faites un clic droit sur Aron puis choisissez Properties. Là, renommez Aron en Aron Editor. Choisissez ensuite l'onglet Build Targets et changez le Output filename (Debug et release) de Aron.exe vers Editor.exe. Faites OK. Compilez à nouveau et votre projet devrait avoir son nouveau nom et compiler le fichier Editor.exe, que l'on pourra facilement différencier du jeu si on les met dans le même dossier plus tard
.


B. Avant de nous lancer : de quoi va-t-on avoir besoin ?

    Avant de nous lancer dans la programmation, il est utile - voire indispensable - de savoir ce que l'on veut faire !
    On veut éditer nos maps. Pour cela, on peut déjà les charger, mais il va aussi falloir les sauvegarder (logique, non  ?). On va donc devoir implémenter une fonction saveMap().

    Pour modifier nos maps, il va nous falloir la souris : on devra donc la réactiver et la gérer.
    Enfin, il faudra qu'on puisse sélectionner quelle tile coller donc pouvoir en changer facilement et des fonctions comme le copier/coller seraient les bienvenues aussi
. On va donc devoir modifier nos fonctions getInput() et update().

    Voilà pour l'essentiel, le reste se fera le moment venu.


C. La programmation

    Commençons par le plus simple : rétablir le curseur de la souris : il suffit de retourner dans la fonction init() et d'enlever la ligne suivante :

Nom du fichier : init.c


     /* Cache le curseur de la souris : ligne à effacer */

    SDL_ShowCursor(SDL_DISABLE);

 

    Changeons maintenant le nom de notre fenêtre : dans la fonction main(), il suffit de changer la ligne suivante :

Nom du fichier : main.c


    /* Initialisation de la SDL dans une fonction séparée (voir après) */

        init("Level editor");


    Si vous compilez maintenant, vous verrez que le curseur de la souris est maintenant visible et que notre fenêtre a changé de nom .

    Il est maintenant temps de modifier nos fonctions getInput() et update() pour gérer les nouvelles entrées souris/clavier.
    Commençons par la fonction getInput() :


Nom du fichier : input.c


  void getInput(void)
 {
    SDL_Event event;

    /* Keymapping : gère les appuis sur les touches et les enregistre
    dans la structure input */

    while (SDL_PollEvent(&event))
    {
        switch (event.type)
        {

            case SDL_QUIT:
                exit(0);
           
break;

            case SDL_KEYDOWN:
                switch (event.key.keysym.sym)
                {
                    
case SDLK_ESCAPE:
                        exit(0);
                   
break;

                    case SDLK_LEFT:
                        input.left = 1;
                   
break;

                    case SDLK_RIGHT:
                        input.right = 1;
                   
break;

                    case SDLK_UP:
                        input.up = 1;
                   
break;

                    case SDLK_DOWN:
                        input.down = 1;
                   
break;

                    /* La touche S sauvegardera */
                    
case SDLK_s:
                        input.save = 1;
                   
break;

                    /* La touche L chargera la map */
                    
case SDLK_l:
                        input.load = 1;
                   
break;

                    /* La touche DEL/Suppr réinitialisera la map */
                    
case SDLK_DELETE:
                        input.reinit = 1;
                   
break;

                    default:
                   
break;
                }
           
break;

            case SDL_KEYUP:
                switch (event.key.keysym.sym)
                {
                    
case SDLK_LEFT:
                        input.left = 0;
                   
break;

                    case SDLK_RIGHT:
                        input.right = 0;
                   
break;

                    case SDLK_UP:
                        input.up = 0;
                   
break;

                    case SDLK_DOWN:
                        input.down = 0;
                   
break;

                    default:
                   
break;
                }
           
break;

            case SDL_MOUSEBUTTONDOWN:
                switch(event.button.button)
                {
                    /* Le clic gauche de la souris ajoutera la tile en cours */
                    
case SDL_BUTTON_LEFT:
                        input.add = 1;
                   
break;

                    /* Le clic central de la souris effacera la tile sélectionnée */
                    
case SDL_BUTTON_MIDDLE:
                        input.remove = 1;
                   
break;

                    /* Le clic droit de la souris copiera la tile sélectionnée */
                    
case SDL_BUTTON_RIGHT:
                        input.copy = 1;
                   
break;

                    /* La roue de la souris fera défiler les tiles */
                    
case SDL_BUTTON_WHEELUP:
                        input.next = 1;
                   
break;

                    case SDL_BUTTON_WHEELDOWN:
                        input.previous = 1;
                   
break;


                    default:
                   
break;
                }
           
break;

            case SDL_MOUSEBUTTONUP:
                switch(event.button.button)
                {
                    
case SDL_BUTTON_LEFT:
                        input.add = 0;
                   
break;

                    case SDL_BUTTON_MIDDLE:
                        input.remove = 0;
                   
break;

                    default:
                    
break;
                }
            break;
        }
    }

    /* Enregistre les coordonnées de la souris */

    SDL_GetMouseState(&input.mouseX, &input.mouseY);

    /* Cette série d'opérations permet d'obtenir les coordonnées exactes de chaque
    tile, tandis que la souris réagit au pixel près */

    input.mouseX /= TILE_SIZE;
    input.mouseY /= TILE_SIZE;

    input.mouseX *= TILE_SIZE;
    input.mouseY *= TILE_SIZE;
}


    Comme vous pouvez le voir, on a changé la détection de certaines touches et on a rajouté la gestion de la souris. Rien de bien extraordinaire ici, c'est plutôt facile à comprendre. Dans notre level editor, les touches suivantes auront les actions suivantes :

Touches fléchées
L
S
DEL / Suppr
Clic droit
Clic gauche
Clic du milieu
Molette / roue de la souris
scrollent la map
Load (charge la map)
Save (sauvegarde la map)
Réinitialise la map
Copie la tile pointée
Colle la tile sélectionnée sur la tile pointée
Efface la tile pointée
Fait défiler les tiles
 
    Vous aurez aussi remarqué qu'on récupère les coordonnées de la souris, mais pour que ceux-ci correspondent aux coordonnées d'une tile précise, on divise par la taille d'une tile (on obtient un entier) et on remultiplie par cette même taille, on tombe ainsi pile poil sur une tile  !

    Bon, voilà pour cette fonction, mais si vous avez bien suivi, vous aurez aussi remarqué que notre structure input n'est plus à jour  ! Eh bien qu'attendons-nous pour la modifier  !
 
Nom du fichier : structs.h


  typedef struct Input
 {
    int left, right, up, down, add, remove;
    
int previous, next, load, save, copy, reinit;
    
int mouseX, mouseY;

 } Input;



   typedef struct Cursor
  {
      
int x, y, tileID;
  } Cursor;


 

    Eh voilà, on a donc rajouté toutes les variables nécessaires pour gérer nos nouvelles entrées (input).
    Tant qu'à faire, on va aussi ajouter une structure qui nous sera utile pour la suite : la structure cursor qui contiendra les coordonnées du curseur de la souris et le numéro de la tile à coller (vraiment très basique 
) !

    N'oublions pas alors de mettre à jour nos en-têtes pour déclarer cette nouvelle structure, et en même temps, nous ajouterons les prototypes dont nous nous servirons par la suite dans la fonction update() qu'on verra plus en détails ci-dessous.

Nom du fichier : main.h


  #include "structs.h"

  /* Prototypes des fonctions utilisées */

  extern void init(char *);
  
extern void cleanup(void);
  
extern void getInput(void);
  
extern void draw(void);
 
extern void loadGame(void);
  
extern void delay(unsigned int frameLimit);
 
extern void update(void);


  /* Déclaration des structures globales utilisées par le jeu */

  Input input;
  Gestion jeu;
  Map map;

  /* Ligne à rajouter : */
  Cursor cursor;

 


Nom du fichier : input.h


  #include "structs.h"

  /* Prototypes des fonctions utilisées */
  extern void loadMap(char *name);

  
extern void reinitMap(char *name);
  
extern void saveMap(char *name);


  
extern Input input;
  
extern Map map;

  
extern Cursor cursor;
 
    Et on va maintenant pouvoir gérer nos inputs dans notre fonction update() ! Mais avant cela, mettons à jour nos définitions car nous allons avoir besoin de deux nouvelles defines pour définir la tile transparente par défaut et le nombre max de tiles. Rajoutons donc les deux lignes suivantes :
 
Nom du fichier : defs.h


  /* N° de la tile transparente */
  #define BLANK_TILE 0

  /* MAX_TILES = numéro de la dernière tile */
  #define MAX_TILES 10

 


    Et nous allons enfin pouvoir nous attaquer au gros de cette mise à jour : la fonction update().
    Comme cette fonction est longue mais pas très complexe, la plupart des explications seront données avec le code ci-dessous :

Nom du fichier : input.c


 void update(void)
{

   
/* Pour l'affichage du curseur (indiquant la tile à copier, mais on reviendra dessus
    dans la fonction draw() ), on récupère les coordonnées de la souris */

    cursor.x = input.mouseX;
    cursor.y = input.mouseY;

    /* Gestion de notre scrolling du chapitre précédent */


     if (input.left == 1)
    {
        map.startX -= TILE_SIZE;

        if (map.startX < 0)
        {
            map.startX = 0;
        }
    }

    else if (input.right == 1)
    {
        map.startX += TILE_SIZE;

        if (map.startX + SCREEN_WIDTH >= map.maxX)
        {
            map.startX = map.maxX - SCREEN_WIDTH;
        }
    }

    if (input.up == 1)
    {
        map.startY -= TILE_SIZE;

        if (map.startY < 0)
        {
            map.startY = 0;
        }
    }

    else if (input.down == 1)
    {
        map.startY += TILE_SIZE;

        if (map.startY + SCREEN_HEIGHT >= map.maxY)
        {
            map.startY = map.maxY - SCREEN_HEIGHT;
        }
    }

    /* Gestion de la copie de tile :
    on retrouve les coordonnées de la tile pointée par la souris et on remplace
    sa valeur par celle de la tile sélectionnée dans le curseur */

    
if (input.add == 1)
    {

        map.tile[(map.startY + cursor.y) / TILE_SIZE][(map.startX + cursor.x) / TILE_SIZE] = cursor.tileID;

    }

    /* Même fonctionnement, sauf qu'on réinitialise la tile pointée en lui donnant
    la valeur BLANK_TILE (0 par défaut) */

    else
if (input.remove == 1)
    {

        map.tile[(map.startY + cursor.y) / TILE_SIZE][(map.startX + cursor.x) / TILE_SIZE] = BLANK_TILE;

        cursor.tileID = 0;
    }

    /* On fait défiler les tiles dans un sens ou dans l'autre */

    
if (input.previous == 1)
    {
        cursor.tileID--;

        
if (cursor.tileID < 0)
        {
            cursor.tileID = MAX_TILES - 1;
        }
        else 
if (cursor.tileID > MAX_TILES)
        {
            cursor.tileID = 0;
        }

        input.previous = 0;
    }

    
if (input.next == 1)
    {
        cursor.tileID++;

        
if (cursor.tileID < 0)
        {
            cursor.tileID = MAX_TILES - 1;
        }
        else 
if (cursor.tileID > MAX_TILES)
        {
            cursor.tileID = 0;
        }

        input.next = 0;
    }

    /* On copie le numéro de la tile pointée dans le curseur pour qu'il affiche et colle
    désormais cette tile */

   
if (input.copy == 1)
    {
        cursor.tileID = map.tile[(map.startY + cursor.y) / TILE_SIZE] [(map.startX + cursor.x) / TILE_SIZE];
        input.copy = 0;
    }

    /* Pour réinitialiser la map, on appelle la fonction reinitMap puis on recharge la map */

    
if (input.reinit == 1)
    {
        reinitMap("map/map1.txt");
        loadMap("map/map1.txt");
        input.reinit = 0;
    }

    /* Sauvegarde la map (cf. plus loin) */

    
if (input.save == 1)
    {
        saveMap("map/map1.txt");
        input.save = 0;
    }

    /* Charge la map (notre bonne vieille fonction ;) ) */

    
if (input.load == 1)
    {
        loadMap("map/map1.txt");
        input.load = 0;
    }

    /* On rajoute un délai entre 2 tours de boucle pour que le scrolling soit moins rapide */

    
if (input.left == 1 || input.right == 1 || input.up == 1 || input.down == 1)
    {
        SDL_Delay(30);
    }

}

    Rien de bien sorcier ici. Si vous avez bien suivi le tuto, normalement tout devrait être clair (enfin, j'espère - sinon, il reste toujours le forum  ).
    A part peut-être un mystère...  Vous ne vous êtes pas demandé pourquoi parfois les variables input sont remises à 0  ?
    En fait, cela vient de la technique dite du keymapping qui permet de gérer le clavier (ou la souris, le joystick, etc.) au mieux.
    En effet, si l'on veut que le scrolling défile tant qu'on appuie sur une touche et qu'il s'arrête si on la relâche (sans utiliser SDL_KeyRepeat, qui n'est pas génial ) on doit tester dans getInput() si la touche est pressée, auquel cas, on met input.action = 1 et on fait défiler, et dès qu'elle est relâchée, on met
input.action = 0 et on ne fait rien.
    Mais on ne peut pas faire pareil avec la fonction save par exemple, sinon on va sauver plein de fois !  Eh, non ! Dans ce cas-là, getInput() ne teste que si la touche save est enfoncée : à ce moment-là, il met input.save = 1 et ce n'est qu'à la fin de la sauvegarde qu'on remet input.save à 0, comme ça, si on veut resauvegarder, il va falloir relâcher la touche et la réenfoncer.

    Il va maintenant falloir mettre à jour notre fichier map.c pour y ajouter les fonctions saveMap() et reinitMap() mais aussi rééditer la fonction loadMap().
    Mais pourquoi
loadMap() ? Elle marche bien !
    Oui, en effet, mais si vous vous rappelez bien, elle limite le scrolling de la map à sa taille dans le fichier, or là, on veut pouvoir scroller au maximum de la taille du fichier. Il va donc falloir changer deux lignes 
!
   
Nom du fichier : map.c


  void loadMap(char *name)
 {
    int x, y;
    FILE *fp;

    fp = fopen(name, "rb");

    /* Si on ne peut pas ouvrir le fichier, on quitte */

    
if (fp == NULL)
    {
        printf("Failed to open map %s\n", name);

        exit(1);
    }

    /* Lit les données du fichier dans la map */

    map.maxX = map.maxY = 0;

    for (y = 0; y < MAX_MAP_Y; y++)
    {
        
for (x = 0; x < MAX_MAP_X; x++)
        {
            /* On lit le numéro de la tile et on le copie dans notre tableau */
            fscanf(fp, "%d", &map.tile[y][x]);

            /* Permet de déterminer la taille de la map (voir plus bas) */
            if (map.tile[y][x] > 0)
            {
                
if (x > map.maxX)
                {
                    map.maxX = x;
                }

                
if (y > map.maxY)
                {
                    map.maxY = y;
                }
            }
        }
    }


    /* On change ces coordonnées pour qu'on puisse scroller et éditer la map
    au maximum */

    map.maxX = MAX_MAP_X * TILE_SIZE;
    map.maxY = MAX_MAP_Y * TILE_SIZE;


    /* Remet à 0 les coordonnées de départ de la map */

    map.startX = map.startY = 0;


    /* Et on referme le fichier */

    fclose(fp);

}

 

    Quant à la fonction saveMap(), elle est très simple, c'est l'inverse de loadMap() : au lieu de lire le fichier et de remplir notre tableau, on lit notre tableau et on remplit le fichier. Et pour reinitMap(), on remplit le fichier de 0. Simple, non ?


  void saveMap(char *name)
 {
    int x, y;
    FILE *fp;

    fp = fopen(name, "wb");

    /* Si on ne peut pas charger la map, on quitte */

    if (fp == NULL)
    {
        printf("Failed to open map %s\n", name);

        exit(1);
    }


    /* Sauvegarde la map */

    
for (y=0;y<MAX_MAP_Y;y++)
    {
        
for (x=0;x<MAX_MAP_X;x++)
        {
            fprintf(fp, "%d ", map.tile[y][x]);
        }

        fprintf(fp, "\n");
    }


    /* On referme le fichier */

    fclose(fp);
 }


  
void reinitMap(char *name)
 {
    
int x, y;
    FILE *fp;

    fp = fopen(name, "wb+");

    /* Si on ne peut pas charger la map, on quitte */

    
if (fp == NULL)
    {
        printf("Failed to open map %s\n", name);

        exit(1);
    }


    /* Remplit la map de 0 */

    for (y=0;y<MAX_MAP_Y;y++)
    {
        
for (x=0;x<MAX_MAP_X;x++)
        {
            fprintf(fp, "0 ");
        }

        fprintf(fp, "\n");
    }


    /* On referme le fichier */

    fclose(fp);
 }

 

    Si vous compilez maintenant, cela devrait se passer sans problème. Vous pourrez scroller la map et copier/coller/supprimer les tiles, mais aussi charger / sauvegarder et réinitialiser la map.
    Cependant, vous aurez remarqué que vous devrez coller les tiles à l'aveuglette car vous ne saurez pas quelle tile vous avez sélectionnée . Pour résoudre ce problème, nous allons afficher la tile sélectionner à côté du curseur. Et pour ça, on va éditer la fonction drawMap()  !
    On va rajouter les quelques lignes en italiques à la fin de la fonction :

Nom du fichier : map.c


  void drawMap(void)
 {
    int x, y, mapX, x1, x2, mapY, y1, y2, xsource, ysource, a;

   /* On initialise mapX à la 1ère colonne qu'on doit blitter.
    Celle-ci correspond au x de la map (en pixels) divisés par la taille d'une tile (32)
    pour obtenir la bonne colonne de notre map
    Exemple : si x du début de la map = 1026, on fait 1026 / 32
    et on sait qu'on doit commencer par afficher la 32eme colonne de tiles de notre map */
    mapX = map.startX / TILE_SIZE;

    /* Coordonnées de départ pour l'affichage de la map : permet
    de déterminer à quels coordonnées blitter la 1ère colonne de tiles au pixel près
    (par exemple, si la 1ère colonne n'est visible qu'en partie, on devra commencer à blitter
    hors écran, donc avoir des coordonnées négatives - d'où le -1). */
    x1 = (map.startX % TILE_SIZE) * -1;

    /* Calcul des coordonnées de la fin de la map : jusqu'où doit-on blitter ?
    Logiquement, on doit aller à x1 (départ) + SCREEN_WIDTH (la largeur de l'écran).
    Mais si on a commencé à blitter en dehors de l'écran la première colonne, il
    va falloir rajouter une autre colonne de tiles sinon on va avoir des pixels
    blancs. C'est ce que fait : x1 == 0 ? 0 : TILE_SIZE qu'on pourrait traduire par:
    if(x1 != 0)
        x2 = x1 + SCREEN_WIDTH + TILE_SIZE , mais forcément, c'est plus long ;)*/
    x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);

    /* On fait exactement pareil pour calculer y */
    mapY = map.startY / TILE_SIZE;
    y1 = (map.startY % TILE_SIZE) * -1;
    y2 = y1 + SCREEN_HEIGHT + (y1 == 0 ? 0 : TILE_SIZE);

   
    /* Dessine la carte en commençant par startX et startY */

    /* On dessine ligne par ligne en commençant par y1 (0) jusqu'à y2 (480)
    A chaque fois, on rajoute TILE_SIZE (donc 32), car on descend d'une ligne
    de tile (qui fait 32 pixels de hauteur) */

    for (y = y1; y < y2; y += TILE_SIZE)
    {
        /* A chaque début de ligne, on réinitialise mapX qui contient la colonne
        (0 au début puisqu'on ne scrolle pas) */

        mapX = map.startX / TILE_SIZE;

        /* A chaque colonne de tile, on dessine la bonne tile en allant
        de x = 0 à x = 640 */

        for (x = x1; x < x2; x += TILE_SIZE)
        {

            /* Suivant le numéro de notre tile, on découpe le tileset */

            a = map.tile[mapY][mapX];

            /* Calcul pour obtenir son y (pour un tileset de 10 tiles
            par ligne, d'où le 10 */

            ysource = a / 10 * TILE_SIZE;

            /* Et son x */

            xsource = a % 10 * TILE_SIZE;

            /* Fonction qui blitte la bonne tile au bon endroit */

            drawTile(map.tileSet, x, y, xsource, ysource);

            mapX++;
        }

        mapY++;
    }

         /* On affiche la tile sélectionnée à côté du curseur */
         ysource = cursor.tileID / 10 * TILE_SIZE;
         xsource = cursor.tileID % 10 * TILE_SIZE;
        drawTile(map.tileSet, cursor.x, cursor.y, xsource, ysource);


  }
 


    Rien de bien compliqué, non plus, la technique est la même que précédemment, sauf qu'on affiche aux coordonnées du curseur le numéro de la tile contenu dans la variable cursor.tileID.

Nom du fichier : map.h


  #include "structs.h"

  /* Prototypes des fonctions utilisées */
  extern void drawImage(SDL_Surface *, int, int);
 
extern  void drawTile(SDL_Surface *image, int destx, int desty, int srcx, int srcy);

  
extern Gestion jeu;
  
extern Map map;

  
extern Cursor cursor;
 
    Eh voilà, notre level editor basique est terminé. Bien entendu, on pourrait encore rajouter bien des choses, comme la gestion de plusieurs levels, de plusieurs couches de tiles, de la position de départ du héros, afficher un message à la sauvegarde, au chargement, etc...

    Nous y reviendrons donc sûrement dans l'avenir. En attendant, vous allez maintenant pouvoir éditer votre map comme vous le désirez ! Et même éditer vos maps avec votre propre tileset, en le modifiant et en changeant la valeur de MAX_TILES !


 


    Et maintenant, prochaine étape : on retourne à notre jeu  !!





 
 

Connexion

CoalaWeb Traffic

Today136
Yesterday282
This week930
This month3223
Total1742430

19/04/24