Big Tuto SDL 2 : Rabidja v. 3.0

Chapitre 12 : Ajoutons un monstre !

 

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Ecriture : 1er novembre 2014

Dernière mise à jour : 9 novembre 2015

 

      Prologue

   Bien, notre héros prend maintenant vie ! smiley Il est temps de tester ses réflexes de ninja avec quelques monstres ! Ahah ! laugh

   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 - ou Bêtise Artificielle, c'est comme vous voulez cheeky !) 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 ? surprise Parce que c'est déjà pas mal wink, mais si vous en voulez des millions, lâchez-vous !! laugh) ! Bon, je ne vous cache pas que ce chapitre va être un peu long et ardu... frown Mais bon, il faut ce qu'il faut ! angel

 

      Structure et spritesheet

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

   Quoi ? surprise La même structure que pour notre super lapin ? frown

   Mais oui, rappelez-vous, on avait créé une structure générique, justement pour pouvoir gérer n'importe quel sprite du jeu. C'est le moment de le mettre en pratique, surtout qu'ici un monstre est très proche du héros et nécessite beaucoup de variables semblables. wink

   Voilà, maintenant, avant de commencer, nous aurons besoin de la feuille de sprites de notre monstre. Je vous la donne ci-dessous. Vous pouvez l'enregistrer dans le dossier graphics, comme d'habitude :

monster1.png

      Le code

   On va commencer par aller dans le fichier defs.h, pour créer quelques nouvelles defines qui vont nous servir ici wink. On va ainsi définir le nombre max qu'on acceptera d'initialiser par niveau (ici 50, mais vous pouvez en mettre plus, ce ne devrait pas être un réel problème en SDL2 wink).

   On indique ensuite les dimensions de notre monstre, qui sont ici les mêmes que celles du héros (soit 40x50 pixels).

Fichier : defs.h : Rajoutez :

//Nombre max de monstres à l'écran
#define MONSTRES_MAX 50
 
