Big Tuto SFML 2 : Rabidja v. 3.0

Chapitre 5 : Affichons notre premier niveau !

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 14 février 2015
Date de révision : 22 novembre 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 par exemple 16 x 16, 8 x 8 ou 16 x 8, etc... Sur des jeux HD, 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

   Vous êtes tout à fait libres de créer le vôtre, mais cela peut être un petit peu délicat, quand on débute et qu'on doit déjà se focaliser sur le jeu en lui-même. C'est pourquoi, je vous conseille d'utiliser le mien, que je mets à votre disposition dans la section téléchargements. wink

   Il est maintenant tout à fait fonctionnel, et tourne en SDL2, mais cela ne fait rien, car vous aurez juste besoin de le lancer (à moins que ne vouliez voir son code source wink). Les maps éditées fonctionneront ensuite très bien avec notre jeu en SFML (ce sont juste des fichiers texte cheeky).

   On verra au chapitre prochain 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 / 8.1 / 10

 


  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, si on devait le tracer à la main, 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 du tile dans notre tileset, que l'on blittera aux dimensions 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 compliqué 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

   Il va maintenant être temps de développer notre classe Map, et de lui faire afficher plus que le seul background ! wink

   Commençons par le header map.h : effacer le contenu de votre fichier et remplacez-le par le code ci-dessous :

 

Fichier : map.h : remplacez le contenu du fichier par :

//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#ifndef MAP_H
#define MAP_H
 
#include <SFML/Graphics.hpp>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
 
 
class Map
{
public:
 
//Constructeur
Map();
 
//Accesseurs
int getBeginX(void) const;
int getBeginY(void) const;
int getStartX(void) const;
int getStartY(void) const;
int getMaxX(void) const;
int getMaxY(void) const;
int getTile(int y, int x) const;
int getLevel(void) const;
 
//Mutateurs
void setLevel(int valeur);
void setStartX(int valeur);
void setStartY(int valeur);
void setTile(int y, int x, int valeur);
 
//Fonctions
void drawBackground(sf::RenderWindow &window);
void loadMap(std::string filename);
void draw(int layer, sf::RenderWindow &window);
void changeLevel(void);
void testDefilement(void);
 
 
private:
//Variables de la classe en accès privé
 
//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[150][400];
 
//Deuxième couche de tiles
int tile2[150][400];
 
//Troisième couche de tiles
int tile3[150][400];
 
/* Timer et numéro du tileset à afficher pour animer la map */
int mapTimer, tileSetNumber;
 
//Numéro du niveau en cours
int level;
 
//Background
sf::Texture backgroundTexture;
sf::Sprite background;
 
//Tilesets
sf::Texture tileSet1Texture;
sf::Sprite tileSet1;
sf::Texture tileSet1BTexture;
sf::Sprite tileSet1B;
 
 
/*******************/
/* Constantes */
/******************/
 
// Taille de la fenêtre : 800x480 pixels
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 480;
 
/* Taille maxi de la map : 400 x 150 tiles */
const int MAX_MAP_X = 400;
const int MAX_MAP_Y = 150;
 
/* Taille d'une tile (32 x 32 pixels) */
const int TILE_SIZE = 32;
 
/* Constante pour l'animation */
const int TIME_BETWEEN_2_FRAMES = 20;
 
 
/*************************/
/* VALEURS DES TILES */
/************************/
 
// Constante définissant le seuil entre les tiles traversables
// (blank) et les tiles solides
const int BLANK_TILE = 99;
 
//Plateformes traversables
const int TILE_TRAVERSABLE = 80;
 
//Tiles Power-ups
const int TILE_POWER_UP_DEBUT = 77;
const int TILE_POWER_UP_FIN = 79;
const int TILE_POWER_UP_COEUR = 78;
 
//Autres Tiles spéciales
const int TILE_RESSORT = 125;
const int TILE_CHECKPOINT = 23;
const int TILE_MONSTRE = 136;
const int TILE_PIKES = 127;
 
//Tiles plateformes mobiles
const int TILE_PLATEFORME_DEBUT = 130;
const int TILE_PLATEFORME_FIN = 131;
 
// Tiles pentes à 26.5° ; BenH = de BAS en HAUT ; HenB = De HAUT en BAS
const int TILE_PENTE_26_BenH_1 = 69;
const int TILE_PENTE_26_BenH_2 = 70;
const int TILE_PENTE_26_HenB_1 = 71;
const int TILE_PENTE_26_HenB_2 = 72;
 
};
#endif

 

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

