Navigation

Tutoriel précédent : présentation générale   Sommaire   Tutoriel suivant : défilement (scrolling)

II. Introduction

Dans ce chapitre, nous allons voir comment lire un niveau depuis un fichier texte, et nous allons aussi parler des propriétés des tuiles.

III. Structure d'un fichier texte pour un niveau

L'inconvénient de l'exemple précédent, c'est qu'on entrait directement le niveau dans le code. Si on veut faire des niveaux supplémentaires, ça devient assez contraignant.

Nous allons donc nous appuyer sur un fichier texte que nous allons structurer.

Ouvrez le répertoire prog2.

Dedans, vous trouvez un fichier appelé level.txt, ouvrez-le avec un bloc-notes.

 
Sélectionnez
Tilemapping Version 1.0 
#tileset 
tileset1.bmp 
8 1 
tile0: vide 
tile1: plein 
tile2: plein 
tile3: plein 
tile4: plein 
tile5: plein 
tile6: plein 
tile7: plein 
#level 
15 13 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
1 0 0 0 0 0 0 0 0 1 1 1 1 1 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 3 4 0 0 0 2 2 2 2 0 0 2 2 
0 0 5 6 0 0 0 0 0 0 0 0 0 0 0 
0 0 5 6 0 0 0 0 0 0 0 0 0 0 0 
0 0 5 6 0 0 0 0 0 0 0 0 0 0 0 
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 
#fin

Expliquons ce fichier.

Tout d'abord, vous pouvez constater que la première ligne est juste une ligne qui donne la version.

Ensuite, nous voyons que le fichier est découpé en deux blocs :

  • #tileset ;
  • #level.

III-A. Le bloc « tileset »

Sous le bloc #tileset, ce sont les informations sur l'ensemble des tuiles. Avec tout d'abord le nom du fichier de tuiles à charger : tileset1.bmp.

Ensemble des tuiles

Ensuite, la ligne suivante contient "8 1". Je définis simplement combien on a de tuiles en X, et combien en Y. L'image contient huit tuiles, toutes sur la même ligne, donc 8 en X, 1 en Y.

Notez que grâce à la taille de l'image et du nombre de tuiles lues, on peut calculer la largeur et la hauteur d'un tuile avec une simple division.

Ensuite, voici la nouveauté de ce chapitre. Comme je l'ai évoqué plus haut, il va falloir définir pour chaque tuile si elle est pleine ou vide, pour les futurs calculs du jeu.

Typiquement, pour un Mario, seule la première tuile (du ciel) est vide : les personnages pourront s'y promener. Toutes les autres sont pleines : on ne pourra pas les traverser.

Pour un Zelda, on aurait en tuiles vides tous les sols sur lesquels on peut marcher et en tuiles pleines toutes les tuiles de rochers, d'arbres, de murs.

C'est ce qu'on appellera les propriétés des tuiles. On pourra plus tard en mettre d'autres.

III-B. Le bloc level

Le bloc « level » contient d'abord le nombre de tuiles en x et y du niveau (15 et 13), puis directement le tableau qui définit le niveau, comme dans le premier chapitre.

Notez cependant que cette fois-ci, je mets des espaces entre chaque chiffre. Pourquoi ? Tout simplement parce qu'avec la version du chapitre précédent, nous étions bloqués si nous avions plus de dix tuiles.

Ici, pas de soucis, on pourra très bien avoir 200 tuiles et écrire :

 
Sélectionnez
0   150  1 8 
101   6  6 2

La balise « fin » explicitera la fin du fichier.

IV. Structure dans le programme

Maintenant que nous avons vu comment était codé notre fichier texte, voyons comment nous allons ranger ça en mémoire.

 
Sélectionnez
typedef unsigned char tileindex; 
 
typedef struct 
{ 
    SDL_Rect R; 
    int plein; 
} TileProp; 
 
typedef struct 
{ 
    int LARGEUR_TILE,HAUTEUR_TILE; 
    int nbtilesX,nbtilesY; 
    SDL_Surface* tileset; 
    TileProp* props; 
    tileindex** schema; 
    int nbtiles_largeur_monde,nbtiles_hauteur_monde; 
} Map;

