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

Chapitre 9 : Gestion des warps spéciales

 

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 31 mai 2016
Date de révision : -

 

      Prologue

   Notre monde commence vraiment à prendre de l'ampleur, maintenant que nous ne sommes plus limités à une seule map et que nous pouvons en rajouter autant que nous le voulons ! angel

   Mais bon, voilà... blush comment on fait pour rentrer dans les maisons, grottes, cavernes, temples, palais et j'en passe ?! surprise On ne peut pas mettre une porte à la limite de la map !... frown

   C'est pour ça que nous allons maintenant compléter notre système de warps, en rajoutant des warps spéciales. wink

  Voilà, donc avant de passer au code, vous pouvez charger, comme d'habitude, les archives complètes du jeu ci-dessous wink : 

 

 

      La théorie

   Comme pour le chapitre précédent, nous allons débuter par un peu de théorie pour voir ce que nous allons faire. wink

   Si vous êtes courageux (là encore indecision), vous pourrez vous en tenir à cette partie et essayer d'implémenter le code vous-même. Mais, rassurez-vous, de toute façon, vous trouverez le code complet ci-dessous. cheeky

 

 

   Comment ça marche ?

- Tout d'abord, cela va se passer dans le level editor (cf. illustration ci-dessus wink) :
 
- on va placer des tiles spéciales sur la couche 4 (la couche réservée aux collisions). Celles-ci ne seront donc visibles QUE dans le level editor. On ne les verra pas en jeu, car cela ferait vraiment moche !!! indecision
 
- on trouve ces tiles spéciales dans le tileset des tiles spéciales, à droite. Je les ai surlignées en bleu pour que vous les voyiez mieux. wink Je les ai nommées, à juste titre, WARP1, WARP2, etc, jusqu'à WARP10. Chaque Warp spéciale renvoie vers son niveau propre, donc vous pouvez mettre jusqu'à 10 warps spéciales par map ! Cela me paraît bien suffisant, maintenant, si vous voulez créer une ville gigantesque, il faudra vous restreindre à 10 maisons par map (ce qui est déjà pas mal) et changer de map pour en rajouter d'autres (dans un autre quartier, par exemple wink). Vous pouvez aussi adapter le jeu pour qu'il prenne en compte plus de warps, bien sûr ! cheeky
 
- Pour régler ces warps, il suffit d'aller juste en-dessous de là où vous avez déjà réglé les warps directionnelles (en jaune). Ce sont les deux lignes surlignées en vert : la première ligne permet de sélectionner la Warp Spéciale à éditer (Spec N°, qui va donc de 1 à 10, logique ! laugh) et la seconde (Value) permet de rentrer la map vers laquelle on doit warper.
 
- Notez que la warp spéciale enverra notre héros à proximité de la même tile spéciale de la nouvelle map. Pensez donc à bien mettre la tile également sur la nouvelle map. Pour donner un exemple concret, cela signifie que la tile WARP1 de la map1 enverra vers la même tile WARP1 de la map4 (exemple pris au hasard wink).
 
- Dans le jeu maintenant, il va falloir qu'on détecte la collision du héros avec l'une de ces tiles et enregistrer la direction dans laquelle il va (en haut, en bas, etc.).
 
- S'il y a collision, on va warper vers la map spéciale indiquée dans le level editor pour la tile spéciale rencontrée (Value sous Spec N°). Comme il y a 10 tiles de Warps Spéciales, on peut donc avoir plusieurs warps spéciales dans une même map, pour pouvoir rentrer dans plusieurs maisons, par exemple. wink
 
- On doit ensuite réinitialiser notre héros dans la nouvelle map, pour le placer correctement, au plus près de la tile Warp prise, et cela en fonction de la direction dans laquelle il allait, afin que notre ne se retrouve ni dans le décor, ni sur l'autre tile spéciale, sans quoi il ferait un retour direct ! angry

 

   Mais alors, comment on va faire ça dans le code ? frown

   Allez, je vous donne encore un petit coup de pouce, pour que vous essayiez de le faire vous-même wink :

- Dans mapCollision(), on doit détecter le contact avec l'une de ces tiles (avec la nouvelle fonction detectWarpSpe()), selon la direction dans laquelle on va.
 
- S'il y a collision, on warpe vers la map spéciale indiquée dans le level editor pour la tile spéciale rencontrée, si celui-ci est différent de 0 (sinon pas de warps).
 
