Navigation

    Sommaire   Tutoriel suivant : propriétés des tuiles

II. Introduction

Bienvenue dans la première partie de ce tutoriel.

Dans cette partie, nous allons présenter le « Tile Mapping », et faire un petit programme qui affiche un petit monde simple de deux façons différentes :

  • avec des nombres directement dans le code ;
  • avec un fichier texte comme modèle.

III. Problématique

Vous avez déjà tous joué à ce qu'on appelle des jeux de plates-formes en 2D. Il s'agit de jeux où un personnage court dans un monde, parfois à toute vitesse, saute, monte sur des blocs, et infatigablement continue à courir et à sauter…
Pendant que vous courez, l'écran défile. Le monde que vous parcourez peut être plus ou moins grand.

Un exemple très connu de jeu de plates-formes, duquel nous allons nous inspirer, est Super Mario Bros.

Super Mario Bros

Vous avez déjà sûrement joué aussi à des jeux vus de dessus, comme ce bon vieux Zelda.

Zelda a link to the past

Dans ce tutoriel, nous allons essayer de voir comment de tels jeux sont faits. Comment le monde est mis en place et comment faire défiler l'écran.

Comment, avec SDL, peut-on arriver à faire un tel type de jeu ? Comment avoir quelque chose de rapide et d'efficace ?

III-A. Cheminement du tutoriel

Ce tutoriel devrait grandir. Voici ce qu'il pourrait enseigner au fur et à mesure des versions.

III-A-1. Présentation des techniques

Les jeux de plates-formes ont très rapidement adopté la technique du « Tile-Mapping », depuis leur plus jeune âge. Je présenterai tout d'abord rapidement une technique pleine d'inconvénients, puis nous passerons sur la technique du tile mapping.

III-A-2. Création d'un mini monde avec des chiffres

Nous verrons comment créer un petit monde, d'un seul écran, qui ne bouge pas, grâce à un tableau de chiffres.

III-A-3. Mise en place de quelques propriétés, isolement du code

Pour un jeu de plates-formes, il sera important de définir où est le sol, où est le ciel, de façon à ce que par la suite, notre personnage puisse évoluer dans le monde logiquement. 
Pour un jeu vu de dessus, il sera important de savoir où on a le droit de marcher et où on n'a pas le droit.
Les exemples fournis seront découpés en couches de façon à bien isoler la gestion du monde du reste, et qu'il soit facile, avec une seule fonction, d'afficher un niveau.

III-A-4. Scrolling

Nous verrons ensuite comment faire défiler l'écran (on parle de scrolling), c'est-à-dire comment faire bouger tout le décor de façon à ce que la caméra suive un personnage et que le fond défile derrière lui. Tout cela sera également très facile à manipuler au niveau du code.

III-A-5. Insertion d'un personnage

Nous verrons finalement en deuxième partie, comment insérer un personnage dans un décor, comment faire en sorte que la caméra le suive automatiquement et comment faire en sorte qu'un mur l'arrête (collisions).

III-A-6. Évolution

À l'heure où j'écris ces lignes, le plan futur de ce tutoriel n'est pas encore écrit, mais nous pourrons envisager les points suivants (en fonction de vos commentaires),

en vrac :

  • des « tuiles » animées ;
  • le scrolling en plusieurs couches (derrière, ça défile moins vite que devant) ;
  • des objets qu'on pourrait ramasser ;
  • des ennemis qu'on pourrait insérer ;
  • plusieurs personnages, avec une caméra intelligente ;
  • des blocs cassables ;
  • des pentes, des échelles ;
  • etc.

IV. Technique de l'image figée

Avant de parler « Tile Mapping », voici une technique qui pourrait être utilisée pour faire un jeu de plates-formes.

L'idée qui vient à l'esprit tout de suite est de dessiner son monde sous un logiciel de dessin, « Paint » par exemple. L'idée serait de charger l'image au démarrage du programme, de l'afficher en tant que fond d'écran, puis afficher un Mario par-dessus. Les inconvénients sont les suivants.

IV-A. C'est coûteux en mémoire

En effet, une grande image, c'est parfois plusieurs mégaoctets de mémoire. Si vous voulez faire un grand monde, multipliez par le nombre d'images nécessaires et vous obtiendrez une utilisation mémoire inacceptable…

IV-B. C'est inexploitable

