Big Tuto SFML 2 / Action-RPG : Legends of Meruvia
Chapitre 8 : Gestion des warps de direction
Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 19 février 2016
Date de révision : 8 juillet 2016
Prologue
Et voilà, notre héros peut se balader dans la map !
Mais bon, comment dire... ça va être un peu limité pour rentrer tout notre jeu dans une seule map !
C'est pour ça que nous allons maintenant implémenter notre système de warps, afin de pouvoir passer d'une map à l'autre pour créer un monde GIGANTESQUE ! Mais là, je vous laisserai faire avec le level editor.
Mais on ne va pas tout implémenter d'un coup. On va commencer, dans ce chapitre, par les warps de direction qui nous permettront de changer de map en touchant le bord de l'écran en haut, en bas, à gauche ou à droite. Dans le chapitre suivant, nous aborderons les warps spéciales qui nous permettront d'entrer dans les maisons, grottes ou tout ce que vous voudrez !
Voilà, donc avant de passer au code, vous pouvez charger, comme d'habitude, les archives complètes du jeu ci-dessous :
La théorie
Commençons par un peu de théorie pour voir ce que nous allons faire.
Si vous êtes courageux, 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.
Comme vous le voyez sur le schéma ci-dessus, ce qu'on veut, c'est que :
- quand on touche un bord de l'écran en haut, en bas, à gauche ou à droite, on warpe vers une nouvelle map.
- pour éviter de warper n'importe où sur le bord de l'écran, il appartiendra au level designer de créer ses maps intelligemment en mettant des obstacles (arbres, murs, etc.) là où il ne veut pas que le joueur warpe. Il faudra aussi aligner les passages / chemins pour éviter que le joueur ne se retrouve coincé dans le décor.
- si on n'a pas défini de map pour warper, dans le level editor (valeur = 0), il faudra aussi interdire la warp en bloquant simplement le joueur contre le bord de l'écran, comme on le fait déjà.
- quand on va warper, il faudra aussi faire attention à changer les coordonnées X et Y de notre héros. En effet, s'il touche le bas de l'écran, il devra se retrouver en haut de la map suivante, logique ! Idem pour les autres directions. Il faudra aussi faire attention à ne pas le coller contre le haut de l'écran, sinon il se mettra à rewarper en sens inverse et ainsi de suite à linfini !
Mais alors, comment on va faire ça dans le code ?
Allez, je vous donne un petit coup de pouce, pour que vous essayiez de le faire vous-même :
- A la fin de la fonction mapCollision() : quand on va toucher un bord de l'écran, on va tester si la warp directionnelle correspondante est différente de 0. Si elle vaut 0, c'est qu'on ne peut pas warper, sinon, on se téléporte au numéro de la map indiqué. Ainsi, si on touche le bord droit de l'écran, on va tester si getWarpUp() > 0, si c'est le cas, on va changer pour la map indiquée et...
- Appeler la fonction reinitialize() qui va placer le joueur correctement suivant la direction d'où il vient (il faudra donc l'enregistrer). S'il venait de la droite, toujours, il va se retrouver, logiquement, à la gauche de la map suivante.
- Notez qu'il faudra rajouter map.setWarpDirection(-1); à la fin de la fonction initialize() pour placer le joueur correctement au début de la map (aux coordonnées de début définies dans le level editor), si on n'a pas pris de warp.
Le code
Bon, allez, passons maintenant à la soluce !
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 !
Reprenons donc notre classe Player, et commençons par player.cpp. Tout le chapitre va se concentrer dans cette fonction.
Commençons par mettre à jour la fin de notre fonction mapCollision() pour détecter les warps de direction (haut, bas, gauche, droite).
Par souci de commodité, je vous remets toute la fonction ci-dessous, mais il n'y a que la fin à modifier.
Fichier : player.cpp : Modifiez la fin de 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 vérifie si les tiles recouvertes sont solides
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)
{
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
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
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;
//****C'EST A PARTIR D'ICI QUE LE CODE CHANGE****
//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);
}
}
}
|
Voilà, comme indiqué précédemment, vous voyez que l'on stoppe toujours le joueur le long du bord de l'écran (comme avant), mais la nouveauté c'est que, selon la direction dans laquelle on va, on teste la valeur de la warp correspondante (avec map.getWarpDown() par exemple, quand on touche le bas de l'écran). Si sa valeur est supérieure à 0, on warpe, sinon on ne fait rien.
Si on doit warper, on commence par enregistrer la direction dont on vient, en appelant map.setWarpDirection(), puis on enregistre les coordonnées de notre héros (X et Y). Cela nous sera utile après pour savoir comment changer les coordonnées de notre héros.
Et puis, on change ensuite le niveau pour la nouvelle map (celle dont la valeur a été enregistrée dans le level editor), on appelle changeLevel() pour la charger, puis reinitialize() pour réinitialier la position de notre héros sur la map.
Et c'est cette dernière fonction, que nous allons maintenant rajouter :
Fichier : player.cpp : Rajoutez la fonction :
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);
}
}
|
Vous voyez que ce n'est pas si compliqué que ça, tout compte fait.
Notez bien le décalage d'1 pixel pour éviter de rewarper en sens inverse. C'est peu, mais ça suffit !
Voilà, il ne nous reste plus qu'à reprendre notre fonction initialize() et à rajouter : map.setWarpDirection(-1); à la fin de la fonction, pour éviter d'éventuels problèmes par la suite, en warpant, par défaut, aux coordonnées de début de la map.
Fichier : player.cpp : Modifiez la fonction (ou remplacez-la) :
void Player::initialize(Map &map)
{
//PV à 3
life = 3;
//Timer d'invincibilité à 0
invincibleTimer = 0;
//Indique l'état et la direction de notre héros
direction = RIGHT;
etat = IDLE;
//Indique le numéro de la frame où commencer
frameNumber = 0;
//...la valeur de son chrono ou timer
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
//... et son nombre de frames max (8 pour l'anim' IDLE
// = ne fait rien)
frameMax = 8;
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));
/* Hauteur et largeur de notre héros */
w = PLAYER_WIDTH;
h = PLAYER_HEIGTH;
//Variables nécessaires au fonctionnement de la gestion des collisions
timerMort = 0;
isAttacking = 0;
map.setWarpDirection(-1);
}
|
Voilà, on rajoute maintenant le prototype de la fonction reinitialize() dans notre en-tête, et on a fini !
Fichier : player.h : Ajoutez le prototype de reinitialize() :
//Fonctions
void initialize(Map &map);
void reinitialize(Map &map);
void draw(Map &map, sf::RenderWindow &window);
void update(Input &input, Map &map);
void centerScrolling(Map &map);
void mapCollision(Map &map);
|
Plus qu'à compiler et notre héros peut maintenant partir explorer le monde !
Je compte sur vous pour l'agrandir cette map, d'ailleurs !
Mais, mais !!?!! On fait comment pour rentrer dans les maisons !? J'ai envie de faire les boutiques, moi !
Pas d'inquiétude ! On verra comment gérer les warps spéciales dans le prochain chapitre !
Alors, @ bientôt pour la suite !
Jay.
Abonnez-vous et devenez Premium pour lire la fin de ce tuto
ainsi que tous les autres tutos du site !
Et voilà le résultat final auquel vous parviendrez à la fin de ce Tuto !
@ bientôt !
Jay