Big Tuto SDL 2 : Rabidja v. 3.0
Chapitre 17 : Ajoutons des niveaux et des checkpoints !
Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Ecriture : 5 novembre 2014
Dernière mise à jour : 11 novembre 2015
Prologue
Bon, voilà, on peut maintenant aller au bout de la 1ère map, grâce à nos plateformes flottantes, mais on ne peut pas passer au niveau 2 ! 
Il va donc nous falloir gérer cela ! 
Pour ce faire, on va utiliser une technique simple mais efficace : on gèrera le passage au niveau supérieur quand notre héros touchera le bord droit de la map.
Et si vous voulez faire un niveau à étages (ce qui est possible
), il vous suffira de mettre des murs sur tout le bord droit de l'écran sauf au niveau de la sortie (pour laquelle vous pouvez dessiner une porte, par exemple !
), et le tour est joué !
Mais bon, comme c'est assez facile à faire et que ça ne suffirait pas à vous occuper tout un chapitre, on va aussi en profiter pour rajouter des checkpoints ! 
Mais, si, vous savez ce que c'est, ce sont ces points de passage qui sauvegardent votre position quand vous mourez et qui vous évitent d'avoir à vous retaper tout le niveau !
Pour cela, on va utiliser 2 tiles de notre tileset : la tile checkpoint de base (un poteau) que nous changerons par la tile checkpoint validé (poteau + drapeau) quand notre héros la touchera.
On enregistrera à ce moment-là aussi les coordonnées du joueur pour le ressusciter au bon endroit. 
Et sinon, dans le level editor, il vous faudra utiliser la tile checkpoint de base, sinon, ça ne fonctionnera pas avec l'autre, comme nous allons le voir dans le code. 
Allez, let's go ! 
Le passage de niveaux
On commence donc par la gestion de nos multi-niveaux. ![]()
Pour cela, on va retourner dans notre fichier defs.h pour créer une nouvelle define LEVEL_MAX, qui contiendra le nombre de niveaux de votre jeu.
Attention, ce nombre doit être exactement égal au nombre de fichiers maps de votre projet !
S'il y en a moins, le jeu plantera car il ne pourra pas lire le fichier. ![]()
Fichier : defs.h : Rajouter :
|
//Nombre max de levels
#define LEVEL_MAX 2
|
On n'a que 2 niveaux pour l'instant, mais libre à vous d'en créer d'autres ! ![]()
Ensuite, pour gérer le passage au niveau sup' en touchant le bord droit de l'écran, il va nous falloir retourner dans notre fonction mapCollision() du fichier map.c. Là, au lieu d'empêcher le joueur de sortir de l'écran, on va maintenant le faire changer de niveau en chargeant la nouvelle map et en réinitialisant le joueur et le checkpoint (que nous allons ajouter juste après
).
Voilà le code en question, et vous remarquerez qu'il est plutôt simple. ![]()
Ne vous embêtez pas à essayer de trouver où changer ce bout de code dans la fonction pour l'instant, je vous redonnerai la fonction complète ci-dessous. ![]()
Fichier : map.c : Modifier mapCollision() vers la fin de la fonction :
|
else if (entity->x + entity->w >= map.maxX)
{
//Si on touche le bord droit de l'écran, on passe au niveau sup
SetValeurDuNiveau(getLevel() + 1);
//Si on dépasse le niveau max, on annule et on limite le déplacement du joueur
if (getLevel() > LEVEL_MAX)
{
SetValeurDuNiveau(LEVEL_MAX);
entity->x = map.maxX - entity->w - 1;
}
//Sinon, on passe au niveau sup, on charge la nouvelle map et on réinitialise le joueur
else
{
//On désactive le checkpoint
entity->checkpointActif = 0;
changeLevel();
initializePlayer(1);
}
}
|
Eh voilà, c'est tout pour le passage de niveaux ! ![]()
Comme je vous le disais, il n'y a pas de quoi en faire tout un chapitre ! ![]()
La gestion des checkpoints
Pour nos checkpoints, il va aussi nous falloir modifier la même fonction.
En effet, c'est aussi ici que l'on va gérer les collisions avec notre tile checkpoint, la modifier en passant à la tile suivante (+1 - il s'agit de la tile avec le drapeau levé
), et enregistrer les coordonnées de notre joueur dans respawnX et respawnY.
Je vous donne un aperçu du code ci-dessous. Ne le copiez pas pour l'instant. Je vous redonne la fonction mapCollision() complète juste après. ![]()
Fichier : map.c : code pour gérer la collision avec les checkpoints dans mapCollision() (à ne pas copier - pour info):
|
//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;
}
|
Et voilà donc la fonction complète, ce sera plus simple pour faire un copier/coller
:
Fichier : map.c : code complet de mapCollision() - à copier pour remplacer le précédent :
|
void mapCollision(GameObject *entity)
{
int i, x1, x2, y1, y2;
/* D'abord, on considère 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 descendre 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 des tiles Power-up : Etoile et vie
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 des tiles Power-up)
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 des pics */
if ((map.tile[y2][x1] == TILE_PIKES) || (map.tile[y2][x2] == TILE_PIKES))
{
//On joue le son
playSoundFx(DESTROY);
//On fait sauter le joueur
entity->dirY = -JUMP_HEIGHT;
if (entity->life > 1)
{
//Si le timer d'invincibilité est à 0
//on perd un coeur
if (entity->invincibleTimer == 0)
{
entity->life--;
entity->invincibleTimer = 80;
}
}
else
{
//On met le timer à 1 pour tuer le joueur intantanément
entity->timerMort = 1;
//On joue le son
playSoundFx(DESTROY);
}
}
/* Gestion du ressort */
else 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);
}
//Gestion des plateformes traversables : elles se situent juste avant
//les tiles bloquantes dans notre tileset (dont la valeur butoire est
//BLANK_TILE). Il suffit donc d'utiliser le numéro de la première tile
//traversable au lieu de BLANK_TILE pour bloquer le joueur,
//seulement quand il tombe dessus (sinon, il passe au-travers
//et le test n'est donc pas effectué dans les autres directions
else if (map.tile[y2][x1] > TILE_TRAVERSABLE || map.tile[y2][x2] > TILE_TRAVERSABLE)
{
//Si la tile est une plateforme ou une tile 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;
}
// Test de collision avec la plateforme mobile
if (getPlateformeNumber > 0)
checkCollisionsWithPlateforms(entity);
}
else if (entity->dirY < 0)
{
/* Déplacement vers le haut */
//Test des tiles Power-up
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.
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
SetValeurDuNiveau(getLevel() + 1);
//Si on dépasse le niveau max, on annule et on limite le déplacement du joueur
if (getLevel() > LEVEL_MAX)
{
SetValeurDuNiveau(LEVEL_MAX);
entity->x = map.maxX - entity->w - 1;
}
//Sinon, on passe au niveau sup, on charge la nouvelle map et on réinitialise le joueur
else
{
//On désactive le checkpoint
entity->checkpointActif = 0;
changeLevel();
initializePlayer(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;
}
}
|
Cela commence à devenir une grosse fonction à présent et vous pouvez la découper en sous-fonctions, si vous préférez (surtout si vous souhaitez rajouter encore des choses
). Cela dit, j'ai choisi de ne pas le faire pour l'instant, de façon à vous donner une vision globale de ce que gère le jeu à chaque tour de boucle. ![]()
Voilà, maintenant, que c'est fait, allons dans le fichier player.c et rajoutons une nouvelle fonction pour réinitialiser notre checkpoint.
Cette fonction ne nous servira pas encore tout de suite, mais dès le chapitre prochain quand nous rajouterons des menus. ![]()
Fichier : player.c : Rajouter la fonction :
|
void resetCheckpoint(void)
{
player.checkpointActif = 0;
}
|
Il ne nous reste plus qu'à gérer le respawn de notre héros suivant qu'il a pris un checkpoint ou pas ! ![]()
Pour cela, on retourne dans la fonction initializePlayer() et on rétablit les coordonnées de départ de notre joueur soit au début s'il n'y a pas de checkpoint actif, soit aux coordonnées de respawn s'il y en a une. C'est aussi simple que ça ! ![]()
Je vous redonne la fonction complète ci-dessous :
Fichier : player.c : Remplacer la fonction précédente par :
|
void initializePlayer(int newLevel)
{
//PV à 3
player.life = 3;
//Timer d'invincibilité à 0
player.invincibleTimer = 0;
//Nombre de plateformes flottantes à 0
resetPlateformes();
//Indique l'état et la direction de notre héros
player.direction = RIGHT;
player.etat = IDLE;
//Indique le numéro de la frame où commencer
player.frameNumber = 0;
//...la valeur de son chrono ou timer
player.frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
//... et son nombre de frames max (8 pour l'anim' IDLE
// = ne fait rien)
player.frameMax = 8;
/* 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 = getBeginX();
player.y = getBeginY();
}
//On réinitiliase les coordonnées de la caméra
//si on change de niveau
if (newLevel == 1)
{
setStartX(getBeginX());
setStartY(getBeginY());
}
/* 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;
//Réinitialise les monstres
/* Libère le sprite des monstres */
resetMonsters();
}
|
Et c'est fini ! ![]()
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 void checkCollisionsWithPlateforms(GameObject *entity);
extern int checkFall(GameObject monster);
extern void cleanHUD(void);
extern void cleanMaps(void);
extern void cleanMonsters(void);
extern void cleanPlateforme(void);
extern void cleanPlayer(void);
extern void cleanup(void);
extern void cleanUpMusic(void);
extern void closeFont(void);
extern void closeJoystick(void);
extern int collide(GameObject *player, GameObject *monster);
extern void delay(unsigned int frameLimit);
extern void doPlateforme(void);
extern void drawGame(void);
extern void drawHud(void);
extern void drawImage(SDL_Texture *, int, int);
extern void drawMap(int);
extern void drawMonster(GameObject *entity);
extern void drawPlateforme(void);
extern void drawPlayer(void);
extern void drawString(char *text, int x, int y, int r, int g, int b, int a);
extern void drawTile(SDL_Texture *image, int destx, int desty, int srcx, int srcy);
extern void freeSound(void);
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 getItem(int itemNumber);
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 int getNombreDetoiles(void);
extern int getNombreDeVies(void);
extern int getPlateformeNumber(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 SDL_Texture *getTileSetA(void);
extern SDL_Texture *getTileSetB(void);
extern int getTileValue(int y, int x);
extern void init(char *);
extern void initHUD(void);
extern void initMaps(void);
extern void initializeNewMonster(int x, int y);
extern void initMonsterSprites(void);
extern void initPlateforme(int x, int y, int type);
extern void initPlayerSprites(void);
extern void initializePlayer(int newLevel);
extern void killPlayer(void);
extern void loadFont(char *, int);
extern void loadGame(void);
extern SDL_Texture *loadImage(char *name);
extern void loadMap(char *name);
extern void loadPlateforme(void);
extern void loadSong(char filename[200]);
extern void loadSound(void);
extern void mapCollision(GameObject *entity);
extern void monsterCollisionToMap(GameObject *entity);
extern void openJoystick(void);
extern void playerHurts(GameObject *monster);
extern void playSoundFx(int type);
extern void resetCheckpoint(void);
extern void resetMonsters(void);
extern void resetPlateformes(void);
extern void setNombreDeVies(int valeur);
extern void setNombreDetoiles(int valeur);
extern void setPlayerx(int valeur);
extern void setPlayery(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à, on compile et on lance le programme !
Maintenant, vous allez enfin pouvoir parcourir tous les niveaux du jeu ! ![]()
Si c'est pas chouette, ça ! ![]()
Bon, il ne nous reste maintenant plus grand chose à ajouter pour arriver au même niveau que celui du chapitre 35 du Big Tuto SDL 2!![]()
Concrètement, il nous manque juste la création de menus, l'ajout des shurikens lancés par notre lapin-ninja et les explosions de monstres !

@ bientôt pour le chapitre 18 ! ![]()
Jay

English
Français 
