Créons un jeu de plateformes de A à Z !



Tutoriel présenté par : Jérémie F. Bellanger

Dernière mise à jour : 15 décembre 2013

Difficulté :





11. La gestion des collisions avec la map et les sauts


    Comme vous l'aurez remarqué, pour l'instant, notre héros "flotte" sur la map, vu qu'il n'y a aucune gestion des collisions ! A nous de remédier à ça, et fissa !
 


Résultat à la fin de ce chapitre :
notre super lapin interagit maintenant avec la map et en plus il saute 
!


    Bon, alors, accrochez-vous car ce chapitre va être assez complexe !
    En effet, nous allons gérer les collisions avec la map, la pesanteur, les sauts et même la mort du perso quand il tombe dans le vide !
    Mais rassurez-vous, ce n'est pas trop grave si vous ne saisissez pas tous les détails du code. Nous y reviendrons plus tard quand nous rajouterons de nouvelles options et à force, tout finira par s'éclairer !


A. Ajout de nouvelles variables/defs pour gérer les collisions

    Commençons par mettre à jour notre structure Hero. En effet, nous allons avoir besoin des éléments suivants :

    - La largeur et la hauteur du sprite : w et h. Pourquoi, me direz-vous, puisqu'on a déjà des constantes se rapportant au joueur ?  Eh bien, tout simplement parce qu'on veut faire une fonction polyvalente qui acceptera aussi les monstres, donc il faut qu'on les intègre dans notre structure qui resservira aussi pour nos monstres (il suffira d'en créer une autre et de l'appeler monster[], mais on verra ça plus tard !)

    - la variable onGround qui vaudra 1 si le joueur touche le sol et 0 sinon (utile pour savoir si on peut sauter ou pas par exemple !) .

    - la variable timerMort qui sera un timer temporisant la mort du héros. Ainsi, on attendra que les joueur ait eu le temps de se rendre compte qu'il était mort avant de le respawner (le faire réapparaître). C'est quand même mieux...

    - les variables dirX et dirY qui seront des vecteurs indiquant dans quelle direction le joueur compte aller. En effet, on ne va plus modifier directement les coordonnées de notre joueur comme avant, mais on va passer par ces vecteurs intermédiaires, avec lesquels on va tester les collisions, et ce n'est qu'après, quand on sera sûr que le mouvement est possible ou non, qu'on mettra à jour les coordonnées de notre joueur !

    Notez que ces vecteurs sont des float pour plus de précisions (même si les coordonnées du joueur seront ensuite des entiers - pixel oblige !)


Nom du fichier : structs.h


 /* Structure pour gérer notre héros */

