Big Tuto SFML 2 / Action-RPG : Legends of Meruvia

Chapitre 4 : Affichons notre première map !

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 10 janvier 2016
Date de révision : 17 avril 2016

      Prologue


   Eh, voilà ! smiley Nous avons maintenant une jolie fenêtre ! cool
   Il ne nous reste plus qu'à la remplir avec notre première map ! wink
   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, 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 dans ce tuto, 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

 

Voici le level editor que je vous propose. Il a été co-développé par moi et la communauté de Meruvia.

 

  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 au moins :

- ajouté un dossier map à votre projet, dans lequel vous aurez mis les fichiers : map1.txt, map2.txt, etc qui seront les fichiers de nos niveaux et que vous trouverez dans l'archive téléchargeable. wink

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

- copié également TOUTES LES RESSOURCES disponibles dans l'archive téléchargeable, et mises dans les dossiers font, graphics, map, music et sounds. Mieux vaut tout avoir au départ, pour éviter les erreurs bêtes (cf. encadré ci-dessous). wink

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

Vous noterez que j'ai déjà mis la totalité des assets (images, musiques, sons, etc.) dans l'archive téléchargeable. Je trouve cela finalement plus pratique que de les ajouter au fur et à mesure, car cela évite les erreurs bêtes mais fréquentes : la fenêtre du jeu se ferme automatiquement au démarrage du jeu, car il ne trouve pas le fichier qu'il doit charger ! surprise Le forum est rempli de questions concernant ce genre de problème. Alors, si cela vous arrive, assurez-vous d'abord que vos fichiers sont bien dans le bon dossier, au bon endroit ! wink

 

      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 rétro, ça sera suffisant. 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 permettant, par exemple, à notre héros, de passer sous le feuillage d'un arbre. 

 

   Mais c'est le même système que pour Rabidja, alors ? indecision

   Pas tout à fait, car contrairement à Rabidja, les tiles ne sont plus rangées selon leurs propriétés : il n'y a plus de tiles bloquantes, spéciales, etc... L'utilisation des 3 couches est là simplement pour nous aider à empiler les tiles pour obtenir un résultat sympa ! wink

 

   Mais, on va passer à travers les murs ?!? surprise

   Euh, non, heureusement ! cheeky

   J'ai prévu le coup en rajoutant une quatrième couche, qui permettra d'ajouter des tiles spéciales. Ainsi, en blittant la tile bloquante (rouge) par dessus notre décor, on définira les zones où le héros ne peut pas aller. wink

   Idem pour les tiles monstres, warps, etc. que nous utiliserons par la suite. Mais on reviendra là-dessus plus en détails dans l'étude du level editor. angel

 

En plaçant des tiles rouges sur la 4ème couche de tiles (collisions), on définit les endroits où le héros pourra passer.

 

   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 + 1 tableau pour les collisions et les tiles spéciales).

 

  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

3

0

0

Y = 32 -> ligne 1

1

1

1

1

1

Y = 64 -> ligne 2, etc.

0

0

0

0

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 de l'herbe, 1 un chemin de pierre, 2 une fleur jaune et 3 un arbre.


     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 ! Alors, relax ! indecision

 

      Le code

   Il va maintenant être temps de rajouter une nouvelle classe à notre projet : la classe Map, qui va se charger de gérer et d'afficher la map, c'est-à-dire le niveau en cours ! wink

   Commençons donc par créer deux nouveaux fichiers pour notre classe : map.cpp et son header (en-tête) : map.h. Copiez ensuite la totalité du code suivant, dans l'en-tête map.h :

 

Fichier : map.h : créer le fichier et y coller le code suivant :

//Legends of Meruvia - C++ / 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 Player;
class Monster;
 
