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



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

Dernière mise à jour : 30 novembre 2011

Difficulté :  




Chapitre 21

Ajoutons des sounds Fx
!


    On a la musique et c'est déjà pas mal ! Mais il nous manque quand même quelques effets sonores (sounds Fx) pour accompagner les pérégrinations de notre lapin ninja ! Ce serait quand même mieux, non ?

    Donc on continue sur notre lancée, toujours avec la bibliothèque SDL_Mixer !

Résultat à la fin de ce chapitre : des sounds Fx accompagnent désormais les mouvements de notre lapin !


    Bon, avant de rajouter des sounds Fx, bah... Il nous faut des sounds Fx !
   
Pour ça, plusieurs choix s'offrent à vous : soit les composer vous-mêmes (ou faire les bruitages avec un micro ), soit trouver des sons gratuits et libres de droits sur internet (quelques liens doivent traîner sur le forum ), soit en acheter auprès d'une société spécialisée ou d'un musicien professionnel !

    Bien entendu, le résultat ne sera pas le même suivant la méthode que vous choisirez, mais tant qu'on reste dans un projet amateur, pas de souci si c'est pas parfait dès le début (on vise pas encore le jeu AAA
!)

    Un autre moyen que j'ai trouvé récemment, c'est d'utiliser le logiciel Sfxr de Dr Petter, disponible sur sa page web ici. Ce logiciel est vraiment super : il vous permet de créer des Sounds Fx façon NES en un clic ! A essayer d'urgence !

    En attendant, si vous n'en avez pas, je vais vous en prêter pour ce tuto (mais attention, comme les graphismes, ils restent sous copyright et vous devrez donc les changer si vous comptez distribuer / vendre votre jeu
).

    Téléchargez l'archive suivante et dézippez-la dans un dossier nommé sounds directement dans le dossier de votre jeu (comme pour la musique) :



    Voilà, vous devriez maintenant avoir 4 fichiers .wav dans votre dossier, nommés : bumper.wav, destroy.wav, jump.wav et star.wav.
    Si vous avez choisi de composer/télécharger d'autres sons, il vous suffit de les renommer avec ces noms-là et de les placer dans le dossier sounds (ou alors, vous modifierez les noms des fichiers directement dans le code source ci-dessous. 


    Et voilà, on va maintenant créer des variables prêtes à accueillir nos fichiers sons dans notre structure gestion :   


