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


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

Ecriture : 30 septembre 2011


Dernière mise à jour : 20 juin 2016
Difficulté :  




17. Réparons les fuites de mémoire !


    Vous ne vous êtes jamais demandés pourquoi Mario était un plombier ? Peut-être tout simplement parce qu'il essayait de réparer les fuites de mémoire dans son jeu ! (En fait, j'en sais rien, mais pourquoi pas ?)

    Comme vous avez pu le remarquer à la fin du chapitre précédent, grâce au Gestionnaire des Tâches de Windows, notre programme enfle petit à petit et prend de plus en plus de mémoire ! Si ça continue comme ça, il va nous exploser la RAM ! 

    Décidémment, on ne peut pas laisser faire !  Mais d'où cela vient-il ?
 
    En fait, comme vous l'avez déjà vu, à chaque fois que l'on crée une variable comme une SDL_Surface, on lui attribue un emplacement en mémoire. Oui, mais voilà, si on veut changer le contenu de cette variable, il faut d'abord supprimer la mémoire allouée à l'ancienne (avec un free) et recréer une nouvelle variable, parce que sinon, on va se retrouver avec X fois la même variable en mémoire mais seule l'adresse de la dernière sera conservée par notre programme. En d'autres termes, notre RAM va se remplir de sprites (toujours les mêmes) mais à l'infini (ou jusqu'à ce qu'on coupe le programme).


    Ainsi, si on fait :

    sprite = 
loadImage("graphics/stars.png");  
-> on charge le fichier en mémoire et on l'attribue à la variable sprite.

    sprite = loadImage("graphics/étoiles.png");  
-> on charge le fichier en mémoire et on l'attribue à la variable sprite, mais on garde celui d'avant.

    sprite = loadImage("graphics/stars.png");  
-> Idem, on a maintenant 3 fichiers en mémoire.

    sprite = loadImage("graphics/étoiles.png");  
-> Idem, on a maintenant 4 fichiers en mémoire.

    sprite = loadImage("graphics/stars.png");    
-> Idem, on a maintenant 5 fichiers en mémoire.

    sprite = loadImage("graphics/étoiles.png");    
-> Idem, on a maintenant 6 fichiers en mémoire.

    Etc...

    Comme vous le voyez, suivant la taille du fichier, ça peut aller assez vite !

    Alors, dans les langages "plus modernes" comme Java ou C#, on a ce qu'on appelle un garbage collector qui s'occupe de détruire tous ces fichiers en trop et devenus inutiles (plus ou moins bien, mais bon globalement ça marche ! )

    Sauf qu'en C, il n'y en a pas, et on doit tout faire manuellement !

    Mais alors, comment trouver d'où vient la fuite ?
    Eh bien, réfléchissons un peu ! Où charge-t-on fréquemment des fichiers graphiques sans libérer les précédents ?

    Hum, ça ne vous dit rien ? Et nos animations ?
    Quand on était dedans, la tête dans le guidon, c'était un peu compliqué, et du coup on a zappé les free ! Ouh là là, c'est pas bien !
    Mais rassurez-vous, vous en ferez souvent des bêtises en programmant. Le tout c'est de trouver après coup comment résoudre ces méchants bugs. Souvent, ce n'est pas grand chose. Quelques lignes de code peuvent tout changer, mais il faut encore les trouver ! (Eh oui, il faut être un peu Sherlock Holmes pour programmer ! )

    Bon allez, c'est parti ! Réparons tout ça !
   


Résultat à la fin de ce chapitre : plus de fuites de mémoire !



    Première chose à faire, regarder nos structs pour voir quelles SDL_Surfaces on a oublié de libérer (free) dans la fonction cleanup(), quand on quitte.
    Vous ne trouvez pas ? Vraiment ?
    Mais si, il s'agit de sprite dans GameObject !  Et c'est lui qui pose problème car on change son animation fréquemment sans libérer l'anim' précédente ! Ouh, mais c'est pas bien, ça !!!

    Allez commençons par rajouter un free dans la fonction cleanup(), ça sera déjà un point de réglé !


