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



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

Dernière mise à jour : 11 novembre 2013

Difficulté :  




Chapitre 22

Comment gérer plusieurs maps ?


    Cette question-là, je la sens venir... Hum, je ne sais pas pourquoi ?

    Alors, tant qu'à faire, autant continuer un peu le tuto pour répondre à cette question, et vous verrez que ce n'est pas si dur !

Résultat à la fin de ce chapitre : un nouveau niveau pour notre lapin !


    Pour vous permettre de voir un maximum de choses dans ce nouveau chapitre, nous allons ajouter une seconde map, qui fonctionnera avec un deuxième tileset pour changer. Et notre lapin accèdera au second niveau quand il arrivera à la fin de la 1ère map !

   
Pour ça, je vais vous donner une nouvelle map et un nouveau tileset. Mais, nous allons aussi devoir adapter notre level editor pour qu'ils gèrent plusieurs maps et que vous puissiez éditer plus d'une map !   



    Dézippez ce dossier et copiez map2.txt dans les dossiers map du jeu et du level editor. Faites de même pour le tileset2 et copiez-le dans le dossier graphics des 2 projets. Le fichier tileset2B.png, lui, ne sera nécessaire que pour le jeu.

    Attention ! Renommez aussi le fichier tileset.png en tileset1.png ! (et de même pour le tileset1B.png nécessaire pour le jeu)


    A. Mise à jour du level editor

    Commençons par le level editor. Ouvrez le projet Code::Blocks, et on est parti !

    Nous allons commencer par créer une variable level pour savoir quelle map afficher. Pour cela, on va la rejouter dans notre structure gestion :



Nom du fichier : structs.h


 typedef struct Gestion
{

    SDL_Surface *screen;
    int level;

} Gestion;

       

   
    Puis, on va initailiser cette variable à 1 au lancement du programme, comme ça on commencera par le niveau 1 (on pourrait commencer par le dernier, si on préfère, à chacun de voir ).


Nom du fichier : init.c


void init(char *title)
{
    /* Initialise SDL Video */

    if (SDL_Init(SDL_INIT_VIDEO ) < 0)
    {
        printf("Could not initialize SDL: %s\n", SDL_GetError());

        exit(1);
    }


    jeu.screen = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 0, SDL_HWPALETTE|SDL_DOUBLEBUF);

    if (jeu.screen == NULL)
        {
            printf("Couldn't set screen mode to %d x %d: %s\n", SCREEN_WIDTH, SCREEN_HEIGHT, SDL_GetError());
            exit(1);
        }

    /* Titre de la fenetre */

    SDL_WM_SetCaption(title, NULL);
    
    //On commence par le level 1
    jeu.level = 1;


}

 


   On va ensuite modifier notre fonction loadGame pour qu'elle ne charge plus un fichier prédéfini mais le fichier correspondant au level en cours :

Nom du fichier init.c


void loadGame(void)
{
    //On crée un tableau de 120 caractères pour contenir le nom de notre fichier
    char file[120];

    /* Si le background est déjà chargé, on ne le recharge pas */
    if (map.background == NULL)
        map.background = loadImage("graphics/background.png");

    /* si on ne peut pas, on quitte */
    if (map.background == NULL)
    {
        printf("Unable to load background");
        exit(1);
    }

    //On décharge le tileset précédent et on charge celui de la map, si nécessaire
    if (map.tileSet != NULL)
    {
        SDL_FreeSurface(map.tileSet);
    }

    sprintf(file, "graphics/tileset%d.png", jeu.level);
    map.tileSet = loadImage(file);

    if (map.tileSet == NULL)
    {
        printf("Unable to load tileset : %d", jeu.level);
        exit(1);
    }


    /* On charge la map */
    sprintf(file, "map/map%d.txt", jeu.level);
    loadMap(file);

    if(file == NULL)
    {
        printf("Unable to load map : %d", jeu.level);
        exit(1);
    }

}

         

    Vous remarquerez qu'on a rajouté pas mal de protection/vérification. Comme ça, si votre jeu plante à cause d'une map ou d'un tileset mal nommé ou d'un format incompatible, vous saurez tout de suite qui a fait le coup sans avoir à chercher pendant des heures ! 
    Si vous compilez et que vous lancez le programme maintenant, vous constaterez que rien n'a changé. Normal, on n'a pas encore assigné de touches pour changer de map !
    N.B. : Si vous avez oublié de renommer le tileset en tileset1, le programme plantera, mais si vous allez dans le stderr.txt, vous verrez qu'il vous indiquera le fautif ! (Ah, la brave bête ! )

    Bon, avant de nous occuper des touches, passons par nos defs pour en rajouter une : LEVEL_MAX. Pour l'instant, il vaudra 2 (pour 2 maps), mais vous pourrez le changer par la suite :

