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é :  





5. Afficher une map de jeu


    Notre code est maintenant prêt à afficher des images, et il va bientôt en avoir beaucoup à afficher ! Et pour cause, nous allons maintenant afficher notre première map ! Ahah !

    Plus que jamais, vous aurez besoin de savoir ce qu'est le tilemapping pour comprendre ce chapitre. Si vous ne savez pas ce qu'est c'est, je vous invite à lire cet autre tuto avant de commencer. Il vous présentera la théorie tandis que nous attaquerons directement le code ici  !



Résultat à la fin de ce chapitre : enfin du concret  !



   Mais avant d'afficher notre map, il va nous falloir un tileset et une map ! 

  Et comme vous n'avez pas forcément eu le temps de dessiner vos premières tiles (même si je vous le conseille par la suite en consultant notamment les tutos de PixelArt du site
) et que vous n'avez pas de level editor pour créer des maps (et que vous n'avez pas envie de le faire à la main  !), je vous prête tout ça !


    Voilà donc la map, à enregistrer dans le dossier du projet (Aron) dans le sous-dossier map (à créer) et le tileset, à enregistrer avec le background dans graphics sous le nom tileset.png (N.B. : La map se trouve dans le projet correspondant au chapitre dans l'archive téléchargeable) :
map1.txt



tileset.png



    Nous allons donc pouvoir retourner à notre code et le modifier en conséquence. Cependant, comme vous allez le voir, les différences ne seront pas énormes et se concentreront surtout sur un nouveau fichier map.c (et son en-tête map.h).

    N.B. : Dorénavant, nous n'indiquerons plus les fichiers qui ne sont pas modifiés. Si un fichier n'est pas indiqué, c'est donc qu'il n'y a rien à changer dedans .


Nom du fichier : defs.h


  #include <stdio.h>
  #include <string.h>
  #include <stdlib.h>
  #include <math.h>
  #include <SDL.h>
 /* On inclut les libs supplémentaires */
  #include <SDL_image.h>

 /* Taille de la fenêtre / résolution en plein écran */
  #define SCREEN_WIDTH 640
  #define SCREEN_HEIGHT 480

 /* Valeur RGB pour la transparence (canal alpha) */
 #define TRANS_R 255
 #define TRANS_G 0
 #define TRANS_B 255


 /* Taille maxi de la map : on voit large : 400 x 300 tiles */
 #define MAX_MAP_X 400
 #define MAX_MAP_Y 300

 /* Taille d'une tile (32 x 32 pixels) */
 #define TILE_SIZE 32




    Mise à jour de nos définitions : on rajoute la taille max de la map : 400 tiles de large et 300 de haut (on voit grand tant qu'à faire  ! ça correspond au format utilisé pour Wiwi's Adventures, si vous voulez faire une idée.), et la taille d'une tile de base : 32 x 32 pixels (taille plutôt standard).


Nom du fichier : structs.h


  #include "defs.h"

  /* Structures qui seront utilisées pour gérer le jeu */

  /* Structure pour gérer l'input (clavier puis joystick) */

   typedef struct Input
  {

    int left, right, up, down, jump, attack, enter, erase, pause;

  } Input;


  /* Structure pour gérer le niveau (à compléter plus tard) */

  typedef struct Gestion
  {

    SDL_Surface *screen;

  } Gestion;


  /* Structure pour gérer la map à afficher (à compléter plus tard) */

  typedef struct Map
  {

      SDL_Surface *background,
*tileSet;
     
    /* Coordonnées de début, lorsqu'on doit dessiner la map */
    int startX, startY;

    /* Coordonnées max de fin de la map */
    int maxX, maxY;

    /* Tableau à double dimension représentant la map de tiles */
    int tile[MAX_MAP_Y][MAX_MAP_X];

  } Map;



    Ici, on met logiquement à jour notre structure map . On crée une nouvelle SDL_Surface pour le tileset, des variables pour savoir quand commence la map à afficher (startX et startY) et quand elle finit (maxX et maxY) et enfin, le plus important, notre tableau à 2 dimensions qui contiendra les numéros des tiles à afficher en fonction de x et y et qui à une taille définie en define (400 x 300 tiles pour rappel).

    Pour vous aider à visualiser ce tableau, voilà à quoi il ressemblerait si on devrait le tracer :

  X = 0 -> colonne 0 X = 32 -> colonne 1 X = 64 -> colonne 2 X = 96 -> colonne 3 X = 128 -> colonne 4, etc.
Y = 0 -> ligne 0 0 0 4 0 6
Y = 32 -> ligne 1 0 0 3 0 0
Y = 64 -> ligne 2, etc. 2 2 2 2 2

    Dans chaque case, on trouve donc un numéro correspondant au numéro du tile dans notre tileset. Regardez le tableau ci-dessus et imaginez que 0 représente du ciel, 2 du sol, 3-4 un palmier et 6 un nuage.

     Vous le voyez ? Plus qu'à entendre la mer . C'est le principe de fonctionnement.

     Bien sûr, plus tard, avec un level editor, ça sera mieux .


Nom du fichier : init.c


  #include "init.h"

   
      void loadGame(void)
    {
       
       /* Charge l'image du fond et le tileset */
       map.background = loadImage("graphics/background.png");

       map.tileSet = loadImage("graphics/tileset.png");

       loadMap("map/map1.txt");

     }



   /* Fonction qui quitte le jeu proprement */

    void cleanup()
   {

     /* Libère l'image du background */

      if (map.background != NULL)
      {
            SDL_FreeSurface(map.background);
      }


       /* Libère l'image du tileset */

       if (map.tileSet != NULL)
      {
           SDL_FreeSurface(map.tileSet);
       }



      /* Quitte la SDL */
      SDL_Quit();

    }


    Ici, rien de bien compliqué, on rajoute le chargement de notre tileset dans la fonction loadGame() et on le libère dans la fonction cleanup(), effectuée quand on quitte le jeu.

     On appelle aussi la fonction loadMap(), qui va charger le fichier map1.txt, contenant notre map. Mais on verra ça plus loin .
 


Nom du fichier : init.h


   #include "structs.h"
   
    /* Prototypes des fonctions utilisées */
     extern SDL_Surface *loadImage(char *name);
    extern void loadMap(char *name);


    extern Gestion jeu;
   
extern Map map;



    Ici on rajoute juste le prototype de notre fonction loadMap().


Nom du fichier : draw.c


   #include "draw.h"

 
  void drawTile(SDL_Surface *image, int destx, int desty, int srcx, int srcy)
  {
    /* Rectangle de destination à blitter */
    SDL_Rect dest;

    dest.x = destx;
    dest.y = desty;
    dest.w = TILE_SIZE;
    dest.h = TILE_SIZE;

    /* Rectangle source à blitter */
    SDL_Rect src;

    src.x = srcx;
    src.y = srcy;
    src.w = TILE_SIZE;
    src.h = TILE_SIZE;

    /* Blitte la tile choisie sur l'écran aux coordonnées x et y */

    SDL_BlitSurface(image, &src, jeu.screen, &dest);

  }



  void draw(void)
  {

       /* Affiche le fond (background) aux coordonnées (0,0) */
      drawImage(map.background, 0, 0);

      /* Affiche la map de tiles */
      drawMap();
   
    /* Affiche l'écran */

    SDL_Flip(jeu.screen);

    /* Delai */
    SDL_Delay(1);

  }

 


    Vous remarquerez tout d'abord l'ajout de la fonction drawMap() dans notre fonction draw() pour afficher la map à chaque tour de boucle du jeu. Nous reviendrons plus tard sur cette fonction.

    Ce qui va plus nous intéresser, c'est la fonction drawTile() dont le but est d'afficher une tile de notre tileset à un endroit précis. Vous remarquerez donc qu'elle est plus fournie que la fonction drawImage() parce qu'elle prend en charge deux rectangles SDL_Rect : le rectangle source (c'est-à-dire le rectangle correspondant à la tile à découper dans notre tileset) et le rectangle cible (c'est-à-dire le rectangle correspondant à l'endroit de l'écran où on doit copier la tile).

    Comme nos tiles font 32 x 32 pixels, la hauteur (h) et la largeur (w) des deux rectangles est facile à trouver. C'est 32  ! Pour trouver les coordonnées x et y du rectangle source (la tile à découper), nous reviendrons dessus dans le fichier map. Quant aux coordonnées x et y de destination ce sont ceux de la ligne / colonne de notre map multipliés par la taille d'une tile.

    Ainsi par exemple, les coordonnées x et y de la tile à la ligne 3 et colonne 4 sont x = 4 x 32 = 128 et y = 3 x 32 = 96 pixels.
    Ces calculs apparaîtront dans la fonction drawMap() eux aussi. Ici drawTile() ne fait que recevoir les coordonnées et blitter.

 
Nom du fichier : draw.h


  #include "structs.h"
  /* Prototypes des fonctions utilisées */
   extern void drawMap(void);

  extern Gestion jeu;
  extern Map map;


Ici, on rajoute simplement le prototype de drawMap().

    On va maintenant rajouter deux nouveaux fichiers à notre projet. Vous devriez savoir comment faire . C'est facile  : File / New / Empty file. Et vous nommez ces deux fichiers map.c et map.h.

    Commençons par map.c :


Nom du fichier : map.c


  #include "map.h"

  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;
                }
            }
        }
    }


    /* maxX et maxY sont les coordonnées de fin de la map.
    On les trouve dès qu'il n'y a plus que des zéros à la suite.
    Comme ça, on peut faire des maps de tailles différentes avec la même
    structure de fichier. */

    map.maxX = (map.maxX + 1) * TILE_SIZE;
    map.maxY = (map.maxY + 1) * 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);

  }




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

    /* Ces calculs compliqués serviront à calculer l'affichage de l'écran pendant le scrolling

    mapX = map.startX / TILE_SIZE;
    x1 = (map.startX % TILE_SIZE) * -1;
    x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);

    mapY = map.startY / TILE_SIZE;
    y1 = (map.startY % TILE_SIZE) * -1;
    y2 = y1 + SCREEN_HEIGHT + (y1 == 0 ? 0 : TILE_SIZE); */


    /* Mais pour l'instant, comme on ne scrolle pas, on peut les simplifier :
    On va en effet dessiner la map du début (0;0) jusqu'au bout de l'écran (640;480) */

    mapX = 0;
    x1 = 0;
    x2 = SCREEN_WIDTH;

    mapY = 0;
    y1 = 0;
    y2 = SCREEN_HEIGHT;



    /* 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++;
    }
  }

 

    C'est ici qu'on va trouver nos deux nouvelles fonctions qui vont faire (presque) tout le boulot. Alors, courage, on y est presque  !

    La fonction loadMap() :

    Cette première fonction va charger notre fichier de niveau (map), d'où son nom. On va donc commencer par ouvrir le fichier dont on nous a donné le nom en argument (char). Bien entendu, on rajoute une sécurité : si le fichier ne s'ouvre pas (ou n'existe pas), on fait un printf vers stderr et on quitte proprement .

    Ensuite on va lire le fichier nombre par nombre et ligne par ligne (d'où la double boucle) et remplir notre tableau à deux dimensions au fur et à mesure. Comme notre fichier reprend le même format que notre tableau, c'est facile 
! Un fscanf suffit à déterminer le nombre de la tile. L'illustration suivante montre ce que fait cette fonction :

FICHIER
0  12  25  0
4  52  47  1
          TABLEAU
0 12 25 0
4 52 47 1
 
    Le mystère de map.maxX et map.maxY :
    Vous vous serez sans doute demandé à quoi servent ces deux variables, et pourquoi elles changent à chaque fois que la valeur d'une tile est différente de 0  ! En fait, si notre fichier a une taille prédéfinie, on veut pouvoir se laisser le choix de faire des niveaux comme on veut (c'est mieux, non 
?).

    Alors pour ça, c'est très simple : les limites du fichier à lire sont clairement indiqués dans le programme (donc inchangeables) mais pas celles de la map : elles sont définies à la lecture du fichier de map : les limites augmentent donc à chaque tour de boucle tant qu'il y a des tiles différentes de 0 (donc non-vides). Dès qu'il n'y a plus que des tiles vides, ça veut dire su'on a atteint la fin de la map et qu'on ne pourra donc pas scroller dans le vide (plus tard).

    Comme ça, avec le même format de fichier, on pourra faire des niveaux horizontaux, verticaux ou les deux de la taille que l'on désire (dans la limite de 400 x 300 tiles soit la taille du fichier). Malin, non 
? En plus, ça marche avec seulement 5 lignes de code  !

    La fonction drawMap() :

    Quand notre scrolling sera opérationnel (dans le chapitre prochain), les calculs au début de cette fonction permettront de déterminer à quel point débuter l'affichage de la map. Ces calculs sont un peu compliqués et nous reviendrons dessus dans le prochain chapitre. C'est pourquoi nous les avons simplifiés pour afficher la map au début du niveau, à x = 0 et y = 0.

    L'affichage se fait là encore grâce à une double boucle : on fait d'abord défiler les y, donc les lignes (on commence par la ligne 0 puis 1, 2, etc...) puis pour chaque ligne toutes les colonnes (x) jusqu'aux limites de la map définies par la taille de l'écran (en largeur et en hauteur). Logique, non 
?
   
    Pour chaque "case" de notre tableau, on reprend la valeur de notre tile (cf. tableau ci-dessus) et on blitte la tile correspondante à cet endroit précis grâce à notre fonction drawTile().

    On définit alors les coordonnées de la tile à découper dans notre tileset à l'aide du calcul suivant :

xsource = numéro de la tile % largeur du tileset

or notre tileset fait 10 tiles de 32 pixels donc

largeur du tileset = 10 x TILE_SIZE = 10 x 32

donc
xsource = numéro de la tile % 10 x TILE_SIZE



ysource = numéro de la tile / hauteur du tileset

or notre tileset fait 10 tiles de 32 pixels donc

hauteur du tileset = 10 x TILE_SIZE = 10 x 32

donc
ysource = numéro de la tile / 10 x TILE_SIZE
 
    Grâce à ce calcul, on obtient donc les coordonnées x et y de notre tile à découper dans notre tileset :


On vérifie  ? Mettons qu'on veuille afficher la tile N° 3 (eau basse) puis la 10 (globulos vert) :
Pour la 3 :
xsource = 3 % 10 x 32 = 96
ysource = 3 / 10 x 32 = 0


Pour la 10 :
xsource = 10 % 10 x 32 = 0 x 32 = 0
ysource = 10 / 10 x 32 = 1 x 32 = 32


Cela marche !!! Ouf  !

    Enfin, pour trouver les coordonnées ou blitter sur la carte, c'est très simple, on envoie notre x et notre y de nos boucles, puisqu'ils s'incrémentent automatiquement de la taille d'un tile (32 pixels) à chaque tour de boucle !

    Et maintenant, drawTile() fait le boulot 
! Ouf, voilà une bonne chose de faite  !

    Pour terminer, éditons notre fichier map.h avec les prototypes de nos fonctions et nos structures déjà déclarées :

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;


    Eh voilà, on est maintenant prêt à compiler... BUILD... RUN et tadaaaaam ! Le résultat n'est-il pas beau  ?!!!



    Et maintenant, prochaine étape : le scrolling !!



 

 

 

Connexion

CoalaWeb Traffic

Today126
Yesterday282
This week920
This month3213
Total1742420

19/04/24