- les Textures et Sprites tileSet1 et tileSet1B 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, contiendront nos niveaux (400 tiles de long par 150 de hauteur - vous pouvez le changer, mais il faudra alors aussi adapter les fichiers map et le level editor en conséquence wink), 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).

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

- enfin level contiendra le numéro du niveau à afficher.

 

   Voilà, après vous pourrez voir tout un tas de constantes qui nous serviront dans la gestion de nos maps.

   Nous définissons ainsi 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.

   Les valeurs des tiles, quant à elle, correspondantes aux numéros des tiles spéciales dans le tileset, mais on verra ça plus tard.

 

   Pour ce qui est des fonctions, on va voir tout ça dans le fichier map.cpp. wink

   Je vous donne d'abord tout le code de la classe d'un coup (plus facile à copier / coller à la place du code existant), puis on reviendra sur chaque fonction une par une. smiley

 

Fichier : map.cpp : remplacez le contenu du fichier par :

//Rabidja 3 - nouvelle version intégralement en SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#include "map.h"
 
using namespace std;
using namespace sf;
 
//Constructeur
Map::Map()
{
//Chargement des ressources graphiques
//Chargement du background
if (!backgroundTexture.loadFromFile("graphics/background.png"))
{
// Erreur
cout << "Erreur durant le chargement de l'image de background." << endl;
}
else
background.setTexture(backgroundTexture);
 
//Chargement des 2 tilesets n°1
if (!tileSet1Texture.loadFromFile("graphics/tileset1.png"))
{
// Erreur
cout << "Erreur durant le chargement de l'image du tileset 1." << endl;
}
else
tileSet1.setTexture(tileSet1Texture);
 
if (!tileSet1BTexture.loadFromFile("graphics/tileset1b.png"))
{
// Erreur
cout << "Erreur durant le chargement de l'image du tileset 1b." << endl;
}
else
tileSet1B.setTexture(tileSet1BTexture);
 
 
//Autres variables
mapTimer = TIME_BETWEEN_2_FRAMES * 3;
tileSetNumber = 0;
level = 1;
startX = startY = 0;
}
 
 
//Accesseurs
int Map::getBeginX(void) const { return beginx; }
int Map::getBeginY(void) const { return beginy; }
int Map::getStartX(void) const { return startX; }
int Map::getStartY(void) const { return startY; }
int Map::getMaxX(void) const { return maxX; }
int Map::getMaxY(void) const { return maxY; }
int Map::getTile(int y, int x) const { return tile[y][x]; }
int Map::getLevel(void) const { return level; }
 
 
//Mutateurs
void Map::setLevel(int valeur) { level = valeur; }
void Map::setStartX(int valeur) { startX = valeur; }
void Map::setStartY(int valeur) { startY = valeur; }
void Map::setTile(int y, int x, int valeur) { tile[y][x] = valeur; }
 
 
//Fonctions
 