Nom du fichier defs.h


 //Nombre maximum de levels
 #define LEVEL_MAX   2

         


    Passons maintenant à la gestion des touches. On changera de level en utilisant les touches F1 et F2 (libre à vous de changer si vous voulez ).
    Commençons par rajouter deux variables à notre structure input pour savoir si on veut monter d'un level ou descendre :



Nom du fichier structs.h


  typedef struct Input
{
    int left, right, up, down, add, remove;
    int previous, next, load, save, copy, reinit;
    int mouseX, mouseY;
    int leveldown, levelup;

} Input;

         


    Puis on rajoute, la gestion de F1 et F2 à notre fonction getInput() ! 


Nom du fichier : input.c


void getInput(void)
{
    SDL_Event event;

    /* Keymapping : gère les appuis sur les touches et les enregistre
    dans la structure input */

    while (SDL_PollEvent(&event))
    {
        switch (event.type)
        {

            case SDL_QUIT:
                exit(0);
            break;

            case SDL_KEYDOWN:
                switch (event.key.keysym.sym)
                {
                    case SDLK_ESCAPE:
                        exit(0);
                    break;

                    case SDLK_LEFT:
                        input.left = 1;
                    break;

                    case SDLK_RIGHT:
                        input.right = 1;
                    break;

                    case SDLK_UP:
                        input.up = 1;
                    break;

                    case SDLK_DOWN:
                        input.down = 1;
                    break;

                    /* La touche S sauvegardera */
                    case SDLK_s:
                        input.save = 1;
                    break;

                    /* La touche L chargera la map */
                    case SDLK_l:
                        input.load = 1;
                    break;

                    /* La touche DEL/Suppr réinitialisera la map */
                    case SDLK_DELETE:
                        input.reinit = 1;
                    break;
                   
                    case SDLK_F1:
                        input.levelup = 1;
                    break;

                    case SDLK_F2:
                        input.leveldown = 1;
                    break;

                    default:
                    break;
                }
            break;


  Etc....

         


    Voilà, ça se passe maintenant dans la fonction update(). La technique est plus ou moins la même que pour la gestion des tiles : on contient le numéro de notre level dans les limites autorisées (de 1 à LEVEL_MAX) et ensuite on appelle loadGame() pour charger la nouvelle map :


Nom du fichier : input.c


 void update(void)
{

    /* Pour l'affichage du curseur (indiquant la tile à copier, mais on reviendra dessus
    dans la fonction draw() ), on récupère les coordonnées de la souris */

    cursor.x = input.mouseX;
    cursor.y = input.mouseY;

    /* Gestion de notre scrolling du chapitre précédent */
     if (input.left == 1)
    {
        map.startX -= TILE_SIZE;

        if (map.startX < 0)
        {
            map.startX = 0;
        }
    }

    else if (input.right == 1)
    {
        map.startX += TILE_SIZE;

        if (map.startX + SCREEN_WIDTH >= map.maxX)
        {
            map.startX = map.maxX - SCREEN_WIDTH;
        }
    }

    if (input.up == 1)
    {
        map.startY -= TILE_SIZE;

        if (map.startY < 0)
        {
            map.startY = 0;
        }
    }

    else if (input.down == 1)
    {
        map.startY += TILE_SIZE;

        if (map.startY + SCREEN_HEIGHT >= map.maxY)
        {
            map.startY = map.maxY - SCREEN_HEIGHT;
        }
    }

    //Gestion du passage d'une map à l'autre

    if (input.levelup == 1)
    {
        jeu.level++;
        if (jeu.level > LEVEL_MAX )
            jeu.level = 1;
        loadGame();

        input.levelup = 0;
    }

    if (input.leveldown == 1)
    {
        jeu.level--;
        if (jeu.level < 1 )
            jeu.level = LEVEL_MAX;
        loadGame();

        input.leveldown = 0;
    }

  Etc...

         

    Plus qu'à mettre à jour notre en-tête :


Nom du fichier : input.h


  #include "structs.h"

  /* Prototypes des fonctions utilisées */
  extern void loadMap(char *name);
  extern void reinitMap(char *name);
  extern void saveMap(char *name);
  extern void loadGame(void);


  extern Input input;
  extern Map map;
  extern Gestion jeu;
  extern Cursor cursor;

         

    
    On compile, et ça marche !
    Euh... Mais si je sauvegarde, ça me sauvegarde pas la bonne map !?!
    Oups !  Ah oui, on a oublié les fonctions saveMap() et reinitMap() ! 

    Bon, bah, on y va alors ! On retourne dans notre fonction update(), et on fait les quelques changements suivants :


Nom du fichier : input.c


  void update(void)
{

    /* Pour l'affichage du curseur (indiquant la tile à copier, mais on reviendra dessus
    dans la fonction draw() ), on récupère les coordonnées de la souris */

    cursor.x = input.mouseX;
    cursor.y = input.mouseY;
   
    char file[120];
   

    /* Gestion de notre scrolling du chapitre précédent */
     if (input.left == 1)
    {
        map.startX -= TILE_SIZE;

        if (map.startX < 0)
        {
            map.startX = 0;
        }
    }

    else if (input.right == 1)
    {
        map.startX += TILE_SIZE;

        if (map.startX + SCREEN_WIDTH >= map.maxX)
        {
            map.startX = map.maxX - SCREEN_WIDTH;
        }
    }

    if (input.up == 1)
    {
        map.startY -= TILE_SIZE;

        if (map.startY < 0)
        {
            map.startY = 0;
        }
    }

    else if (input.down == 1)
    {
        map.startY += TILE_SIZE;

        if (map.startY + SCREEN_HEIGHT >= map.maxY)
        {
            map.startY = map.maxY - SCREEN_HEIGHT;
        }
    }

    //Gestion du passage d'une map à l'autre
    if (input.levelup == 1)
    {
        jeu.level++;
        if (jeu.level > LEVEL_MAX )
            jeu.level = 1;
        loadGame();

        input.levelup = 0;
    }

    if (input.leveldown == 1)
    {
        jeu.level--;
        if (jeu.level < 1 )
            jeu.level = LEVEL_MAX;
        loadGame();

        input.leveldown = 0;
    }

    /* Gestion de la copie de tile :
    on retrouve les coordonnées de la tile pointée par la souris et on remplace
    sa valeur par celle de la tile sélectionnée dans le curseur */

    if (input.add == 1)
    {

        map.tile[(map.startY + cursor.y) / TILE_SIZE][(map.startX + cursor.x) / TILE_SIZE] = cursor.tileID;

    }

    /* Même fonctionnement, sauf qu'on réinitialise la tile pointée en lui donnant
    la valeur BLANK_TILE (0 par défaut) */

    else if (input.remove == 1)
    {

        map.tile[(map.startY + cursor.y) / TILE_SIZE][(map.startX + cursor.x) / TILE_SIZE] = BLANK_TILE;

        cursor.tileID = 0;
    }

    /* On fait défiler les tiles dans un sens ou dans l'autre */

    if (input.previous == 1)
    {
        cursor.tileID--;

        if (cursor.tileID < 0)
        {
            cursor.tileID = MAX_TILES - 1;
        }
        else if (cursor.tileID > MAX_TILES)
        {
            cursor.tileID = 0;
        }


        input.previous = 0;
    }

    if (input.next == 1)
    {
        cursor.tileID++;

        if (cursor.tileID < 0)
        {
            cursor.tileID = MAX_TILES - 1;
        }
        else if (cursor.tileID > MAX_TILES)
        {
            cursor.tileID = 0;
        }

        input.next = 0;
    }

    /* On copie le numéro de la tile pointée dans le curseur pour qu'il affiche et colle
    désormais cette tile */

    if (input.copy == 1)
    {
        cursor.tileID = map.tile[(map.startY + cursor.y) / TILE_SIZE] [(map.startX + cursor.x) / TILE_SIZE];

        input.copy = 0;
    }

    /* Pour réinitialiser la map, on appelle la fonction reinitMap puis on recharge la map */

    if (input.reinit == 1)
    {
        sprintf(file, "map/map%d.txt", jeu.level);
        reinitMap(file);
        loadMap(file);

        input.reinit = 0;
    }

    /* Sauvegarde la map (cf. plus loin) */

    if (input.save == 1)
    {
        sprintf(file, "map/map%d.txt", jeu.level);
        saveMap(file);

        input.save = 0;
    }

    /* Charge la map */

    if (input.load == 1)
    {
        sprintf(file, "map/map%d.txt", jeu.level);
        loadMap(file);

        input.load = 0;
    }

    /* On rajoute un délai entre 2 tours de boucle pour que le scrolling soit moins rapide */

    if (input.left == 1 || input.right == 1 || input.up == 1 || input.down == 1)
    {
        SDL_Delay(30);
    }

}



         


    Et voilà, du coup ça marche ! Notre level editor est opérationnel !
    Avant de continuer, je vous laisse bidouiller un peu vos propres niveaux ! Déténdez-vous un peu, parce que la suite arrive !! Gnark, gnark !!!





    B. Mise à jour du jeu pour gérer plusieurs maps

    Passons maintenant au jeu. On va recommencer un peu la même chose, forcément...
    On va donc, là encore, rajouter une variable level à notre structure gestion :



Nom du fichier : structs.h


 typedef struct Gestion
{

    SDL_Surface *screen;
    int nombreMonstres;
    int level;

    //HUD
    SDL_Surface *HUD_vie, *HUD_etoiles;
    int vies, etoiles;

    //Sons
    Mix_Music  *musique;

    //Sounds Fx
    Mix_Chunk  *bumper_sound, *destroy_sound, *jump_sound, *star_sound;



} Gestion;

         

    On va ensuite créer une nouvelle fonction changeLevel() qui va s'occuper de charger la map correspondant au level en cours ainsi que les tilesets qui sont ici subordonnés aux levels (mais on pourrait aussi intégrer une variable au fichier map qui indiquerait le tileset à charger en fonction de la map - de même pour le background qui reste ici le même ) :


Nom du fichier : map.c


 void changeLevel(void)
{

    char file[200];

    /* Charge la map depuis le fichier */
    sprintf(file, "map/map%d.txt", jeu.level );
    loadMap(file);

    //Charge le tileset
    if(map.tileSet != NULL)
    {
        SDL_FreeSurface(map.tileSet);
    }
    if(map.tileSetB != NULL)
    {
        SDL_FreeSurface(map.tileSetB);
    }
    sprintf(file, "graphics/tileset%d.png", jeu.level );
    map.tileSet = loadImage(file);
    sprintf(file, "graphics/tileset%dB.png", jeu.level );
    map.tileSetB = loadImage(file);

}

         

    Puis on met à jour notre en-tête :


Nom du fichier : map.h


 
#include "structs.h"

/* Prototypes des fonctions utilisées */
extern void drawImage(SDL_Surface *, int, int);
extern void drawTile(SDL_Surface *image, int destx, int desty, int srcx, int srcy);
extern void initializeMonster(int x, int y);
extern void getItem(void);
extern void playSoundFx(int type);
extern SDL_Surface *loadImage(char *name);


extern Gestion jeu;
extern Map map;


         

    On va maintenant mettre à jour notre fonction loadGame() (comme ça on pourra par la suite directement commencer au level en cours et pas forcément au premier - dans le cas de sauvegardes par exemple).
    Attention, ce qui est en bleu a été ajouté mais il y a aussi eu des suppressions des lignes !!



Nom du fichier : init.c


void loadGame(void)
{

    /* Charge l'image du fond et le tileset */
    map.background = loadImage("graphics/background.png");

   
    //On commence au premier niveau
    jeu.level = 1;
    changeLevel();
       
  
  //On initialise le timer
     map.mapTimer = TIME_BETWEEN_2_FRAMES*3;
     map.tileSetNumber = 0;

    /* On initialise les variables du jeu */
    jeu.vies = 3;
    jeu.etoiles = 0;

    /* On charge le HUD */
    jeu.HUD_vie = loadImage("graphics/life.png");
    jeu.HUD_etoiles = loadImage("graphics/stars.png");

    //On charge la musique
    loadSong("music/RabidjaGo.mp3");

    /* On charge les sounds Fx */
    loadSound();

}

         

    Et on met à jour l'en-tête en rajoutant la ligne suivante dans les prototypes :


Nom du fichier : init.h


  extern void changeLevel(void);
  


    On va aussi mettre à jour la fonction initializePlayer() en supprimant le rechargement de la map.


Nom du fichier player.c


  void initializePlayer(void)
{

    /* Charge le sprite de notre héros */
    changeAnimation(&player, "graphics/walkright.png");

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

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

    //Nombre de monstres
    jeu.nombreMonstres = 0;


}


   Et on va recharger la bonne map directement après la mort de notre héros dans la fonction updatePlayer() :

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;
        player.direction = LEFT;

        if(player.etat != WALK_LEFT && player.onGround == 1)
        {
            player.etat = WALK_LEFT;
            changeAnimation(&player, "graphics/walkleft.png");
        }
    }

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

        if(player.etat != WALK_RIGHT && player.onGround == 1)
        {
            player.etat = WALK_RIGHT;
            changeAnimation(&player, "graphics/walkright.png");
        }
    }

    //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)
        {
            player.etat = IDLE;
            //On change l'animation selon la direction
            if(player.direction == LEFT)
            {
                changeAnimation(&player, "graphics/IdleLeft.png");
            }
            else
            {
                changeAnimation(&player, "graphics/IdleRight.png");
            }

        }
    }

    //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;
            playSoundFx(JUMP);
        }
        /* 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;
            playSoundFx(JUMP);
        }
        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)
    {
        if(player.direction == RIGHT && player.etat != JUMP_RIGHT)
        {
            player.etat = JUMP_RIGHT;
            changeAnimation(&player, "graphics/JumpRight.png");
        }
        else if(player.direction == LEFT && player.etat != JUMP_LEFT)
        {
            player.etat = JUMP_LEFT;
            changeAnimation(&player, "graphics/JumpLeft.png");
        }

    }


    //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 */
            jeu.vies--;
            if(jeu.vies < 0)
                jeu.vies = 0;
            initializePlayer();
            changeLevel();
        }
    }

}


    Et on met, là encore, à jour l'en-tête en rajoutant la ligne suivante dans les prototypes :