class Map
{
 
public:
 
// Structures pour les A-RPGs
struct PNJ { int type, text, quest; };
struct CHEST { int type; };
struct WARPSPE { int value; };
struct BUTTON { int type; };
struct TRAP { int type; };
struct POINT { int x, y; };
 
//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;
int getNombreMonstres(void) const;
int getWarpUp(void) const;
int getWarpDown(void) const;
int getWarpLeft(void) const;
int getWarpRight(void) const;
int getWarpSP(int number) const;
int getWarpDirection(void) const;
int getWarp_coming_from_x(void) const;
int getWarp_coming_from_y(void) const;
 
//Mutateurs
void setLevel(int valeur);
void setStartX(int valeur);
void setStartY(int valeur);
void setTile(int y, int x, int valeur);
void setNombreMonstres(int valeur);
void setWarpDirection(int valeur);
void setWarp_coming_from_x(int valeur);
void setWarp_coming_from_y(int valeur);
 
//Fonctions
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[80][80];
 
//Deuxième couche de tiles
int tile2[80][80];
 
//Troisième couche de tiles
int tile3[80][80];
 
//Quatrième couche de tiles pour les collisions
int tile4[80][80];
 
/* Timer et numéro du tileset à afficher pour animer la map */
int mapTimer, tileSetNumber;
 
//Numéro du niveau en cours
int level;
 
//Variable pour testDefilement()
int testdefil;
 
//Nouvelles variables pour les A-RPG
int warpUp, warpDown, warpLeft, warpRight;
WARPSPE warpSP[10];
PNJ pnj[20];
CHEST chest[20];
int loot, weather;
int cinematics, cinematics_condition;
int musicNumber;
BUTTON button[10];
TRAP trap[10];
 
//Pour gérer les warps
int warpDirection;
int warp_coming_from_x;
int warp_coming_from_y;
 
//Tilesets
sf::Texture tileSet1Texture;
sf::Sprite tileSet1;
sf::Texture tileSet1BTexture;
sf::Sprite tileSet1B;
 
//Nombre max de monstres à l'écran
int nombreMonstres;
 
//Police de caractères
sf::Font font;
 
//HUD (Informations affichées à l'écran, comme le nombre de coeurs, la magie, etc.)
sf::Texture HUDTexture;
sf::Sprite HUD;
sf::Texture HUDHeartsTexture;
sf::Sprite HUDHearts;
sf::Texture HUDMagicTexture;
sf::Sprite HUDMagic;
float HUDtimer;
int HUDDirection;
 
 
/*******************/
/* 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 = 80;
const int MAX_MAP_Y = 80;
 
/* Taille d'une tile (32 x 32 pixels) */
const int TILE_SIZE = 32;
 
/* Constante pour l'animation */
const int TIME_BETWEEN_2_FRAMES = 20;
 
//Nombre max de monstres gérés
const int MONSTRES_MAX = 50;
 
//Directions
const int DOWN = 0;
const int UP = 1;
const int RIGHT = 2;
const int LEFT = 3;
 
 
/*************************/
/* VALEURS DES TILES */
/************************/
 
const int TILE_MONSTRE_DEBUT = 20;
const int TILE_MONSTRE_FIN = 39;
 
};
#endif

 

   Comme vous pouvez le voir, on rajoute pas mal de variables ! wink J'ai en effet choisi d'inclure dès le début toutes les variables nécessaires pour la première partie du jeu, ainsi que leurs accesseurs / mutateurs. Cela nous évitera ainsi de fastidieux rajouts par la suite. Ne vous inquiétez donc pas si vous ne voyez pas à quoi sert une variable au début : c'est qu'elle ne sert à rien pour l'instant, et tout s'éclairera quand on en fera usage par la suite ! laugh

 

  Je vous fais quand même l'article rapidement pour les principales variables :