Le plus gros inconvénient est que c'est inexploitable. En effet, si vous affichez votre image de fond, puis que vous affichez Mario, pouvez-vous dire facilement si Mario est sur une plateforme ? Dans l'air ? Dans un mur ? Que s'il avance d'un pas, il tombe ?
Eh non… Vous pouvez éventuellement vous en sortir sur une image où le fond est uni, mais si vous avez une image de fond comme CastleVania ci-dessous, vous ne pouvez pas vous en sortir de cette façon…

Castlevania

Cette technique a trop d'inconvénients pour être utilisée. Je voulais en parler, car c'est souvent la première idée qui vient, de « dessiner » son monde, mais nous allons l'oublier, et passer enfin à la technique dont je vous parle depuis tout à l'heure…

V. Présentation du tile mapping

Avant de définir ce qu'est le Tile Mapping, nous allons ensemble regarder quelques images de jeux connus. Comme on dit « un bon croquis vaut mieux qu'un long discours, cela devrait nous aider. Image non disponible

Super Mario Bros
Zelda

Quelle est la particularité des cartes de ces jeux ?

Eh bien nous pouvons constater que des motifs se répètent. En effet, les sols sont des briques identiques, collées les unes à côté des autres.

Mieux que ça, on peut remarquer la régularité parfaite de la chose : les points d'interrogation de Mario sont exactement au-dessus des briques de sol.

Pareil, dans Zelda, que nous voyons en dessous, il y a des motifs identiques qui se répètent avec une grande régularité.

Le concept de « Tile Mapping » est de coller côte à côte des tuiles (« tiles » en anglais) dans une zone régulière. Pour cela, nous subdivisons l'écran en une grille régulière, et nous mettrons un carreau dans chaque case.

Vous voulez voir la grille ?

Grille Super Mario Bros

Si on regarde bien cette dernière image, et qu'on oublie Mario, l'ennemi, l'étoile, et les nuages, qui sont des sprites, nous avons affaire à un décor très régulièrement placé. Chaque brique s'emboîte parfaitement dans la grille.

Comme déjà dit, chacune de ces petites briques est appelée tuile, ou « tile ».

Il y a les tuiles uniques, comme les briques et les points d'interrogation, et les tuiles composées, comme le pot de fleurs, qui est plus gros qu'une case. Cependant, ce pot de fleurs, bien qu'il semble être un objet unique, sera vu par la machine comme huit cases infranchissables…

L'avantage d'une telle méthode est donc qu'au lieu de définir le monde (considérons qu'il ne bouge pas) par une grande image, on le définit par une grille de 13*15 cases.

VI. Coût mémoire

Nous disions que nous définissons l'image d'au-dessus par 13*15 cases.

Cela fait 195 cases.

Combien y a-t-il de tuiles différentes ?

Sur notre image, on a :

  • le bloc ciel ;
  • le bloc sol ;
  • le point d'interrogation ;
  • quatre tuiles différentes pour le pot de fleurs.

… moins d'une dizaine…

Nous pouvons imaginer un tableau de 13 * 15 cases qui contiennent un nombre. Si le nombre est 0, on met du « ciel », si le nombre est 1, on met un bloc cassable, si c'est 2, on met un « ? », 3 le bord supérieur gauche du pot, 4 le bord supérieur droit, 5 le bord gauche, 6 le bord droit, 7 le sol d'en bas, etc.

On définit, pour notre Mario, le tableau suivant :

 
Sélectionnez
000000000000000 
000000000000000 
000000000000000 
000000000000000 
100000000111110 
000000000000000 
000000000000000 
000000000000000 
003400022220022 
005600000000000 
005600000000000 
005600000000000 
777777777777777

Ce tableau de nombres décrit parfaitement notre monde, car il nous dit, pour chaque case, quel bloc mettre.

Vous suivez toujours ?

Du coup, avec :

  • quelques tuiles,
  • un tableau de nombres,

on définit un monde ! Schématiquement, cela donne :

Schéma conception d'un niveau

La partie de gauche s'appelle « TileSet ». Elle contient les différents carreaux à poser. Ce sont les Tilesets qui définissent le graphisme. Dans mon cas, j'ai fait un tileset d'une seule ligne, mais comme les jeux contiennent quand même davantage de tiles, on définit souvent un tileset sur plusieurs lignes.

Vous trouverez de nombreux exemples sur Google Images en cherchant « tileset » !

La partie du milieu est le tableau de correspondance.

La partie de droite est bien sûr le résultat final.

Revenons maintenant au coût mémoire.

Pour faire mon monde de Mario, j'ai besoin d'un ensemble de tuiles (une petite image en soi), et du tableau.