Nom du fichier : init.h


  extern void changeLevel(void);
  

    Voilà, notre jeu gère maintenant le multi-level !
    Si vous changez la valeur de jeu.level à 2 dans la fonction loadGame(), vous pourrez essayer notre level 2 (avec le nouveau tileset) !
    Génial, non ?!

    Mais attendez ! Comment on fait pour passer du level 1 au level 2 ?

    Eh bien, pour l'instant, on ne peut pas !
    Réglons ce problème.



    C. Passer d'un level à l'autre

    Il y a plusieurs moyens de gérer le passage d'un niveau à l'autre. Et ce moyen dépend aussi du type de jeu souhaité.
    Par exemple, dans un jeu ayant un monde ouvert, on peut passer d'une map à l'autre en touchant l'un des bords de l'écran, sachant que l'aller-retour est possible.
    On peut aussi changer de map en prenant une porte, en utilisant un téléporteur, un objet, etc...

    Dans le cas d'un jeu de plateforme classique avec des niveaux, on change de level le plus souvent soit en touchant le bord droit de l'écran, soit en touchant/ramassant un item (une pancarte qui tourne avec la tête du héros, un sandwich, une étoile jaune, une barre avec un drapeau, etc...)
    Pour notre jeu, on va se contenter de toucher le bord droit de l'écran, ce sera le plus simple.

    Si vous vous rappelez bien, on teste déjà si le joueur touche le bord droit de l'écran et si c'est le cas, on l'empêche d'avancer plus loin.
    C'est ce bout de code qu'il va nous falloir trouver et modifier pour qu'à la place on passe au niveau suivant (si on n'est pas déjà au dernier niveau, bien sûr
).
    Bon d'abord, on va définir le nombre max de levels dans nos defs :