- On appelle alors reinitialize() qui nous place correctement en fonction de la direction dans laquelle on allait, et pour ne pas que notre héros touche l'autre tile spéciale en entrant dans la nouvelle map, sans quoi il ferait un retour direct !

   Voilà, je vous laisse chercher (trouver ? cheeky) un peu, et on met en commun ci-dessous. wink

 

      Le code

   Bon, allez, passons maintenant à la soluce ! cheeky

   J'espère que vous avez un peu cherché, quand même. Et si vous avez trouvé une solution, elle sera peut-être un peu différente de la mienne, ce qui est toujours intéressant ! cool N'hésitez pas à partager sur les forums !

   Reprenons donc notre classe Player, et commençons par player.cpp. Mettons d'abord à jour notre fonction mapCollision() pour détecter les warps spéciales. 

   Par souci de commodité, je vous remets toute la fonction ci-dessous :

 

Fichier : player.cpp : Modifiez la fonction ou recopiez-la en entier :

void Player::mapCollision(Map &map)
{
 
int i, x1, x2, y1, y2;
 
/* Ensuite, on va tester les mouvements horizontaux en premier
(axe des X). On va se servir de i comme compteur pour notre boucle.
En fait, on va découper notre sprite en blocs de tiles pour voir
quelles tiles il est susceptible de recouvrir.
On va donc commencer en donnant la valeur de Tile_Size à i pour qu'il
teste la tile où se trouve le x du joueur mais aussi la suivante SAUF
dans le cas où notre sprite serait inférieur à la taille d'une tile.
Dans ce cas, on lui donnera la vraie valeur de la taille du sprite
Et on testera ensuite 2 fois la même tile. Mais comme ça notre code
sera opérationnel quelle que soit la taille de nos sprites ! */
 
if (h > TILE_SIZE)
i = TILE_SIZE;
else
i = h;
 
 
//On lance alors une boucle for infinie car on l'interrompra selon
//les résultats de nos calculs
for (;;)
{
//On va calculer ici les coins de notre sprite à gauche et à
//droite pour voir quelle tile ils touchent.
x1 = (x + dirX) / TILE_SIZE;
x2 = (x + dirX + w - 1) / TILE_SIZE;
 
//Même chose avec y, sauf qu'on va descendre au fur et à mesure
//pour tester toute la hauteur de notre sprite, grâce à notre
//fameuse variable i.
y1 = (y) / TILE_SIZE;
y2 = (y + i - 1) / TILE_SIZE;
 
//De là, on va tester les mouvements initiés dans updatePlayer
//grâce aux vecteurs dirX et dirY, tout en testant avant qu'on
//se situe bien dans les limites de l'écran.
if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)
{
//Si on a un mouvement à droite
if (dirX > 0)
{
//On teste les warps spéciales
if ((map.getTile(y1, x2) >= SPE1 && map.getTile(y1, x2) <= SPE10)
|| (map.getTile(y2, x2) >= SPE1 && map.getTile(y2, x2) <= SPE10))
{
 
//On enregistre le numéro de la warp spéciale
if (map.getTile(y1, x2) >= SPE1 && map.getTile(y1, x2) <= SPE10)
numberSPE = map.getTile(y1, x2) - SPE1;
else
numberSPE = map.getTile(y2, x2) - SPE1;
 
 
//On teste si on doit warper
if (map.getWarpSP(numberSPE) > 0)
{
// Si c'est le cas, on passe au niveau de la warp spéciale.
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(4);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpSP(numberSPE));
map.changeLevel();
reinitialize(map);
}
 
}
 
//Sinon, on vérifie si les tiles recouvertes sont solides
else if (map.getTile(y1, x2) == MUR || map.getTile(y2, x2) == MUR)
{
// Si c'est le cas, on place le joueur aussi près que possible
// de ces tiles, en mettant à jour ses coordonnées. Enfin, on
//réinitialise son vecteur déplacement (dirX).
 
x = x2 * TILE_SIZE;
x -= (w + 1);
dirX = 0;
 
}
}
 
//Même chose à gauche
else if (dirX < 0)
{
//On teste les warps spéciales
if ((map.getTile(y1, x1) >= SPE1 && map.getTile(y1, x1) <= SPE10)
|| (map.getTile(y2, x1) >= SPE1 && map.getTile(y2, x1) <= SPE10))
{
 
//On enregistre le numéro de la warp spéciale
if (map.getTile(y1, x1) >= SPE1 && map.getTile(y1, x1) <= SPE10)
numberSPE = map.getTile(y1, x1) - SPE1;
else
numberSPE = map.getTile(y2, x1) - SPE1;
 
 
//On teste si on doit warper
if (map.getWarpSP(numberSPE) > 0)
{
// Si c'est le cas, on passe au niveau de la warp spéciale.
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(4);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpSP(numberSPE));
map.changeLevel();
reinitialize(map);
}
 
}
 
else if (map.getTile(y1, x1) == MUR || map.getTile(y2, x1) == MUR)
{
x = (x1 + 1) * TILE_SIZE;
dirX = 0;
}
 
}
 
}
 