Nom du fichier : init.c


   void cleanup()
{

    int i;

    /* Libère l'image du background */

    if (map.background != NULL)
    {
        SDL_FreeSurface(map.background);
    }

    /* Libère l'image du tileset */
    if (map.tileSet != NULL)
    {
        SDL_FreeSurface(map.tileSet);
    }

    /* Libère le sprite du héros */
    if (player.sprite != NULL)
    {
        SDL_FreeSurface(player.sprite);
    }

    /* Libère le sprite des monstres */
    for(i = 0 ; i < MONSTRES_MAX ; i++)
    {
        if (monster[i].sprite != NULL)
        {
        SDL_FreeSurface(monster[i].sprite);
        }
    }

    //Libère le HUD
    if (jeu.HUD_etoiles != NULL)
    {
        SDL_FreeSurface(jeu.HUD_etoiles);
    }
    if (jeu.HUD_vie != NULL)
    {
        SDL_FreeSurface(jeu.HUD_vie);
    }

    /* Close the font */
    closeFont(font);

    /* Close SDL_TTF */
    TTF_Quit();

    /* Quitte la SDL */
    SDL_Quit();

}

         

   
    Comme vous le voyez, on libère le sprite du joueur (il n'y en a qu'un, facile ! ) mais aussi celui de tous les monstres possibles (un peu plus dur... ), en faisant une boucle. Bien sûr, on ne libère que s'il y a quelque chose ( != NULL), sinon, c'est pas la peine !

    Voilà, on rajoute donc nos structs utilisées dans le fichier en-tête :

Nom du fichier : init.h


   #include "structs.h"

/* Prototypes des fonctions utilisées */
extern SDL_Surface *loadImage(char *name);
extern void loadMap(char *name);
extern void closeFont(TTF_Font *font);
extern TTF_Font *loadFont(char *, int);


extern Gestion jeu;
extern Map map;
extern TTF_Font *font;
extern GameObject player;
extern GameObject monster[MONSTRES_MAX];

         

    Passons maintenant à la gestion du changement d'animations. Pour ce faire, on va créer une nouvelle fonction qui va s'en charger automatiquement !


Nom du fichier : animation.c


  void changeAnimation(GameObject *entity, char *name)
{
    //On libère l'animation précédente
    if (entity->sprite != NULL)
    {
        SDL_FreeSurface(entity->sprite);
    }
    //On charge la nouvelle
    entity->sprite = loadImage(name);
    //On réinitialise la frame et le timer
    entity->frameNumber = 0;
    entity->frameTimer = TIME_BETWEEN_2_FRAMES;
}

         


    Rien de bien sorcier ici, on fait ni plus ni moins ce qu'on faisait avant, sauf qu'on libère la varaible sprite avant de la rallouer !

    Vous remarquerez quand même que notre fonction prend en argument un GameObject par l'intermédiaire d'un pointeur et un nom de fichier. Ainsi notre structure sera universelle et sera aussi bien utilisable pour notre player que pour nos monstres ! Je vous avais bien dit que ce serait pratique d'avoir une structure commune !

    Bon, comme d'hab', on met à jour l'en-tête :

Nom du fichier : animation.h


 #include "structs.h"

  /* Structures globales */
  extern Gestion jeu;
  extern GameObject player;
  extern Map map;


  /* Prototypes */
  extern SDL_Surface *loadImage(char *name);

         


    Maintenant que nous avons notre fonction, commençons par mettre à jour le fichier player :


Nom du fichier : player.c


  void initializePlayer(void)
 {

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

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

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

    //Nombre de monstres (à déplacer plus tard dans initialzeGame()
    jeu.nombreMonstres = 0;

    //On recharge la map
    loadMap("map/map1.txt");

}


  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;
        }
        /* 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)
    {
        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 */
            initializePlayer();
        }
    }

}


    Voilà, donc on supprime les réinitialisations du frmaetimer et les allocations de fichiers pour faire un simple appel à notre fonction changeAnimation() !
    C'est-y pas beau, tout ça ! 

    On met à jour l'en-tête :

