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


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

Dernière mise à jour : 18 août 2011

Difficulté :  
ou



15. TP : améliorer l'IA des monstres !


    Chose promise, chose due, ça va être à vous de travailler pendant ce chapitre!

    Votre but va être de gérer les demi-tours des monstres quand ils se cognent dans un mur (assez facile ), mais aussi de les empêcher de se suicider dans les trous en leur faisant faire demi-tour (plus dur ) !

    Allez, c'est parti !
   


Résultat à la fin de ce chapitre : les monstres seront plus intelligents grâce aux épinards à vous !


A. Avant de commencer

    Bon, avant que vous ne vous lanciez tête bêche dans le challenge, un peu de brainstorming !
    Tout d'abord, je vais vous offrir une nouvelle map, qui vous sera plus utile pour voir si vos bidouillages fonctionnent bien ! Enregistrez-la à la place de l'ancienne et le tour est joué !

Téléchargez map1.txt


    Ensuite, je vais aussi vous offrir les sprites de notre monstre en miroir pour le demi-tour (Eh oui ! Il va aussi falloir gérer son animation ! )



monster1right.png

    Bon, passons maintenant à la réflexion. Dans un premier temps, vous allez faire faire demi-tour au monstre quand il se cognera dans un mur.
    Mais comment savoir quand il se cogne dans un mur ?
    Eh bien réfléchissons 5 secondes. S'il est coincé dans un mur, il ne bouge plus !
    Eh oui, mon cher Watson. Donc...
    Si on teste pour voir si ses coordonnées (x ici) ne changent pas, on peut savoir quand il est coincé et lui faire changer de direction (et changer son animation de la même façon !)
    Excellent ! Voilà qui ne devrait pas être trop dur !

    Là, où ça se complique, c'est quand, dans un deuxième temps, vous allez vouloir lui faire faire demi-tour pour éviter de tomber d'une plateforme...
    Pas le choix, il va falloir tester la tile qui se situe sous ses pieds pour voir si c'est du sol ou pas (rappelez-vous de notre define BLANK_TILE !). Mais attention , si on peut calculer cette tile en utilisant monster.x quand le monstre va à gauche, il va falloir prendre en compte sa largeur quand il ira à droite (cf. le schéma). Sinon, on se retrouvera avec un monstre qui marchera à moitié dans le vide à droite mais qui s'arrêtera au bord de la plateforme à gauche (comme dans certains (mauvais) jeux)  !


    Bon j'avoue que cette deuxième partie est plus ardue. Essayez au moins de faire la première et si vous êtes motivés, passez à la seconde !

    Allez, à vos marques, c'est parti !



Chut !   On travaille ici !



B. La soluce !

    Attention, ne lisez les lignes qui suivent QUE si vous avez travaillé au moins un peu (quand même, hein ? ) !

    Bon, on va commencer par gérer le demi-tour des monstres quand ils se cognent dans un mur. Pour ça, on va avoir besoin de deux nouvelles variables permettant de sauvegarder les valeurs de x et y de la boucle précédente. Il ne nous restera plus alors qu'à faire un petit test pour voir si ces valeurs sont identiques et à gérer le changement de direction. Rien de bien sorcier !