//On sort de la boucle si on a testé toutes les tiles le long de la hauteur du sprite.
if (i == h)
{
break;
}
 
//Sinon, on teste les tiles supérieures en se limitant à la heuteur du sprite.
i += TILE_SIZE;
 
if (i > h)
{
i = h;
}
}
 
 
//On recommence la même chose avec le mouvement vertical (axe des Y)
if (w > TILE_SIZE)
i = TILE_SIZE;
else
i = w;
 
 
for (;;)
{
x1 = (x) / TILE_SIZE;
x2 = (x + i) / TILE_SIZE;
 
y1 = (y + dirY) / TILE_SIZE;
y2 = (y + dirY + h) / TILE_SIZE;
 
if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)
{
if (dirY > 0)
{
// Déplacement en bas
 
//On teste les warps spéciales
if ((map.getTile(y2, x1) >= SPE1 && map.getTile(y2, x1) <= SPE10)
|| (map.getTile(y2, x2) >= SPE1 && map.getTile(y2, x2) <= SPE10))
{
 
//On enregistre le numéro de la warp spéciale
if (map.getTile(y2, x1) >= SPE1 && map.getTile(y2, x1) <= SPE10)
numberSPE = map.getTile(y2, x1) - SPE1;
else
numberSPE = map.getTile(y2, x2) - SPE1;
 
 
//On teste si on doit warper
if (map.getWarpSP(numberSPE) > 0)
{
// Si c'est le cas, on passe au niveau de la warp spéciale.
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(4);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpSP(numberSPE));
map.changeLevel();
reinitialize(map);
}
 
}
 
else if (map.getTile(y2, x1) == MUR || map.getTile(y2, x2) == MUR)
{
//Si la tile est une tile solide, on y colle le joueur
y = y2 * TILE_SIZE;
y -= (h + 1);
dirY = 0;
}
 
}
 
else if (dirY < 0)
{
 
// Déplacement vers le haut
 
//On teste les warps spéciales
if ((map.getTile(y1, x1) >= SPE1 && map.getTile(y1, x1) <= SPE10)
|| (map.getTile(y1, x2) >= SPE1 && map.getTile(y1, x2) <= SPE10))
{
 
//On enregistre le numéro de la warp spéciale
if (map.getTile(y1, x1) >= SPE1 && map.getTile(y1, x1) <= SPE10)
numberSPE = map.getTile(y1, x1) - SPE1;
else
numberSPE = map.getTile(y1, x2) - SPE1;
 
 
//On teste si on doit warper
if (map.getWarpSP(numberSPE) > 0)
{
// Si c'est le cas, on passe au niveau de la warp spéciale.
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(4);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpSP(numberSPE));
map.changeLevel();
reinitialize(map);
}
 
}
 
else if (map.getTile(y1, x1) == MUR || map.getTile(y1, x2) == MUR)
{
y = (y1 + 1) * TILE_SIZE;
dirY = 0;
}
 
}
}
 
//On teste la largeur du sprite (même technique que pour la hauteur précédemment)
if (i == w)
{
break;
}
 
i += TILE_SIZE;
 
if (i > w)
{
i = w;
}
}
 
/* Maintenant, on applique les vecteurs de mouvement si le sprite n'est pas bloqué */
x += dirX;
y += dirY;
 
//Si on touche les bords de l'écran, on warp au niveau indiqué
//si celui-ci est différent de 0
if (x < 0)
{
//On stoppe le joueur
x = 0;
 
//On teste si on doit warper à gauche
if (map.getWarpLeft() > 0)
{
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(LEFT);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpLeft());
map.changeLevel();
reinitialize(map);
}
 
}
 
else if (x + w >= map.getMaxX())
{
//On stoppe le joueur
x = map.getMaxX() - w;
 
//On teste si on doit warper à droite
if (map.getWarpRight() > 0)
{
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(RIGHT);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpRight());
map.changeLevel();
reinitialize(map);
}
}
 
else if (y < 0)
{
//On stoppe le joueur
y = 0;
 
//On teste si on doit warper en haut
if (map.getWarpUp() > 0)
{
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(UP);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpUp());
map.changeLevel();
reinitialize(map);
}
}
 
