Big Tuto SDL 2 : Rabidja v. 3.0

Chapitre 6 : Affichons notre premier niveau !

 

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Dernière mise à jour : 8 octobre 2014
Date de révision : 30 mai 2016

 

      Prologue

 

   Eh, voilà ! smiley Nous avons maintenant un magnifique background ! cool

   C'est... comment dire ? frown C'est zen ! angel C'est même très (trop) zen ! indecision

   Il va nous falloir un peu d'action, sinon on va s'ennuyer ferme, quand même ! laugh

   Mais avant d'ajouter le héros du jeu ou même quelques ennemis, il va falloir ajouter un niveau, autrement appelé une map ! wink

   Eh oui, sinon, nos bonhommes tomberaient dans le vide ! cheeky

   Et pour afficher nos maps, nous allons avoir besoin de deux choses :

- d'une part, des fichiers maps, que nous générerons à l'aide d'un éditeur de niveaux,

- et d'autre part, de tilesets.

 

Mais, c'est quoi un tileset ? frown

Un tileset est un ensemble de tiles, autrement appelées tuiles en français, qui sont en fait des fragments de niveaux (elles font généralement 32 x 32 pixels, mais elles ont pu faire moins sur d'anciens jeux (NES ou Master System par exemple) comme : 16 x 16, 8 x 8 ou 16 x 8, etc... Sur des jeux HD, comme, au hasard, Aron's Journey in Dreamland HD wink, on peut en trouver de 64 x 64 pixels voire plus wink). On les copie ensuite de façon répétitive pour créer une map à l'aide d'un éditeur de niveaux.

Si vous débarquez tout juste et que vous découvrez cette notion, je vous conseille alors de lire d'abord ce chapitre théorique avant de continuer, sinon, vous risquez d'être largué ! cheeky

 

   Ok, mais où je trouve un level editor (ou éditeur de niveaux) ? angry

   Alors, il faut savoir que les éditeurs de niveaux que nous avions déjà créé en SDL 1.2 fonctionnent toujours, même s'il va falloir les retoucher pour qu'ils prennent en compte un affichage sur 3 couches. Cela dit, comme notre jeu est maintenant plus complexe, la création d'un véritable level editor digne de ce nom, prend plus de temps. C'est pourquoi, je vous conseille d'utiliser le mien, que je mets à votre disposition dans la section téléchargements. wink

   La version que nous utiliserons pour le début de ce tuto est une ancienne bêta (v 0.82), qui sera bien suffisante pour l'instant. Mais vous pourrez télécharger la version complète avec toutes les fonctions pour la fin du tuto, afin de pouvoir créer votre propre jeu avec le maximum de confort. On verra plus tard comment l'utiliser. wink

 

Télécharger le level editor

 

 

Le nouveau level editor est plus sympathique et gère même les écrans tactiles sous Windows 8

 

   D'accord, mais je n'ai pas de fichiers map, ni de tilesets, non plus ?! surprise
   Alors, pour les tilesets, vous pourrez les télécharger ci-dessous (ou avec le projet complet) :
 

 

tileset1.png et tileset1b.png, à enregistrer dans le dossier graphics

 

   ...et les fichiers map se trouvent dans le dossier map du projet complet, téléchargeable ci-dessous (dans la section téléchargements du site wink).

 

 

 

   Alors, on récapitule : en ce début de chapitre, vous devez donc avoir :