typedef struct Hero
{
    //Sprite du héros (pas d'animation pour l'instant)
    SDL_Surface *sprite;

    /* Coordonnées du héros */
    int x, y;
    /* Largeur, hauteur du sprite */
    int h, w;

    /* Variables utiles pour l'animation */
    int frameNumber, frameTimer;
    int etat, direction;

    /* Variables utiles pour la gestion des collisions */
    int onGround, timerMort;
    float dirX, dirY;


} Hero;



    On va ensuite créer quelques nouvelles constantes dans defs.h :
    - BLANK_TILE qui indiquera la tile à partir de laquelle, les tiles suivantes seront solides. Chez nous, il s'agit de la 6ème tile, mais comme on compte à partir de 0, BLANK_TILE vaudra 5. Ainsi, si la tile testée est supérieure à 5, elle sera solide, sinon, elle sera traversable (blank).


    - GRAVITY_SPEED qui va indiquer la valeur de la gravité dans notre jeu. Je l'ai mise à 0.6 pour permettre des sauts lunaires, à vous de tester en changeant cette valeur !

    - MAX_FALL_SPEED indique la valeur maximale de la chute (ici 10 pixels). A vous de voir, plus cette valeur est élevée, plus le joueur tombera vite (mais si vous dépassez Tile_Size vous risquez d'atterrir à-travers un mur parfois...).

    - JUMP_HEIGHT qui indique la hauteur d'un saut. Là aussi, je vous invite à modifier cette valeur pour expérimenter. Plus elle sera élevée, plus les bonds seront importants.

   

Nom du fichier : defs.h


    // Constante définissant le seuil entre les tiles traversables (blank) et les tiles solides
    #define BLANK_TILE 5


   //Constantes définissant la gravité et la vitesse max de chute
   #define GRAVITY_SPEED 0.6
   #define MAX_FALL_SPEED 10

   #define JUMP_HEIGHT 13
 

    Maintenant que nous avons créé toutes ces nouvelles variables, mettons les à jour dans notre fonction qui initialise le héros :

Nom du fichier : player.c

 
   void initializePlayer(void)
  {

    /* Charge le sprite de notre héros */
    player.sprite = loadImage("graphics/walkright.png");

    //Indique l'état et la direction de notre héros
    player.direction = RIGHT;
    player.etat = WALK;

    //Réinitialise le timer de l'animation et la frame
    player.frameNumber = 0;
    player.frameTimer = TIME_BETWEEN_2_FRAMES;

    /* Coordonnées de démarrage de notre héros */
    player.x = 0;
    player.y = 0;
   
    /* 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;

  }

   

     Vous noterez que j'ai réinitialisé les coordonnées du héros à 0. Comme ça, il commencera au début de la map, tombera jusqu'à toucher le sol, et ce sera ensuite à vous de jouer !
    

B. La gestion du saut et des collisions

    On reprend ensuite notre fichier map.c pour lui ajouter une nouvelle fonction : mapCollision().

    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 ayons 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 le code et ses commentaires.

Nom du fichier : map.c


  void mapCollision(Hero *entity)
 {

    int i, x1, x2, y1, y2;

    /* D'abord, on place 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 monter 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 */
                if (map.tile[y2][x1] > BLANK_TILE || map.tile[y2][x2] > BLANK_TILE)
                {
                    //Si la tile est 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, comme avant.
    if (entity->x < 0)
    {
        entity->x = 0;
    }

    else if (entity->x + entity->w >= map.maxX)
    {
        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 ! Voilà un gros morceau de fait ! Il ne reste plus maintenant qu'à mettre à jour notre fonction updatePlayer(). Mais quand je parle de mise à jour, c'est plutôt une refonte totale car il ne va pas rester grande chose de notre fonction initiale. Jugez par vous-même !

    Là encore, les commentaires accompagnent le code pour plus de lisibilité.
   
   
Nom du fichier : player.c


  void updatePlayer(void)
 {

   //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 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;

        //On teste le sens pour l'animation : si le joueur allait dans le sens contraire
        //précédemment, il faut recharger le spritesheet pour l'animation.
        if(player.direction == RIGHT)
        {
            player.direction = LEFT;
            player.sprite = loadImage("graphics/walkleft.png");
        }
    }

    else if (input.right == 1)
    {
        player.dirX += PLAYER_SPEED;

        if(player.direction == LEFT)
        {
            player.direction =  RIGHT;
            player.sprite = loadImage("graphics/walkright.png");
        }

    }

    //Et voici la fonction de saut très simple :
    //Si on appuie sur la touche saut (C par défaut) 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 && player.onGround)
    {
        player.dirY = -JUMP_HEIGHT;
        player.onGround = 0;

    }

    //On rajoute notre fonction de détection des collisions qui va mettre à jour les coordonnées
    //de notre super lapin, puis on centre le scrolling comme avant.
    mapCollision(&player);
    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 */
            initializePlayer();
        }
    }

  }


    Voilà, n'oublions pas de mettre à jour les prototypes en rajoutant la fonction mapCollision(), et on est bon ! 

 
Nom du fichier : player.h

 
 #include "structs.h"

extern Gestion jeu;
extern Hero player;
extern Input input;
extern Map map;

/* Prototypes des fonctions utilisées */
extern SDL_Surface *loadImage(char *name);
extern void centerScrollingOnPlayer(void);
extern void mapCollision(Hero *entity);

    

    Eh voilà, un gros morceau est fait !
Vous pouvez maintenant compiler et lancer le programme ! Vous contrôlerez alors Aron le lapin-ninja, pourrez le faire sauter et même... tomber dans les trous ! Argh !

    Ah quel moment émouvant ! Sniff !  Vous venez de donner la vie à votre premier lapin ! 


      Bon, prochain chapitre, un petit travail pratique pour voir si vous avez (presque) tout compris  !  

   


   





 

 

 

Connexion

CoalaWeb Traffic

Today127
Yesterday240
This week1438
This month5112
Total1744319

28/04/24