- 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. 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 + tile4 pour les collisions et les tiles spéciales (voir au-dessus), contiendront nos niveaux (qui feront 80 tiles de long par 80 de hauteur - vous pouvez changer ces valeurs, 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. 

   Et pour les autres variables, nous les aborderons plus tard, au fur et à mesure mais les commentaires vous permettent déjà de vous faire une idée de leur utilité ! laugh

 

Je reviens quand même sur les structures (struct) pour les Action-RPGs définies en haut du fichier. Ces structs nous permettront d'enregistrer les données de la map, entrées dans le level editor et permettront par la suite de gérer toutes sortes d'éléments liés aux spécificités des Action-RPGs. Certains seront déjà abordés dans cette première partie du tuto, tandis que d'autres, plus complexes, attendront... wink 

Mais bon, vous pouvez voir que j'ai quand même prévu des warps (pour changer de niveau et entrer dans les maisons), des PNJs (Personnages Non-joueurs qui parlent ou vendent des objets), des pièges, des mécanismes (boutons, etc.) et des coffres.
La structure POINT, quant à elle, sera surtout pratique pour enregistrer un point, avec son abscisse et son ordonnée, dans une seule variable. wink

   Si nous reprenons les dimensions de nos tableaux de tiles, nous voyons que nous définissons donc notre map pour avoir les dimensions max de 80 tiles de large par 80 de hauteur, ce qui est plus petit que pour Rabidja, mais cela devrait être suffisant puisque cela représente un carré de : (80 x 32 = 2560 pixels de côté ! angel De plus, dans un Action-RPG, on passe très souvent d'un écran/niveau à un autre, donc cela ne devrait pas poser de problème. wink

   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 : créer le fichier et y coller le code suivant :

//Legends of Meruvia - C++ / 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 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);
 
//Chargement de la police Gentium
if (!font.loadFromFile("font/GenBasB.ttf"))
{
// Erreur
cout << "Erreur durant le chargement de la police." << endl;
}
 
//Chargement du HUD
if (!HUDTexture.loadFromFile("graphics/HUD.png"))
{
// Erreur
cout << "Erreur durant le chargement de l'image du HUD." << endl;
}
else
HUD.setTexture(HUDTexture);
 
if (!HUDMagicTexture.loadFromFile("graphics/energy.png"))
{
// Erreur
cout << "Erreur durant le chargement de l'image du HUD Magic." << endl;
}
else
HUDMagic.setTexture(HUDMagicTexture);
 
if (!HUDHeartsTexture.loadFromFile("graphics/life.png"))
{
// Erreur
cout << "Erreur durant le chargement de l'image du HUD Vies." << endl;
}
else
HUDHearts.setTexture(HUDHeartsTexture);
 
 
//Autres variables
mapTimer = TIME_BETWEEN_2_FRAMES * 3;
tileSetNumber = 0;
testdefil = 0;
level = 1;
startX = startY = 0;
warpDirection = warp_coming_from_x = warp_coming_from_y = 0;
HUDtimer = 3;
HUDDirection = DOWN;
}
 
 
//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 tile4[y][x]; }
int Map::getLevel(void) const { return level; }
int Map::getNombreMonstres(void) const { return nombreMonstres; }
int Map::getWarpUp(void) const { return warpUp; }
int Map::getWarpDown(void) const { return warpDown; }
int Map::getWarpLeft(void) const { return warpLeft; }
int Map::getWarpRight(void) const { return warpRight; }
int Map::getWarpSP(int number) const { return warpSP[number].value; }
int Map::getWarpDirection(void) const { return warpDirection; }
int Map::getWarp_coming_from_x(void) const { return warp_coming_from_x; }
int Map::getWarp_coming_from_y(void) const { return warp_coming_from_y; }
 
 
//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; }
void Map::setNombreMonstres(int valeur) { nombreMonstres = valeur; }
void Map::setWarpDirection(int valeur) { warpDirection = valeur; }
void Map::setWarp_coming_from_x(int valeur) { warp_coming_from_x = valeur; }
void Map::setWarp_coming_from_y(int valeur) { warp_coming_from_y = valeur; }
 
 
//Fonctions
 
void Map::changeLevel(void)
{
string filename;
filename = "map/map" + to_string(level) + ".txt";
loadMap(filename);
}
 
 
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
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)
{
//Test de défilement de la map
 
 
//On commence à faire défiler vers la droite (valeur == 0)
if (testdefil == 0)
{
//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 pour ne pas afficher
//du noir), on fait défiler la map.
if (startX < maxX - SCREEN_WIDTH)
//Vous pouvez changer cette valeur pour faire défiler la map plus ou moins vite
startX += 2;
else
testdefil = 1;
}
//Une fois au bout, on fait défiler vers le bas (valeur == 1)
else if (testdefil == 1)
{
if (startY < maxY - SCREEN_HEIGHT)
//Vous pouvez changer cette valeur pour faire défiler la map plus ou moins vite
startY += 2;
else
testdefil = 2;
}
//Une fois en bas, on fait défiler vers la gauche (valeur == 2)
else if (testdefil == 2)
{
if (startX > 0)
//Vous pouvez changer cette valeur pour faire défiler la map plus ou moins vite
startX -= 2;
else
testdefil = 3;
}
//Puis on remonte au point de départ (valeur == 3) et on recommence
else if (testdefil == 3)
{
if (startY > 0)
//Vous pouvez changer cette valeur pour faire défiler la map plus ou moins vite
startY -= 2;
else
testdefil = 0;
}
}
 
 
 
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];
}
}
 