IV-A. La structure Map

Regardons d'abord la structure « Map », qui contiendra tout pour afficher le monde.

  • LARGEUR_TILE, HAUTEUR_TILE : les noms parlent d'eux-mêmes.
  • nbtilesX,nbtilesY : nombre de tuiles dans l'ensemble de tuiles, en x et y. Dans notre cas, ce sera 8 et 1.
  • nbtiles_largeur_monde,nbtiles_hauteur_monde : garderont les 15 et 13 de notre exemple.
  • tileset : lien vers la surface de l'ensemble des tuiles chargées.
  • props : un tableau de propriétés pour chaque tuile. Sa taille sera nbtilesX * nbtilesY. L'élément du tableau est une autre structure, TileProp, que nous allons voir plus bas.
  • Schema : tableau à deux dimensions, qui contiendra tous les chiffres de la partie « level ». Sa taille sera nbtiles_largeur_monde en X, nbtiles_hauteur_monde en Y.

IV-A-1. Le type tileindex

schema est un tableau vers des types « tileindex » que j'ai définis plus haut.

Dans notre cas, j'ai défini tileindex à « unsigned char », car je n'aurai pas plus de 256 tuiles.

Si vous en avez davantage, changez juste le typedef en « unsigned short ».

Si les « unsigned short » ne suffisent pas, on pourrait mettre « unsigned long », mais en réalité, plus de 65536 tuiles dans la même map, c'est une folie…

IV-A-2. La structure TileProp

La structure TileProp contient pour l'instant deux choses :

  • un rectangle R ;
  • la propriété plein/vide.

Pourquoi stockons-nous un SDL_Rect ?

Dans l'exemple du fichier précédent, vous avez pu constater dans la fonction Affichage que pour toutes les tuiles à afficher, je calculais un rectangle source sur l'image des tuiles, à chaque fois.

Comme on a beaucoup de tuiles identiques, on recalculait de nombreuses fois les mêmes.

Je propose de les calculer une fois pour toutes et de les stocker. Ainsi, pour les futurs affichages, on a déjà le rectangle source, il ne reste plus qu'à poser la tuile à la bonne destination.

Schématiquement, on peut voir le lien ici entre le tableau de propriétés et l'ensemble de tuiles. Je n'ai rempli que la case 1, mais on aura la même chose avec toutes les autres cases.

Image non disponible

V. Code exemple

Voyons maintenant une illustration de tout cela.

Ouvrez le projet « prog2 ».

Compilez-le, lancez-le. Vous devriez avoir le même résultat que le programme précédent… mais avec un code plus complexe !

Pourquoi faire compliqué quand on peut faire simple ?

Parce que le code précédent était inexploitable.

On peut envisager la métaphore suivante :

Si vous voulez construire une maison, la méthode la plus rapide est de poser tous les murs brutalement, puis le toît.

Par contre, si on vous dit « maintenant, où on fait passer les tuyaux d'eau ? Les câbles ? », eh bien vous n'avez plus qu'à démolir.

Alors que si vous avez construit votre maison en prévoyant les goulottes, vous pourrez rajouter l'électricité par la suite, même si avant ça, les maisons avec et sans avoir prévu les goulottes se ressemblent.

Étudions le programme.

Regardons d'abord le fmap.h :

 
Sélectionnez
#include <sdl/sdl.h> 

#pragma comment (lib,"sdl.lib")      // ignorez ces lignes si vous ne linkez pas les libs de cette façon. 
#pragma comment (lib,"sdlmain.lib") 

typedef unsigned char tileindex; 
 
typedef struct 
{ 
    SDL_Rect R; 
    int plein; 
    // tout ce que vous voulez... 
} TileProp; 

typedef struct 
{ 
    int LARGEUR_TILE,HAUTEUR_TILE; 
    int nbtilesX,nbtilesY; 
    SDL_Surface* tileset; 
    TileProp* props; 
    tileindex** schema; 
    int nbtiles_largeur_monde,nbtiles_hauteur_monde; 
} Map; 