Nom du fichier : structs.h


  typedef struct Gestion
{

    SDL_Surface *screen;
    int nombreMonstres;

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

       

   
    Notez que ces variables sont du type Mix_Chunk qui est propre à la bibliothèque SDL_Mixer !
    Maintenant, pour nommer ces variables plus tard dans notre fonction playSoundFx(), nous allons utiliser une enum. Bon, pour 4 sons, ça paraît pas essentiel, mais ça va vous faciliter la vie quand vous en aurez 50 !

    On va la définir dans notre fichier defs, comme d'habitude :


Nom du fichier : defs.h


  enum
{
    BUMPER,
    DESTROY,
    JUMP,
    STAR
};


    Pour ceux, qui ne voient pas trop à quoi ça sert, ça nous permettra de faire playSoundFx(BUMPER); au lieu de playSoundFx(1); en devant garder en tête le numéro de chaque son. C'est plus facile pour s'y retrouver, pour débugger et on fait moins d'erreurs bêtes... 

    Bon, il va maintenant falloir charger nos sons. Comme on a un petit jeu avec un petit nombre de fichiers, on va tous les charger au démarrage du jeu et on les déchargera tous en quittant. Si on devait faire un jeu (beaucoup) plus gros, on pourrait envisager de ne charger que les fichiers nécessaires en fonction du niveau et de les décharger à la fin de celui-ci avant de recharger les nouveaux. Et on aurait ce fameux écran : Now Loading... ! (Et bien sûr on ferait de même pour la map, les graphismes, etc...).

    Dans notre fonction loadGame(), on va faire appel à une nouvelle sous-fonction : loadSound() qui va s'en charger :


Nom du fichier init.c


 void loadGame(void)
{

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

    //On initialise le timer
     map.mapTimer = TIME_BETWEEN_2_FRAMES*3;
     map.tileSetNumber = 0;

    loadMap("map/map1.txt");

    /* 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 donc voilà, notre fonction loadSound(), qu'on va mettre dans notre fichier sounds.c. Comme vous pouvez le voir, elle est très simple. On charge les fichiers, et on vérifie tout de suite s'il n'y a pas eu d'erreur. S'il y en a une, c'est que le fichier n'a pas été chargé correctement et on affiche donc lequel.

Nom du fichier sounds.c


 void loadSound(void)
{

   jeu.bumper_sound = Mix_LoadWAV("sounds/bumper.wav");
   if (jeu.bumper_sound == NULL)
    {
        fprintf(stderr, "Can't read the bumper sound FX \n");
        exit(1);
    }

   jeu.destroy_sound = Mix_LoadWAV("sounds/destroy.wav");
   if (jeu.destroy_sound == NULL)
    {
        fprintf(stderr, "Can't read the destroy sound FX \n");
        exit(1);
    }

   jeu.jump_sound = Mix_LoadWAV("sounds/jump.wav");
   if (jeu.jump_sound == NULL)
    {
        fprintf(stderr, "Can't read the jump sound FX \n");
        exit(1);
    }

   jeu.star_sound = Mix_LoadWAV("sounds/star.wav");
   if (jeu.star_sound == NULL)
    {
        fprintf(stderr, "Can't read the star sound FX \n");
        exit(1);
    }
 

}

         

    Maintenant, que les fichiers sont chargés, on va s'occuper tout de suite (avant de l'oublier ! ), de les décharger en quittant le jeu. On retourne donc dans notre fonction cleanup(), et on va rajouter un appel vers la fonction freeSound() qui va s'en occuper.


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 des tilesets */
    if (map.tileSet != NULL)
    {
        SDL_FreeSurface(map.tileSet);
    }
    if (map.tileSetB != NULL)
    {
        SDL_FreeSurface(map.tileSetB);
    }


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


    /* On libère la chanson */
    if ( jeu.musique != NULL )
        Mix_FreeMusic(jeu.musique);
       
    //On libère les sons
    freeSound();

    // Quitte SDL_Mixer et décharge la mémoire
    Mix_CloseAudio();
    Mix_Quit();

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

    /* Close SDL_TTF */
    TTF_Quit();

    /* Quitte la SDL */
    SDL_Quit();

}

         


    Et on rajoute notre fonction freeSound() après la fonction loadSound() comme ça on saura où les trouver !


Nom du fichier : sounds.c


void freeSound(void)
{

   Mix_FreeChunk(jeu.bumper_sound);
   Mix_FreeChunk(jeu.destroy_sound);
   Mix_FreeChunk(jeu.jump_sound);
   Mix_FreeChunk(jeu.star_sound);
 
}

         

    Voilà, il ne nous manque plus qu'à mettre à jour init.h pour qu'elle ait connaissance des fonctions loadSound() et freeSound() :


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 void loadSong( char filename[200] );
  extern void loadSound(void);
  extern void freeSound(void);

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

         

    On compile, et ça marche !
    Euh... Mais j'entends pas de sons, moi !?!
    Normal, pour l'instant, on a chargé/déchargé nos sons mais on les joue pas !

    On va donc maintenant créer une fonction playSoundFx() qui va prendre en argument le nom du son qu'on va jouer et se débrouiller avec 
:


Nom du fichier : sounds.c


void playSoundFx(int type)
{

    switch (type)
    {

        case BUMPER:
            Mix_PlayChannel(-1, jeu.bumper_sound, 0);
        break;

        case DESTROY:
            Mix_PlayChannel(-1, jeu.destroy_sound, 0);
        break;

        case JUMP :
            Mix_PlayChannel(-1, jeu.jump_sound, 0);
        break;

        case STAR:
            Mix_PlayChannel(-1, jeu.star_sound, 0);
        break;

   }


}

         

    Et voilà le travail, grâce à notre enum et un simple switch, plus besoin de s'embêter avec le nom du fichier ou l'appel SDL_Mixer. En plus, si on change de lib, plus tard, ce sera plus simple. Il suffira de changer l'appel à SDL_Mixer une fois ici, et pas partout dans le code dès qu'un son est joué !    

    Bon, maintenant, il ne reste plus qu'à appeler notre fonction, chaque fois qu'on a besoin de jouer un son !

    Commençons par le son du saut, dans notre 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;

    Etc...



         


    Puis notre son DESTROY qu'on va jouer quand le joueur ou le monstre est touché et meurt (vous pouvez aussi créer un autre son pour les différencier) :

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;
                
playSoundFx(DESTROY);
            }
            else if (collide(&player, &monster[i]) == 2)
            {
                //On met le timer à 1 pour tuer le monstre intantanément
                monster[i].timerMort = 1;
                playSoundFx(DESTROY);
            }


   Etc...

         

    Plus qu'à s'occuper des sons BUMPER et STAR quand on rebondit sur le bumper et qu'on attrape un étoile !
    Pour le bumper, ça se passe 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);
                }


    Etc....
         

    Et pour les étoiles, ça se passe dans la fonction getItem() :


Nom du fichier : player.c


  void getItem(void)
{
    //On incrémente le compteur Etoile
    jeu.etoiles++;
    playSoundFx(STAR);

    //On teste s'il y a 100 étoiles : on remet le compteur à 0 et on rajoute une vie ;)
    if(jeu.etoiles >= 100)
    {
        jeu.etoiles = 0;
        jeu.vies++;
    }
}


         

    Reste plus qu'à mettre à jour nos en-têtes, en rajoutant la ligne suivante à map.h, player.h et monster.h parmi les prototypes :

Nom du fichier : map.h, player.h, monster.h


  extern void playSoundFx(int type);
         


   Voilà, si vous compilez maintenant, vous aurez la joie, d'entendre des effets sonores chaque fois que vous serez tué, que vous tuerez un monstre, que vous ramasserez une étoile ou que vous rebondirez sur un bumper ! 
    Whaouh !!! Cela nous manquait quand même !!


    Et voilà qui clôture notre vingt et unième chapitre ! Vous devez maintenant commencer à être calé niveau programmation de jeux !
    Je pourrais, pour ainsi dire, clore le tuto ici. La plupart d'entre vous s'en sortiraient sans trop de problèmes, je pense !

    Mais il reste néanmoins quelques petits détails à terminer, et je préfère vous montrer la marche à suivre (sinon, je sens que les questions vont fuser sur le forum, de toute façon !
). Allez, à bientôt pour la suite !
 
   


   




 

 

 

Connexion

CoalaWeb Traffic

Today79
Yesterday238
This week1002
This month480
Total1745379

3/05/24