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

Chapitre 7 : Déplacement, scrolling et collisions

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

      Prologue

   Bon, c'est bien sympa, on a un héros dans notre jeu ! cool Mais il est nul : il n'avance pas ! indecision

   Il va donc être temps de remédier au problème, histoire de pouvoir au moins faire le tour de notre première map ! wink Pour cela, nous allons agir dans notre classe Player en rajoutant deux fonctions essentielles : mapCollision(), que vous connaissez sans doute déjà si vous avez suivi les tutos précédants wink, et qui nous permettra de gérer les collisions avec la couche 4 (collisions) de notre map, ainsi que centerScrolling(), qui permettra de centrer la caméra sur notre héros, pour gérer le scrolling. Là encore, riende nouveau, puisque ce sera grosso modo la même chose que pour Rabidja (on va pas réinventer la roue à chaque fois, non plus ! indecision).

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

 

      Le code

   Reprenons donc notre classe Player, et commençons par son en-tête : player.h. Rajoutons simplement les prototypes de nos deux nouvelles fonctions. wink

Fichier : player.h : Rajouter les prototypes de centerScrolling() et mapCollision() :

//Fonctions
void initialize(Map &map);
void draw(Map &map, sf::RenderWindow &window);
void update(Input &input, Map &map);
void centerScrolling(Map &map);
void mapCollision(Map &map); 

 

   Voilà, passons maintenant au fichier player.cpp, dans lequel on va trouver toutes les nouveautés de ce chapitre.

   Commençons par modifier notre fonction update() pour qu'elle appelle nos nouvelles fonctions, juste avant la gestion de la mort. wink
   Par souci de commodité, je vous remets toute la fonction ci-dessous, mais il n'y a en fait que deux lignes à rajouter : les appels à mapCollision() puis à centerScrolling()cheeky

Fichier : player.cpp : Remplacez la fonction par :