Map* ChargerMap(const char* fic); 
int AfficherMap(Map* m,SDL_Surface* screen); 
int LibererMap(Map* m);

On retrouve les structures étudiées plus haut, ainsi que trois fonctions seulement :

  • ChargerMap ;
  • AfficherMap ;
  • LibererMap.

Si vous n'êtes pas curieux, ce sont des fonctions magiques.

À la première, vous passez le fichier level.txt et elle vous remplit une Map qu'elle vous donne.

À la seconde, vous donnez la map, et l'écran « screen » où vous voulez l'afficher, et elle l'affiche.

À la troisième, vous donnez la map, elle nettoie proprement.

Maintenant, regardons le main, dans prog2.c :

 
Sélectionnez
#include "fmap.h" 


int main(int argc,char** argv) 
{ 
    SDL_Surface* screen; 
    SDL_Event event; 
    Map* m; 
    SDL_Init(SDL_INIT_VIDEO);        // prepare SDL 
    screen = SDL_SetVideoMode(360, 208, 32,SDL_HWSURFACE|SDL_DOUBLEBUF); 
    m = ChargerMap("level.txt"); 
    AfficherMap(m,screen); 
    SDL_Flip(screen); 
    do 
    { 
        SDL_WaitEvent(&event); 
    } while (event.type!=SDL_KEYDOWN); 
    LibererMap(m); 
    SDL_Quit(); 
    return 0; 
}

Regardez la simplicité d'utilisation : je charge la carte, je l'affiche, j'attends qu'on appuie sur une touche, et je la libère avant de la quitter.

Si vous n'êtes pas curieux donc, voilà comment faire simple.

Pour les curieux, regardons fmap.c

 
Sélectionnez
#define _CRT_SECURE_NO_DEPRECATE   // pour visual C++ qui met des warning pour fopen et fscanf : aucun effet négatif pour les autres compilos. 
#include <string.h> 
#include "fmap.h" 

#define CACHE_SIZE 5000 

SDL_Surface* LoadImage32(const char* fichier_image) 
{ 
    SDL_Surface* image_result; 
    SDL_Surface* image_ram = SDL_LoadBMP(fichier_image);    // charge l'image dans image_ram en RAM 
    if (image_ram==NULL) 
    { 
        printf("Image %s introuvable !! \n",fichier_image); 
        SDL_Quit(); 
        system("pause"); 
        exit(-1); 
    } 
    image_result = SDL_DisplayFormat(image_ram); 
    SDL_FreeSurface(image_ram); 
    return image_result; 
} 

void ChargerMap_tileset(FILE* F,Map* m) 
{ 
    int numtile,i,j; 
    char buf[CACHE_SIZE];  // un buffer, petite mémoire cache 
    char buf2[CACHE_SIZE];  // un buffer, petite mémoire cache 
    fscanf(F,"%s",buf); // nom du fichier 
    m->tileset = LoadImage32(buf); 
    fscanf(F,"%d %d",&m->nbtilesX,&m->nbtilesY); 
    m->LARGEUR_TILE = m->tileset->w/m->nbtilesX; 
    m->HAUTEUR_TILE = m->tileset->h/m->nbtilesY; 
    m->props = malloc(m->nbtilesX*m->nbtilesY*sizeof(TileProp)); 
    for(j=0,numtile=0;j<m->nbtilesY;j++) 
    { 
        for(i=0;i<m->nbtilesX;i++,numtile++) 
        { 
            m->props[numtile].R.w = m->LARGEUR_TILE; 
            m->props[numtile].R.h = m->HAUTEUR_TILE; 
            m->props[numtile].R.x = i*m->LARGEUR_TILE; 
            m->props[numtile].R.y = j*m->HAUTEUR_TILE; 
            fscanf(F,"%s %s",buf,buf2); 
            m->props[numtile].plein = 0; 
            if (strcmp(buf2,"plein")==0) 
                m->props[numtile].plein = 1; 
        } 
    } 
} 