- ajouté un dossier map à votre projet, dans lequel vous aurez mis 2 fichiers : map1.txt et map2.txt, qui seront les fichiers de nos 2 niveaux (mais rassurez-vous, vous pourrez en créer bien d'autres après ! wink)

- copié les fichiers tileset1.png et tileset1b.png dans le dossier graphics de votre projet.

- (facultatif) téléchargé le level editor pour modifier les fichiers map à votre guise.

 

 

      Comment afficher une map ? Un peu de théorie...

 

   Mais pourquoi on a 2 tilesets identiques ? devil

   Si vous regardez attentivement (c'est le jeu des 7 erreurs ! laugh ), vous verrez qu'ils ne sont pas tout à fait identiques : certaines tiles sont un peu différentes. wink Nous allons ainsi créer une animation assez sommaire (sur 2 frames) pour donner l'illusion de la vie à nos niveaux. Bien sûr, on pourrait viser plus de frames avec un système plus compliqué pour gagner en rendu, mais pour un petit jeu de plateforme rétro, ça sera suffisant (pensez que Mario 1 sur NES n'avait pas de tiles animées, ce qui ne l'empêche pas d'être un super jeu ! wink).

   Concrètement, nous allons donc alterner les deux tilesets à la suite avec un timer (chronomètre), pour donner l'illusion du mouvement: un coup, on blittera la tile du tileset1 et l'autre fois la même tile mais dans le tileset1B. smiley

 

   C'est quoi un affichage sur 3 couches (ou layers) ? A quoi cela va-t-il nous servir ? frown

   On pourrait se contenter de tout afficher sur une seule couche (aussi appelée layer en anglais, ou calque en français), comme au début du Big Tuto SDL 1.2. Cependant, l'affichage sur 3 couches est bien plus beau, et pas réellement plus difficile à gérer (la difficulté se trouve essentiellement dans le level editor, mais comme je vous le donne avec son code source ! wink).

 

   Et en quoi, c'est plus beau ? blush

   Cela permet d'empiler jusqu'à 3 tiles au même endroit : on peut ainsi dessiner un arbre devant un mur, par exemple, et encore blitter une fleur à ses pieds. Nos niveaux gagnent ainsi en complexité, sont moins répétitifs et donc plus jolis, et tout cela avec le même tileset minimaliste. wink

   Qui plus est, cela nous permettra aussi de gérer la profondeur. En effet, nos 3 couches vont se répartir ainsi :

- 1. Background : tiles blittées dans le fond : tous les sprites passeront devant et ne pourront pas entrer en collision avec : elles sont juste là pour décorer. wink
- 2. Tiles d'action : ces tiles apparaîtront devant le background et nos sprites entreront en collision avec : c'est donc sur cette couche qu'on trouvera le sol, les power-ups, etc...

- 3. Foreground : tiles blittées par dessus toutes les autres et par-dessus les sprites : tous nos sprites passeront derrière ces tiles : cela nous permettra, par exemple, de faire passer notre héros derrière un arbre, une plante, etc...
 

   Et comment va-t-on stocker notre map, concrètement ? indecision

   Dans des fichiers txt. Vous pouvez d'ailleurs ouvrir ceux que je vous ai donnés, pour voir à quoi ils ressemblent. wink

   En fait, ce sont des lignes et des colonnes de chiffres, représentant chacun le numéro d'une tile à blitter à un emplacement précis de notre niveau, comme nous l'avons vu dans le tutoriel que je vous ai conseillé de lire plus haut.

   Quand nous lirons ces fichiers, nous rentrerons ces valeurs dans des tableaux de tiles (3 tableaux, 1 pour chaque couche de notre niveau).

  Pour vous aider à visualiser ce à quoi ressemblerait un de ces tableaux, en voici un petit exemple :
 

 

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 de la tile dans notre tileset, que l'on blittera aux coordonnées correspondant au numéro de sa case (ligne, colonne) multiplié par la taille d'une tile (soit dans notre cas 32 pixels, car nos tiles font toutes 32 x 32 pixels wink). Regardez à nouveau 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  ? Je vous ai mis des jolies couleurs pour que ce soit mieux !  wink

   Bon, tout ça, c'est encore un petit peu compliqué, mais je vous rassure tout de suite, le code n'est pas si difficile que ça à comprendre, et il est très répétitif ! wink

   Qui plus est, ça peut paraître une tâche immense que de blitter toutes ces centaines de tiles, 60 fois par seconde, mais c'est le PC qui va bosser ! indecision

 

      Le code

   Nous allons, logiquement, commencer par développer notre structure Map, dans notre header (en-tête) structs.h. Nous y ajouterons toutes les variables dont nous aurons besoin pour afficher notre map de niveau. wink

 

Fichier : structs.h

// Structure pour gérer la map à afficher (à compléter plus tard)
typedef struct Map
{
 
SDL_Texture *background;
SDL_Texture *tileSet, *tileSetB;
 
//Numéro du tileset à utiliser
int tilesetAffiche;
 
/* Coordonnées de départ du héros, lorsqu'il commence le niveau */
int beginx, beginy;
 
/* 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];
 
//Deuxième couche de tiles
int tile2[MAX_MAP_Y][MAX_MAP_X];
 
//Troisième couche de tiles
int tile3[MAX_MAP_Y][MAX_MAP_X];
 
/* Timer et numéro du tileset à afficher pour animer la map */
int mapTimer, tileSetNumber;
 
} Map;

 

   Comme vous pouvez le voir, on rajoute pas mal de variables ! wink
   Je vous fais l'article rapidement :

- les SDL_Textures tileSet et tileSetB contiendront, selon toute logique, nos deux tilesets. cheeky

- tilesetAffiche gardera en mémoire le numéro du tileset affiché à l'écran,

- beginx et beginy contiendront les coordonnées du point de départ de notre héros. Ce point de départ est paramétrable dans l'éditeur de niveaux wink, on n'est pas obligé de toujours commencer en haut à gauche ! indecision

- startx et starty contiendront le point de départ à partir duquel on doit dessiner la map. Pour l'instant, ce sera (0 ; 0), mais ces valeurs seront amenées à changer plus tard quand on mettra en place notre caméra et notre scrolling. wink

- maxX et maxY sont les coordonnées de la fin de la map. On verra dans la fonction loadMap() qu'on scanne le fichier de la map, jusqu'à ce qu'il n'y ait plus que des 0 (= absence de tile) : ce sera alors la fin de notre niveau. Ainsi, peu importe la taille de notre map dans le level editor, le jeu s'y adaptera automatiquement ! cool

- nos 3 tableaux tile, tile2 et tile3, de taille MAX_MAP_X et MAX_MAP_Y contiendront nos niveaux, en enregistrant pour chaque ligne et chaque colonne, le numéro de la tile à afficher, comme nous l'avons vu plus haut (et dans le tuto dédié que je vous avais invité à lire wink).

- enfin mapTimer sera notre chrono pour savoir quel tileset (A ou B) afficher, valeur qui sera contenue dans tileSetNumbercheeky

   Voilà, complétons maintenant nos defs :

 

Fichier : defs.h - rajoutez :

/* Taille maxi de la map : 400 x 150 tiles */
#define MAX_MAP_X 400
#define MAX_MAP_Y 150
 
/* Taille d'une tile (32 x 32 pixels) */
#define TILE_SIZE 32
 
/* Constantes pour l'animation */
#define TIME_BETWEEN_2_FRAMES 20

 

   Nous définissons donc notre map pour avoir les dimensions max de 400 tiles de large par 150 de hauteur, ce qui est assez grand, puisque cela représente : (400 x 32 =) 12 800 x (150 x 32 =) 4800 pixels ! angel

   La taille de notre tile de base sera de 32 x 32 pixels (si plus tard, vous souhaitez faire un jeu rétro, ou au contraire HD, vous pourrez changer facilement cette variable, et tout le reste devrait s'adapter automatiquement wink).

   Et enfin, on définit un timer, ou chrono, de 20 tours de boucle (soit 1/3 de seconde puisqu'on est en 60 fps) pour l'anim' de notre map.

   Passons maintenant à l'initialisation de notre map :

 

Fichier : init.c

void loadGame(void)
{
 
//On charge les données pour la map
initMaps();
 
//On commence au premier niveau
SetValeurDuNiveau(1);
changeLevel();
 
}

 

   Dans la fonction loadGame(), on va rajouter l'appel à deux fonctions que nous créerons ensuite : setValeurDuNiveau() qui se chargera simplement de changer la valeur du niveau à 1, pour afficher la map 1, et changeLevel() qui chargera le niveau en question.

   Et c'est tout, passons maintenant au fichier map.c :

Attention : En fonction de l'IDE que vous utiliserez le code sera sensiblement différent pour certaines fonctions. En effet, si Code::Blocks ne lève pas de warnings pour des fonctions telles que sprint_f ou fopen, Visual Studio les considère comme dépréciées et ne les aime pas du tout. Il existe un moyen d'ignorer ces warnings, mais tant qu'à utiliser VS, j'ai choisi de suivre ses recommandations en matière de sécurité, et j'ai donc mis à jour le code. wink

Fichier : map.c - modifiez :

void initMaps(void)
{
// Charge l'image du fond (background)
map.background = loadImage("graphics/background.png");
 
//On initialise le timer
map.mapTimer = TIME_BETWEEN_2_FRAMES * 3;
map.tileSetNumber = 0;
 
}

 

   Commençons par initMaps(). Vous remarquerez que c'est très simple, puisqu'on rajoute simplement la valeur de notre timer, et on initialise à 0 la valeur du tileset (même si cette valeur sera changée par la suite, car on n'a pas de tileset 0 wink).

 

 

Version Visual Studio

Fichier : map.c - ajoutez la fonction :

void loadMap(char *name)
{
int x, y;
FILE *fp;
errno_t err;
 
//Ouvre le fichier en lecture (renvoie une erreur s'il n'existe pas)
if ((err = fopen_s(&fp, name, "rb")) != 0)
{
printf("Le fichier map n'a pas pu etre ouvert.\n");
exit(1);
}
 
/* Lit les données du fichier dans la map */
 
/* Lit les coordonnées de début du joueur */
fscanf_s(fp, "%d", &map.beginx);
fscanf_s(fp, "%d", &map.beginy);
 
/* Read the number of the tileset */
fscanf_s(fp, "%d", &map.tilesetAffiche);
 
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_s(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;
}
}
}
}
 
//Deuxième couche de tiles
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_s(fp, "%d", &map.tile2[y][x]);
}
}
 
