Chapitre 27 :

T.P. : Les checkpoints !



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

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




Eh voilà, notre beau checkpoint actif



    Autre chose qui m'a souvent été demandée : on va maintenant rajouter des checkpoints de mi-niveau ! 

    Mais d'abord, qu'est-ce qu'un checkpoint ?

    Eh bien, c'est tout simplement un point du niveau, qui en général change de forme quand on le touche et qui permet au joueur de recommencer à partir de ce point s'il meurt, au lieu de recommencer tout le niveau depuis le début !

     Bien entendu, il y a plusieurs façons de programmer un checkpoint, et on va prendre l'une des solutions les plus simples. Un checkpoint pourrait très bien être un sprite, mais dans notre cas, ce sera une tile. Et vous la connaissez déjà puisque je vous l'ai présentée dans le chapitre 1. Il s'agit d'un petit mât, et un drapeau y apparaîtra quand on le touchera pour dire au joueur qu'il est activé.

    Mais alors, que doit-on programmer exactement ?

    Eh bien, déjà, il va falloir détecter si le joueur touche ou non la tile. Ca, on sait faire, on va utiliser la même technique que pour prendre les power-ups (sauf qu'on n'appellera pas getItem() bien sûr !

    Quand ce sera le cas, on remplacera la tile affichée (mât) par la suivante (drapeau) : il suffit de faire un +1, facile !

    Ensuite, on enregistrera les coordonnées du checkpoint dans des variables de respawn (à créer dès le départ) et on créera aussi un booléen checkpointActif pour savoir si on doit respawner au début du niveau ou depuis les coordonnées du dernier checkpoint enregistré.

    Enfin, quand le joueur meurt, on le respawne au bon endroit. Et, on n'oublie pas, pour finir, de remettre notre booléen checkpointActif à 0, après chaque passage de niveau (sinon, gare aux problèmes ! ).

    Eh maintenant ? Eh, bien, comme il s'agit d'un TP, je vous laisse réfléchir un peu à la manière de mettre tout ça en place. Ce n'est vraiment pas très compliqué !

    Et, si vous voulez la correction, eh bien, lisez la suite (mais pas avant d'avoir essayé, hein !? ) !


I - Préparons nos nouvelles variables


    On va donc d'abord modifier notre structure GameObject pour rajouter les coordonnées de respawn et notre booléen :


 Fichier : structs.h

typedef struct GameObject
{
    //Sprite du héros
	SDL_Surface *sprite;

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

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

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




    C'était pas très difficile, non ?


II - Détectons la collision avec la tile checkpoint


    On va ensuite dans la fonction mapCollision(GameObject *entity) du fichier map.c et on va rajouter une collision avec la tile checkpoint pour les 4 directions (Haut, Bas, Gauche, Droite). Une fois cette collision détectée, on va changer la tile (notre +1), on va mettre le booléen à 1 et on va enregistrer les coordonnées de respawn proportionnellement à la taille de notre perso (on va donc conserver le x tel quel, mais changer le y qui est celui de la tile en y enlevant la hauteur du sprite du héros).

    Voilà l'exemple de la première occurence, pour le mouvement à droite :

 Fichier : map.c

//Test de la tile checkpoint
if (map.tile[y1][x2] == TILE_CHECKPOINT)
{
	//On active le booléen checkpoint
	entity->checkpointActif = 1;

	//On enregistre les coordonnées
    entity->respawnX = x2 * TILE_SIZE;
    entity->respawnY= (y1 * TILE_SIZE) - entity->h;

    //On change la tile
    map.tile[y1][x2] += 1;
}
else if (map.tile[y2][x2] == TILE_CHECKPOINT)
{
	//On active le booléen checkpoint
	entity->checkpointActif = 1;

	//On enregistre les coordonnées
    entity->respawnX = x2 * TILE_SIZE;
    entity->respawnY= (y2 * TILE_SIZE) - entity->h;

    //On change la tile
    map.tile[y2][x2] += 1;
}
	




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


				//Test de la tile checkpoint
				if (map.tile[y1][x2] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x2 * TILE_SIZE;
                    entity->respawnY = (y1 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y1][x2] += 1;
				}
				else if (map.tile[y2][x2] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x2 * TILE_SIZE;
                    entity->respawnY = (y2 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y2][x2] += 1;
				}


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


				//Test de la tile checkpoint
				if (map.tile[y1][x1] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x1 * TILE_SIZE;
                    entity->respawnY= (y1 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y1][x1] += 1;
				}
				else if (map.tile[y2][x1] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x1 * TILE_SIZE;
                    entity->respawnY= (y2 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y2][x1] += 1;
				}


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


				//Test de la tile checkpoint
				if (map.tile[y2][x1] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x1 * TILE_SIZE;
                    entity->respawnY= (y2 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y2][x1] += 1;
				}
				else if (map.tile[y2][x2] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x2 * TILE_SIZE;
                    entity->respawnY= (y2 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y2][x2] += 1;
				}


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


				//Test de la tile checkpoint
				if (map.tile[y1][x1] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x1 * TILE_SIZE;
                    entity->respawnY= (y1 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y1][x1] += 1;
				}
				else if (map.tile[y1][x2] == TILE_CHECKPOINT)
				{
					//On active le booléen checkpoint
					entity->checkpointActif = 1;

					//On enregistre les coordonnées
                    entity->respawnX = x2 * TILE_SIZE;
                    entity->respawnY= (y1 * TILE_SIZE) - entity->h;

                    //On change la tile
                    map.tile[y1][x2] += 1;
				}


				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
		{
		    entity->checkpointActif = 0;
		    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;
	}
}



Voilà, si maintenant vous compilez le jeu et que vous touchez un checkpoint, vous verrez qu'il s'active !
    Passons maintenant au respawn !



III - Gestion du respawn du joueur

    Quand notre héros meurt, on le réinitialise avec la fonction initializePlayer(void). C'est là qu'on remet ses coordonnées à 0 et c'est donc là qu'on va agir en changeant ses coordonnées si le checkpoint est actif :


 Fichier : player.c

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

    //Timer d'invincibilité à 0
    player.invincibleTimer = 0;

    //Nombre de boules de feu / shurikens à 0
    jeu.nombreFireballs = 0;

    /* 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/respawn de notre héros */
    if(player.checkpointActif == 1)
    {
        player.x = player.respawnX;
        player.y = player.respawnY;
    }
    else
    {
        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
    jeu.nombreMonstres = 0;

}




    Voilà, maintenant, si on meurt, on respawne au dernier checkpoint. Maintenant, vous constaterez qu'il y a un problème si vous changez de niveau ou redémarrez le jeu.
    C'est normal, on ne réinitialise pas notre booléen à 0 !
    Il nous faut donc le réinitialiser à la fin de notre fonction mapCollision(), quand on change de niveau :

 Fichier : map.c

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
		{
		    entity->checkpointActif = 0;
		    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;
	}
}




    Et au début du jeu, quand on démarre une nouvelle partie, dans la fonction updateStartMenu(void) du fichier menu.c :


 Fichier menu.c

void updateStartMenu(void)
{
    //Si on appuie sur BAS
    if(input.down == 1)
    {
        //Si choice = O (il est sur start), on le met à 1 (quit)
        if(jeu.choice == 0)
            jeu.choice++;

        input.down = 0;
    }

    //Si on appuie sur HAUT
    if(input.up == 1)
    {
        //Si choice = 1 (il est sur Quit), on le met à 0 (Start)
        if(jeu.choice == 1)
            jeu.choice--;

        input.up = 0;
    }

    //Si on appuie sur Enter et qu'on est sur Start, on recharge le jeu et on quitte l'état menu
    if(input.enter)
    {
         if(jeu.choice == 0)
        {
			
	    player.checkpointActif = 0;
            jeu.level = 1;
            initializePlayer();
            changeLevel();
            /* On réinitialise les variables du jeu */
            jeu.vies = 3;
            jeu.etoiles = 0;
            jeu.onMenu = 0;
        }
        //Sinon, on quitte le jeu
        else if(jeu.choice == 1)
        {
            exit(0);
        }
        input.enter = 0;
    }


}




    Et on met à jour l'en-tête pour qu'il reconnaisse notre structure player :

Fichier menu.h

#include "structs.h"

/* Structures globales */
extern Gestion jeu;
extern TTF_Font *font;
extern Input input;
extern GameObject player;

extern void drawString(char *text, int x, int y, TTF_Font *font);
extern void changeLevel(void);
extern void initializePlayer(void);




    Eh, voilà, si on compile et qu'on teste, maintenant ça marche correctement !
    Alors, il n'était pas si dur que ça ce TP, non ?

     A bientôt pour la suite ! 
 




Télécharger le projet complet !



 

Connexion

CoalaWeb Traffic

Today66
Yesterday238
This week989
This month467
Total1745366

3/05/24