void ErrorQuit(const char* error) 
{ 
    puts(error); 
    SDL_Quit(); 
    system("pause"); 
    exit(-1); 
} 

void ChargerMap_level(FILE* F,Map* m) 
{ 
    int i,j; 
    fscanf(F,"%d %d",&m->nbtiles_largeur_monde,&m->nbtiles_hauteur_monde); 
    m->schema = malloc(m->nbtiles_largeur_monde*sizeof(tileindex*)); 
    for(i=0;i<m->nbtiles_largeur_monde;i++) 
        m->schema[i] = malloc(m->nbtiles_hauteur_monde*sizeof(tileindex)); 
    for(j=0;j<m->nbtiles_hauteur_monde;j++) 
    { 
        for(i=0;i<m->nbtiles_largeur_monde;i++) 
        { 
            int tmp; 
            fscanf(F,"%d",&tmp); 
            if (tmp>=m->nbtilesX*m->nbtilesY) 
                ErrorQuit("level tile hors limite\n"); 
            m->schema[i][j] = tmp; 
        } 
    } 
} 

Map* ChargerMap(const char* level) 
{ 
    FILE* F; 
    Map* m; 
    char buf[CACHE_SIZE]; 
    F = fopen(level,"r"); 
    if (!F) 
        ErrorQuit("fichier level introuvale\n"); 
    fgets(buf,CACHE_SIZE,F); 
    if (strstr(buf,"Tilemapping Version 1.0")==NULL) 
        ErrorQuit("Mauvaise version du fichier level. Ce programme attend la version 1.0\n"); 
    m = malloc(sizeof(Map)); 
    do 
    { 
        fgets(buf,CACHE_SIZE,F); 
        if (strstr(buf,"#tileset")) 
            ChargerMap_tileset(F,m); 
        if (strstr(buf,"#level")) 
            ChargerMap_level(F,m); 
    } while (strstr(buf,"#fin")==NULL); 
    fclose(F); 
    return m; 
} 

int AfficherMap(Map* m,SDL_Surface* screen) 
{ 
    int i,j; 
    SDL_Rect Rect_dest; 
    int numero_tile; 
    for(i=0;i<m->nbtiles_largeur_monde;i++) 
    { 
        for(j=0;j<m->nbtiles_hauteur_monde;j++) 
        { 
            Rect_dest.x = i*m->LARGEUR_TILE; 
            Rect_dest.y = j*m->HAUTEUR_TILE; 
            numero_tile = m->schema[i][j]; 
            SDL_BlitSurface(m->tileset,&(m->props[numero_tile].R),screen,&Rect_dest); 
        } 
    } 
    return 0; 
} 

int LibererMap(Map* m) 
{ 
    int i; 
    SDL_FreeSurface(m->tileset); 
    for(i=0;i<m->nbtiles_hauteur_monde;i++) 
        free(m->schema[i]); 
    free(m->schema); 
    free(m->props); 
    free(m); 
    return 0; 
}

Ce fichier n'est pas très complexe si vous savez lire un fichier texte.

Car la fonction ChargerMap ne fait finalement que des fscanf et des fgets.

Notons la fonction LoadImage32 qui va charger l'image, et la mettre au format de votre écran grâce à SDL_DisplayFormat, pour une vitesse d'affichage optimale.

La fonction ChargerMap_tileset va remplir le tableau, et le rectangle R pour chaque type de tuile.

La fonction AfficherMap en est donc simplifiée, puisqu'elle va directement lire le rectangle source, et non le calculer à chaque fois.

A la fin de ce chapitre, vous voyez qu'on peut enfermer la difficulté dans un fichier (fmap.c) et le niveau dans un fichier texte (level.txt).

Finalement, le pilotage de l'ensemble, dans le main, reste très simple.

Amusez-vous à modifier le fichier level.txt (par exemple en changeant les chiffres du tableau de schéma pour voir les modifications appliquées en relançant le programme, même sans le recompiler.

Navigation

Tutoriel précédent : présentation générale   Sommaire   Tutoriel suivant : défilement (scrolling)