//Troisième couche de tiles
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_s(fp, "%d", &map.tile3[y][x]);
}
}
 
/* 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;
 
/* Et on referme le fichier */
fclose(fp);
 
}

   Et la même chose pour Code::Blocks. Attention, ne copiez bien qu'une seule de ces deux versions (la bonne de préférence ! laugh).

 
 

Version Code::Blocks

Fichier : map.c - ajoutez la fonction :

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 */
 
/* Lit les coordonnées de début du joueur */
fscanf(fp, "%d", &map.beginx);
fscanf(fp, "%d", &map.beginy);
 
/* Read the number of the tileset */
fscanf(fp, "%d", &map.tilesetAffiche);
 
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;
}
}
}
}
 
//Deuxième couche de tiles
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.tile2[y][x]);
}
}
 
//Troisième couche de tiles
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.tile3[y][x]);
}
}
 
/* 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;
 
/* Et on referme le fichier */
fclose(fp);
 
}

 

   C'est cette fonction qui va se charger de lire et de charger nos fichiers maps.

   Pour cela, elle ouvre le fichier dont on lui envoie le nom en paramètre. 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, elle enregistre les 3 premiers chiffres du fichier dans les variables beginx, beginy et tilesetAffiche. C'est un choix que nous avons fait, en créant notre level editor : celui-ci enregistre les coordonnées de départ du joueur et le tileset à utiliser au début du fichier. Notez qu'on aurait aussi pu les mettre à la fin (cela aurait même été plus simple en fait, mais bon... indecision).

   Ensuite, elle va balayer toutes les valeurs des tiles et les stocker au bon endroit dans nos tableaux de tiles. Notez que le procédé se répète pour nos 3 tableaux et que le plus simple (et le plus clair) était un simple copier/coller. cheeky

   Elle lit donc le fichier nombre par nombre et ligne par ligne (d'où la double boucle) et remplit 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 numéro 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é pourquoi ces 2 variables changent à chaque fois que la valeur d'une tile est différente de 0. frown En fait, si notre fichier a une taille prédéfinie, on veut pouvoir se laisser le choix de faire des niveaux de la taille qu'on veut (c'est mieux, non ? cheeky).

   Alors pour ça, c'est très simple : les limites du fichier à lire sont clairement indiqués dans le programme (donc inchangeables, c'est MAX_MAP_X et Y) mais pas celles de la map. Comme je vous l'ai déjà dit, elles sont définies à la lecture du fichier 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 qu'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 150 tiles soit la taille du fichier). Malin, non ? wink En plus, ça marche avec seulement 5 lignes de code !

   Et on n'oublie pas de fermer le fichier à la fin ! (C'est hyper important ! cheeky)

   Passons maintenant à la fonction drawMap() :

 