Si je considère que je n'aurai pas plus de 256 tuiles différentes (je tape très large, et c'est souvent le cas), je peux compter un octet par case de mon tableau.

Avec mon tableau de 13 * 15, j'ai moins de 200 octets…. C'est très petit.

Maintenant, supposons un monde entier de Mario (qui défile). Imaginons le monde déplié ainsi :

Niveau Super Mario Bros déplié

Combien il y a-t-il de cases là-dedans ? À la louche je dirais 300 en largeur et 20 en hauteur.

300 * 20 = 6000 octets.

Voilà, le monde tout entier tient sur une petite image de tuiles + 6 Ko de données : une cacahuète quoi…

Et avec ça, on fait un grand monde.

C'est ainsi qu'ont procédé les consoles 8 et 16 bits, qui n'avaient pas beaucoup de mémoire.

Imaginez que si on avait codé tout le monde sous forme d'une graaaande image, on en aurait eu pour des dizaines de mégaoctets, pour une seule « texture », ce qui aurait bien chargé la carte graphique, et qui, outre ceci, aurait été inexploitable par la suite. (Nous verrons les avantages du « Tile Mapping » lorsque nous parlerons de collisions avec le décor.)

VII. Code exemple

L'algorithme n'est pas complexe. Nous définissons, une fois pour toutes, une longueur et une hauteur de tuiles (qui resteront fixes).

Puis nous faisons un double for (i,j) sur le tableau, et nous affichons la bonne tuile à la position (i * largeur, j * hauteur).

Voici l'exemple suivant en C qui reconstruit le petit monde de Mario (juste la partie qu'on a étudiée)

Téléchargez et dézippez l'ensemble des fichiers de ce tutoriel ci-dessous :

Tous les fichiers.

VII-A. Explication sur les programmes

Vous pouvez constater que le fichier téléchargé contient plusieurs programmes. Je vous dirai au fur et à mesure du tutoriel quel programme ouvrir.

Chaque programme a été fait avec Visual C++ 2008 Express, mais pourra être compilé avec d'autres versions, avec Code::Blocks, GCC, etc.

Si vous ouvrez le répertoire prog1, en dessous, vous avez un autre répertoire prog1, ainsi qu'un fichier .sln. Si vous avez Visual C++, double-cliquez sur le sln : le projet s'ouvre et est prêt à compiler.

Sinon, ouvrez le sous-répertoire prog1. Vous voyez un .vcproj (également pour Visual C++), et les sources et images utilisées.

Tous les projets sont faits de la même façon.

Ouvrez maintenant le projet prog1.

Vous pouvez le compiler et le lancer, ça doit marcher tout seul. (Je rappelle que vous devez avoir de bonnes bases en C,et avec la bibliothèque SDL pour poursuivre ce tutoriel.)

prog1.c
Sélectionnez
#include <SDL/SDL.h> 

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

#define LARGEUR_TILE 24  // hauteur et largeur des tiles. 
#define HAUTEUR_TILE 16 

#define NOMBRE_BLOCS_LARGEUR 15  // nombre a afficher en x et y 
#define NOMBRE_BLOCS_HAUTEUR 13 

char* table[] = { 
"000000000000000", 
"000000000000000", 
"000000000000000", 
"000000000000000", 
"100000000111110", 
"000000000000000", 
"000000000000000", 
"000000000000000", 
"003400022220022", 
"005600000000000", 
"005600000000000", 
"005600000000000", 
"777777777777777"}; 


void Afficher(SDL_Surface* screen,SDL_Surface* tileset,char** table,int nombre_blocs_largeur,int nombre_blocs_hauteur) 
{ 
    int i,j; 
    SDL_Rect Rect_dest; 
    SDL_Rect Rect_source; 
    Rect_source.w = LARGEUR_TILE; 
    Rect_source.h = HAUTEUR_TILE; 
    for(i=0;i<nombre_blocs_largeur;i++) 
    { 
        for(j=0;j<nombre_blocs_hauteur;j++) 
        { 
            Rect_dest.x = i*LARGEUR_TILE; 
            Rect_dest.y = j*HAUTEUR_TILE; 
            Rect_source.x = (table[j][i]-'0')*LARGEUR_TILE; 
            Rect_source.y = 0; 
            SDL_BlitSurface(tileset,&Rect_source,screen,&Rect_dest); 
        } 
    } 
    SDL_Flip(screen); 
} 

