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à ! Nous avons maintenant un magnifique background !
C'est... comment dire ? C'est zen ! C'est même très (trop) zen !
Il va nous falloir un peu d'action, sinon on va s'ennuyer ferme, quand même !
Mais avant d'ajouter le héros du jeu ou même quelques ennemis, il va falloir ajouter un niveau, autrement appelé une map !
Eh oui, sinon, nos bonhommes tomberaient dans le vide !
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 ?
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 ). 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é !
Ok, mais où je trouve un level editor (ou éditeur de niveaux) ?
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.
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 ). Les maps éditées fonctionneront ensuite très bien avec notre jeu en SFML (ce sont juste des fichiers texte ).
On verra au chapitre prochain comment l'utiliser.
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 ?!
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 ).
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 ! )
- 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 ?
Si vous regardez attentivement (c'est le jeu des 7 erreurs ! ), vous verrez qu'ils ne sont pas tout à fait identiques : certaines tiles sont un peu différentes. 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 ! ).
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.
C'est quoi un affichage sur 3 couches (ou layers) ? A quoi cela va-t-il nous servir ?
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 ! ).
Et en quoi, c'est plus beau ?
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.
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.
- 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 ?
Dans des fichiers txt. Vous pouvez d'ailleurs ouvrir ceux que je vous ai donnés, pour voir à quoi ils ressemblent.
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 ). 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 !
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 !
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 !
Le code
Il va maintenant être temps de développer notre classe Map, et de lui faire afficher plus que le seul background !
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 !
Je vous fais l'article rapidement :
- les Textures et Sprites tileSet1 et tileSet1B contiendront, selon toute logique, nos deux tilesets.
- 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 , on n'est pas obligé de toujours commencer en haut à gauche !
- 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.
- 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 !
- 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 ), 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 ).
- mapTimer sera notre chrono pour savoir quel tileset (A ou B) afficher, valeur qui sera contenue dans tileSetNumber.
- 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 !
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 ).
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.
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.
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 !
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 ).
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... ).
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.
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
|
TABLEAU
|
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. 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 ? ).
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 ? 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 ! ).
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.
Quand notre caméra et notre scrolling seront opérationnels (bientôt, bientôt ), 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 ). 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.
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 ), 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 ?
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.
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 % 10 x TILE_SIZE
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 ? 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 !
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 !
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.
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 !
Retour au main()
Voilà, il ne nous reste plus qu'à mettre à jour notre main() et le tour sera joué !
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 argc, char *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.
Ensuite, on appelle changeLevel() qui va elle-même appeler loadMap() pour charger le fichier map.
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 ! ), 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é ).
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 !
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.
@ bientôt et merci de votre fidélité au site !
Jay.