Nom du fichier : player.h


  #include "structs.h"

   extern Gestion jeu;
   extern GameObject 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(GameObject *entity);
   extern void loadMap(char *name);
   extern void changeAnimation(GameObject *entity, char *name);
   

    Puis on s'attaque à la fonction updateMonsters(), en faisant grosso modo la même chose, hein ?  

    On n'oubliera pas de libérer le sprite aussi, quand on supprime un monstre, ça nous évitera de créer des doublons !


Nom du fichier : monster.c


   void updateMonsters(void)
  {

    int i;

    //On passe en boucle tous les monstres du tableau
    for ( i = 0; i < jeu.nombreMonstres; i++ )
    {
        //Même fonctionnement que pour le joueur
        if (monster[i].timerMort == 0)
        {

            monster[i].dirX = 0;
            monster[i].dirY += GRAVITY_SPEED;


            if (monster[i].dirY >= MAX_FALL_SPEED)
                monster[i].dirY = MAX_FALL_SPEED;

            //Test de collision dans un mur : si la variable x reste la même, deux tours de boucle
            //durant, le monstre est bloqué et on lui fait faire demi-tour.
            if (monster[i].x == monster[i].saveX || checkFall(monster[i]) == 1 )
            {
                if (monster[i].direction == LEFT)
                {
                    monster[i].direction = RIGHT;
                    changeAnimation(&monster[i], "graphics/monster1right.png");
                }
                else
                {
                    monster[i].direction = LEFT;
                    changeAnimation(&monster[i], "graphics/monster1.png");
                }

            }

            //Déplacement du joueur selon la direction
            if(monster[i].direction == LEFT)
                monster[i].dirX -= 2;
            else
                monster[i].dirX += 2;


            //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
            mapCollision(&monster[i]);


            //On détecte les collisions avec le joueur
            //Si c'est égal à 1, on tue le joueur... Sniff...
            if (collide(&player, &monster[i]) == 1)
            {
                //On met le timer à 1 pour tuer le joueur intantanément
                player.timerMort = 1;
            }
            else if (collide(&player, &monster[i]) == 2)
            {
                //On met le timer à 1 pour tuer le monstre intantanément
                monster[i].timerMort = 1;
            }




          }

        //Si le monstre meurt, on active une tempo
        if (monster[i].timerMort > 0)
        {
            monster[i].timerMort--;

            /* Et on le remplace simplement par le dernier du tableau puis on
            rétrécit le tableau d'une case (on ne peut pas laisser de case vide) */
            if (monster[i].timerMort == 0)
            {
                /* Libère le sprite */
                if (monster[i].sprite != NULL)
                {
                    SDL_FreeSurface(monster[i].sprite);
                }
                monster[i] = monster[jeu.nombreMonstres-1];
                jeu.nombreMonstres--;
            }
        }

    }


  }

         


    Voilà, il suffit maintenant de compléter notre en-tête :

Nom du fichier : monster.h


   #include "structs.h"

   extern GameObject monster[];
   extern Gestion jeu;
   extern GameObject player;
   extern Map map;

   /* Prototypes des fonctions utilisées */
   extern SDL_Surface *loadImage(char *name);
   extern void mapCollision(GameObject *entity);
   extern int collide(GameObject *player, GameObject *monster);
   extern int checkFall(GameObject monster);
   extern void changeAnimation(GameObject *entity, char *name);
         

    Et ça y est !

   Si vous compilez maintenant, et que vous ouvrez le gestionnaire de tâches, vous devriez voir que la mémoire allouée à Aron oscille entre 5,2 et 5,8 Mo, et ne s'envole plus jusqu'à 20 Mo et plus comme avant !


    Bon, je pense que la chasse aux fuites est terminée ! (Mais on sait jamais avec ces bêtes-là !  Restez vigilants et précipitez-vous vers le forum si vous en trouvez une autre !! )

    Voilà, maintenant que notre programme est plus clean, nous pourrons passer à l'ajout du power-up "étoile" dans le chapitre suivant, et faire en sorte que notre lapin-ninja puisse les attraper !
 
   


   



 

Connexion

CoalaWeb Traffic

Today277
Yesterday178
This week777
This month4451
Total1743658

25/04/24