int main(int argc,char** argv) 
{ 
    SDL_Surface* screen,*tileset; 
    SDL_Event event; 
    SDL_Init(SDL_INIT_VIDEO);        // prepare SDL 
    screen = SDL_SetVideoMode(LARGEUR_TILE*NOMBRE_BLOCS_LARGEUR, HAUTEUR_TILE*NOMBRE_BLOCS_HAUTEUR, 32,SDL_HWSURFACE|SDL_DOUBLEBUF); 
    tileset = SDL_LoadBMP("tileset1.bmp"); 
    if (!tileset) 
    { 
        printf("Echec de chargement tileset1.bmp\n"); 
        SDL_Quit(); 
        system("pause"); 
        exit(-1); 
    } 
    Afficher(screen,tileset,table,NOMBRE_BLOCS_LARGEUR,NOMBRE_BLOCS_HAUTEUR); 

    do  // attend qu'on appuie sur une touche. 
    { 
        SDL_WaitEvent(&event); 
    } while (event.type!=SDL_KEYDOWN); 
     
    SDL_FreeSurface(tileset); 
    SDL_Quit(); 
    return 0; 
}

Le code n'est pas complexe :

je veux afficher une image de 15 * 13 tuiles (NOMBRE_BLOCS_LARGEUR et NOMBRE_BLOCS_HAUTEUR dans les #define).

Chaque tuile fait 24 * 16 pixels (définis dans LARGEUR_TILE et HAUTEUR_TILE).

Dans le main, j'initialise SDL, la taille de l'image finale, obtenue en faisant l'opération nombre de cases en X * taille d'une tuile, et pareil pour y, bien entendu. Image non disponible

Je charge l'image des tuiles et je lance la fonction Afficher. J'attends qu'on appuie sur une touche pour quitter.

Dans la fonction afficher, je définis deux SDL_Rect, celui de destination dont vous avez l'habitude, et celui source qui sera passé en second paramètre de SDL_BlitSurface pour un blit partiel.

Je fixe Rect_source.w et .h une fois pour toutes, car les tuiles auront toujours la même largeur et la même hauteur.

Ensuite, je fais un double for. Je fixe Rect_dest.x et y à la bonne position (qui dépend de i et de j), puis je définis Rect_source.x, qui lui dépend directement du nombre correspondant. (Nous allons détailler cette ligne ci-dessous.)

Rect_source.y est lui toujours à 0, car dans mon image des tuiles, toutes les tuiles partent de y=0.

Détaillons la ligne suivante :

 
Sélectionnez
Rect_source.x = (table[j][i]-'0')*LARGEUR_TILE;

Nous avons le tableau table défini, en dur, en haut du code.

Nous voulons récupérer le chiffre à la colonne i, ligne j.

D'où l'idée de faire table[i][j].

Cependant, vous remarquerez que j'ai mis [j][i] et non [i][j]. Cela vient de la définition même du tableau dans le code.

Prenons l'exemple avec des mots plutôt que des chiffres :

 
Sélectionnez
char* table[] = { 
"Bonjour", 
"Salut!!", 
"Hello!!", 
"Saloute", 
"Hola!!!" 
}

Si vous choisissez table[1], vous avez « Salut », puis table[1][2] pour avoir le 'l' de Salut.

Donc la lecture d'un tableau de chaîne se fait en ligne/colonne, alors que nous attendons colonne/ligne dans un repère 2D, d'où la transposée [j][i] au lieu de [i][j].

Maintenant, deuxième souci, pourquoi est-ce que je fais -'0' ?

Si je prends table[j][i], je ne tombe pas sur un nombre, mais sur un caractère, sur le code ASCII de '1', celui de '0' etc.

Or le code ASCII de '0' vaut 48. Moi, je ne veux pas 48, je veux 0. En soustrayant '0' à un chiffre en ASCII, on obtient le chiffre réel. C'est une astuce classique, sachant que les chiffres sont contigus dans la table ASCII.

Et voilà, nous avons notre monde de Mario, à partir d'un ensemble de tuiles et de la table de correspondance. Heureux ?

(J'ai peut-être inversé deux tuiles dans mon dessin, mais bon…)

Respirez, et repensez à ce concept à tête reposée. L'essentiel est surtout de comprendre l'idée. Le code va peu à peu évoluer, et surtout se retrouver enfermé dans un autre fichier de façon à vous fournir des fonctions puissantes et simples à utiliser.

Navigation

    Sommaire   Tutoriel suivant : propriétés des tuiles