//Dimensions du monstre
#define MONSTER_WIDTH 40
#define MONSTER_HEIGTH 50

 

      Un nouveau fichier : monster.c   

    Voilà, maintenant, il va nous falloir rajouter (créer) le fichier monster.c dans lequel nous allons gérer nos monstres.

    Oui, mais de quoi va-t-on avoir besoin dans ce fichier ? frown

   Hum, réfléchissons un peu. cheeky Nos monstres vont apparaître quand on va rencontrer une tile monstre sur la map (que vous avez sans doute déjà placées un peu partout avec le level editor, sinon rassurez-vous, il y en a déjà avec les fichiers téléchargeables wink). 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, la supprimer et 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() (que c'est original ! cheeky).

   Ensuite, comme pour le joueur, il va falloir tester les collisions la map, pour qu'il puisse se déplacer dessus, et il nous faudra aussi tester les collisions avec le joueur pour pouvoir le blesser ou tuer le monstre ! wink Cool ! angel

   Je vous donne ci-dessous le code complet du fichier monster.c.
   Vous voyez qu'en haut de ce fichier, on crée 3 variables :
- nombreMonstres qui indiquera combien de monstres sont initialisés (et qu'il nous faudra donc gérer / dessiner wink).
- MonsterSprite qui contiendra notre feuille de sprites.
- et enfin monster[] qui sera notre tableau de sprites (monstres).

   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... Pratique, non ? wink

   Ensuite, nous avons toute une suite de get / set / reset dont vous devez commencer à avoir l'habitude et qui nous permettront de retrouver / modifier / réinitialiser les variables précédentes.

   initMonsterSprites() charge, quant à elle, notre feuille de sprites et cleanMonsters() la décharge. 

   La fonction qui nous permettra d'initialiser un nouveau monstre s'appelle initializeNewMonster(). Elle ressemble forcément un peu à celle du joueur, mais en moins complexe wink.

   Le principal à retenir c'est que, comme il n'y aura pas qu'un seul monstre, il nous faudra d'abord tester si on peut en rajouter un (et qu'on ne dépasse donc pas MONSTRES_MAX). Si c'est bon, on pourra alors initialiser le dernier monstre du tableau (égal à nombreMonstres) avant d'incrémenter le nombre de monstres de 1.

Fichier : monster.c : Créer le fichier et y copier :

#include "prototypes.h"
 
 
int nombreMonstres;
GameObject monster[MONSTRES_MAX];
SDL_Texture *MonsterSprite;
 
 
GameObject *getMonster(int nombre)
{
return &monster[nombre];
}
 
 
int getMonsterNumber(void)
{
return nombreMonstres;
}
 
 
void resetMonsters(void)
{
nombreMonstres = 0;
}
 
 
void initMonsterSprites(void)
{
MonsterSprite = loadImage("graphics/monster1.png");
}
 
 
void cleanMonsters(void)
{
/* Libère le sprite des monstres */
if (MonsterSprite != NULL)
{
SDL_DestroyTexture(MonsterSprite);
MonsterSprite = NULL;
}
}
 
 
 
void initializeNewMonster(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 (nombreMonstres < MONSTRES_MAX)
{
 
//On réinitialise la frame et le timer
monster[nombreMonstres].frameNumber = 0;
monster[nombreMonstres].frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
 
//On indique sa direction (il viendra à l'inverse du joueur, logique)
monster[nombreMonstres].direction = LEFT;
 
/* Ses coordonnées de démarrage seront envoyées par la fonction drawMap() en arguments */
monster[nombreMonstres].x = x;
monster[nombreMonstres].y = y;
 
/* Hauteur et largeur de notre monstre (rajouté dans les defs ;) ) */
monster[nombreMonstres].w = MONSTER_WIDTH;
monster[nombreMonstres].h = MONSTER_HEIGTH;
 
//Variables nécessaires au fonctionnement de la gestion des collisions comme pour le héros
monster[nombreMonstres].timerMort = 0;
monster[nombreMonstres].onGround = 0;
 
nombreMonstres++;
 
}
 
}
 
 
 
void updateMonsters()
{
int i;
 
//On passe en boucle tous les monstres du tableau
for (i = 0; i < 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;
}
else
{
monster[i].direction = LEFT;
}
}
 
//Déplacement du monstre selon la direction
if (monster[i].direction == LEFT)
monster[i].dirX -= 2;
else
monster[i].dirX += 2;
 
 
//On sauvegarde les coordonnées du monstre 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]);
 
//On détecte les collisions avec le joueur
//Si c'est égal à 1, on diminue ses PV
if (collide(getPlayer(), &monster[i]) == 1)
{
if (getLife() > 1)
{
playerHurts(&monster[i]);
}
else
{
killPlayer();
}
}
 
else if (collide(getPlayer(), &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[nombreMonstres - 1];
nombreMonstres--;
 
}
}
}
 
}
 
 
//Fonction de gestion des collisions
int collide(GameObject *player, GameObject *monster)
{
//On teste pour voir s'il n'y a pas collision, si c'est le cas, on renvoie 0
if ((player->x >= monster->x + monster->w)
|| (player->x + player->w <= monster->x)
|| (player->y >= monster->y + monster->h)
|| (player->y + player->h <= monster->y)
)
return 0;
 
//Sinon, il y a collision. Si le joueur est au-dessus du monstre (avec une marge
//de 10 pixels pour éviter les frustrations dues au pixel perfect), on renvoie 2.
//On devra alors tuer le monstre et on fera rebondir le joueur.
else if (player->y + player->h <= monster->y + 10)
{
player->dirY = -JUMP_HEIGHT;
return 2;
}
 
//Sinon, on renvoie 1 et c'est le joueur qui meurt...
else
return 1;
}
 
 
 
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 (getTileValue(y + 1, x) < BLANK_TILE - 20)
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 (getTileValue(y + 1, x) < BLANK_TILE - 20)
return 1;
else
return 0;
}
}
 
 
void drawMonster(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_PLAYER;
 
//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, sauf si c'est une animation unique
//(qui ne s'exécute qu'une seule fois comme les explosions), auquel cas, OneTimeAnimation vaudra 1.
//SDL2 : on retrouve la largeur de la feuille de sprite
int w;
SDL_QueryTexture(MonsterSprite, NULL, NULL, &w, NULL);
 
if (entity->frameNumber >= 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 à dessiner */
SDL_Rect dest;
 
// On soustrait des coordonnées de notre monstre, ceux du début de la map, pour qu'il colle
//au scrolling :
dest.x = entity->x - getStartX();
dest.y = entity->y - getStartY();
dest.w = entity->w;
dest.h = entity->h;
 
/* Rectangle source */
SDL_Rect src;
 
//Pour connaître le X de la bonne frame à dessiner, 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;
 
//Gestion du flip (retournement de l'image selon que le monstre regarde à droite ou à gauche
if (entity->direction == LEFT)
SDL_RenderCopyEx(getrenderer(), MonsterSprite, &src, &dest, 0, 0, SDL_FLIP_HORIZONTAL);
else
SDL_RenderCopyEx(getrenderer(), MonsterSprite, &src, &dest, 0, 0, SDL_FLIP_NONE);
 
}

   On va ensuite développer un peu plus longuement les fonctions suivantes : wink

     La fonction updateMonsters() :

   Comme vous pouvez le voir, elle ressemble beaucoup à notre fonction updatePlayer() en moins compliquée. On gère chaque monstre à son tour, en parcourant notre tableau de monstres. De base, notre monstre ne fait que d'avancer, cependant, en plus de tester les collisions avec la map comme pour le joueur, on teste si celui-ci reste coincé (2 tours de suite à la même position, car bloqué contre un mur) et dans ce cas-là, on lui fait faire demi-tour. On teste aussi s'il arrive au bord d'un trou avec la fonction checkFall() (cf. ci-dessous). Si c'est le cas, au lieu de le laisser tomber dans le trou comme un gros naze, on lui fait faire également demi-tour, wink ça c'est de l'intelligence ! laugh

    Là où ça se complique un petit peu, c'est quand l'un d'eux 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 wink).
    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 !! smiley

 

     La fonction collide() :

   C'est elle qui va gérer les collisions entre le joueur et les monstres. Nous utiliserons ici un système à la Mario (avec bond sur la tête) parce que c'est sans doute le plus simple. Je ne m'attarderai pas sur la théorie de cette technique, car j'ai déjà écrit un autre tuto là-dessus et je vous y renvoie donc si vous ne l'avez pas déjà luwink

   Son fonctionnement est simple : on teste si les deux sprites ne se touchent pas. Si c'est la cas, on renvoie 0, et c'est fini ! wink

   Sinon, c'est qu'ils se touchent ! surprise Mais là, on rajoute un test pour savoir si le joueur est au-dessus du monstre ou pas. Si c'est le cas, il lui rebondit dessus, on renvoie 2, et on devra supprimer le monstre. Sinon, c'est notre lapin qui morfle et on renvoie 1 pour le blesser...  Notons que notre lapin a 3 coeurs (qu'on afiichera plus tard dans le HUD cheeky), et que s'il les perd tous, il meurt ! surprise

   

     La fonction checkFall() :

   Nous avons déjà vu précédemment qu'on pouvait faire faire demi-tour à notre monstre quand il se cognait dans un mur, en testant pour voir si ses coordonnées (x ici) ne changeaient pas. Si c'était le cas, il était coincé et il ne manquait plus qu'à lui faire changer de direction. indecision

   Mais, 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 comme un naze...

   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. 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) ! indecision

   C'est donc ce que fait ici notre fonction checkFall() qui va tester si la tile sous le monstre est du vide ou pas. Ainsi, 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. Ouf ! cheeky
   On va aussi s'assurer par sécurité qu'on ne sort pas du tableau de notre map, pour éviter de planter notre jeu (ce serait plus cool ! laugh).   

 

     La fonction drawMonster() :

   Cette fonction est similaire à celle qui affiche le héros. Mais cette fois, on va afficher de méchants monstres ! laugh

   Eh voilà, pour ce fichier monster.c qui compose le plus gros de ce chapitre avec ce qui va suivre : la détection des tiles monstres ! On est parti ! wink

 

      Détectons les tiles Monstre  

   On retourne dans notre fichier map.c pour créer la fonction getTileValue() qui renverra la valeur de la tile indiquée par ses coordonnées x et y. Cette fonction est utile pour notre autre fonction checkFall(). wink

Fichier : map.c : Rajouter la fonction :

int getTileValue(int y, int x)
{
return map.tile[y][x];
} 

    Modifions désormais notre fonction drawMap(). Cela ne va pas être bien compliqué, mais redondant, car il va falloir écrire presque la même chose pour chaque couche de tiles (ou layer). En effet, on veut pouvoir placer une tile monstre et l'initialiser depuis n'importe quelle couche. wink

   La technique est simple : au moment de dessiner la tile, on teste si c'est une tile monstre. Si c'est le cas, on ne l'affiche pas, mais on la supprime (en la remplaçant par la tile 0) et on initialise un monstre en renvoyant les coordonnées x et y (correspondant à la tile monstre) à la fonction initializeNewMonster()wink

   C'est long mais pas bien compliqué. Je vous laisse lire :

Fichier : map.c : Remplacer la fonction précédente par :

void drawMap(int layer)
{
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);
 
 
//On met en place un timer pour animer la map (chapitre 19)
if (map.mapTimer <= 0)
{
if (map.tileSetNumber == 0)
{
map.tileSetNumber = 1;
map.mapTimer = TIME_BETWEEN_2_FRAMES * 3;
}
else
{
map.tileSetNumber = 0;
map.mapTimer = TIME_BETWEEN_2_FRAMES * 3;
}
 
}
else
map.mapTimer--;
 
 
/* 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) */
if (layer == 1)
{
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] == TILE_MONSTRE)
{
//On initialise un monstre en envoyant les coordonnées de la tile
initializeNewMonster(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 suivant le timer */
if (map.tileSetNumber == 0)
drawTile(map.tileSet, x, y, xsource, ysource);
else
drawTile(map.tileSetB, x, y, xsource, ysource);
 
mapX++;
}
mapY++;
}
}
 