void Map::changeLevel(void)
{
string filename;
filename = "map/map" + to_string(level) + ".txt";
loadMap(filename);
}
 
 
void Map::drawBackground(RenderWindow &window)
{
window.draw(background);
}
 
 
void Map::loadMap(string filename)
{
//On crée un flux (stream) pour lire notre fichier
//x et y nous serviront pour les boucles ci-dessous
fstream fin;
int x = 0;
int y = 0;
 
//On réinitialise maxX et maxY qui nous permettront de
//déterminer la taille de notre map
maxX = 0;
maxY = 0;
 
//On crée un vecteur en 2 dimensions (un vecteur de vecteurs, quoi)
vector < vector < int > > lignes;
 
//On crée un vecteur temporaire pour lire une ligne
vector < int > myVectData;
 
//On crée des chaînes de caractères temporaires
string strBuf, strTmp;
 
//On crée un stringstream pour gérer nos chaînes
stringstream iostr;
 
//On ouvre le fichier
fin.open(filename, fstream::in);
 
//Si on échoue, on fait une erreur
if (!fin.is_open())
{
cerr << "Erreur de chargement du fichier.\n";
exit(1);
}
 
//On lit notre fichier jusqu'à la fin (eof = end of file)
while (!fin.eof())
{
//On récupère la ligne dans la chaîne strBuf
getline(fin, strBuf);
 
//Si la ligne est vide, on continue la boucle
if (!strBuf.size())
continue;
 
//Sinon on poursuit et on réinitialise notre stringstream
iostr.clear();
 
//On y envoie le contenu du buffer strBuf
iostr.str(strBuf);
 
//On réinitialise le vecteur ligne
myVectData.clear();
 
//On boucle pour lire chaque numéro de tile du fichier map
while (true)
{
//Pour chaque ligne on récupère le numéro de la tile, en
//les parsant grâce aux espaces qui les séparent (' ')
getline(iostr, strTmp, ' ');
 
//On récupère ce numéro dans dans notre vecteur ligne
myVectData.push_back(atoi(strTmp.c_str()));
 
//Si on a fini, on quitte la boucle
if (!iostr.good()) break;
}
 
//Si le vecteur ligne n'est pas vide, on l'envoie dans notre vecteur à 2 dimensions
if (myVectData.size())
lignes.push_back(myVectData);
}
 
//On ferme le fichier
fin.close();
 
//On va maintenant remplir les variables de notre classe à l'aide de notre vecteur
//à 2 dimensions temporaire.
//On commence par récupérer les 3 premières valeurs de la 1ère ligne (0)
//qui sont les valeurs de départ du héros et du tileset à afficher
beginx = lignes[0][0];
 
beginy = lignes[0][1];
 
tilesetAffiche = lignes[0][2];
 
//On charge ensuite la première ligne individuellement car elle contient + de données
//(décalage de 3 numéros à cause des 3 précédents)
for (x = 3; x < MAX_MAP_X + 3; x++)
{
tile[y][x - 3] = lignes[y][x];
}
 
//Puis on charge le reste du tableau de tiles pour la couche 1.
//On boucle jusqu'à MAX_MAP_Y et MAX_MAP_X, soit les dimensions
//maxi de la map (400 x 150 tiles, pour rappel)
for (y = 1; y < MAX_MAP_Y; y++)
{
for (x = 0; x < MAX_MAP_X; x++)
{
//On copie la valeur de notre vecteur temporaire
//dans notre tableau à deux dimensions
tile[y][x] = lignes[y][x];
 
//On détecte si la tile n'est pas vide
if (tile[y][x] > 0)
{
//Si c'est la cas, on augmente la valeur de maxX ou
//maxY car la map n'est pas encore finie.
if (x > maxX)
{
maxX = x;
}
 
if (y > maxY)
{
maxY = y;
}
}
}
}
 
//On fait la même chose pour la seconde couche de tiles :
for (y = 0; y < MAX_MAP_Y; y++)
{
for (x = 0; x < MAX_MAP_X; x++)
{
tile2[y][x] = lignes[y + MAX_MAP_Y][x];
}
}
 
//Puis pour la troisième :
for (y = 0; y < MAX_MAP_Y; y++)
{
for (x = 0; x < MAX_MAP_X; x++)
{
tile3[y][x] = lignes[y + MAX_MAP_Y * 2][x];
}
}
 
//On convertit les dimensions max de notre map en pixels, en ajoutant
//1 (car on commence à la ligne/colonne 0) et en multipliant par la valeur
//en pixels d'un tile (32 pixels).
maxX = (maxX + 1) * TILE_SIZE;
maxY = (maxY + 1) * TILE_SIZE;
}
 
 
void Map::draw(int layer, RenderWindow &window)
{
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 = 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 = (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 = startY / TILE_SIZE;
y1 = (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 (mapTimer <= 0)
{
if (tileSetNumber == 0)
{
tileSetNumber = 1;
mapTimer = TIME_BETWEEN_2_FRAMES * 3;
}
else
{
tileSetNumber = 0;
mapTimer = TIME_BETWEEN_2_FRAMES * 3;
}
 
}
else
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 = 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 = 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 (tileSetNumber == 0)
{
tileSet1.setPosition(Vector2f(x, y));
tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));
window.draw(tileSet1);
}
else
{
tileSet1B.setPosition(Vector2f(x, y));
tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));
window.draw(tileSet1B);
}
 
