• Difficile

Mis à jour le 06/12/2013

Présentation générale

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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.

Problématique

Présentation

Vous avez déjà tous joué à ce qu'on appelle des jeux de plateforme 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 plateforme, duquel nous allons nous inspirer, est Super Mario Bros.

Image utilisateur

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

Image utilisateur

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 ?

Cheminement du tutoriel

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

Présentation des techniques.

Les jeux de plateforme 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.

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 a un tableau de chiffres.

Mise en place de quelques propriétés, isolement du code

Pour un jeu de plateformes, 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 ou on a le droit de marcher, et ou 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.

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.

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).

Evolution

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

En vrac :

  • des « tiles » 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.

Technique de l'image figée

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

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 ensuite, d'afficher un Mario par dessus. Les inconvénients sont les suivants.

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'image nécessaires, et vous obtiendrez une utilisation mémoire inacceptable...

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 ?
Et non... Vous pouvez éventuellement vous en sortir sur une image ou 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...

Image utilisateur

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...

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 qu'un schéma en dit plus qu'un long discours, cela devrait nous aider. :)

Image utilisateur
Image utilisateur

Quelle est la particularité des cartes de ces jeux ?
Et bien nous pouvons constater que des motifs se répètent. En effet, les sols sont des briques identiques, collées les unes à coté 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 "tiles" (c'est à dire des tuiles 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 ?

Image utilisateur

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 tiles uniques, comme les briques et les points d'interrogation, et les tiles composés, comme le pot de fleur, qui est plus gros qu'une case. Cependant, ce pot de fleur, bien qu'il semble être un objet unique, sera vu par la machine comme 8 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.

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 tiles différents ?
Sur notre image, on a :

  • le bloc ciel ;

  • le bloc sol ;

  • le point d'interrogation ;

  • quatre tiles différents 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 :

000000000000000
000000000000000
000000000000000
000000000000000
100000000111110
000000000000000
000000000000000
000000000000000
003400022220022
005600000000000
005600000000000
005600000000000
777777777777777

Ce tableau de nombre décrit parfaitement notre monde, car il nous dit, pour chaque case, quel bloc mettre.
Vous suivez toujours ?

Du coup, avec :

  • quelques tiles ;

  • un tableau de nombres.

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

Image utilisateur

La partie de gauche s'appelle "TileSet". Elle contient les différents carreaux a 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 exemple sur google image en cherchant "tileset" !
La partie du milieu est le tableau de correspondance.
La partie de droite est bien sur le résultat final.

Revenons maintenant au coût mémoire.

Pour faire mon monde de Mario, j'ai besoin du tileset (une petite image en soi), et du tableau.
Si je considère que je n'aurai pas plus de 256 tiles différents (je tape très large, et c'est souvent le cas), je peux compter 1 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 :

Image utilisateur

Combien il y a-t-il de cases la dedans ? A la louche je dirais 300 en largeur, et 20 en hauteur.
300 * 20 = 6000 octets.

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

Et avec ça, on fait un monde grand.
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.)

Code exemple

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

Puis nous faisons un double for (i,j) sur le tableau, et nous "blittons" le bon tile à 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é)

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

Tous les fichiers

Explication sur les programmes

Vous pouvez constater que le fichier téléchargé contient plusieurs programmes. Je vous dirais 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::blokcs, 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 d'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.

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 librairie SDL pour poursuivre ce tutoriel).

prog1.c

#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")

#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 tiles (NOMBRE_BLOCS_LARGEUR et NOMBRE_BLOCS_HAUTEUR dans les #define).
Chaque tile 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'un tile, et pareil pour y, bien entendu. :)
Je charge le tileset, et je lance la fonction afficher. J'attends qu'on appuie sur une touche pour quitter.

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

Je fixe Rect_source.w et .h une fois pour toutes, car les tiles auront toujours la même largeur et la même hauteur.
Puis 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_dest.x, qui lui dépend directement du nombre correspondant. (nous allons détailler cette ligne ci dessous).
Rect_source.y est lui toujours a 0, car dans mon tileset, tous les tiles partent de y=0.

Détaillons la ligne suivante :

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'ou l'idée de faire table[i][j].

Cependant, vous remarquerez je j'y 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 :

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 soucis, pourquoi est ce que je fais -'0' ?
Si je prends table[j][i], je tombe 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 contiguës dans la table ASCII.

Et voilà, nous avons notre monde de Mario, à partir du tileset et de la table de correspondance. Heureux ?

(J'ai peut être inversé 2 tiles 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.

Exemple de certificat de réussite
Exemple de certificat de réussite