else if (layer == 2)
{
//Deuxième couche de tiles ;)
for (y = y1; y < y2; y += TILE_SIZE)
{
mapX = map.startX / TILE_SIZE;
 
for (x = x1; x < x2; x += TILE_SIZE)
{
//Si la tile à dessiner n'est pas une tile vide
if (map.tile2[mapY][mapX] != 0)
{
/*On teste si c'est une tile monstre (tile numéro 10) */
if (map.tile2[mapY][mapX] == TILE_MONSTRE)
{
//On initialise un monstre en envoyant les coordonnées de la tile
initializeNewMonster(mapX * TILE_SIZE, mapY * TILE_SIZE);
 
//Et on efface cette tile de notre tableau pour éviter un spawn de monstres
//infini !
map.tile2[mapY][mapX] = 0;
}
}
 
/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro
de la tile */
a = map.tile2[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 suivant le timer */
if (map.tileSetNumber == 0)
drawTile(map.tileSet, x, y, xsource, ysource);
else
drawTile(map.tileSetB, x, y, xsource, ysource);
 
mapX++;
}
mapY++;
}
}
else if (layer == 3)
{
//Troisième couche de tiles ;)
for (y = y1; y < y2; y += TILE_SIZE)
{
mapX = map.startX / TILE_SIZE;
 
for (x = x1; x < x2; x += TILE_SIZE)
{
//Si la tile à dessiner n'est pas une tile vide
if (map.tile3[mapY][mapX] != 0)
{
/*On teste si c'est une tile monstre (tile numéro 10) */
if (map.tile3[mapY][mapX] == TILE_MONSTRE)
{
//On initialise un monstre en envoyant les coordonnées de la tile
initializeNewMonster(mapX * TILE_SIZE, mapY * TILE_SIZE);
 
//Et on efface cette tile de notre tableau pour éviter un spawn de monstres
//infini !
map.tile3[mapY][mapX] = 0;
}
}
 
/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro
de la tile */
a = map.tile3[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 suivant le timer */
if (map.tileSetNumber == 0)
drawTile(map.tileSet, x, y, xsource, ysource);
else
drawTile(map.tileSetB, x, y, xsource, ysource);
 
mapX++;
}
mapY++;
}
}
 
}

 

     Gérons les collisions de notre monstre avec la map !

   De même que pour le héros, on crée la fonction monsterCollisionToMap() qui testera les collisions avec la map.

Fichier : map.c : Rajouter la fonction :

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] > TILE_TRAVERSABLE || map.tile[y2][x2] > TILE_TRAVERSABLE)
{
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;
}
}

   Pour l'instant, cette fonction est identique à celle du joueur, mais ce ne sera bientôt plus le cas. wink

   En effet, le joueur pourra faire bien des choses qu'un monstre ne pourra pas, comme par exemple, ramasser des power-ups ! laugh

     Mise à jour du fichier player.c

   Mettons désormais à jour notre fichier player.c avec 3 fonctions basiques :
