Big Tuto SFML 2 : Rabidja v. 3.0

Chapitre 17 : Ajoutons des niveaux et des checkpoints !

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 24 février 2015
Date de révision : 20 mars 2016

      Prologue

   Bon, voilà, on peut maintenant aller au bout de la 1ère map, grâce à nos plateformes flottantes, mais on ne peut pas passer au niveau 2 ! surprise

   Il va donc nous falloir gérer cela ! wink

   Pour ce faire, on va utiliser une technique simple mais efficace : on gèrera le passage au niveau supérieur quand notre héros touchera le bord droit de la map.

   Et si vous voulez faire un niveau à étages (ce qui est possible angel), il vous suffira de mettre des murs sur tout le bord droit de l'écran sauf au niveau de la sortie (pour laquelle vous pouvez dessiner une porte, par exemple ! cheeky), et le tour est joué !

   Mais bon, comme c'est assez facile à faire et que ça ne suffirait pas à vous occuper tout un chapitre, on va aussi en profiter pour rajouter des checkpoints ! angel

   Mais, si, vous savez ce que c'est, ce sont ces points de passage qui sauvegardent votre position quand vous mourez et qui vous évitent d'avoir à vous retaper tout le niveau !

   Pour cela, on va utiliser 2 tiles de notre tileset : la tile checkpoint de base (un poteau) que nous changerons par la tile checkpoint validé (poteau + drapeau) quand notre héros la touchera. wink On enregistrera à ce moment-là aussi les coordonnées du joueur pour le ressusciter au bon endroit. indecision
   Et sinon, dans le level editor, il vous faudra utiliser la tile checkpoint de base, sinon, ça ne fonctionnera pas avec l'autre, comme nous allons le voir dans le code. wink

   Allez, let's go ! angel

 

      Le passage de niveaux

   On commence donc par la gestion de nos multi-niveaux. angel

   Je vous rappelle que la constante gérant le nombre max de niveaux : LEVEL_MAX, se trouve dans le header player.h.wink

   

Fichier : player.h : Ne rien rajouter, elle y est déjà ! :

//Nombre max de levels
const int LEVEL_MAX = 2;

    On n'a que 2 niveaux pour l'instant, mais libre à vous d'en créer d'autres ! wink

   Maintenant, pour gérer le passage au niveau sup' en touchant le bord droit de l'écran, il va nous falloir retourner dans notre fonction mapCollision() du fichier player.cpp. Là, au lieu d'empêcher le joueur de sortir de l'écran, on va maintenant le faire changer de niveau en chargeant la nouvelle map et en réinitialisant le joueur et le checkpoint (que nous allons ajouter juste après wink).

   Voilà le code en question, et vous remarquerez qu'il est plutôt simple. cheeky

   Ne vous embêtez pas à essayer de trouver où changer ce bout de code dans la fonction pour l'instant, je vous redonnerai la fonction complète ci-dessous. wink

 

Fichier : player.cpp : Ne rien modifier pour l'instant, vous pourrez copier/coller la fonction entière ci-dessous :