Fichier : map.c - ajoutez la fonction

void drawMap(int layer)
{
int x, y, mapX, x1, x2, mapY, y1, y2, xsource, ysource, a;
 
/* On initialise mapX à la 1ère colonne qu'on doit blitter.
Celle-ci correspond au x de la map (en pixels) divisés par la taille d'une tile (32)
pour obtenir la bonne colonne de notre map
Exemple : si x du début de la map = 1026, on fait 1026 / 32
et on sait qu'on doit commencer par afficher la 32eme colonne de tiles de notre map */
mapX = map.startX / TILE_SIZE;
 
/* Coordonnées de départ pour l'affichage de la map : permet
de déterminer à quels coordonnées blitter la 1ère colonne de tiles au pixel près
(par exemple, si la 1ère colonne n'est visible qu'en partie, on devra commencer à blitter
hors écran, donc avoir des coordonnées négatives - d'où le -1). */
x1 = (map.startX % TILE_SIZE) * -1;
 
/* Calcul des coordonnées de la fin de la map : jusqu'où doit-on blitter ?
Logiquement, on doit aller à x1 (départ) + SCREEN_WIDTH (la largeur de l'écran).
Mais si on a commencé à blitter en dehors de l'écran la première colonne, il
va falloir rajouter une autre colonne de tiles sinon on va avoir des pixels
blancs. C'est ce que fait : x1 == 0 ? 0 : TILE_SIZE qu'on pourrait traduire par:
if(x1 != 0)
x2 = x1 + SCREEN_WIDTH + TILE_SIZE , mais forcément, c'est plus long ;)*/
x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);
 
