07/05/2023 : Malheureusement, suite à un manque de financement et à des échéances prochaines impliquant de refaire TOUT le site en l'adaptant à une nouvelle plateforme pour pallier les risques croissants de sécurité, il est pour l'instant prévu que le site ferme prochainement ses portes...
 
13/06/2023 : Finalement, le site a été refinancé pour une nouvelle année complète. En l'état, il est fonctionnel mais notre hébergeur ne nous permet pas une mise à jour vers Joomla 4 ou php 8 (il a été racheté et n'est plus que l'ombre de lui-même...). Cela nous laisse cependant un peu de temps pour trouver une solution.

 

 
 




Chapitre 25 :

Gérons la santé de notre lapin !



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

Dernière mise à jour: 17 octobre 2014
Difficulté :






    Bon, maintenant que nous sommes relancés, profitons-en pour ajouter quelques coeurs à notre lapin, parce qu'il faut bien dire que mourir directement, c'est quand même un peu frustrant !
    Et on va ensuite en profiter pour gérer notre nouveau power-up en forme de coeur !


I - Rajoutons des coeurs à notre lapin


    Pour ce faire, nous allons commencer par rajouter une variable Life à la structure de notre lapin, qui est un GameObject (si, si, rappelez-vous !  ).

 Fichier : structs.h

/* Structure pour gérer notre héros */