else if (y + h > map.getMaxY())
{
//On stoppe le joueur
y = map.getMaxY() - h;
 
//On teste si on doit warper en bas
if (map.getWarpDown() > 0)
{
//On enregistre la direction du warp et les coordonnées du joueur
map.setWarpDirection(DOWN);
map.setWarp_coming_from_x(x);
map.setWarp_coming_from_y(y);
 
//On change de level
map.setLevel(map.getWarpDown());
map.changeLevel();
reinitialize(map);
}
}
}

 

   Vous aurez sans doute remarqué le code, un peu similaire, qui apparaît pour les 4 directions dans lesquelles peut se déplacer notre héros. wink

   Il s'agit d'abord de détecter si le joueur entre en collision avec une Warp Spéciale (dont les valeurs sont comprises entre SPE1 et SPE10 (cf. constantes dans le header).

   Si c'est le cas, on retrouve le numéro de la wap (1, 2, 3... 10) en faisant un simple calcul : on retranche SPE1, soit le numéro de la 1ère tile warp, au numéro de notre tile warp détectée et on l'enregsitre dans la variable numberSPEcool

   On appelle ensuite : getWarpSP(numberSPE) pour retrouver la valeur du niveau vers lequel on doit warper, comme entrée dans le level editor. Bien entendu, si cette valeur vaut 0, cela signifie qu'on ne warpe pas. wink

   Maintenant qu'on connaît le numéro de la map vers laquelle on doit se téléporter, on enregistre la direction du warp (le joueur se dirigeait-il à droite, à gauche, en haut ou en bas ?) et les coordonnées du joueur.


   Puis, on change de level en appelant consécutivement : map.setLevel(map.getWarpLeft()); pour changer le numéro de la map en cours, map.changeLevel(); pour charger la nouvelle map et enfin reinitialize(map); pour réinitialiser la position de notre héros sur cette nouvelle map. wink

   Passons d'ailleurs à la mise à jour de cette dernière fonction :

 

Fichier : player.cpp : Modifiez la fonction (ou remplacez-la) :

void Player::reinitialize(Map &map)
{
 
// Coordonnées de démarrage de notre héros selon la direction de warp
//Si on n'a pas warpé, alors on commence aux coordonnées de départ de la map
if (map.getWarpDirection() == -1)
{
x = map.getBeginX();
y = map.getBeginY();
 
//On recentre la caméra
map.setStartX(map.getBeginX() - (SCREEN_WIDTH / 2));
map.setStartY(map.getBeginY() - (SCREEN_HEIGHT / 2));
}
//Si on a warpé en haut
else if (map.getWarpDirection() == UP)
{
//On change la valeur en y du héros pour qu'il se
//trouve en bas de la map
y = map.getMaxY() - h - 1;
 
//On recentre la caméra
map.setStartY(map.getMaxY() - SCREEN_HEIGHT);
}
//Si on a warpé en bas
else if (map.getWarpDirection() == DOWN)
{
//On change la valeur en y du héros pour qu'il se
//trouve en haut de la map
y = 1;
 
//On recentre la caméra
map.setStartY(0);
}
//Si on a warpé à gauche
else if (map.getWarpDirection() == LEFT)
{
//On change la valeur en x du héros pour qu'il se
//trouve à droite de la map
x = map.getMaxX() - w - 1;
 
//On recentre la caméra
map.setStartX(map.getMaxX() - SCREEN_WIDTH);
}
//Si on a warpé à droite
else if (map.getWarpDirection() == RIGHT)
{
//On change la valeur en x du héros pour qu'il se
//trouve à gauche de la map
x = 1;
 
//On recentre la caméra
map.setStartX(0);
}
 
//Si on a utilisé une warp spéciale
else if (map.getWarpDirection() == 4)
{
POINT point;
 
//On détecte la warp spéciale utilisée (1, 2, etc)
//et on enregistre ses coordonnées dans point
point.x = map.detectWarpSpe(numberSPE + SPE1).x;
point.y = map.detectWarpSpe(numberSPE + SPE1).y;
 
//On place le héros près de cette warp (sans la toucher)
//suivant la direction qu'il avait en arrivant :
//Ainsi s'il allait vers le haut, on va le faire réapparaître
//au-dessus de la warp, et ainsi de suite...
if (direction == UP)
{
x = point.x + 6;
y = point.y - h - 6;
}
else if (direction == DOWN)
{
x = point.x + 6;
y = point.y + TILE_SIZE + 6;
}
else if (direction == RIGHT)
{
x = point.x + TILE_SIZE + 6;
y = point.y + 6;
}
else if (direction == LEFT)
{
x = point.x - w - 6;
y = point.y + 6;
}
else
{
x = map.getBeginX();
y = map.getBeginY();
}
 
//On recentre la caméra
map.setStartX(x - (SCREEN_WIDTH / 2));
map.setStartY(y - (SCREEN_HEIGHT / 2));
}
 
}

 

   C'est ici la fin de la fonction qui nous intéresse et qui est nouvelle. wink

   En effet, on a une nouvelle condition, si l'on prend une tile warp spéciale. Dans ce cas, on va détecter les coordonnées de notre tile warp spéciale dans la nouvelle map à l'aide de la fonction detectWarpSpe() que nous allons implémenter juste après. cheeky Pour cela, on va lui envoyer le numéro de notre warp (1-10) auquel on va rajouter SPE1, pour retrouver le numéro de la 1ère tile dans le tileset (soit l'inverse de ce qu'on avait fait précédemment, en fait... cheeky).

   Une fois que nous aurons récupéré ses coordonnées dans la variable Point, nous allons nous en servir pour adapter les coordonnées de notre héros, selon la direction dans laquelle il allait, de façon à ce que le déplacement apparaisse logique et à ce qu'il n'entre pas en collision avec la nouvelle tile warp, sans quoi, il rewarperait direct, le pauvre ! surprise

   Ainsi, par exemple, si notre héros a touché la tile warp en se dirigeant vers le haut, il apparaîtra logique que notre héros réapparaisse en haut de la tile warp, dans la nouvelle map, pour ainsi donner l'illusion qu'il vient d'entrer par la porte. wink

Notez que pour que ce soit logique, les numéros des maps vers lesquels les warps pointent doivent correspondre d'une map à l'autre.
Ainsi, si dans la map 1, la Warp1 mène vers la map 10, dans la map 10, la warp1 doit pointer sur la map 1, afin de pouvoir faire un aller-retour logique.
Ce n'est toutefois pas obligé, et dans le cas de téléporteurs, vous pouvez vous amuser à créer un labyrinthe de portes qui pointent vers des maps différentes (on voit ça dans certains jeux). wink

   Passons maintenant à notre nouvelle fonction detectWarpSpe() dans le fichier map.cpp :

 

Fichier : map.cpp : Ajoutez la fonction detectWarpSpe() :

Map::POINT Map::detectWarpSpe(int number)
{
//On détecte la warp Spéciale sélectionnée dans la map
int x, y, MAXX, MAXY;
POINT point;
x = y = 0;
 
MAXX = maxX / TILE_SIZE;
MAXY = maxY / TILE_SIZE;
 
for (y = 0; y < MAXY; y++)
{
for (x = 0; x < MAXX; x++)
{
 
//Si la tile n'est pas vide vide
if (tile4[y][x] != 0)
{
/*On teste si c'est une tile warp */
if (tile4[y][x] == number)
{
point.x = x * TILE_SIZE;
point.y = y * TILE_SIZE;
return point;
}
 
}
 
}
}
}

 

   C'est une fonction très utile pour détecter une tile précise dans la map, et vous pourrez vous en reservir, en l'adaptant, pour détecter d'autres types de tiles. wink

   On déroule simplement tout le tableau de la map (ici simplement pour la couche 4, celle des collisions, mais on pourrait aussi s'en servir pour les autres couches wink) et pour chaque case du tableau on teste si la valeur de la tile enregistrée est la même que celle qu'on a envoyée à notre fonction, soit ici le numéro de la tile warp à détecter. Si c'est le cas, on renvoie ses coordonnées dans une variable Point.

Notez que la fonction s'interrompt dès la 1ère tile de ce type détectée (return). S'il y en a plusieurs, seule la première, dans l'ordre du tableau, sera détectée. wink

   Voilà, on rajoute maintenant le prototype de la fonction detectWarpSpe() dans notre en-tête, et on a fini ! angel

 

Fichier : map.h : Ajoutez le prototype de detectWarpSpe() :

//Fonctions
void loadMap(std::string filename);
void draw(int layer, sf::RenderWindow &window);
void changeLevel(void);
void testDefilement(void);
POINT detectWarpSpe(int number);

 

   Plus qu'à compiler et notre héros peut maintenant rentrer dans les bâtiments ! angel

   Je compte maintenant sur vous pour me construire un monde gigantesque, et qui ait de la classe ! wink

 

 

   @ bientôt pour la suite ! angel

                                                      Jay.

 

 

 

This site uses cookies to enable you to log in. We do not store or sell any personal data. By continuing to use this website, you agree to their use. Thanks!