mapX++;
}
 
mapY++;
}
}
 
else if (layer == 2)
{
//Deuxième couche de tiles ;)
for (y = y1; y < y2; y += TILE_SIZE)
{
mapX = 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 = 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 (tileSetNumber == 0)
{
tileSet1.setPosition(Vector2f(x, y));
tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));
window.draw(tileSet1);
}
else
{
tileSet1B.setPosition(Vector2f(x, y));
tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));
window.draw(tileSet1B);
}
 
mapX++;
}
 
mapY++;
}
}
 
else if (layer == 3)
{
//Troisième couche de tiles ;)
for (y = y1; y < y2; y += TILE_SIZE)
{
mapX = 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 = 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 (tileSetNumber == 0)
{
tileSet1.setPosition(Vector2f(x, y));
tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));
window.draw(tileSet1);
}
else
{
tileSet1B.setPosition(Vector2f(x, y));
tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));
window.draw(tileSet1B);
}
 
mapX++;
}
 
mapY++;
}
}
 
 
}
 
 
 
void Map::testDefilement(void)
{

//Tant que le début du blittage de la map est inférieur aux coordonnées

//en X de la fin de la map (- la largeur de l'écran à laquelle on retire aussi

//10 pixels, car il s'agit de notre vitesse de scrolling.

//On doit en effet s'arrêter 10 pixels avant, sinon on sort de la map

//et ce n'est pas beau !), on fait défiler la map.

if (startX < maxX - SCREEN_WIDTH - 10)

//Vous pouvez changer cette valeur pour faire défiler la map plus ou moins vite

startX += 10;

 

}

 

   Passons rapidement sur le constructeur : vous aurez remarqué qu'on y initialise en plus nos variables.

   On a aussi un nouvel accesseur et un nouveau mutateur pour changer la valeur du niveau : getLevel() et setLevel().

   changeLevel() est aussi plutôt simple, elle se charge d'aller chercher le bon fichier map en fonction du level en cours (dans une chaîne de caractères ou string) et appelle la fonction loadMap(), qui, elle, va charger le fichier map en question.

   Rien de bien sorcier pour l'instant. Mais ça se complique dans les fonctions suivantes ! cheeky

      La fonction loadMap()

   C'est cette fonction qui va se charger de lire et de charger nos fichiers maps. Elle est plutôt complexe, ce n'est donc pas très grave si vous ne comprenez pas exactement son fonctionnement.

   Dans le détail, elle commence par ouvrir 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 écrit l'erreur et on quitte proprement.

   Après, elle enregistre le contenu du fichier ligne par ligne dans un ensemble de vecteurs, en triant chaque nombre inscrit dans le fichier grâce à l'espace qui les sépare les uns des autres (d'où le ' ', qui signifie le caractère espace wink).

   Ensuite, elle ferme le fichier et récupère les 3 premiers chiffres de la première ligne de vecteurs pour les enregistrer 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, ou les mettre seuls sur la première ligne du fichier (cela aurait même été plus simple en fait, mais bon... indecision).

   Elle va par la suite balayer toutes les valeurs des tiles contenues dans les vecteurs 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 remplit ainsi notre tableau à deux dimensions au fur et à mesure. Comme notre fichier reprend le même format que notre tableau, c'est facile ! 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 maxX et 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 comme 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 draw().

      La fonction draw()

   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. 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 136 (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 = 136 % 10 x 32 = 6 x 32 = 192
ysource = 136 / 10 x 32 = 13 x 32 = 416

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

   Il ne nous reste plus que la fonction testDefilement() à voir, et elle est très simple.

   En fait, on la rajoute juste ici pour pouvoir tester que l'affichage de la map fonctionne : on pourra la supprimer par la suite. wink

   Son but est simplement de déplacer les coordonnées d'affichage de la map (ici startX suffit) de 10 pixels à chaque tour de boucle. Vous pourrez baisser cette valeur, si vous voulez que la map défile plus lentement, ou au contraire l'augmenter pour plus de vitesse ! wink

 

      Retour au main()

   Voilà, il ne nous reste plus qu'à mettre à jour notre main() et le tour sera joué ! laugh

   

Fichier : main.cpp

//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
//Big Tuto C++/SFML 2.2 - Février 2015 - Mise à jour 1.2
 
#include "main.h"
 
 
int main(int argcchar *argv[])
{
// Création d'une fenêtre en SFML
RenderWindow window(VideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32),
"Rabidja 3.0 - Chapitre 5 : La map - Big Tuto SFML2 - www.meruvia.fr");
 
//Limite les fps à 60 images / seconde
window.setFramerateLimit(60);
 
//On active la synchro verticale
window.setVerticalSyncEnabled(true);
 
//Instanciation des classes
Input input;
Map map;
 
//On commence au premier niveau (vous pouvez aussi mettre 2 pour tester le 2ème niveau)
map.setLevel(1);
map.changeLevel();
 
// Boucle infinie, principale, du jeu
while (window.isOpen())
{
/** GESTION DES INPUTS (CLAVIER, JOYSTICK) **/
input.gestionInputs(window);
 
/** DESSIN - DRAW **/
//On dessine tout
window.clear();
 
//Fonction provisoire pour tester le défilement de la map
map.testDefilement();
 
//On affiche le background
map.drawBackground(window);
 
// Affiche la map de tiles : layer 2 (couche du fond)
map.draw(2, window);
 
// Affiche la map de tiles : layer 1 (couche active : sol, etc.)
map.draw(1, window);
 
// Affiche la map de tiles : layer 3 (couche en foreground / devant)
map.draw(3, window);
 
window.display();
}
 
// On quitte
return 0;
 
}

 

   Avant la boucle principale du jeu, on ajoute un appel à setLevel() pour mettre le niveau à 1. Vous pouvez aussi essayer le niveau 2 si vous voulez, mais mettre une autre valeur planterait le jeu, vu qu'on n'a que deux maps. wink

   Ensuite, on appelle changeLevel() qui va elle-même appeler loadMap() pour charger le fichier map. smiley

 

   Dans la boucle principale, maintenant :

- on appelle map.testDefilement() pour faire défiler la map (si vous ne le mettez pas, la map restera statique).

- puis, on appelle 3 fois map.draw() pour afficher les 3 couches de la map à la suite : la couche 2 sera celle du fond, du background et le héros passera devant (pas de murs donc ! wink), la couche 1 sera la couche action, celle avec laquelle le héros entrera en collision (on y mettra les murs, les obstacles, le sol, les power-ups, etc.) et la couche 3 sera la couche de devant, ou foreground, et le héros passera derrière les éléments dessinés (cf. ci-dessus, si vous avez oublié indecision).

   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

Today172
Yesterday196
This week494
This month4168
Total1743375

24/04/24