typedef struct GameObject
{
    //Sprite du héros (pas d'animation pour l'instant)
	SDL_Surface *sprite;
	
	//Points de vie/santé
	int life;

	/* 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à qui est fait. maintenant, il va falloir initiliser cette valeur dans void initializePlayer(void) dans le fichier player.c.
    On va la mettre à 3. Comme ça, notre lapin commencera son aventure avec 3 coeurs. Classique, non ?

 Fichier : player.c

void initializePlayer(void)
{
    //PV à 3
    player.life = 3;

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

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

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

}



    Maintenant, il va nous falloir gérer ses coeurs, quand il se fait toucher, et ne plus le faire mourir direct ! Bon, alors, ça se passe dans la fonction updateMonsters(void) du fichier monster.c.
    On va donc diminuer le nombre de coeurs à chaque fois que notre lapin se fera toucher, et à 0, on le fera mourir ! Rien de plus simple :


Fichier : monster.c

	    //On détecte les collisions avec le joueur
            //Si c'est égal à 1, on diminue ses PV
            if (collide(&player, &monster[i]) == 1)
            {
                if(player.life > 1)
                    player.life--;
                else
                {
                    //On met le timer à 1 pour tuer le joueur intantanément
                    player.timerMort = 1;
                }
                //On joue le son
                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);
            }



    Voilà, si maintenant vous compilez le jeu, vous remarquerez que votre lapin continue de mourir en une fois, mais vous entendez 3 fois le son de la mort quasi simultanément !
    Mais, mais... comment ça se fait !
    En fait, quand votre lapin entre en contact avec le monstre, il vous faut presque 1 seconde pour réagir et vous en éloigner, or le jeu teste les collisions 60 fois par seconde...
    Le jeu vous fait donc perdre un coeur toutes les 16 millisecondes ! 

    Alors comment faire ?
    Bah, il faut juste rajouter une petite tempo qui rendra le joueur invincible pendant quelques secondes, le temps qu'il puisse se dégager. C'est pour ça que votre personnage clignote dans beaucoup de jeux quand il est touché. Il clignote le temps de la tempo.

    Bon, dans notre cas, on va d'abord rajouter cette tempo. Retour dans la structure du GameObject de notre lapin :


II - Rajoutons une tempo d'invincibilité

    Voilà, donc on reprend notre fichier structs.h et on rajoute un timer d'invincibilité :

 Fichier : structs.h

/* Structure pour gérer notre héros */

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

	//Points de vie/santé
	int life, invincibleTimer;




    Ensuite, même chose que pour les coeurs, sauf qu'on initialise notre timer à 0, vu que de base, le joueur n'est pas invincible !

Fichier : player.c

void initializePlayer(void)
{
    //PV à 3
    player.life = 3;
    
    //Timer d'invincibilité à 0
    player.invincibleTimer = 0;
	




    Il va maintenant nous falloir mettre à jour ce timer pour le descendre lentement à 0, s'il est initialisé. On va rajouter ça, logiquement à la fonction updatePlayer(void) :

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 gère le timer de l'invincibilité
      if(player.invincibleTimer > 0)
        player.invincibleTimer--;




    Et enfin, on va initialiser notre timer, si l'on est touché et bloqué les coups si l'on est invincible. Tout cela se passe à nouveau dans la fonction updateMonsters(void) du fichier monster.c.
    On va mettre le timer à 1sec * 60 frames = 60, mais vous pouvez le changer à votre guise  :


Fichier : monster.c

	    //On détecte les collisions avec le joueur
            //Si c'est égal à 1, on diminue ses PV
            if (collide(&player, &monster[i]) == 1)
            {
                if(player.life > 1)
                {
                    //Si le timer d'invincibilité est à 0
                    //on perd un coeur
                    if(player.invincibleTimer == 0)
                    {
                        player.life--;
                        player.invincibleTimer = 60;
                        //On joue le son
                        playSoundFx(DESTROY);
                    }
                }
                else
                {
                    //On met le timer à 1 pour tuer le joueur intantanément
                    player.timerMort = 1;
                    //On joue le son
                    playSoundFx(DESTROY);
                }
                

            }




    Voilà, si on lance le jeu, maintenant, ça fonctionne, mais c'est quand même bizarre, car après avoir touché le joueur, le monstre continue de lui passer au travers...
    C'est pas terrible ça...
    Alors, comme on est sympa et qu'on veut favoriser le joueur, on va faire en sorte que notre lapin se venge de son coeur perdu en sautant et en tuant le monstre ! On va donc rajouter les quelques lignes suivantes :

Fichier : monster.c

	    //On détecte les collisions avec le joueur
            //Si c'est égal à 1, on diminue ses PV
            if (collide(&player, &monster[i]) == 1)
            {
                if(player.life > 1)
                {
                    //Si le timer d'invincibilité est à 0
                    //on perd un coeur
                    if(player.invincibleTimer == 0)
                    {
                        player.life--;
                        player.invincibleTimer = 60;
                        //On joue le son
                        playSoundFx(DESTROY);
                        //On met le timer à 1 pour tuer le monstre intantanément
                        monster[i].timerMort = 1;
                        player.dirY = -JUMP_HEIGHT;
                    }
                }
                else
                {
                    //On met le timer à 1 pour tuer le joueur intantanément
                    player.timerMort = 1;
                    //On joue le son
                    playSoundFx(DESTROY);
                }


            }



    En fait, c'est très simple, on rajoute une ligne pour tuer le monstre (comme plus bas) et une autre pour faire sauter le joueur (comme dans la fonction collide() ).

    Si on compile, maintenant, c'est mieux !
    Alors, pour le clignotement en SDL, le plus simple que j'aie trouvé
, c'est de le dessiner directement dans les frames du perso (avec un spritesheet différent en fait), alors qu'on peut le faire directement dans le code, au blittage en XNA... Bien sûr, on pourrait chercher dans les bibliothèques tierces de la SDL, mais on risque de faire ramer le blittage et le code risque de ne plus être compatible avec glsdl, qu'on rajoutera plus tard, pour optimiser notre jeu. Si toutefois quelqu'un trouve une solution simple et idéale, merci de la partager avec tout le monde sur le forum ! Je la rajouterai ici.


III - Affichons les coeurs

    Bon, c'est bien, mais maintenant il faudrait peut-être afficher les coeurs, qu'on sache où on en est, non ?!
    Bien sûr, c'est ce qu'on va faire dès maintenant. Pour afficher nos coeurs, on va simplement réutiliser la tile de notre tileset (plus simple et plus économe en mémoire ! ) .

    Pour ça, direction la fonction drawHud(void) dans le fichier draw.c :

Fichier : draw.c

void drawHud(void)
{
    //On crée une variable qui contiendra notre texte (jusqu'à 200 caractères, y'a de la marge ;) ).
    char text[200];

    int i;

    //Affiche le nombre de coeurs
    //On crée une boucle pour afficher de 1 à 3 coeurs
    //selon la vie, avec un décalage de 32 pixels
    for( i = 0; i < player.life; i++)
    {
        // Calcul pour découper le tileset comme dans la fonction drawMap()
        int ysource = TILE_POWER_UP_FIN / 10 * TILE_SIZE;
        int xsource = TILE_POWER_UP_FIN % 10 * TILE_SIZE;

        drawTile(map.tileSet, 60 + i * 32, 20, xsource, ysource);
    }


    /* Affiche le nombre de vies en bas à droite */
    drawImage(jeu.HUD_vie, 480, 410);



    Le principe est simple, on fait une boucle suivant la vie qu'il reste au joueur, et on blitte un coeur à chaque fois, en blittant la bonne tile (même principe que pour la map - cf. tuto 1 - chapitre 5, si vous avez oublié comment on faisait ). On décale chaque coeur de 32 pixels en utilisant l'index i qu'on multiplie tout simplement par 32 ! C'est si simple, en fait !

    Et voilà, plus qu'à compiler et Magie ! Les coeurs sont gérés !


IV - Rajoutons le power-up : coeur

    Il ne nous reste maintenant plus qu'à gérer le power-up coeur, et à rajouter un coeur au joueur s'il le ramasse (enfin sauf s'il en a déjà 3, bien sûr  !).
    Pour ça, on va reprendre la fonction mapCollision() qu'on avait un peu laissée en plan dans le chapitre précédent et mettre à jour tous les passages qui gèrent les power-ups. On va pour cela utiliser nos 2 defs : TILE_POWER_UP_DEBUT et TILE_POWER_UP_FIN. Et on va envoyer le numéro de l'item ramassé à la fonction getItem(), pour qu'elle sache quoi faire.
    Pour le calculer, on va prendre le numéro de la tile donné par map.tile[y1][x2] et on va lui enlever le numéro du premier power-up, soit TILE_POWER_UP_DEBUT, ensuite on va y rajouter 1 pour que notre premier item vale 1 et pas 0 (mais on aurait pu garder 0 aussi si on voulait après tout... ). Ce qui nous donne :

getItem(map.tile[y1][x2] - TILE_POWER_UP_DEBUT + 1);

    Voilà maintenant la première occurence de la fonction modifiée :

Fichier : map.c

				//Test des tiles Power-up
				if (map.tile[y1][x2] > = TILE_POWER_UP_DEBUT
                    && map.tile[y1][x2] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y1][x2] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y1][x2] = 0;
				}
				else if (map.tile[y2][x2] > = TILE_POWER_UP_DEBUT
                    && map.tile[y2][x2] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y2][x2] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y2][x2] = 0;
				}