- getLife() renvoie le nombre de coeurs du  héros.
- killPlayer() tue le monster en mettant son timerMort à 1 (comme quand il tombe dans un troutrou ! laugh).
- playerHurts() enlève un coeur au joueur, le fait rebondir de douleur et le rend temporairement invincible.

 

Fichier : player.c : Ajouter les fonctions :

int getLife(void)
{
return player.life;
}
 
 
void killPlayer(void)
{
//On met le timer à 1 pour tuer le joueur intantanément
player.timerMort = 1;
}
 
 
void playerHurts(GameObject *monster)
{
//Si le timer d'invincibilité est à 0
//on perd un coeur
if (player.invincibleTimer == 0)
{
player.life--;
player.invincibleTimer = 80;
monster->timerMort = 1;
player.dirY = -JUMP_HEIGHT;
}
}

   Dans notre fonction initializePlayer(), on fait un reset du nombre de monstres, pour réinitialiser le niveau (sinon les anciens monstres resteraient ou passeraient d'un niveau à l'autre ! surprise) :

Fichier : player.c : initializePlayer() - rajouter à la fin de la fonction :

//Réinitialise les monstres
/* Libère le sprite des monstres */
resetMonsters();

 

     On fait clignoter le héros quand il est touché     

   Enfin, cerise sur le gâteau (et qui n'était pas dans le Big Tuto précédent wink), on va faire clignoter notre héros lorsqu'il est invincible, après s'être fait toucher.

   Pour cela, on va modifier notre fonction drawPlayer() : au moment de dessiner la bonne frame, on va tester s'il est invincible. S'il ne l'est pas, on continue comme avant. Sinon, on va tester si la frame est paire ou impaire en utilisant un modulo 2 (%2). Si c'est paire, le modulo renverra 0 et on affichera la frame, sinon, on ne l'affichera pas.

   Ainsi, et ce très simplement, nous ferons clignoter notre héros 1 frame sur 2 lorsqu'il sera touché ! angel

Fichier : player.c : Remplacer la fonction existante par :

void drawPlayer(void)
{
/* Gestion du timer */
// Si notre timer (un compte à rebours en fait) arrive à zéro
if (player.frameTimer <= 0)
{
//On le réinitialise
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
 
//Et on incrémente notre variable qui compte les frames de 1 pour passer à la suivante
player.frameNumber++;
 
//Mais si on dépasse la frame max, il faut revenir à la première :
if (player.frameNumber >= player.frameMax)
player.frameNumber = 0;
 
}
//Sinon, on décrémente notre timer
else
player.frameTimer--;
 
 
//Ensuite, on peut passer la main à notre fonction
 
/* Rectangle de destination à dessiner */
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 = player.x - getStartX();
dest.y = player.y - getStartY();
dest.w = player.w;
dest.h = player.h;
 
/* Rectangle source */
SDL_Rect src;
 
//Pour connaître le X de la bonne frame à dessiner, il suffit de multiplier
//la largeur du sprite par le numéro de la frame à afficher -> 0 = 0; 1 = 40; 2 = 80...
src.x = player.frameNumber * player.w;
src.w = player.w;
src.h = player.h;
 
//On calcule le Y de la bonne frame à dessiner, selon la valeur de l'état du héros :
//Aucun Mouvement (Idle) = 0, marche (walk) = 1, etc...
//Tout cela en accord avec notre spritesheet, of course ;)
src.y = player.etat * player.h;
 
//Si on a été touché et qu'on est invincible
if (player.invincibleTimer > 0)
{
//On fait clignoter le héros une frame sur deux
//Pour ça, on calcule si le numéro de la frame en
//cours est un multiple de deux
if (player.frameNumber % 2 == 0)
{
//Gestion du flip (retournement de l'image selon que le sprite regarde à droite ou à gauche
if (player.direction == LEFT)
SDL_RenderCopyEx(getrenderer(), playerSpriteSheet, &src, &dest, 0, 0, SDL_FLIP_HORIZONTAL);
else
SDL_RenderCopyEx(getrenderer(), playerSpriteSheet, &src, &dest, 0, 0, SDL_FLIP_NONE);
}
//Sinon, on ne dessine rien, pour le faire clignoter
}
 
//Sinon, on dessine normalement
else
{
//Gestion du flip (retournement de l'image selon que le sprite regarde à droite ou à gauche
if (player.direction == LEFT)
SDL_RenderCopyEx(getrenderer(), playerSpriteSheet, &src, &dest, 0, 0, SDL_FLIP_HORIZONTAL);
else
SDL_RenderCopyEx(getrenderer(), playerSpriteSheet, &src, &dest, 0, 0, SDL_FLIP_NONE);
}
 
}

 

     Chargement / déchargement de la spritesheet du monstre 

   Passons maintenant aux mises à jour de base : on rajoute le chargement de la feuille de sprites des monstres dans loadGame() (sinon, pas de monstre à l'écran ! indecision) :

Fichier : init.c : Remplacer la fonction existante par :

void loadGame(void)
{
 
//On charge les données pour la map
initMaps();
 
//On charge la feuille de sprites du monstre
initMonsterSprites();
 
//On charge la feuille de sprites (spritesheet) de notre héros
initPlayerSprites();
 
//On commence au premier niveau
SetValeurDuNiveau(1);
changeLevel();
 
}

   Même chose pour le déchargement :

Fichier : init.c : Mettre à jour avec l'ajout de cleanMonsters() :

void cleanup()
{
//Nettoie les sprites de la map
cleanMaps();
 
/* Libère le sprite du héros */
cleanPlayer();
 
/* Libère le sprite des monstres */
cleanMonsters();

 

 

     Branchements dans la boucle principale du jeu    

   Et enfin, on rajoute updateMonsters() dans la boucle principale de notre main() :

Fichier : main.c : Mettre à jour avec l'ajout de updateMonsters() :

int main(int argc, char *argv[])
{
unsigned int frameLimit = SDL_GetTicks() + 16;
int go;
 
// Initialisation de la SDL
init("Rabidja 3 - SDL 2 - www.meruvia.fr");
 
// Chargement des ressources (graphismes, sons)
loadGame();
 
/* On initialise le joueur */
initializePlayer(1);
 
// Appelle la fonction cleanup à la fin du programme
atexit(cleanup);
 
go = 1;
 
// Boucle infinie, principale, du jeu
while (go == 1)
{
//Gestion des inputs clavier
gestionInputs(&input);
 
// On met à jour le jeu, en commençant par le joueur
updatePlayer(&input);
 
//On update les monstres
updateMonsters();
 
//On dessine tout
drawGame();
 
// Gestion des 60 fps (1000ms/60 = 16.6 -> 16
delay(frameLimit);
frameLimit = SDL_GetTicks() + 16;
}
 
// On quitte
exit(0);
 
}

    Et on rajoute l'affichage des monstres dans drawGame() avec une boucle pour tous les afficher l'un après l'autre :

Fichier : draw.c : Remplacer la fonction existante par :

void drawGame(void)
{
int i;
 
// Affiche le fond (background) aux coordonnées (0,0)
drawImage(getBackground(), 0, 0);
 
/* Affiche la map de tiles : layer 2 (couche du fond) */
drawMap(2);
 
/* Affiche la map de tiles : layer 1 (couche active : sol, etc.)*/
drawMap(1);
 
/* Affiche le joueur */
drawPlayer();
 
/* Affiche les monstres */
for (i = 0; i < getMonsterNumber(); i++)
{
drawMonster(getMonster(i));
}
 
/* Affiche la map de tiles : layer 3 (couche en foreground / devant) */
drawMap(3);
 
// Affiche l'écran
SDL_RenderPresent(getrenderer());
 
// Délai pour laisser respirer le proc
SDL_Delay(1);
}

   Plus qu'à mettre à jour notre catalogue de prototypes :

        Fichier : prototypes.h : Remplacez par :

#ifndef PROTOTYPES
#define PROTOTYPES
 
#include "structs.h"
 
/* Catalogue des prototypes des fonctions utilisées.
On le complétera au fur et à mesure. */
 
extern void centerScrollingOnPlayer(void);
extern void changeLevel(void);
extern int checkFall(GameObject monster);
extern void cleanMaps(void);
extern void cleanMonsters(void);
extern void cleanPlayer(void);
extern void cleanup(void);
extern void closeJoystick(void);
extern int collide(GameObject *player, GameObject *monster);
extern void delay(unsigned int frameLimit);
extern void drawGame(void);
extern void drawImage(SDL_Texture *, int, int);
extern void drawMap(int);
extern void drawMonster(GameObject *entity);
extern void drawPlayer(void);
extern void drawTile(SDL_Texture *image, int destx, int desty, int srcx, int srcy);
extern void gestionInputs(Input *input);
extern SDL_Texture *getBackground(void);
extern int getBeginX(void);
extern int getBeginY(void);
extern void getInput(Input *input);
extern void getJoystick(Input *input);
extern int getLevel(void);
extern int getLife(void);
extern int getMaxX(void);
extern int getMaxY(void);
extern GameObject *getMonster(int nombre);
extern int getMonsterNumber(void);
extern GameObject *getPlayer(void);
extern int getPlayerDirection(void);
extern int getPlayerx(void);
extern int getPlayery(void);
extern SDL_Renderer *getrenderer(void);
extern int getStartX(void);
extern int getStartY(void);
extern int getTileValue(int y, int x);
extern void init(char *);
extern void initMaps(void);
extern void initializeNewMonster(int x, int y);
extern void initMonsterSprites(void);
extern void initPlayerSprites(void);
extern void initializePlayer(int newLevel);
extern void killPlayer(void);
extern void loadGame(void);
extern SDL_Texture *loadImage(char *name);
extern void loadMap(char *name);
extern void mapCollision(GameObject *entity);
extern void monsterCollisionToMap(GameObject *entity);
extern void openJoystick(void);
extern void playerHurts(GameObject *monster);
extern void resetMonsters(void);
extern void setNombreDeVies(int valeur);
extern void setNombreDetoiles(int valeur);
extern void setStartX(int valeur);
extern void setStartY(int valeur);
extern void SetValeurDuNiveau(int valeur);
extern void updateMonsters();
extern void updatePlayer(Input *input);
 
#endif

   Voilà, plus qu'à compiler et à lancer le programme ! wink   

   De beaux zombies s'éveillent maintenant à la vie, prêts à en découdre ! indecision

   Argh !! Mais qu'avons-nous fait !?! surprise

   Je vous dis donc à bientôt pour le chapitre 13 ! angel

                                                                            Jay 

 

 

 

This site uses cookies to enable you to log in. We do not store or sell any personal data. By continuing to use this website, you agree to their use. Thanks!