//Et enfin pour la quatrième (la couche des collisions
//et des tiles spéciales) :
for (y = 0; y < MAX_MAP_Y; y++)
{
for (x = 0; x < MAX_MAP_X; x++)
{
tile4[y][x] = lignes[y + MAX_MAP_Y * 3][x];
}
}
 
 
 
//On charge les variables supplémentaires
y = MAX_MAP_Y * 4;
 
warpUp = lignes[y][0];
warpDown = lignes[y][1];
warpLeft = lignes[y][2];
warpRight = lignes[y][3];
 
for (int i = 0; i < 10; i++)
{
warpSP[i].value = lignes[y][i + 4];
}
 
for (int i = 0; i < 20; i++)
{
pnj[i].type = lignes[y][14 + i * 3];
pnj[i].text = lignes[y][15 + i * 3];
pnj[i].quest = lignes[y][16 + i * 3];
}
 
for (int i = 0; i < 20; i++)
{
chest[i].type = lignes[y][74 + i];
}
 
loot = lignes[y][94];
weather = lignes[y][95];
cinematics = lignes[y][96];
cinematics_condition = lignes[y][97];
musicNumber = lignes[y][98];
 
for (int i = 0; i < 10; i++)
{
button[i].type = lignes[y][99 + i];
}
 