Nom du fichier : structs.h


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

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

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

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

    //Variable pour le double saut
    int jump;



} GameObject;

         


    Voilà, nos variables sont maintenant créées, plus qu'à modifier la fonction updateMonsters() :


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)
            {
                if (monster[i].direction == LEFT)
                {
                    monster[i].direction = RIGHT;
                    monster[i].sprite = loadImage("graphics/monster1right.png");
                }
                else
                {
                    monster[i].direction = LEFT;
                    monster[i].sprite = loadImage("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)
            {
                monster[i] = monster[jeu.nombreMonstres-1];
                jeu.nombreMonstres--;
            }
        }

    }


}

         

    Comme vous pouvez le voir, cette partie du TP n'était pas vraiment difficile. Juste quelques tests logiques !
    Vous remarquerez quand même, que j'ai accéléré la vitesse des monstres. En effet, quand on reste au pixel près, il peut leur arriver de rester coincés dans un mur (à cause de la conversion float/int sans doute...! ). En choisissant un déplacement de 2 pixels, plus de soucis ! Et le jeu n'en devient que plus dynamique !

    Bon, la seconde partie du tuto est un peu plus difficile...
    Mais à coeur vaillant, rien d'impossible !
    On va commencer par ajouter la structure map à notre en-tête, car on va en avoir besoin pour tester les tiles de la map.


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

    Le plus dur arrive. On va créer une fonction checkFall() qui va tester si la tile sous le monstre est du vide ou pas.
    Pour ne pas que le monstre marche dans le vide, on va anticiper les mouvements du monstre en prenant en compte son vecteur de déplacement, pour lui faire changer de direction à temps.
    On va aussi s'assurer par sécurité qu'on ne sort pas du tableau de notre map, pour éviter de planter notre jeu.

    Allez, c'est parti pour le code (j'entends des soupirs de soulagement, c'est bizarre...)  !

Nom du fichier : monster.c


  int checkFall(GameObject monster)
 {
    int x, y;

    //Fonction qui teste s'il y a du sol sous un monstre
    //Retourne 1 s'il doit tomber, 0 sinon

    //On teste la direction, pour savoir si on doit prendre en compte x ou x + w (cf. schéma)
    if (monster.direction == LEFT)
    {
        //On va à gauche : on calcule là où devrait se trouver le monstre après déplacement.
        //S'il sort de la map, on met à jour x et y pour éviter de sortir de notre tableau
        //(source d'erreur possible qui peut planter notre jeu...).
        x = (int)(monster.x + monster.dirX) / TILE_SIZE;
        y = (int)(monster.y + monster.h - 1) /  TILE_SIZE;
        if (y < 0)
            y = 1;
        if (y > MAX_MAP_Y)
            y = MAX_MAP_Y;
        if (x < 0)
            x = 1;
        if (x > MAX_MAP_X)
            x = MAX_MAP_X;

        //On teste si la tile sous le monstre est traversable (du vide quoi...).
        //Si c'est le cas, on renvoie 1, sinon 0.
        if (map.tile[y + 1][x] < BLANK_TILE)
            return 1;

        else
            return 0;
    }
    else
    {
        //Même chose quand on va à droite
        x = (int)(monster.x + monster.w + monster.dirX) / TILE_SIZE;
        y = (int)(monster.y + monster.h - 1) / TILE_SIZE;
        if (y <= 0)
            y = 1;
        if (y >= MAX_MAP_Y)
            y = MAX_MAP_Y - 1;
        if (x <= 0)
            x = 1;
        if (x >= MAX_MAP_X)
            x = MAX_MAP_X - 1;

        if (map.tile[y + 1][x] < BLANK_TILE)
            return 1;

        else
            return 0;
    }
}

         


    Voilà, il suffit maintenant de mettre à jour notre fonction updateMonsters() en rajoutant un appel vers notre nouvelle fonction :

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;
                    monster[i].sprite = loadImage("graphics/monster1right.png");
                }
                else
                {
                    monster[i].direction = LEFT;
                    monster[i].sprite = loadImage("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)
            {
                monster[i] = monster[jeu.nombreMonstres-1];
                jeu.nombreMonstres--;
            }
        }

    }


}
         

   Eh oui, un simple || (ou) suffit et le tour est joué !
    On met à jour le fichier en-tête et c'est parti ! 

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


    Eh voilà ! C'est pas mieux maintenant ? Les monstres sont devenus intelligents et restent sur leur plateforme. Cela fait quand même plus professionnel !  

    Bon, le prochain chapitre sera maintenant consacré à l'affichage du HUD, vous savez ces informations qui indiquent le nombre de coeurs, de vies, etc ! Et comme ça, notre joueur pourra avoir plusieurs coeurs et ne pas mourir comme un &#:!%! dès qu'il se fait croquer par le premier monstre ! 

   


   




 

 

 

Connexion

CoalaWeb Traffic

Today128
Yesterday240
This week1439
This month5113
Total1744320

28/04/24