void Player::update(Input &input, Map &map)
{
//On rajoute un timer au cas où notre héros mourrait lamentablement...
//Si le timer vaut 0, c'est que tout va bien, sinon, on le décrémente jusqu'à 0, et là,
//on réinitialise.
//C'est pour ça qu'on ne gère le joueur que si ce timer vaut 0.
if (timerMort == 0)
{
//On gère le timer de l'invincibilité
if (invincibleTimer > 0)
invincibleTimer--;
 
//On réinitialise nos vecteurs de déplacement, pour éviter que le perso
//ne fonce de plus en plus vite pour atteindre la vitesse de la lumière ! ;)
//Essayez de le désactiver pour voir !
dirX = 0;
dirY = 0;
 
//Gestion de la course en appuyant sur la touche courir
if (input.getButton().run)
isrunning = 1;
else
isrunning = 0;
 
//Voilà, au lieu de changer directement les coordonnées du joueur, on passe par un vecteur
//qui sera utilisé par la fonction mapCollision(), qui regardera si on peut ou pas déplacer
//le joueur selon ce vecteur et changera les coordonnées du player en fonction.
if (input.getButton().left == true)
{
dirX -= PLAYER_SPEED + isrunning;
//Et on indique qu'il va à gauche (pour le flip
//de l'affichage, rappelez-vous).
direction = LEFT;
 
//Si ce n'était pas son état auparavant :
if (etat != WALK)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
etat = WALK;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
//Si on détecte un appui sur la touche fléchée droite
else if (input.getButton().right == true)
{
//On augmente les coordonnées en x du joueur
dirX += PLAYER_SPEED + isrunning;
//Et on indique qu'il va à droite (pour le flip
//de l'affichage, rappelez-vous).
direction = RIGHT;
 
//Si ce n'était pas son état auparavant
if (etat != WALK)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
etat = WALK;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
//Si on détecte un appui sur la touche fléchée haut
else if (input.getButton().up == true)
{
//On augmente les coordonnées en x du joueur
dirY -= PLAYER_SPEED + isrunning;
//Et on indique qu'il va à droite (pour le flip
//de l'affichage, rappelez-vous).
direction = UP;
 
//Si ce n'était pas son état auparavant
if (etat != WALK)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
etat = WALK;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
//Si on détecte un appui sur la touche fléchée bas
else if (input.getButton().down == true)
{
//On augmente les coordonnées en x du joueur
dirY += PLAYER_SPEED + isrunning;
//Et on indique qu'il va à droite (pour le flip
//de l'affichage, rappelez-vous).
direction = DOWN;
 
//Si ce n'était pas son état auparavant
if (etat != WALK)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
etat = WALK;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
//Si on n'appuie sur rien, on charge l'animation marquant l'inactivité (Idle)
else if (input.getButton().right == false && input.getButton().left == false &&
input.getButton().up == false && input.getButton().down == false)
{
//On teste si le joueur n'était pas déjà inactif, pour ne pas recharger l'animation
//à chaque tour de boucle
if (etat != IDLE)
{
//On enregistre l'anim' de l'inactivité et on l'initialise à 0
etat = IDLE;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
//On rajoute notre fonction de détection des collisions qui va mettre à
//jour les coordonnées de notre héros.
mapCollision(map);
 
//On gère le scrolling (fonction ci-dessous)
centerScrolling(map);
 
}
 
//Gestion de la mort :
//Si timerMort est différent de 0, c'est qu'il faut réinitialiser le joueur.
//On ignore alors ce qui précède et on joue cette boucle (un wait en fait) jusqu'à ce que
// timerMort == 1. A ce moment-là, on le décrémente encore -> il vaut 0 et on réinitialise
//le jeu avec notre bonne vieille fonction d'initialisation ;) !
if (timerMort > 0)
{
timerMort--;
 
if (timerMort == 0)
{
 
// Si on est mort, on réinitialise le niveau
map.changeLevel();
initialize(map);
 
}
}
} 

   Et voilà, rien de bien dur ici non plus... cheeky

   Mais ça ne va pas durer, car on passe à nos nouvelles fonctions (enfin, nouvelles, si vous n'avez jamais lu les précédents tutos ! indecision)

   Commençons par centerScrolling() qui va gérer la caméra et le scrolling, et que nous allons implémenter de ce pas, à la suite du même fichier :

Fichier : player.cpp : Rajoutez :

void Player::centerScrolling(Map &map)
{
// Nouveau scrolling à sous-boîte limite :
//Pour éviter les effets de saccades dus à une caméra qui se
//centre automatiquement et constamment sur le joueur (ce qui
//peut en rendre malade certains...), on crée une "boîte" imaginaire
//autour du joueur. Quand on dépasse un de ses bords (en haut, en bas,
//à gauche ou à droite), on scrolle.
//Mais là encore, au lieu de centrer sur le joueur, on déplace simplement
//la caméra jusqu'à arriver au joueur. On a changé ici la valeur à 4 pixels
//pour que le jeu soit plus rapide.
int cxperso = x + w / 2;
int cyperso = y + h / 2;
int xlimmin = map.getStartX() + LIMITE_X;
int xlimmax = xlimmin + LIMITE_W;
int ylimmin = map.getStartY() + LIMITE_Y;
int ylimmax = ylimmin + LIMITE_H;
 
//Effet de retour en arrière quand on est mort :
//Si on est très loin de la caméra, plus loin que le bord
//de la map, on accélère le scrolling :
if (cxperso < map.getStartX())
{
map.setStartX(map.getStartX() - 30);
}
 
//Si on dépasse par la gauche, on recule la caméra
else if (cxperso < xlimmin)
{
map.setStartX(map.getStartX() - 3 - isrunning);
}
 
//Effet de retour en avant quand on est mort (au
//cas où le joueur s'amuse à faire le niveau à rebours
//après une checkpoint) :
//Si on est très loin de la caméra, plus loin que le bord
//de la map, on accélère le scrolling :
if (cxperso > map.getStartX() + SCREEN_WIDTH)
{
map.setStartX(map.getStartX() + 30);
}
 
//Si on dépasse par la droite, on avance la caméra
else if (cxperso > xlimmax)
{
map.setStartX(map.getStartX() + 3 + isrunning);
}
 
//Si on arrive au bout de la map à gauche, on stoppe le scrolling
if (map.getStartX() < 0)
{
map.setStartX(0);
}
 
//Si on arrive au bout de la map à droite, on stoppe le scrolling à la
//valeur Max de la map - la moitié d'un écran (pour ne pas afficher du noir).
else if (map.getStartX() + SCREEN_WIDTH >= map.getMaxX())
{
map.setStartX(map.getMaxX() - SCREEN_WIDTH);
}
 
//Si on dépasse par le haut, on remonte la caméra
if (cyperso < ylimmin)
{
map.setStartY(map.getStartY() - 3 - isrunning);
}
 
//Si on dépasse par le bas, on descend la caméra
if (cyperso > ylimmax)
{
map.setStartY(map.getStartY() + 3 + isrunning);
}
 
//Si on arrive au bout de la map en haut, on stoppe le scrolling
if (map.getStartY() < 0)
{
map.setStartY(0);
}
 
//Si on arrive au bout de la map en bas, on stoppe le scrolling à la
//valeur Max de la map - la moitié d'un écran (pour ne pas afficher du noir).
else if (map.getStartY() + SCREEN_HEIGHT >= map.getMaxY())
{
map.setStartY(map.getMaxY() - SCREEN_HEIGHT);
}
 
}

   Voilà, c'est quasiment le copier / coller de la fonction de Rabidja, mis à part que je gère ici aussi la course, à l'aide de la variable isrunning. En effet, le scrolling doit accélérer, si le personnage court, logique, non ? indecision Sinon, j'ai largement commenté la fonction et je vous redonne les explications que j'avais écrites pour Rabidja, ça ne fera pas de mal ! cheeky

   Si vous comparez avec la fonction équivalente du Big Tuto SDL 1.2 / 2, vous remarquerez qu'elle est largement plus complexe ! wink En effet, à l'époque, on se contentait de calculer le centre de notre personnage et d'adapter la caméra (et donc le scrolling) autour. C'est simple, mais l'inconvénient c'est que la caméra se déplace par saccades, et à chaque mouvement du perso (même si ce n'est que d'un pixel). Et cela peut causer de la motion sickness chez certaines personnes (qui sont malades à cause du mouvement - un peu comme en voiture, en fait cheeky).

   Pour éviter cela, nous avons créé une nouvelle caméra plus fluide, qui ne se déplace que si le héros sort du cadre central et qui reste fluide en ayant un mouvement continu et toujours identique.

   Le schéma suivant vous montre maintenant comment cela fonctionne :

   On va, en effet, considérer un cadre central (ou sous-boîte limite), dans lequel le joueur pourra se déplacer sans faire scroller la caméra (il est ici assez petit, mais vous pourrez l'adapter comme bon vous semblera à l'aide des constantes situées dans player.h, que nous avons vues au chapitre précédent : LIMITE_X = 400; LIMITE_Y = 220; LIMITE_W = 100; et LIMITE_H = 80; wink).

   Dès que le joueur va sortir de ce cadre, on fera scroller la map dans la direction dans laquelle se dirige le perso, de 3 pixels + la valeur de isrunning (0 ou 1, selon qu'il court ou pas).

   Bien entendu, ce cadre agit aussi bien horizontalement que verticalement. 

 

   Les limites de la map :

   Une dernière chose : vous noterez que le scrolling s'arrêtera automatiquement aux limites de la map, et cela pour éviter de blitter du noir, qui ne ferait pas très joli ! Il ne pourra donc pas avoir de valeurs négatives (en-dehors de la map en haut et à gauche) ou supérieures à maxX - la moitié de la taille de l'écran en largeur, à droite, et maxY - la moitié de la taille de l'écran en hauteur, en bas. Il ne faut pas oublier de compter la moitié de la taille de l'écran dans ce calcul, sinon vous vous retrouveriez avec une moitié d'écran noire voire un beau plantage, car la fonction irait chercher en-dehors des limites du tableau de notre map (vous pouvez tester si le coeur vous en dit, mais sauvegardez votre travail avant ! cheeky).

   Voilà, pour cette fonction ! Passons maintenant à l'autre grosse fonction de ce chapitre : mapCollision()wink

   Comme c'est encore une fonction connue des tutos précédents, son code est extrêmement commenté, et devrait suffire.

Fichier : player.cpp : Rajoutez :

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;
 
//Si on touche les bords de l'écran,
if (x < 0)
{
//On stoppe le joueur
x = 0;
}
 
else if (x + w >= map.getMaxX())
{
//On stoppe le joueur
x = map.getMaxX() - w;
}
 
else if (y < 0)
{
//On stoppe le joueur
y = 0;
}
 
else if (y + h > map.getMaxY())
{
//On stoppe le joueur
y = map.getMaxY() - h;
}
}

   La fonction reste encore très embryonnaire, car on ne teste que les collisions avec les tiles MUR. Notez d'ailleurs que l'accesseur map.getTile(y1, x1) renvoie automatiquement le numéro de la tile contenue dans la couche (layer) 4, celle des collisions. Contrairement à Rabidja, nous n'aurons pas besoin de tester les autres couches car nous avons désormais une couche dédiée spécialement aux collisions. wink

   En ce qui concerne la fin de la fonction, vous noterez qu'on arrête bien le héros aux limites de la map, pour éviter qu'on en sorte. Contrairement à Rabidja, on évite aussi au héros de sortir de l'écran par en haut ou par en bas. En effet, nous ne sommes plus dans un jeu de plateformes, et il ne pourra pas sauter hors de l'écran ou tomber dans un trou ! indecision

   C'est d'ailleurs à ce niveau-là, que nous détecterons ensuite si notre héros doit changer de map, pour aller en haut, en bas, à droite ou à gauche, en utilisant une warp. wink

   Bon allez, ça fait déjà beaucoup pour aujourd'hui ! Compilons un peu pour voir ! cool

         Oui ! On peut désormais déplacer notre héros à-travers la map, en nous cognant bien dans les murs et la caméra le suit ! angel

 

   Mais, mais !!?!! sad On fait comment pour changer de map et aller en bas ou à droite ? surprise J'ai envie de visiter, moi ! blush

   Pas d'inquiétude ! laugh On verra comment gérer les warps dans les deux prochains chapitres ! cool

         Alors, @ bientôt pour la suite ! angel

                                                      Jay.

 

 

Connexion

CoalaWeb Traffic

Today127
Yesterday232
This week1153
This month3446
Total1742653

20/04/24