for (int i = 0; i < 10; i++)
{
trap[i].type = lignes[y][109 + i];
}
 
 
//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'une tile (32 pixels).
maxX = (maxX + 1) * TILE_SIZE;
maxY = (maxY + 1) * TILE_SIZE;
}

 

   Passons rapidement sur le constructeur : vous aurez remarqué qu'on y initialise nos variables et qu'on charge dès à présent toutes nos ressources graphiques nécessaires à l'affichage de la map, comme ça, ce sera fait ! laugh

   J'ai aussi rajouté directement la plupart des accesseurs / mutateurs qui nous seront nécessaires pour gérer la valeur de nos variables. Comme ça, on n'en oubliera pas, par la suite ! wink

   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. Si votre programme plante subitement, ce sera donc, peut-être, car il ne trouvera pas le fichier de la map ! surprise

   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 4 tableaux (3 couches de tiles + une surcouche pour les collisions et les tiles spéciales) 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 80 x 80 tiles soit la taille du fichier). Malin, non ? wink En plus, ça marche avec seulement 5 lignes de code !

 

   Jusque là, notre fonction est complètement identique à celle de Rabidja, donc cela ne vous aura sans doute pas surpris. wink Maintenant, là où ça change, c'est qu'on a besoin de plus de variables pour gérer un Action-RPG qu'un jeu de plateforme. Le level editor, comme on le verra dans le chapitre suivant, va donc permettre de rajouter plein de précisions sur les PNJs qu'on va vouloir mettre, ce qu'ils vont dire, les warps qu'on va placer et où elles vont mener, etc.

   On va donc charger toutes ces variables supplémentaires de la même façon que les précédentes, sachant simplement qu'elles se situent désormais tout à la fin du fichier. cheeky

   Et c'est une autre des raisons pour lesquelles j'ai choisi d'intégrer toutes nos variables dès le début, même si on n'en a pas besoin pour l'instant : pour pouvoir charger correctement le fichier de map. wink

   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 Pour info, la fonction reste la même que celle de Rabidja, donc pas la peine de perdre trop de temps, si vous la connaissez déjà ! cheeky On affiche toujours nos 3 couches de tiles (mais pas la couche de collisions, qui servira simplement pour les tests de collisions / tiles spéciales et qui nous économisera notre processeur, car on ne sera plus obligé de parcourir les 3 couches de tiles, mais une seule). 

   Les calculs au début de cette fonction permettent 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 (mur gris avec parapet) puis la 136 (coin droit de l'arbre bleu foncé) :


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'une tile (32 pixels) à chaque tour de boucle ! wink

 

      La fonction testDefilement()

   Il ne nous reste maintenant plus que la fonction testDefilement() à voir.

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

   Son but est simplement de déplacer les coordonnées d'affichage de la map (startX et startY) de 2 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

   La fonction se base sur la valeur d'une variable testdefil, qui définira la direction du scrolling auto. Son but est, en effet, de tourner en boucle dans la map pour tester qu'elle s'affiche et défile correctement. wink

   On va donc commencer à aller vers la droite (testdefil == 0) jusqu'à atteindre le bord de la map, puis on va aller vers le bas (testdefil == 1), puis retour à gauche (testdefil == 2) et enfin vers haut (testdefil == 3). Là, on retourne au point de départ et on recommence ! laugh

 

      Retour au main()

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

   Commençons par l'en-tête (header) :

 

   Fichier : main.h : Remplacez par :

//Legends of Meruvia - C++ / SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#include <cstdlib>
#include <iostream>
#include <SFML/Graphics.hpp>
 
#include "input.h"
#include "map.h"
 
using namespace std;
using namespace sf;
 
 
//Fonctions
void update(Input &input,Map &map);
void draw(sf::RenderWindow &window, Map &map);
 
 
// Taille de la fenêtre : 800x480 pixels
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 480;
 

 

   Ici, c'est très simple, on rajoute simplement : #include "map.h", pour signaler son existence au compilateur et on rajout deux fonctions : draw() et update() qui nous permettront respectivement de dessiner / afficher le jeu et de le mettre à jour. Pour l'instant, ce sera très basique car nous n'allons gérer que la map et son scrolling auto, mais plus tard, elles vont se complexifier, croyez-moi ! wink

   Mettons maintenant à jour le main() :

 

 Fichier : main.cpp : Remplacez par :

//Legends of Meruvia - C++ / SFML 2.3.2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#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),
"Meruvia - Big Tuto A-RPG/SFML2 - Chapitre 4 - www.meruvia.fr");
 
 
//On active la synchro verticale
window.setVerticalSyncEnabled(true);
 
//Instanciation des classes
Input input;
Map map;
 
//On commence au premier niveau
map.setLevel(1);
map.changeLevel();
 
// Boucle infinie, principale, du jeu
while (window.isOpen())
{
 
// Gestion des inputs
input.gestionInputs(window);
 
//Updates
update(input, map);
 
// Dessin - draw
draw(window, map);
 
window.display();
}
 
// On quitte
return 0;
 
}
 
 
 
//Fonction de mise à jour du jeu : gère la logique du jeu
void update(Input &input, Map &map)
{
map.testDefilement();
}
 
 
 
//Fonction de dessin du jeu : dessine tous les éléments
void draw(RenderWindow &window, Map &map)
{
//On efface tout
window.clear();
 
// 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);
}

 

   Rien de très complexe ici non plus (la difficulté était avant, comme vous avez pu le voir ! laugh).

   Avant la boucle principale du jeu, on instancie notre classe Map, on ajoute ensuite un appel à setLevel() pour mettre le niveau à 1. Vous pouvez aussi essayer un autre niveau, mais, attention, si la map est vide, cela plantera le jeu. wink

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

   Dans la boucle principale, maintenant, on ajoute un appel à nos nouvelles fonctions update() et draw(), qu'on va définir en-dehors du main(), au-dessous, pour gagner en lisibilité.

 

   Dans notre fonction update(), on va simplement appeler map.testDefilement() pour faire défiler la map (si vous ne le mettez pas, la map restera statique). wink

   Et dans notre fonction draw(), on va effacer la fenêtre puis appeler 3 fois map.draw() pour afficher les 3 couches de la map à la suite.

   Et c'est fini ! On peut maintenant compiler, lancer le programme et admirer le scrolling de notre map défiler 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

Today69
Yesterday157
This week581
This month2874
Total1742081

18/04/24