Big Tuto SDL 2 : Rabidja v. 3.0
Chapitre 10 : Collisions avec la map
Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Ecriture : 22 octobre 2014
Dernière mise à jour : 9 novembre 2015
Prologue
On a maintenant un héros flottant dans les airs !
C'est cool, mais c'est pas superman, non plus !... Nous, c'est super lapin ninja, alias Rabidja ! Et comme tout bon lapin, il doit bondir partout !
Mais pour bondir, il faut aussi qu'on gère les collisions avec la map !
Alors, qu'est-ce qu'on attend !?! Au boulot !
Le code
Nous allons d'abord commencer par rajouter quelques nouvelles defs dont nous allons avoir besoin par la suite pour gérer nos tiles (cf. chapitre 7).
Ouvrez donc le fichier defs.h et rajoutez les valeurs suivantes :
Fichier : defs.h : Rajoutez :
/* VALEURS DES TILES (cf. chapitre 7) */
// Constante définissant le seuil entre les tiles traversables
// (blank) et les tiles solides
#define BLANK_TILE 89
//Plateformes traversables
#define TILE_TRAVERSABLE 70
//Tiles Power-ups
#define TILE_POWER_UP_DEBUT 67
#define TILE_POWER_UP_FIN 69
#define TILE_POWER_UP_COEUR 68
//Autres Tiles spéciales
#define TILE_RESSORT 115
#define TILE_CHECKPOINT 23
#define TILE_MONSTRE 126
#define TILE_PIKES 117
//Tiles plateformes mobiles
#define TILE_PLATEFORME_DEBUT 120
#define TILE_PLATEFORME_FIN 121
|
Ces valeurs correspondent tout simplement au numéro de la ou les tile(s) correspondantes. Ainsi, nos tiles sont traversables jusqu'à la tile 89, sauf les tiles 70 à 89 qui sont des plateformes (et donc elles ne seront pas traversables par le haut ). Les autres tiles seront toutes solides (le joueur se cognera dedans ).
Bien entendu, ces tiles spéciales ne seront pas actives dès ce chapitre et s'afficheront donc telles quelles (on pourra parfois se cogner dedans, comme pour les tiles monstres). On reverra leur fonctionnement plus en détails dans des chapitres ultérieurs qui leur seront dédiés.
Retournons maintenant dans notre fichier player.c et complétons notre fonction updatePlayer() :
Fichier : player.c : Remplacez la fonction précédente par :
void updatePlayer(Input *input)
{
//On rajoute un timer au cas où notre héros mourrait lamentablement en tombant dans un trou...
//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 (player.timerMort == 0)
{
//On gère le timer de l'invincibilité
if (player.invincibleTimer > 0)
player.invincibleTimer--;
//On réinitialise notre vecteur de déplacement latéral (X), 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 !
player.dirX = 0;
// La gravité fait toujours tomber le perso : on incrémente donc le vecteur Y
player.dirY += GRAVITY_SPEED;
//Mais on le limite pour ne pas que le joueur se mette à tomber trop vite quand même
if (player.dirY >= MAX_FALL_SPEED)
player.dirY = MAX_FALL_SPEED;
//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->left == 1)
{
player.dirX -= PLAYER_SPEED;
//Et on indique qu'il va à gauche (pour le flip
//de l'affichage, rappelez-vous).
player.direction = LEFT;
//Si ce n'était pas son état auparavant et qu'il est bien sur
//le sol (car l'anim' sera différente s'il est en l'air)
if (player.etat != WALK && player.onGround == 1)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
player.etat = WALK;
player.frameNumber = 0;
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
player.frameMax = 8;
}
}
//Si on détecte un appui sur la touche fléchée droite
else if (input->right == 1)
{
//On augmente les coordonnées en x du joueur
player.dirX += PLAYER_SPEED;
//Et on indique qu'il va à droite (pour le flip
//de l'affichage, rappelez-vous).
player.direction = RIGHT;
//Si ce n'était pas son état auparavant et qu'il est bien sur
//le sol (car l'anim' sera différente s'il est en l'air)
if (player.etat != WALK && player.onGround == 1)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
player.etat = WALK;
player.frameNumber = 0;
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
player.frameMax = 8;
}
}
//Si on n'appuie sur rien et qu'on est sur le sol, on charge l'animation marquant l'inactivité (Idle)
else if (input->right == 0 && input->left == 0 && player.onGround == 1)
{
//On teste si le joueur n'était pas déjà inactif, pour ne pas recharger l'animation
//à chaque tour de boucle
if (player.etat != IDLE)
{
//On enregistre l'anim' de l'inactivité et on l'initialise à 0
player.etat = IDLE;
player.frameNumber = 0;
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
player.frameMax = 8;
}
}
//Et voici la fonction de saut très simple :
//Si on appuie sur la touche saut et qu'on est sur le sol, alors on attribue une valeur
//négative au vecteur Y
//parce que sauter veut dire se rapprocher du haut de l'écran et donc de y=0.
if (input->jump == 1)
{
if (player.onGround == 1)
{
player.dirY = -JUMP_HEIGHT;
player.onGround = 0;
player.jump = 1;
}
/* Si on est en saut 1, on peut faire un deuxième bond et on remet jump1 à 0 */
else if (player.jump == 1)
{
player.dirY = -JUMP_HEIGHT;
player.jump = 0;
}
input->jump = 0;
}
/* Réactive la possibilité de double saut si on tombe sans sauter */
if (player.onGround == 1)
player.jump = 1;
//On gère l'anim du saut
if (player.onGround == 0)
{
//Si on est en saut 1, on met l'anim' du saut normal
if (player.jump == 1)
{
if (player.etat != JUMP1)
{
player.etat = JUMP1;
player.frameNumber = 0;
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
player.frameMax = 2;
}
}
else
{
if (player.etat != JUMP2)
{
player.etat = JUMP2;
player.frameNumber = 0;
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
player.frameMax = 4;
}
}
}
//On rajoute notre fonction de détection des collisions qui va mettre à
//jour les coordonnées de notre super lapin.
mapCollision(&player);
//On gère le scrolling (fonction ci-dessous)
centerScrollingOnPlayer();
}
//Gestion de la mort quand le héros tombe dans un trou :
//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 (player.timerMort > 0)
{
player.timerMort--;
if (player.timerMort == 0)
{
// Si on est mort, on réinitialise le niveau
changeLevel();
initializePlayer(0);
}
}
}
|
Le plus simple pour vous sera sans doute de remplacer votre fonction existante, après avoir regardé les morceaux de code qui changent.
Et qu'est-ce qui change justement ?
J'ai largement commenté la fonction pour vous aider à vous y repérer, mais globalement, on ajoute d'abord notre timerMort qui permettra de savoir si notre héros est vivant ou non. Ainsi, si on le met à 1, on déclare notre héros mort (ça marchera avec les monstres aussi par la suite ) et il nous sert alors aussi à faire une boucle de temporisation, avant de le ressusciter (sinon, ça aurait été un peu déstabilisant de le faire revivre dans la seconde, avant même que le joueur ait pigé pourquoi il était mort ! ).
Ensuite, on rajoute notre saut et notre double saut : le système fonctionne ainsi : on applique un vecteur pesanteur à notre héros (GRAVITY_SPEED sur dirY) qui le fait tomber jusqu'à sa vitesse max (MAX_FALL_SPEED). Ensuite, dans notre fonction mapCollision() que nous étudierons ensuite et qui gèrera les collisions avec la map, on testera si notre héros est sur (ou dans) une tile solide, et on le calera à ce moment-là contre elle (parce que coincé dedans, ça serait pas top... ). Notre héros sera alors onGround (sur le sol).
Dès lors, si on appuie sur la touche SAUT, on enlève -JUMP_HEIGHT à son dirY pour le faire monter vers le haut de l'écran, et donc sauter, et on change la valeur de son anim'. Même chose, la fonction mapCollision() vérifiera que le perso ne rentre pas dans le plafond en haut et le stoppera avant !
Pour le double saut, on ne peut plus utiliser notre fonction onGround, on va donc utiliser une nouvelle variable jump, de la même façon. Sans elle, on pourrait double sauter à volonté (vous pouvez tester ).
Voilà globalement ce qui a changé. Je vous laisse suivre les commentaires pour le reste. Reprenons maintenant notre fonction centerScrollingOnPlayer() :
Fichier : player.c : Remplacez la fonction précédente par :
void centerScrollingOnPlayer(void)
{
// 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 = player.x + player.w / 2;
int cyperso = player.y + player.h / 2;
int xlimmin = getStartX() + LIMITE_X;
int xlimmax = xlimmin + LIMITE_W;
int ylimmin = 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 < getStartX())
{
setStartX(getStartX() - 30);
}
//Si on dépasse par la gauche, on recule la caméra
else if (cxperso < xlimmin)
{
setStartX(getStartX() - 4);
}
//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 > getStartX() + SCREEN_WIDTH)
{
setStartX(getStartX() + 30);
}
//Si on dépasse par la droite, on avance la caméra
else if (cxperso > xlimmax)
{
setStartX(getStartX() + 4);
}
//Si on arrive au bout de la map à gauche, on stoppe le scrolling
if (getStartX() < 0)
{
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 (getStartX() + SCREEN_WIDTH >= getMaxX())
{
setStartX(getMaxX() - SCREEN_WIDTH);
}
//Si on dépasse par le haut, on remonte la caméra
if (cyperso < ylimmin)
{
setStartY(getStartY() - 4);
}
//Si on dépasse par le bas, on descend la caméra
if (cyperso > ylimmax)
{
//Sauf si on tombe très vite, auquel cas, on accélère la caméra :
if (player.dirY >= MAX_FALL_SPEED - 2)
{
setStartY(getStartY() + MAX_FALL_SPEED + 1);
}
else
{
setStartY(getStartY() + 4);
}
}
//Si on arrive au bout de la map en haut, on stoppe le scrolling
if (getStartY() < 0)
{
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 (getStartY() + SCREEN_HEIGHT >= getMaxY())
{
setStartY(getMaxY() - SCREEN_HEIGHT);
}
}
|
Il est maintenant temps de mettre à jour notre fonction initializePlayer() :
Fichier : player.c : Remplacez la fonction précédente par :
void initializePlayer(int newLevel)
{
//PV à 3
player.life = 3;
//Timer d'invincibilité à 0
player.invincibleTimer = 0;
//Indique l'état et la direction de notre héros
player.direction = RIGHT;
player.etat = IDLE;
//Indique le numéro de la frame où commencer
player.frameNumber = 0;
//...la valeur de son chrono ou timer
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
//... et son nombre de frames max (8 pour l'anim' IDLE
// = ne fait rien)
player.frameMax = 8;
player.x = getBeginX();
player.y = getBeginY();
//On réinitiliase les coordonnées de la caméra
//si on change de niveau
if (newLevel == 1)
{
setStartX(getBeginX());
setStartY(getBeginY());
}
/* Hauteur et largeur de notre héros */
player.w = PLAYER_WIDTH;
player.h = PLAYER_HEIGTH;
//Variables nécessaires au fonctionnement de la gestion des collisions
player.timerMort = 0;
player.onGround = 0;
}
|
Afin que notre petit effet de caméra ne se produise que quand on meurt et pas quand on commence un niveau, il nous faut modifier cette fonction, pour qu'elle prenne en argument une nouvelle variable newLevel. Si celle-ci vaut 1, on commence un nouveau niveau et on réinitialise la caméra, sinon non.
Il nous faut donc changer ses appels dans le main() en rajoutant 1 comme argument (sinon, c'est 0 dans les fonctions ci-dessus ) :
Fichier : main.c : Modifiez l'appel à initializePlayer() :
int main(int argc, char *argv[])
{
unsigned int frameLimit = SDL_GetTicks() + 16;
int go;
// Initialisation de la SDL
init("Rabidja 3 - SDL 2 - www.meruvia.fr");
// Chargement des ressources (graphismes, sons)
loadGame();
/* On initialise le joueur */
initializePlayer(1);
|
Passons maintenant à notre fameuse fonction mapCollision() située dans le fichier map.c.
Comme cette fonction est longue et complexe (mais aussi assez répétitive), j'ai préféré la commenter directement dans le code. C'est sans conteste, l'une des fonctions les plus complexes que nous ayions vues jusqu'ici. Ne vous inquiétez donc pas si vous ne comprenez pas tout du premier coup, nous y reviendrons par la suite.
Pour faire simple, elle décortique notre sprite en blocs correspondant chacun à une tile pour voir quelles tiles le sprite recouvre. Si celles-ci sont traversables (blank), c'est OK, sinon, elle stoppe le sprite en le collant contre les tiles solides.
Je vous laisse lire les commentaires :
Fichier : map.c : Ajoutez la fonction :
void mapCollision(GameObject *entity)
{
int i, x1, x2, y1, y2;
/* D'abord, on considère le joueur en l'air jusqu'à temps
d'être sûr qu'il touche le sol */
entity->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 (entity->h > TILE_SIZE)
i = TILE_SIZE;
else
i = entity->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 = (entity->x + entity->dirX) / TILE_SIZE;
x2 = (entity->x + entity->dirX + entity->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 = (entity->y) / TILE_SIZE;
y2 = (entity->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 (entity->dirX > 0)
{
//On vérifie si les tiles recouvertes sont solides
if (map.tile[y1][x2] > BLANK_TILE || map.tile[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).
entity->x = x2 * TILE_SIZE;
entity->x -= entity->w + 1;
entity->dirX = 0;
}
}
//Même chose à gauche
else if (entity->dirX < 0)
{
if (map.tile[y1][x1] > BLANK_TILE || map.tile[y2][x1] > BLANK_TILE)
{
entity->x = (x1 + 1) * TILE_SIZE;
entity->dirX = 0;
}
}
}
//On sort de la boucle si on a testé toutes les tiles le long de la hauteur du sprite.
if (i == entity->h)
{
break;
}
//Sinon, on teste les tiles supérieures en se limitant à la heuteur du sprite.
i += TILE_SIZE;
if (i > entity->h)
{
i = entity->h;
}
}
//On recommence la même chose avec le mouvement vertical (axe des Y)
if (entity->w > TILE_SIZE)
i = TILE_SIZE;
else
i = entity->w;
for (;;)
{
x1 = (entity->x) / TILE_SIZE;
x2 = (entity->x + i) / TILE_SIZE;
y1 = (entity->y + entity->dirY) / TILE_SIZE;
y2 = (entity->y + entity->dirY + entity->h) / TILE_SIZE;
if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)
{
if (entity->dirY > 0)
{
/* Déplacement en bas */
//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
if (map.tile[y2][x1] > TILE_TRAVERSABLE || map.tile[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).
entity->y = y2 * TILE_SIZE;
entity->y -= entity->h;
entity->dirY = 0;
entity->onGround = 1;
}
}
else if (entity->dirY < 0)
{
/* Déplacement vers le haut */
if (map.tile[y1][x1] > BLANK_TILE || map.tile[y1][x2] > BLANK_TILE)
{
entity->y = (y1 + 1) * TILE_SIZE;
entity->dirY = 0;
}
}
}
//On teste la largeur du sprite (même technique que pour la hauteur précédemment)
if (i == entity->w)
{
break;
}
i += TILE_SIZE;
if (i > entity->w)
{
i = entity->w;
}
}
/* Maintenant, on applique les vecteurs de mouvement si le sprite n'est pas bloqué */
entity->x += entity->dirX;
entity->y += entity->dirY;
//Et on contraint son déplacement aux limites de l'écran.
if (entity->x < 0)
{
entity->x = 0;
}
else if (entity->x + entity->w >= map.maxX)
{
//Si on touche le bord droit de l'écran, on annule
//et on limite le déplacement du joueur
entity->x = map.maxX - entity->w - 1;
}
//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 (entity->y > map.maxY)
{
entity->timerMort = 60;
}
}
|
Ouf ! C'était une bonne grosse fonction !
Ne vous inquiétez pas trop si vous n'êtes pas encore trop à l'aise avec, car nous allons souvent revenir dessus pour implémenter nos nouvelles tiles spéciales, au fur et à mesure. En effet, pour l'instant, elle ne gère que les tiles traversables, solides et plateformes (qu'on ne teste en fait qu'une fois : dans la direction vers le bas ).
Pour finir, on met à jour nos prototypes :
Fichier : prototypes.h : Remplacez par :
#ifndef PROTOTYPES
#define PROTOTYPES
#include "structs.h"
/* Catalogue des prototypes des fonctions utilisées.
On le complétera au fur et à mesure. */
extern void centerScrollingOnPlayer(void);
extern void changeLevel(void);
extern void cleanMaps(void);
extern void cleanPlayer(void);
extern void cleanup(void);
extern void delay(unsigned int frameLimit);
extern void drawGame(void);
extern void drawImage(SDL_Texture *, int, int);
extern void drawMap(int);
extern void drawPlayer(void);
extern void drawTile(SDL_Texture *image, int destx, int desty, int srcx, int srcy);
extern void gestionInputs(Input *input);
extern SDL_Texture *getBackground(void);
extern int getBeginX(void);
extern int getBeginY(void);
extern void getInput(Input *input);
extern int getLevel(void);
extern int getMaxX(void);
extern int getMaxY(void);
extern GameObject *getPlayer(void);
extern int getPlayerDirection(void);
extern int getPlayerx(void);
extern int getPlayery(void);
extern SDL_Renderer *getrenderer(void);
extern int getStartX(void);
extern int getStartY(void);
extern void init(char *);
extern void initializePlayer(int newLevel);
extern void initMaps(void);
extern void initPlayerSprites(void);
extern void loadGame(void);
extern SDL_Texture *loadImage(char *name);
extern void loadMap(char *name);
extern void mapCollision(GameObject *entity);
extern void setNombreDeVies(int valeur);
extern void setNombreDetoiles(int valeur);
extern void setStartX(int valeur);
extern void setStartY(int valeur);
extern void SetValeurDuNiveau(int valeur);
extern void updatePlayer(Input *input);
#endif
|
Voilà, plus qu'à compiler et à lancer le programme !
Notre héros peut maintenant se balader dans la map et sauter comme un cabri ! Cool !
Je vous dis donc à bientôt pour le chapitre 11 !
Jay