Et voilà la fonction complète :

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 des tiles Power-up
				if (map.tile[y1][x2] >= TILE_POWER_UP_DEBUT
                    && map.tile[y1][x2] <= TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y1][x2] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y1][x2] = 0;
				}
				else if (map.tile[y2][x2] > = TILE_POWER_UP_DEBUT
                    && map.tile[y2][x2] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y2][x2] - TILE_POWER_UP_DEBUT + 1);

				    //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] > = TILE_POWER_UP_DEBUT
                    && map.tile[y1][x1] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y1][x1] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y1][x1] = 0;
				}
				else if (map.tile[y2][x1] > = TILE_POWER_UP_DEBUT
                    && map.tile[y2][x1] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y2][x1] - TILE_POWER_UP_DEBUT + 1);

				    //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] > = TILE_POWER_UP_DEBUT
                    && map.tile[y2][x1] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y2][x1] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y2][x1] = 0;
				}
				else if (map.tile[y2][x2] > = TILE_POWER_UP_DEBUT
                    && map.tile[y2][x2] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y2][x2] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y2][x2] = 0;
				}


				/* Gestion du ressort */
				if ((map.tile[y2][x1] == TILE_RESSORT ) || (map.tile[y2][x2] == TILE_RESSORT ))
				{
					entity->dirY = -20;
					//On indique au jeu qu'il a atterri pour réinitialiser le double saut
					entity->onGround = 1;
					playSoundFx(BUMPER);
				}


				else if (map.tile[y2][x1] > BLANK_TILE || map.tile[y2][x2] > BLANK_TILE)
				{
                    //Si la tile est solide, on y colle le joueur et
                    //on le déclare sur le sol (onGround).
					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 */

				//Test de la tile Power-up : Etoile (= tile N°5)
				if (map.tile[y1][x1] > = TILE_POWER_UP_DEBUT
                    && map.tile[y1][x1] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y1][x1] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y1][x1] = 0;
				}
				if (map.tile[y1][x2] > = TILE_POWER_UP_DEBUT
                    && map.tile[y1][x2] < = TILE_POWER_UP_FIN)
				{
				    //On appelle la fonction getItem()
				    getItem(map.tile[y1][x2] - TILE_POWER_UP_DEBUT + 1);

				    //On remplace la tile power-up par une tile transparente
				    map.tile[y1][x2] = 0;
				}


				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)
	{
	    //Si on touche le bord droit de l'écran, on passe au niveau sup
	    jeu.level++;
	    //Si on dépasse le niveau max, on annule et on limite le déplacement du joueur
		if(jeu.level > LEVEL_MAX)
		{
		    jeu.level = LEVEL_MAX;
		    entity->x = map.maxX - entity->w - 1;
		}
		//Sion, on passe au niveau sup, on charge la nouvelle map et on réinitialise le joueur
		else
		{
		    changeLevel();
		    initializePlayer();
		}

	}

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




    On va ensuite modifier notre fonction getItem() pour qu'elle gère les coeurs :

Fichier : player.c

void getItem(int itemNumber)
{
    switch(itemNumber)
    {
        //Gestion des étoiles
        case 1:
        //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++;
        }
        break;

        //Gestion des coeurs
        case 2:
        //On incrémente le compteur Etoile
        if(player.life < 3)
            player.life++;

        playSoundFx(STAR);
        break;

        default:
        break;
    }


}



    Voilà, on gère tout ça avec un simple switch.
    Sans oublier maintenant de mettre à jour notre prototype de getItem() dans map.h, vu qu'on a modifié les arguments de la fonction :

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 initializeMonster(int x, int y);
extern void getItem(int itemNumber);
extern void playSoundFx(int type);
extern SDL_Surface *loadImage(char *name);
extern void changeLevel(void);
extern void initializePlayer(void);


extern Gestion jeu;
extern Map map;




    On compile et ça marche !
    Voilà la conclusion d'un chapitre relativement long mais pas bien compliqué, pour un résultat plutôt satisfaisant !

     A bientôt pour la suite !
 




Télécharger le projet complet !



 

Connexion

CoalaWeb Traffic

Today141
Yesterday211
This week828
This month4644
Total1738859

29/03/24