/* On fait exactement pareil pour calculer y */
mapY = map.startY / TILE_SIZE;
y1 = (map.startY % TILE_SIZE) * -1;
y2 = y1 + SCREEN_HEIGHT + (y1 == 0 ? 0 : TILE_SIZE);
 
 
//On met en place un timer pour animer la map (chapitre 19)
if (map.mapTimer <= 0)
{
if (map.tileSetNumber == 0)
{
map.tileSetNumber = 1;
map.mapTimer = TIME_BETWEEN_2_FRAMES * 3;
}
else
{
map.tileSetNumber = 0;
map.mapTimer = TIME_BETWEEN_2_FRAMES * 3;
}
 
}
else
map.mapTimer--;
 
 
/* 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) */
if (layer == 1)
{
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 = le numéro
de la tile */
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 suivant le timer */
if (map.tileSetNumber == 0)
drawTile(map.tileSet, x, y, xsource, ysource);
else
drawTile(map.tileSetB, x, y, xsource, ysource);
 
mapX++;
}
 
mapY++;
}
}
 
else if (layer == 2)
{
//Deuxième couche de tiles ;)
for (y = y1; y < y2; y += TILE_SIZE)
{
mapX = map.startX / TILE_SIZE;
 
for (x = x1; x < x2; x += TILE_SIZE)
{
 
/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro
de la tile */
a = map.tile2[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 suivant le timer */
if (map.tileSetNumber == 0)
drawTile(map.tileSet, x, y, xsource, ysource);
else
drawTile(map.tileSetB, x, y, xsource, ysource);
 
mapX++;
}
 
mapY++;
}
}
 
else if (layer == 3)
{
//Troisième couche de tiles ;)
for (y = y1; y < y2; y += TILE_SIZE)
{
mapX = map.startX / TILE_SIZE;
 
for (x = x1; x < x2; x += TILE_SIZE)
{
 
/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro
de la tile */
a = map.tile3[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 suivant le timer */
if (map.tileSetNumber == 0)
drawTile(map.tileSet, x, y, xsource, ysource);
else
drawTile(map.tileSetB, x, y, xsource, ysource);
 
mapX++;
}
 
mapY++;
}
}
 
 
}

 

   Je vous laisse d'abord lire les commentaires de ce fichier, qui sont déjà très complets. wink

   Quand notre caméra et notre scrolling seront opérationnels (bientôt, bientôt wink), 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 mais pour faire court, on calcule par quelles colonne et ligne de tiles on doit commencer l'affichage, selon les valeurs de map.startX et map.startY (qui augmenteront/diminueront plus tard selon les inputs et l'avancée de notre héros wink). Ensuite, on calcule où commencer à blitter la première colonne et la première ligne (parfois hors-écran selon le scrolling). Enfin, on calcule où arrêter l'affichage de la map : si la 1ère ligne/colonne a été blittée hors écran, il va en effet falloir blitter une ligne/colonne de plus. cheeky

   Sinon, vous aurez remarqué que, là encore, on a 3 versions du même code selon la couche à afficher. En effet, afin de pouvoir afficher la couche que l'on veut, quand on veut (et pas les 3 à la suite, puisqu'il va nous falloir ensuite intercaler nos sprites entre elles wink ), on prend en argument le numéro de la couche ou layer à afficher et on la traite. A l'intérieur de chaque couche, l'affichage se fait 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 ? wink

    Pour chaque "case" de notre tableau, on reprend la valeur de notre tile (comme on l'a vu précédemment) et on blitte la tile correspondante à cet endroit précis grâce à notre fonction drawTile(), que nous rajouterons par la suite. wink

    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 ? cheeky Mettons qu'on veuille afficher la tile N° 43 (eau basse bleue) puis la 126 (Monster 1) :

 


Pour la 43 :
xsource = 43 % 10 x 32 = 3 x 32 = 96
ysource = 43 / 10 x 32 = 4 x 32 = 128
 
Pour la 126 :
xsource = 126 % 10 x 32 = 6 x 32 = 192
ysource = 126 / 10 x 32 = 12 x 32 = 384 
 
Cela marche !!! Ouf ! indecision
 

   Enfin, pour trouver les coordonnées où 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 ! wink

   Bon, je sais que ce chapitre peut paraître un peu indigeste jusque-là puisqu'on voit beaucoup de choses nouvelles d'un coup, mais on arrive bientôt au bout, courage ! wink

   Qui plus est, n'oubliez pas que vous pouvez aussi consulter le Big Tuto SDL 1.2 / 2 qui va un peu plus lentement (ici, on est dans le tuto Experts ! laugh).

   Passons à notre fonction changeLevel() dont nous avons parlé précédemment. Là encore, elle va changer selon notre IDE :

 

Version Visual Studio

Fichier : map.c - fonction à rajouter :

 

void changeLevel(void)
{
 
char file[200];
 
/* Charge la map depuis le fichier */
sprintf_s(file, sizeof(file), "map/map%d.txt", getLevel());
loadMap(file);
 
//Charge le tileset
if (map.tileSet != NULL)
{
SDL_DestroyTexture(map.tileSet);
}
if (map.tileSetB != NULL)
{
SDL_DestroyTexture(map.tileSetB);
}
 
sprintf_s(file, sizeof(file), "graphics/tileset%d.png", map.tilesetAffiche);
map.tileSet = loadImage(file);
 
sprintf_s(file, sizeof(file), "graphics/tileset%dB.png", map.tilesetAffiche);
map.tileSetB = loadImage(file);
 
}

 

 

Version Code::Blocks

Fichier : map.c - fonction à rajouter :

void changeLevel(void)
{
 
char file[200];
 
/* Charge la map depuis le fichier */
sprintf(file, "map/map%d.txt", getLevel());
loadMap(file);
 
//Charge le tileset
if (map.tileSet != NULL)
{
SDL_DestroyTexture(map.tileSet);
}
if (map.tileSetB != NULL)
{
SDL_DestroyTexture(map.tileSetB);
}
 
sprintf(file, "graphics/tileset%d.png", map.tilesetAffiche);
map.tileSet = loadImage(file);
 
sprintf(file, "graphics/tileset%dB.png", map.tilesetAffiche);
map.tileSetB = loadImage(file);
 
}

 

   Heureusement, cette fonction est bien plus simple (les 2 plus compliquées étaient loadMap() et drawMap(), on a donc passé le pire ! cheeky).

   On envoie simplement le nom du fichier map à charger en fonction du niveau. Puis, on détruit les tilesets chargés précédemment s'il y en a, pour charger ceux correspondant à notre map. Comme c'est très rapide, on n'a pas besoin d'écran de Loading. wink

   Enfin, il est temps de faire un peu de ménage :

 

Fichier : map.c - mettre à jour :

void cleanMaps(void)
{
// Libère la texture du background
if (map.background != NULL)
{
SDL_DestroyTexture(map.background);
map.background = NULL;
}
 
// Libère les textures des tilesets
if (map.tileSet != NULL)
{
SDL_DestroyTexture(map.tileSet);
map.tileSet = NULL;
}
 
if (map.tileSetB != NULL)
{
SDL_DestroyTexture(map.tileSetB);
map.tileSetB = NULL;
}
 
}

 

   Comme le numéro du niveau en cours va être stocké dans le fichier player.c, il va nous falloir le créer et l'ajouter au projet.

   Copiez-y ensuite le code suivant :

 

Fichier : player.c - créer le fichier

#include "prototypes.h"
 
 
int level;
 
 
int getLevel(void)
{
return level;
}
 
void SetValeurDuNiveau(int valeur)
{
level = valeur;
}

 

 

   Voilà, pour l'instant c'est très embryonnaire : on y trouve simplement notre variable level et une fonction pour y accéder ainsi qu'une autre pour la modifier. wink

   Rien de plus ! cheeky

   Passons maintenant à notre fichier draw.c !

 

Fichier : draw.c - fonction à mettre à jour :

void drawGame(void)
{
// Affiche le fond (background) aux coordonnées (0,0)
drawImage(getBackground(), 0, 0);
 
/* Affiche la map de tiles : layer 2 (couche du fond) */
drawMap(2);
 
/* Affiche la map de tiles : layer 1 (couche active : sol, etc.)*/
drawMap(1);
 
/* Affiche la map de tiles : layer 3 (couche en foreground / devant) */
drawMap(3);
 
// Affiche l'écran
SDL_RenderPresent(getrenderer());
 
// Délai pour laisser respirer le proc
SDL_Delay(1);
}

 

   On met d'abord à jour notre fonction drawGame() pour qu'elle affiche les 3 couches de la map dans l'ordre (couche 1 = action, couche 2 = background et couche 3 = foreground - cf. ci-dessus). wink

 

Fichier : draw.c - fonction à rajouter :

void drawTile(SDL_Texture *image, int destx, int desty, int srcx, int srcy)
{
/* Rectangle de destination à dessiner */
SDL_Rect dest;
 
dest.x = destx;
dest.y = desty;
dest.w = TILE_SIZE;
dest.h = TILE_SIZE;
 
/* Rectangle source */
SDL_Rect src;
 
src.x = srcx;
src.y = srcy;
src.w = TILE_SIZE;
src.h = TILE_SIZE;
 
/* Dessine la tile choisie sur l'écran aux coordonnées x et y */
SDL_RenderCopy(getrenderer(), image, &src, &dest);
}

 

   Et voici enfin notre fonction drawTile() dont le but va être de découper notre tileset pour afficher uniquement la bonne tile.

   Pour cela, elle crée 2 rectangles :
- le rectangle source correspond aux coordonnées de la tile à découper dans notre tileset et à ses dimensions (32 x 32 pixels).
- le rectangle de destination correspond aux coordonnées de la map où on va blitter la tile.

   Plus qu'à mettre à jour nos prototypes et c'est fini ! indecision

 

Fichier : prototypes.h - mettre à jour :

 
#ifndef PROTOTYPES
#define PROTOTYPES
 
#include "structs.h"
 
/* Catalogue des prototypes des fonctions utilisées.
On le complétera au fur et à mesure. */
 
extern void changeLevel(void);
extern void cleanMaps(void);
extern void cleanup(void);
extern void delay(unsigned int frameLimit);
extern void drawGame(void);
extern void drawImage(SDL_Texture *, int, int);
extern void drawMap(int);
extern void drawTile(SDL_Texture *image, int destx, int desty, int srcx, int srcy);
extern void gestionInputs(Input *input);
extern SDL_Texture *getBackground(void);
extern void getInput(Input *input);
extern int getLevel(void);
extern SDL_Renderer *getrenderer(void);
extern void init(char *);
extern void initMaps(void);
extern void loadGame(void);
extern SDL_Texture *loadImage(char *name);
extern void loadMap(char *name);
extern void SetValeurDuNiveau(int valeur);
 
 
#endif

 

 

   Et c'est fini ! On peut maintenant compiler, lancer le programme et admirer le début de notre niveau s'animer sous nos yeux ébahis ! laugh

 

 

   Bon, c'était un chapitre un peu dense, avec beaucoup de nouveautés et de choses à digérer, c'est pourquoi le chapitre prochain sera plus light : je vous montrerai comment utiliser le level editor pour modifier la map, et revoir un peu tout ce que nous avons vu ici. wink

   @ bientôt et merci de votre fidélité au site !

                                                                      Jay.

 

 

Connexion

CoalaWeb Traffic

Today78
Yesterday297
This week875
This month4549
Total1743756

26/04/24