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é :




13. Créons des monstres ! 


    Ahah ! Soyons fous et créons maintenant des monstres pour pourrir la vie à notre lapin fraîchement né !

    Eh oui, pour être créateur de jeux vidéos, il faut être un peu sadique !  Mais pas trop non plus, sinon vous allez faire fuir les pauvres joueurs apeurés...  Enfin bon, il faut quand même reconnaître qu'un jeu de plateforme sans monstre, c'est comme... (je vous laisse finir la phrase ! ).
   


Résultat à la fin de ce chapitre : les monstres ont envahi la map  !


    Dans ce chapitre, nous allons réemployer beaucoup de techniques déjà utilisées pour gérer notre joueur. A la différence près, que nos monstres devront se diriger tout seul (on leur programmera donc une Intelligence Artificielle de cacahuète ! Mais, rassurez-vous, pour ce qu'ils ont à faire, ça leur suffira ! ) et il n'y en aura pas qu'un (imaginez un jeu de plateformes avec UN SEUL MONSTRE dans tout le niveau...) !

    Il va donc falloir gérer un tableau de monstres, qui nous permettra d'afficher jusqu'à 50 monstres à l'écran (pourquoi 50 ? Parce que c'est déjà pas mal, et qu'au dessus, ça risque de commencer à ramer avec la SDL... )
! Bon, je ne vous cache que ce chapitre va être un peu ardu... Mais bon, il faut ce qu'il faut !


A. Créons une structure pour nos monstres

    Pour nos monstres, nous allons réutiliser notre structure Hero.

    Quoi ? La structure de super-lapin ?

    Bah oui, en fait, un monstre est très proche du héros et nécessite beaucoup de variables semblables. Du coup, c'est plus simple d'utiliser la même structure. En plus, comme ça, nos fonctions deviendront universelles : si elles marchent pour le player, elles marcheront pour les monstres (on verra comment !).

    Mais si cela vous perturbe trop, vous pouvez renommer la structure en GameObject ou en Entity.


    Dans notre code, nous allons la renommer GameObject. Je vous laisse faire les changements nécessaires : rien de bien difficile, il suffit de changer tous les Hero par GameObject dans les fichiers : structs.h, player.h, main.h, animation.h, map.c -> mapCollision() (et son prototype dans map.h) !


    Autre chose, avant de commencer, nous aurons besoin de la feuille de sprites de notre monstre. Je vous la donne ci-dessous. Vous aurez remarqué sans difficulté qu'il s'agit du même monstre qui est sur la tile monstre, et pour cause, comme on le verra après, on va se servir de cette tile pour initialiser notre monstre !

    En attendant, vous pouvez l'enregistrer dans le dossier graphics, comme d'habitude :



monster1.png

   Voilà, initialisations maintenant notre tableau de monstres dans main.h.
    Il ne faut pas que la notion de tableau de monstres vous effraie, en fait, on va juste créer 50 structures GameObject monster qu'on va mettre dans un tableau pour pouvoir les appeler ainsi : monster[1], monster[2], etc...
    Et comment on fait ça ?
    Comme ça tout simplement (avec un def qu'on va définir après) :

Nom du fichier : main.h


   #include "structs.h"

   /* Prototypes des fonctions utilisées */

   extern void init(char *);
   extern void cleanup(void);
   extern void getInput(void);
   extern void draw(void);
   extern void loadGame(void);
   extern void delay(unsigned int frameLimit);
   extern void updatePlayer(void);
   extern void initializePlayer(void);


   /* Déclaration des structures globales utilisées par le jeu */

   Input input;
   Gestion jeu;
   Map map;
   GameObject player;
   GameObject monster[MONSTRES_MAX];


         


    Et notre def pour initialiser un tableau de 50 monstres max :

Nom du fichier : defs.h


   //Nombre max de monstres à l'écran
   #define MONSTRES_MAX 50

         

    Il nous faudra aussi une nouvelle variable pour compter le nombre de monstres. Nous allons la rajouter dans la structure jeu :

Nom du fichier : structs.h


   /* Structure pour gérer le niveau (à compléter plus tard) */

  typedef struct Gestion
  {

    SDL_Surface *screen;
    int nombreMonstres;

  } Gestion;

         

    Et, pour l'instant, nous la réinitialiserons avec le joueur (pour que les monstres reviennent à 0, quand on meurt) :

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

}

         




B. Initialisons nos monstres


    Voilà, maintenant, il va nous falloir rajouter les fichiers monster.c et monster.h dans lesquels nous allons gérer nos monstres.

    Oui, mais de quoi va-t-on avoir besoin ?

    Réfléchissons un peu. Nos monstres vont apparaître quand on va rencontrer une tile monstre sur la map. Il va donc falloir tester dans la fonction drawMap() si on s'apprête à dessiner une tile monstre et si c'est le cas, ne pas le faire, mais initialiser le monstre à la place dans une fonction initializeMonster().

    Une fois notre monstre initialisé, il va falloir le gérer dans une fonction updateMonsters() puis le dessiner dans une fonction drawMonsters() (en réutilisant notre fonction d'animation et en la modifiant un peu pour qu'elle devienne générique). 

    Nous en resterons là pour ce chapitre (et ça sera déjà pas mal ! ). On verra plus loin comment tester les collisions avec le joueur et comment détruire un monstre mort (et libérer une case dans notre tableau de monstres).  
   
    Commençons donc par la fonction initializeMonster(). Pour nous aider, nous pourrons partir de celle du player.
    Mais attention, comme il n'y aura pas qu'un monstre, il nous faudra aussi tester si on peut rajouter un monster (et qu'on ne dépasse pas NOMBRE_MAX), puis il faudra initialiser le dernier monstre du tableau (égal à nombreMonstres) avant d'incrémenter le nombre de monstres.

    Bon, c'est quand même pas très dur. Je vous laisse voir ce que ça donne !


Nom du fichier : monster.c


include "monster.h"; //A ne pas oublier en tête de fichier

   void initializeMonster(int x, int y)
{
    //Si on n'est pas rendu au max, on rajoute un monstre dont le numéro est égal
    //à nombreMonstres : monster[0] si c'est le 1er, monster[1], si c'est le 2eme, etc...
    if (jeu.nombreMonstres < MONSTRES_MAX )
    {
        /* On charge son sprite */
        monster[jeu.nombreMonstres].sprite = loadImage("graphics/monster1.png");

        //On indique sa direction (il viendra à l'inverse du joueur, logique)
        monster[jeu.nombreMonstres].direction = LEFT;

        //On réinitialise le timer de l'animation et la frame comme pour le joueur
        monster[jeu.nombreMonstres].frameNumber = 0;
        monster[jeu.nombreMonstres].frameTimer = TIME_BETWEEN_2_FRAMES;

        /* Ses coordonnées de démarrage seront envoyées par la fonction drawMap() en arguments */
        monster[jeu.nombreMonstres].x = x;
        monster[jeu.nombreMonstres].y = y;

        /* Hauteur et largeur de notre monstre (une tile ici, soit 32x32) */
        monster[jeu.nombreMonstres].w = TILE_SIZE;
        monster[jeu.nombreMonstres].h = TILE_SIZE;

        //Variables nécessaires au fonctionnement de la gestion des collisions comme pour le héros
        monster[jeu.nombreMonstres].timerMort = 0;
        monster[jeu.nombreMonstres].onGround = 0;
       
        jeu.nombreMonstres++;
       
    }

}

         

   Maintenant n'oubliez pas de mettre à jour les prototypes !


Nom du fichier : monster.h

 
    #include "structs.h"

  extern GameObject monster[];
  extern Gestion jeu;

  /* Prototypes des fonctions utilisées */
  extern SDL_Surface *loadImage(char *name);

   

    Voilà donc de quoi créer un monstre basique.
    Bien sûr, cela deviendra plus compliqué quand on rajoutera des fonctions à notre monstre, et surtout quand on gèrera plusieurs types. Mais commençons par faire simple !

    A noter : les coordonnées x et y correspondront à celles de la tile monstre et seront envoyées par la fonction drawMap(), que l'on peut modifier tout de suite (ça sera fait ! Les modifs sont toujours en bleu.) :

Nom du fichier : map.c


 void drawMap(void)
{
    int x, y, mapX, x1, x2, mapY, y1, y2, xsource, ysource, a;

    /* On initialise mapX à la 1ère colonne qu'on doit blitter.
    Celle-ci correspond au x de la map (en pixels) divisés par la taille d'une tile (32)
    pour obtenir la bonne colonne de notre map
    Exemple : si x du début de la map = 1026, on fait 1026 / 32
    et on sait qu'on doit commencer par afficher la 32eme colonne de tiles de notre map */
    mapX = map.startX / TILE_SIZE;

    /* Coordonnées de départ pour l'affichage de la map : permet
    de déterminer à quels coordonnées blitter la 1ère colonne de tiles au pixel près
    (par exemple, si la 1ère colonne n'est visible qu'en partie, on devra commencer à blitter
    hors écran, donc avoir des coordonnées négatives - d'où le -1). */
    x1 = (map.startX % TILE_SIZE) * -1;

    /* Calcul des coordonnées de la fin de la map : jusqu'où doit-on blitter ?
    Logiquement, on doit aller à x1 (départ) + SCREEN_WIDTH (la largeur de l'écran).
    Mais si on a commencé à blitter en dehors de l'écran la première colonne, il
    va falloir rajouter une autre colonne de tiles sinon on va avoir des pixels
    blancs. C'est ce que fait : x1 == 0 ? 0 : TILE_SIZE qu'on pourrait traduire par:
    if(x1 != 0)
        x2 = x1 + SCREEN_WIDTH + TILE_SIZE , mais forcément, c'est plus long ;)*/
    x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);

    /* On fait exactement pareil pour calculer y */
    mapY = map.startY / TILE_SIZE;
    y1 = (map.startY % TILE_SIZE) * -1;
    y2 = y1 + SCREEN_HEIGHT + (y1 == 0 ? 0 : TILE_SIZE);


    /* Dessine la carte en commençant par startX et startY */

    /* On dessine ligne par ligne en commençant par y1 (0) jusqu'à y2 (480)
    A chaque fois, on rajoute TILE_SIZE (donc 32), car on descend d'une ligne
    de tile (qui fait 32 pixels de hauteur) */
    for (y = y1; y < y2; y += TILE_SIZE)
    {
        /* A chaque début de ligne, on réinitialise mapX qui contient la colonne
        (0 au début puisqu'on ne scrolle pas) */
        mapX = map.startX / TILE_SIZE;

        /* A chaque colonne de tile, on dessine la bonne tile en allant
        de x = 0 à x = 640 */
        for (x = x1; x < x2; x += TILE_SIZE)
        {
            //Si la tile à dessiner n'est pas une tile vide
            if (map.tile[mapY][mapX] != 0)
            {
                /*On teste si c'est une tile monstre (tile numéro 10) */
                if (map.tile[mapY][mapX] == 10)
                {
                    //On initialise un monstre en envoyant les coordonnées de la tile
                    initializeMonster(mapX * TILE_SIZE, mapY * TILE_SIZE);

                    //Et on efface cette tile de notre tableau pour éviter un spawn de monstres
                    //infini !
                    map.tile[mapY][mapX] = 0;
                }
            }

            /* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro
            de la tile */
            a = map.tile[mapY][mapX];

            /* Calcul pour obtenir son y (pour un tileset de 10 tiles
            par ligne, d'où le 10 */
            ysource = a / 10 * TILE_SIZE;
            /* Et son x */
            xsource = a % 10 * TILE_SIZE;

            /* Fonction qui blitte la bonne tile au bon endroit */
            drawTile(map.tileSet, x, y, xsource, ysource);

            mapX++;
        }

        mapY++;
    }
}


 
    Maintenant n'oubliez pas de mettre à jour les prototypes !


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 mapCollision(GameObject *entity);
  extern void initializeMonster(int x, int y);


  extern Gestion jeu;
  extern Map map;

   
    Si vous compilez et lancez le programme maintenant, vous remarquerez que les tiles monstres ont disparu. Normal !
    Nos monstres sont même initialisés ! Il ne reste plus qu'à les gérer, les dessiner et les animer !


C. Gérons nos monstres


     Bon, il va nous falloir maintenant afficher et gérer nos monstres, sinon, ça risque d'être dur d'éviter des monstres invisibles !
    Commençons par l'affichage. On va reprendre notre structure drawanimatedplayer() et la rendre opérationnelle pour afficher n'importe quel GameObject. Dans le même temps, on va la fusionner avec la fonction drawplayer() par souci pratique.

    Bon, alors, on commence d'abord par supprimer la fonction drawplayer() dans player.c et les prototypes associés dans animation.h.
    
    On fusionne alors nos deux fonctions en remplaçant player par entity, qu'on passera en argument (sous forme de pointeur). Cela devrait donner ça normalement :

   
Nom du fichier : animation.c


  void drawAnimatedEntity(GameObject *entity)
  {
    /* Gestion du timer */
    // Si notre timer (un compte à rebours en fait) arrive à zéro
    if (entity->frameTimer <= 0)
    {
        //On le réinitialise
        entity->frameTimer = TIME_BETWEEN_2_FRAMES;

        //Et on incrémente notre variable qui compte les frames de 1 pour passer à la suivante
        entity->frameNumber++;

        //Mais si on dépasse la frame max, il faut revenir à la première
        if(entity->frameNumber >= entity->sprite->w / entity->w)
            entity->frameNumber = 0;

    }
    //Sinon, on décrémente notre timer
    else
        entity->frameTimer--;


    //Ensuite, on peut passer la main à notre fonction

    /* Rectangle de destination à blitter */
    SDL_Rect dest;

    // On soustrait des coordonnées de notre héros, ceux du début de la map, pour qu'il colle
    //au scrolling :
    dest.x = entity->x - map.startX;
    dest.y = entity->y - map.startY;
    dest.w = entity->w;
    dest.h = entity->h;

    /* Rectangle source à blitter */
    SDL_Rect src;

    //Pour connaître le X de la bonne frame à blitter, il suffit de multiplier
    //la largeur du sprite par le numéro de la frame à afficher -> 0 = 0; 1 = 40; 2 = 80...
    src.x = entity->frameNumber * entity->w;
    src.y = 0;
    src.w = entity->w;
    src.h = entity->h;

    /* Blitte notre héros sur l'écran aux coordonnées x et y */

    SDL_BlitSurface(entity->sprite, &src, jeu.screen, &dest);

}
 

    On remarquera que nos variables w et h deviennent ici inévitables car elles vont dépendre de l'entité passée en argument : le joueur ou le monstre. On ne peut donc plus rester avec nos defs.

    Attention aussi à mettre à jour le fichier header :
   
   
Nom du fichier : animation.h


   #include "structs.h"

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


    Et voilà, maintenant, il faut mettre à jour la fonction draw() pour qu'elle passe les bonnes entités en arguments, et qu'elle gère les monstres :

Nom du fichier : draw.c


  void draw(void)
 {

    int i;

    /* Affiche le fond (background) aux coordonnées (0,0) */
    drawImage(map.background, 0, 0);

    /* Affiche la map de tiles */
    drawMap();

    /* Affiche le joueur */
    drawAnimatedEntity(&player);

    /* Affiche les monstres */
    for(i = 0 ; i < jeu.nombreMonstres ; i++)
    {
        drawAnimatedEntity(&monster[i]);
    }

    /* Affiche l'écran */
    SDL_Flip(jeu.screen);

    /* Delai */

    SDL_Delay(1);

}


    Comme on le voit, on passe maintenant l'adresse de notre structure player en argument, et on fait pareil pour nos monstres au sein d'une boucle for qui fait défiler tous les monstres de notre tableau.
    Voilà comment gérer 50 monstres + le joueur en quelques lignes !
    Et après, comme on est très fort, on pourra gérer de la même manière les projectiles, les boules de piques, les jets de lave, les plateformes mobiles, etc...

    Bon, n'oublions pas notre header et on peut compiler !


Nom du fichier : draw.h


   
#include "structs.h"

/* Prototypes des fonctions utilisées */
extern void drawMap(void);
extern void drawAnimatedEntity(GameObject *entity);

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



    Et voilà, nos monstres s'affichent et marchent sur place ! Youpi !
    Mais ça manque encore un peu de mouvement... Donc, on y va pour la fonction updateMonsters() !

    Dans cette fonction, on va passer en boucle chaque monstre et gérer ses déplacements de façon analogue à ceux du joueur, sauf qu'ici pas d'input. Bah, alors, pour faire simple, on va les faire avancer droit devant eux et tant pis s'ils restent coincés ou tombent dans le vide ! De vrais idiots ! Vous essaierez plus tard d'améliorer leur IA dans un petit TP... (comment ça ? Argh, non, par pitié ?!!!... ).
   

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;

            //Le monstre va toujours à gauche
            monster[i].dirX -= 1;

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

          }

        //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)
            {
                monster[i] = monster[jeu.nombreMonstres-1];
                jeu.nombreMonstres--;
            }
        }

    }


}


    Voilà, donc comme vous pouvez le voir, ça ressemble beaucoup à notre fonction updatePlayer() en moins compliquée (pour l'instant ! ). On gère chaque monstre, on le déplace à gauche d'un pixel et on teste les collisions avec la map.

    Là où ça se complique un petit peu, c'est quand l'un deux meurt.  En effet, on ne peut pas laisser de case vide dans notre tableau (on pourrait utiliser une variable : alive ou active éventuellement, mais on perdrait de la place dans notre tableau et ça compliquerait les choses au final).
    Non, le plus simple, c'est tout simplement de copier le monstre de la dernière case du tableau dans la case du monstre mort et de réduire la taille de notre tableau d'une case (la dernière déjà copiée) en réduisant d'1 la variable nombreMonstres. Et le plus beau, c'est que ça se fait en une ligne en C !!

    Bon, on met à jour les prototypes :
 
Nom du fichier : monster.h


   #include "structs.h"

   extern GameObject monster[];
   extern Gestion jeu;

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


    On rajoute notre fonction dans le main() :


Nom du fichier : main.c


       /* Boucle infinie, principale, du jeu */

    while (go == 1)
    {

        /* On prend on compte les input (clavier, joystick... */
        getInput();

        /* On met à jour le jeu */
        updatePlayer();
        updateMonsters();

        /* On affiche tout */
        draw();

        /* Gestion des 60 fps (1000ms/60 = 16.6 -> 16 */
        delay(frameLimit);
        frameLimit = SDL_GetTicks() + 16;

    }



    On met encore à jour les prototypes :


Nom du fichier : main.h


   
   #include "structs.h"

   /* Prototypes des fonctions utilisées */

   extern void init(char *);
   extern void cleanup(void);
   extern void getInput(void);
   extern void draw(void);
   extern void loadGame(void);
   extern void delay(unsigned int frameLimit);
   extern void updatePlayer(void);
   extern void initializePlayer(void);
   extern void updateMonsters(void);


   /* Déclaration des structures globales utilisées par le jeu */

   Input input;
   Gestion jeu;
   Map map;
   GameObject player;
   GameObject monster[MONSTRES_MAX];



      Et c'est bon ! Si vous compilez puis lancez le programme, vous verrez maintenant les monstres avancer vers vous. Vous pourrez aussi vous amuser à changer leur vitesse dans la fonction updateMonsters() : je l'ai mise à 1 par défaut.    

    Voilà, donc dans le chapitre suivant, on va gérer les collisions avec le joueur, et tuer le monstre ou le joueur, suivant l'impact du choc !
   


   




 

 

 

Connexion

CoalaWeb Traffic

Today167
Yesterday240
This week1478
This month5152
Total1744359

28/04/24