• Difficile

Mis à jour le 06/12/2013

Propriétés des tiles

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

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

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émentaire, ça devient assez contraignant.

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

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

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 2 blocs :

  • #tileset ;

  • #level.

Le bloc tileset

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

Image utilisateur

Ensuite, la ligne suivante contient "8 1". Je définis simplement combien on a de tiles en X, et combien en Y. L'image contient 8 tiles, tous 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 tiles lus, on peut calculer la largeur et la hauteur d'un tile 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 tile s'il est plein ou vide, pour les futurs calculs du jeu.

Typiquement, pour un Mario, seul le premier tile (du ciel) est vide : les personnages pourront s'y promener. Tous les autres sont pleins : on ne pourra pas les traverser.
Pour un Zelda, on aurait en tiles vides tous les sols sur lesquels on peut marcher, et en tiles pleins tous les tiles de rochers, d'arbres, de murs.

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

Le bloc level

Le bloc level contient d'abord le nombre de tiles en x et y du level (15 et 13), puis directement le tableau qui définit le level, 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 10 tiles.

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

0   150  1 8 
101   6  6 2

La balise "fin" explicitera la fin du fichier.

Structure dans le programme

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

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;

La structure Map

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

  • LARGEUR_TILE, HAUTEUR_TILE : les noms parlent d'eux mêmes.

  • nbtilesX,nbtilesY : nombre de tiles dans le tileset, 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 tileset chargée.

  • props : Un tableau de propriétés pour chaque tile. 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.

Le type tileindex

schema est un tableau vers des types "tileindex" que j'ai défini plus haut.
Dans notre cas, j'ai défini tileindex à "unsigned char", car je n'aurai pas plus de 256 tiles.
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 tiles dans la même map, c'est une folie...

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 tous les tiles à afficher, je calculais un rectangle source sur le tileset, à chaque fois.
Comme on a beaucoup de tiles identiques, on recalculait plein de fois les mêmes.

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

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

Image utilisateur

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 toit.
Par contre, si on vous dit "maintenant, ou on fait passer les tuyaux d'eau ? Les cables ?", et 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.

Etudions le programme.

Regardons d'abord le fmap.h

#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 3 fonctions seulement :

  • ChargerMap ;

  • AfficherMap ;

  • LibererMap.

Si vous n'êtes pas curieux, ce sont des fonctions magiques.
A la première, vous passez le fichier level.txt, et elle vous remplit une Map qu'elle vous donne.
A la seconde, vous donnez la map, et l'écran "screen" ou vous voulez l'afficher, et elle l'affiche.
A la troisième, vous donnez la map, elle nettoie proprement.

Maintenant, regardons le main, dans prog2.c

#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 map, 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, voila comment faire simple.

Pour les curieux, regardons fmap.c

#define _CRT_SECURE_NO_DEPRECATE   // pour visual C++ qui met des warning pour fopen et fscanf : aucun effet negatif 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 de blit optimale.

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

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.

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