else if (x + w >= map.getMaxX())
{
//Si on touche le bord droit de l'écran, on passe au niveau sup
map.setLevel(map.getLevel() + 1);
 
//Si on dépasse le niveau max, on annule et on limite le déplacement du joueur
if (map.getLevel() > LEVEL_MAX)
{
map.setLevel(LEVEL_MAX);
x = map.getMaxX() - w - 1;
}
 
//Sinon, on passe au niveau sup, on charge la nouvelle map et on réinitialise le joueur
else
{
//On désactive le checkpoint
checkpointActif = 0;
 
map.changeLevel();
initialize(maptrue);
}
}

 

   Eh voilà, c'est tout pour le passage de niveaux ! angel

   Comme je vous le disais, il n'y a pas de quoi en faire tout un chapitre ! cheeky

      La gestion des checkpoints

   Pour nos checkpoints, il va aussi nous falloir modifier la même fonction.

   En effet, c'est aussi ici que l'on va gérer les collisions avec notre tile checkpoint, la modifier en passant à la tile suivante (+1 - il s'agit de la tile avec le drapeau levé wink), et enregistrer les coordonnées de notre joueur dans respawnX et respawnY.

   Je vous donne un aperçu du code ci-dessous. Ne le copiez pas pour l'instant. Je vous redonne la fonction mapCollision() complète juste après. wink

 

Fichier : player.cpp : Ne rien modifier pour l'instant, vous pourrez copier/coller la fonction entière ci-dessous :

//Test de la tile checkpoint
if (map.getTile(y1, x2) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x2 * TILE_SIZE;
respawnY = (y1 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y1, x2, map.getTile(y1, x2) + 1);
}
 
else if (map.getTile(y2, x2) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x2 * TILE_SIZE;
respawnY = (y2 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y2, x2, map.getTile(y2, x2) + 1);
}

     Et voilà donc la fonction complète, ce sera plus simple pour faire un copier/coller wink :

Fichier : player.cpp : code complet de mapCollision() - à copier pour remplacer le précédent :

void Player::mapCollision(Map &map, Sounds &sounds)
{
 
int i, x1, x2, y1, y2;
 
// Récup des infos pour la gestion des pentes (par Stephantasy)
dirXmem = dirX;
wasOnGround = onGround;
dirYmem = dirY;
posXmem = x;
posYmem = y;
 
/* D'abord, on considère le joueur en l'air jusqu'à temps
d'être sûr qu'il touche le sol */
onGround = 0;
 
/* 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)
{
 
//Test des tiles Power-up
if (map.getTile(y1, x2) >= TILE_POWER_UP_DEBUT
&& map.getTile(y1, x2) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y1, x2) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y1, x2, 0);
}
else if (map.getTile(y2, x2) >= TILE_POWER_UP_DEBUT
&& map.getTile(y2, x2) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y2, x2) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y2, x2, 0);
}
 
//Test de la tile checkpoint
if (map.getTile(y1, x2) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x2 * TILE_SIZE;
respawnY = (y1 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y1, x2, map.getTile(y1, x2) + 1);
}
else if (map.getTile(y2, x2) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x2 * TILE_SIZE;
respawnY = (y2 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y2, x2, map.getTile(y2, x2) + 1);
}
 
//On vérifie si les tiles recouvertes sont solides
if (map.getTile(y1, x2) > BLANK_TILE || map.getTile(y2, x2) > BLANK_TILE)
{
// 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)
{
 
//Test des tiles Power-up : Etoile et vie
if (map.getTile(y1, x1) >= TILE_POWER_UP_DEBUT
&& map.getTile(y1, x1) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y1, x1) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y1, x1, 0);
}
else if (map.getTile(y2, x1) >= TILE_POWER_UP_DEBUT
&& map.getTile(y2, x1) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y2, x1) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y2, x1, 0);
}
 
//Test de la tile checkpoint
if (map.getTile(y1, x1) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x1 * TILE_SIZE;
respawnY = (y1 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y1, x1, map.getTile(y1, x1) + 1);
}
else if (map.getTile(y2, x1) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x1 * TILE_SIZE;
respawnY = (y2 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y2, x1, map.getTile(y2, x1) + 1);
}
 
if (map.getTile(y1, x1) > BLANK_TILE || map.getTile(y2, x1) > BLANK_TILE)
{
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
//Test des tiles Power-up)
if (map.getTile(y2, x1) >= TILE_POWER_UP_DEBUT
&& map.getTile(y2, x1) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y2, x1) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y2, x1, 0);
}
else if (map.getTile(y2, x2) >= TILE_POWER_UP_DEBUT
&& map.getTile(y2, x2) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y2, x2) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y2, x2, 0);
}
 
//Test de la tile checkpoint
if (map.getTile(y2, x1) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x1 * TILE_SIZE;
respawnY = (y2 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y2, x1, map.getTile(y2, x1) + 1);
}
else if (map.getTile(y2, x2) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x2 * TILE_SIZE;
respawnY = (y2 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y2, x2, map.getTile(y2, x2) + 1);
}
 
 
/* Gestion des pics */
if ((map.getTile(y2, x1) == TILE_PIKES) || (map.getTile(y2, x2) == TILE_PIKES))
{
 
//On joue le sound Fx
sounds.PlaySoundFx(sounds.DESTROY);
 
//On fait sauter le joueur
dirY = -JUMP_HEIGHT;
 
if (life > 1)
{
//Si le timer d'invincibilité est à 0
//on perd un coeur
if (invincibleTimer == 0)
{
life--;
invincibleTimer = 80;
}
}
else
{
//On met le timer à 1 pour tuer le joueur intantanément
timerMort = 1;
//On joue le sound Fx
sounds.PlaySoundFx(sounds.DESTROY);
}
}
 
/* Gestion du ressort */
else if ((map.getTile(y2, x1) == TILE_RESSORT) || (map.getTile(y2, x2) == TILE_RESSORT))
{
dirY = -20;
//On indique au jeu qu'il a atterri pour réinitialiser le double saut
onGround = 1;
//On joue le sound Fx
sounds.PlaySoundFx(sounds.BUMPER);
}
 
/** !! Attention à ne pas oublier le else avant le prochain if, sinon vous resterez
coincés sur les pics et les ressorts !! **/
 
 
//Gestion des plateformes traversables : elles se situent juste avant
//les tiles bloquantes dans notre tileset (dont la valeur butoire est
//BLANK_TILE). Il suffit donc d'utiliser le numéro de la première tile
//traversable au lieu de BLANK_TILE pour bloquer le joueur,
//seulement quand il tombe dessus (sinon, il passe au-travers
//et le test n'est donc pas effectué dans les autres directions
else if (map.getTile(y2, x1) > TILE_TRAVERSABLE || map.getTile(y2, x2) > TILE_TRAVERSABLE)
{
//Si la tile est une plateforme ou une tile solide, on y colle le joueur et
//on le déclare sur le sol (onGround).
y = y2 * TILE_SIZE;
y -= h;
dirY = 0;
onGround = 1;
}
 
}
 
else if (dirY < 0)
{
//Test des tiles Power-up
if (map.getTile(y1, x1) >= TILE_POWER_UP_DEBUT
&& map.getTile(y1, x1) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y1, x1) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y1, x1, 0);
}
if (map.getTile(y1, x2) >= TILE_POWER_UP_DEBUT
&& map.getTile(y1, x2) <= TILE_POWER_UP_FIN)
{
//On appelle la fonction getItem()
getItem(map.getTile(y1, x2) - TILE_POWER_UP_DEBUT + 1, sounds);
 
//On remplace la tile power-up par une tile transparente
map.setTile(y1, x2, 0);
}
 