Nom du fichier : defs.h


  //Nombre max de levels
  #define LEVEL_MAX 2

  

    Et ensuite, le bout de code en question se trouve dans la fonction mapCollision() :


Nom du fichier : map.c


  void mapCollision(GameObject *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)
            {

                //Test de la tile Power-up : Etoile (= tile N°5)
                if (map.tile[y1][x2] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();

                    //On remplace la tile power-up par une tile transparente
                    map.tile[y1][x2] = 0;
                }
                else if(map.tile[y2][x2] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();
                    //On remplace la tile power-up par une tile transparente
                    map.tile[y2][x2] = 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)
            {

                //Test de la tile Power-up : Etoile (= tile N°5)
                if (map.tile[y1][x1] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();

                    //On remplace la tile power-up par une tile transparente
                    map.tile[y1][x1] = 0;
                }
                else if(map.tile[y2][x1] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();
                    //On remplace la tile power-up par une tile transparente
                    map.tile[y2][x1] = 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 */

                //Test de la tile Power-up : Etoile (= tile N°5)
                if (map.tile[y2][x1] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();

                    //On remplace la tile power-up par une tile transparente
                    map.tile[y2][x1] = 0;
                }
                else if(map.tile[y2][x2] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();
                    //On remplace la tile power-up par une tile transparente
                    map.tile[y2][x2] = 0;
                }


                /* Gestion du ressort */
                if ((map.tile[y2][x1] == 7 ) || (map.tile[y2][x2] == 7 ))
                {
                    entity->dirY = -20;
                    //On indique au jeu qu'il a atterri pour réinitialiser le double saut
                    entity->onGround = 1;
                    playSoundFx(BUMPER);
                }


                else 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 */

                //Test de la tile Power-up : Etoile (= tile N°5)
                if (map.tile[y1][x1] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();

                    //On remplace la tile power-up par une tile transparente
                    map.tile[y1][x1] = 0;
                }
                else if(map.tile[y1][x2] == 5)
                {
                    //On appelle la fonction getItem()
                    getItem();
                    //On remplace la tile power-up par une tile transparente
                    map.tile[y1][x2] = 0;
                }


                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)
    {
        //Si on touche le bord droit de l'écran, on passe au niveau sup
        jeu.level++;
        //Si on dépasse le niveau max, on annule et on limite le déplacement du joueur
        if(jeu.level > LEVEL_MAX)
        {
            jeu.level = LEVEL_MAX;
            entity->x = map.maxX - entity->w - 1;
        }
        //Sion, on passe au niveau sup, on charge la nouvelle map et on réinitialise le joueur
        else
        {
            changeLevel();
            initializePlayer();
        }

    }

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

  

    Voilà qui n'est pas bien compliqué en fait !
    Et on met à jour notre en-tête :


Nom du fichier : map.h


  extern void changeLevel(void);
  extern void initializePlayer(void);

  

    Si vous compilez maintenant, vous remarquerez que ça marche et que notre lapin peut bien passer du level 1 au 2 et qu'à la fin du 2 il est bloqué parce que c'est le dernier niveau !
    Par contre, vous aurez peut-être aussi remarqué que si un monstre touche le bord de l'écran, le niveau passe aussi !
    Idem, pour les étoiles : les monstres peuvent les prendre ! (et ça rapporte quand même au lapin : radin !)
    Mais pourquoi ?
    Tout simplement parce que notre fonction mapCollision() est en ressource partagée entre le joueur et les monstres !
    Mais comment faire alors ?!
    Deux possibilités : soit on teste si l'Entity est le player ou pas (en rajoutant une variable dans notre struct GameObject) à chaque fois qu'on ramasse une étoile, qu'on passe un niveau, etc... Mais dans ce cas-là, on va beaucoup alourdir notre fonction (surtout quand on aura plus de power-ups, des plateformes mobiles, des pièges en tous genres, etc...) Soit, et c'est la meilleure solution, on crée une seconde fonction qui ne teste que l'essetiel pour les monstres et on réserve celle-là au player.

    Alors, on y va, en créant une fonction monsterCollisionToMap() :


Nom du fichier : map.c


void monsterCollisionToMap(GameObject *entity)
{

    int i, x1, x2, y1, y2;

    entity->onGround = 0;

    if(entity->h > TILE_SIZE)
        i = TILE_SIZE;
    else
        i = entity->h;

    for (;;)
    {
        x1 = (entity->x + entity->dirX) / TILE_SIZE;
        x2 = (entity->x + entity->dirX + entity->w - 1) / TILE_SIZE;

        y1 = (entity->y) / TILE_SIZE;
        y2 = (entity->y + i - 1) / TILE_SIZE;

        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)
                {
                    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)
                {
                    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;
    }
}

  

    Voilà la même fonction mais simplifiée à l'essentiel pour les monstres.
    Il ne reste maintenant plus qu'à la relier à la fonction updateMonsters() en changeant cette ligne !



Nom du fichier : monster.c


    //On sauvegarde les coordonnées du joueur pour gérer le demi-tour
    //avant que mapCollision ne les modifie.
    monster[i].saveX = monster[i].x;


    //On détecte les collisions avec la map comme pour le joueur
    monsterCollisionToMap(&monster[i]);


  

    Et en mettant à jour l'en-tête :

Nom du fichier : monster.h


  extern void monsterCollisionToMap(GameObject *entity);
  


   Voilà, si vous compilez maintenant, tout fonctionne ! Et les monstres ne se prennent plus pour des héros ! 
    Ouf, c'était quand même un gros, Gros, GROS chapitre ! J'espère que ça vous aura néanmoins aidé !
    Si c'est le cas, n'hésitez pas à laisser un petit mot sur le forum ou en commentaire, pour que je sache que ce que je fais sert à quelqu'un !  (et éventuellement que je continue !  )
.
 
   


   




 

 

 

Connexion

CoalaWeb Traffic

Today78
Yesterday238
This week1001
This month479
Total1745378

3/05/24