//Test de la tile checkpoint
if (map.getTile(y1, x1) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x1 * TILE_SIZE;
respawnY = (y1 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y1, x1, map.getTile(y1, x1) + 1);
}
else if (map.getTile(y1, x2) == TILE_CHECKPOINT)
{
//On active le booléen checkpoint
checkpointActif = 1;
 
//On enregistre les coordonnées
respawnX = x2 * TILE_SIZE;
respawnY = (y1 * TILE_SIZE) - h;
 
//On change la tile
map.setTile(y1, x2, map.getTile(y1, x2) + 1);
}
 
// Déplacement vers le haut
if (map.getTile(y1, x1) > BLANK_TILE || map.getTile(y1, x2) > BLANK_TILE)
{
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;
}
}
 
// Contrôle des pentes
checkSlope(map);
 
/* Maintenant, on applique les vecteurs de mouvement si le sprite n'est pas bloqué */
x += dirX;
y += dirY;
 
//Et on contraint son déplacement aux limites de l'écran.
if (x < 0)
{
x = 0;
}
 
else if (x + w >= map.getMaxX())
{
//Si on touche le bord droit de l'écran, on passe au niveau sup
map.setLevel(map.getLevel() + 1);
 
//Si on dépasse le niveau max, on annule et on limite le déplacement du joueur
if (map.getLevel() > LEVEL_MAX)
{
map.setLevel(LEVEL_MAX);
x = map.getMaxX() - w - 1;
}
 
//Sinon, on passe au niveau sup, on charge la nouvelle map et on réinitialise le joueur
else
{
//On désactive le checkpoint
checkpointActif = 0;
 
map.changeLevel();
initialize(map, true);
}
}
 
//Maintenant, s'il sort de l'écran par le bas (chute dans un trou sans fond), on lance le timer
//qui gère sa mort et sa réinitialisation (plus tard on gèrera aussi les vies).
if (y > map.getMaxY())
{
timerMort = 60;
}
}

  Cela commence à devenir une grosse fonction à présent et vous pouvez la découper en sous-fonctions, si vous préférez (surtout si vous souhaitez rajouter encore des choses wink ). Cela dit, j'ai choisi de ne pas le faire pour l'instant, de façon à vous donner une vision globale de ce que gère le jeu à chaque tour de boucle.  wink

   Voilà, sinon je vous rappelle que le respawn de notre héros est déjà géré dans la fonction player.initialize(), suivant qu'il a pris un checkpoint ou pas ! wink

   Je vous redonne la fonction complète ci-dessous, pour rappel :

Fichier : player.cpp : Ne rien rajouter, la fonction y est déjà ! :

void Player::initialize(Map &map, bool newLevel)
{
//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;
 
/* Coordonnées de démarrage/respawn de notre héros */
if (checkpointActif == true)
{
x = respawnX;
y = respawnY;
}
else
{
x = map.getBeginX();
y = map.getBeginY();
}
 
//On réinitiliase les coordonnées de la caméra
//si on change de niveau
if (newLevel == true)
{
map.setStartX(map.getBeginX());
map.setStartY(map.getBeginY());
}
 
/* 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;
onGround = false;
 
//On réinitialise le nombre de monstres
map.setNombreMonstres(0);
 
//On réinitialise le nombre de plateformes
map.setNombrePlateformes(0);
}

   Et c'est fini ! angel

   Voilà, on compile et on lance le programme ! wink   

   Maintenant, vous allez enfin pouvoir parcourir tous les niveaux du jeu ! angel

   Si c'est pas chouette, ça ! angel

   Bon, il ne nous reste maintenant plus grand chose à ajouter pour arriver au même niveau que celui du Big Tuto SDL 2 ! wink

   Concrètement, il nous manque juste la création de menus, l'ajout des shurikens lancés par notre lapin-ninja et les explosions de monstres !

   @ bientôt pour le chapitre 18 ! 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!