French Chinese (Simplified) Dutch English German Greek Italian Japanese Korean Spanish

     

 



 

 

Big Tuto SFML 2 : Rabidja v. 3.0

Chapitre 9 : Collisions avec la map

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 17 février 2015
Date de révision : 20 mars 2016

      Prologue

   On a maintenant un héros flottant dans les airs ! cheeky

   C'est cool, mais c'est pas superman, non plus !... indecision Nous, c'est : super lapin ninja, alias Rabidja ! Et comme tout bon lapin, il doit bondir partout ! angel

   Mais pour bondir, il faut aussi qu'on gère les collisions avec la map ! wink

   Alors, qu'est-ce qu'on attend !?! Au boulot ! laugh

 

      Le code

   Nous allons d'abord revenir rapidement sur les constantes dont nous allons avoir besoin par la suite pour gérer nos tiles (cf. chapitre 6).

   Pour rappel, elles se trouvent déjà dans le header player.h (ne les recopiez donc pas une seconde fois ! indecision) :

Fichier : player.h : Ne rien copier, elles y sont déjà !

 
/*************************/
/* VALEURS DES TILES */
/************************/
 
// Constante définissant le seuil entre les tiles traversables
// (blank) et les tiles solides
const int BLANK_TILE = 99;
 
//Plateformes traversables
const int TILE_TRAVERSABLE = 80;
 
//Tiles Power-ups
const int TILE_POWER_UP_DEBUT = 77;
const int TILE_POWER_UP_FIN = 79;
const int TILE_POWER_UP_COEUR = 78;
 
//Autres Tiles spéciales
const int TILE_RESSORT = 125;
const int TILE_CHECKPOINT = 23;
const int TILE_MONSTRE = 136;
const int TILE_PIKES = 127;
 
//Tiles plateformes mobiles
const int TILE_PLATEFORME_DEBUT = 130;
const int TILE_PLATEFORME_FIN = 131;
 
 
// Tiles pentes à 26.5° ; BenH = de BAS en HAUT ; HenB = De HAUT en BAS
const int TILE_PENTE_26_BenH_1 = 69;
const int TILE_PENTE_26_BenH_2 = 70;
const int TILE_PENTE_26_HenB_1 = 71;
const int TILE_PENTE_26_HenB_2 = 72;

 

   Ces valeurs correspondent tout simplement au numéro de la ou les tile(s) correspondantes. Ainsi, nos tiles sont traversables jusqu'à la tile 99, sauf les tiles 80 à 99 qui sont des plateformes (et donc elles ne seront pas traversables par le haut wink ). Les autres tiles seront toutes solides (le joueur se cognera dedans cheeky).

   Sauf les tiles spéciales : 

- pour les tiles power-ups (étoiles, coeurs, vies), on les fera disparaître quand le joueur les touchera et on lui attribuera le power-up correspondant.

- pour la tile ressort, elle fera rebondir le joueur quand il tombera dessus (sinon, elle sera solide sur les autres côtés),

- au contraire, la tile pics blessera le joueur,

- pour la tile checkpoint, elle changera quand le joueur la touchera (on aura un petit drapeau qui se lèvera) et on enregistrera le checkpoint,

- pour la tile monstre, on la remplacera par un sprite de monstres à l'affichage,

- idem pour les tiles de plateformes mobiles.

- et enfin, les tiles pentes seront gérées dès le chapitre prochain grâce à la contribution de Stephantasy. wink

   Bien entendu, ces tiles spéciales ne seront pas actives dès ce chapitre et s'afficheront donc telles quelles (on pourra parfois se cogner dedans, comme pour les tiles monstres). On reverra leur fonctionnement plus en détails dans des chapitres ultérieurs qui leur seront dédiés. cool

   Retournons maintenant dans notre fichier player.cpp et complétons notre fonction update() :

Fichier : player.cpp : Remplacez la fonction précédente par :

void Player::update(Input &input, Map &map)
{
//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 (timerMort == 0)
{
//On gère le timer de l'invincibilité
if (invincibleTimer > 0)
invincibleTimer--;
 
//On réinitialise notre vecteur de déplacement latéral (X), pour éviter que le perso
//ne fonce de plus en plus vite pour atteindre la vitesse de la lumière ! ;)
//Essayez de le désactiver pour voir !
dirX = 0;
 
// La gravité fait toujours tomber le perso : on incrémente donc le vecteur Y
dirY += GRAVITY_SPEED;
 
//Mais on le limite pour ne pas que le joueur se mette à tomber trop vite quand même
if (dirY >= MAX_FALL_SPEED)
dirY = MAX_FALL_SPEED;
 
 
//Voilà, au lieu de changer directement les coordonnées du joueur, on passe par un vecteur
//qui sera utilisé par la fonction mapCollision(), qui regardera si on peut ou pas déplacer
//le joueur selon ce vecteur et changera les coordonnées du player en fonction.
if (input.getButton().left == true)
{
dirX -= PLAYER_SPEED;
//Et on indique qu'il va à gauche (pour le flip
//de l'affichage, rappelez-vous).
direction = LEFT;
 
//Si ce n'était pas son état auparavant et qu'il est bien sur
//le sol (car l'anim' sera différente s'il est en l'air)
if (etat != WALK && onGround == true)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
etat = WALK;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
//Si on détecte un appui sur la touche fléchée droite
else if (input.getButton().right == true)
{
//On augmente les coordonnées en x du joueur
dirX += PLAYER_SPEED;
//Et on indique qu'il va à droite (pour le flip
//de l'affichage, rappelez-vous).
direction = RIGHT;
 
//Si ce n'était pas son état auparavant et qu'il est bien sur
//le sol (car l'anim' sera différente s'il est en l'air)
if (etat != WALK && onGround == true)
{
//On enregistre l'anim' de la marche et on l'initialise à 0
etat = WALK;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
//Si on n'appuie sur rien et qu'on est sur le sol, on charge l'animation marquant l'inactivité (Idle)
else if (input.getButton().right == false && input.getButton().left == false && onGround == true)
{
//On teste si le joueur n'était pas déjà inactif, pour ne pas recharger l'animation
//à chaque tour de boucle
if (etat != IDLE)
{
//On enregistre l'anim' de l'inactivité et on l'initialise à 0
etat = IDLE;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
}
}
 
 
//Et voici la fonction de saut très simple :
//Si on appuie sur la touche saut et qu'on est sur le sol, alors on attribue une valeur
//négative au vecteur Y
//parce que sauter veut dire se rapprocher du haut de l'écran et donc de y=0.
if (input.getButton().jump == true)
{
if (onGround == true)
{
dirY = -JUMP_HEIGHT;
onGround = false;
Playerjump = true;
}
// Si on est en saut 1, on peut faire un deuxième bond et on remet jump1 à 0
else if (Playerjump == true)
{
dirY = -JUMP_HEIGHT;
Playerjump = false;
}
input.setButton(jump, false);
}
 
 
/* Réactive la possibilité de double saut si on tombe sans sauter */
if (onGround == true)
Playerjump = true;
 
 
//On gère l'anim du saut
if (onGround == false)
{
//Si on est en saut 1, on met l'anim' du saut normal
if (Playerjump == true)
{
if (etat != JUMP1)
{
etat = JUMP1;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 2;
}
}
else
{
if (etat != JUMP2)
{
etat = JUMP2;
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 4;
}
}
}
 
//On rajoute notre fonction de détection des collisions qui va mettre à
//jour les coordonnées de notre super lapin.
mapCollision(map);
 
//On gère le scrolling (fonction ci-dessous)
centerScrolling(map);
 
}
 
//Gestion de la mort quand le héros tombe dans un trou :
//Si timerMort est différent de 0, c'est qu'il faut réinitialiser le joueur.
//On ignore alors ce qui précède et on joue cette boucle (un wait en fait) jusqu'à ce que
// timerMort == 1. A ce moment-là, on le décrémente encore -> il vaut 0 et on réinitialise
//le jeu avec notre bonne vieille fonction d'initialisation ;) !
if (timerMort > 0)
{
timerMort--;
 
if (timerMort == 0)
{
//On perd une vie
vies--;
 
// Si on est mort, on réinitialise le niveau
map.changeLevel();
initialize(map, false);
}
}
}

   Le plus simple pour vous sera sans doute de remplacer votre fonction existante, après avoir regardé les morceaux de code qui changent. wink

   Et qu'est-ce qui change justement ? cheeky

   J'ai largement commenté la fonction pour vous aider à vous y repérer, mais globalement, on ajoute d'abord notre timerMort qui permettra de savoir si notre héros est vivant ou non. Ainsi, si on le met à 1, on déclare notre héros mort (ça marchera avec les monstres aussi par la suite wink) et il nous sert alors aussi à faire une boucle de temporisation, avant de le ressusciter (sinon, ça aurait été un peu déstabilisant de le faire revivre dans la seconde, avant même que le joueur ait pigé pourquoi il était mort ! laugh).

   Ensuite, on rajoute notre saut et notre double saut : le système fonctionne ainsi : on applique un vecteur pesanteur à notre héros (GRAVITY_SPEED sur dirY) qui le fait tomber jusqu'à sa vitesse max (MAX_FALL_SPEED). Ensuite, dans notre fonction mapCollision() que nous étudierons après et qui gèrera les collisions avec la map, on testera si notre héros est sur (ou dans) une tile solide, et on le calera à ce moment-là contre elle (parce que coincé dedans, ça serait pas top... cheeky). Notre héros sera alors onGround (sur le sol).

   Dès lors, si on appuie sur la touche SAUT, on enlève -JUMP_HEIGHT à son dirY pour le faire monter vers le haut de l'écran, et donc sauter, et on change la valeur de son anim'. Même chose, la fonction mapCollision() vérifiera que le perso ne rentre pas dans le plafond en haut et le stoppera avant ! laugh

   Pour le double saut, on ne peut plus utiliser notre fonction onGround, on va donc utiliser une nouvelle variable Playerjump, de la même façon. Sans elle, on pourrait double sauter à volonté (vous pouvez tester wink).

   Voilà globalement ce qui a changé. Je vous laisse suivre les commentaires pour le reste. wink Reprenons maintenant notre fonction centerScrolling() :

Fichier : player.cpp : Remplacez la fonction précédente par :

void Player::centerScrolling(Map &map)
{
// Nouveau scrolling à sous-boîte limite :
//Pour éviter les effets de saccades dus à une caméra qui se
//centre automatiquement et constamment sur le joueur (ce qui
//peut en rendre malade certains...), on crée une "boîte" imaginaire
//autour du joueur. Quand on dépasse un de ses bords (en haut, en bas,
//à gauche ou à droite), on scrolle.
//Mais là encore, au lieu de centrer sur le joueur, on déplace simplement
//la caméra jusqu'à arriver au joueur. On a changé ici la valeur à 4 pixels
//pour que le jeu soit plus rapide.
int cxperso = x + w / 2;
int cyperso = y + h / 2;
int xlimmin = map.getStartX() + LIMITE_X;
int xlimmax = xlimmin + LIMITE_W;
int ylimmin = map.getStartY() + LIMITE_Y;
int ylimmax = ylimmin + LIMITE_H;
 
//Effet de retour en arrière quand on est mort :
//Si on est très loin de la caméra, plus loin que le bord
//de la map, on accélère le scrolling :
if (cxperso < map.getStartX())
{
map.setStartX(map.getStartX() - 30);
}
 
//Si on dépasse par la gauche, on recule la caméra
else if (cxperso < xlimmin)
{
map.setStartX(map.getStartX() - 4);
}
 
//Effet de retour en avant quand on est mort (au
//cas où le joueur s'amuse à faire le niveau à rebours
//après une checkpoint) :
//Si on est très loin de la caméra, plus loin que le bord
//de la map, on accélère le scrolling :
if (cxperso > map.getStartX() + SCREEN_WIDTH)
{
map.setStartX(map.getStartX() + 30);
}
 
//Si on dépasse par la droite, on avance la caméra
else if (cxperso > xlimmax)
{
map.setStartX(map.getStartX() + 4);
}
 
//Si on arrive au bout de la map à gauche, on stoppe le scrolling
if (map.getStartX() < 0)
{
map.setStartX(0);
}
 
//Si on arrive au bout de la map à droite, on stoppe le scrolling à la
//valeur Max de la map - la moitié d'un écran (pour ne pas afficher du noir).
else if (map.getStartX() + SCREEN_WIDTH >= map.getMaxX())
{
map.setStartX(map.getMaxX() - SCREEN_WIDTH);
}
 
//Si on dépasse par le haut, on remonte la caméra
if (cyperso < ylimmin)
{
map.setStartY(map.getStartY() - 4);
}
 
//Si on dépasse par le bas, on descend la caméra
if (cyperso > ylimmax)
{
//Sauf si on tombe très vite, auquel cas, on accélère la caméra :
if (dirY >= MAX_FALL_SPEED - 2)
{
map.setStartY(map.getStartY() + MAX_FALL_SPEED + 1);
}
else
{
map.setStartY(map.getStartY() + 4);
}
}
 
//Si on arrive au bout de la map en haut, on stoppe le scrolling
if (map.getStartY() < 0)
{
map.setStartY(0);
}
 
//Si on arrive au bout de la map en bas, on stoppe le scrolling à la
//valeur Max de la map - la moitié d'un écran (pour ne pas afficher du noir).
else if (map.getStartY() + SCREEN_HEIGHT >= map.getMaxY())
{
map.setStartY(map.getMaxY() - SCREEN_HEIGHT);
}
 
}

   Dans cette fonction, j'ai changé 2 choses :

1. La vitesse de la caméra que j'ai portée à 4 pixels / frame, pour avoir un caméraman un peu plus nerveux, dopé à la caféine ! laugh

2. Comme notre héros va maintenant pouvoir se tuer en tombant dans les trous (comme un... indecision), j'ai créé un petit effet de retour en arrière sympa avec la caméra. Pour cela, on détecte si la caméra est à plus d'un écran de distance du héros. Si c'est le cas, c'est certainement parce qu'il est mort et qu'on vient de le ressusciter à l'autre bout de la map. On accélère alors la caméra à 30 pixels / frame pour qu'elle fasse un retour rapide sur notre héros. Mais attention, comme plus tard, on pourra sauvegarder à un checkpoint et revenir se tuer en arrière, il faut que la caméra gère les deux sens ! wink

   Il est maintenant temps de mettre à jour notre fonction initialize() :

Fichier : player.cpp : Remplacez la fonction précédente par :

void Player::initialize(Map &map, bool newLevel)
{
//PV à 3
life = 3;
 
//Timer d'invincibilité à 0
invincibleTimer = 0;
 
//Indique l'état et la direction de notre héros
direction = RIGHT;
etat = IDLE;
 
//Indique le numéro de la frame où commencer
frameNumber = 0;
 
//...la valeur de son chrono ou timer
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
 
//... et son nombre de frames max (8 pour l'anim' IDLE
// = ne fait rien)
frameMax = 8;
 
/* Coordonnées de démarrage/respawn de notre héros */
if (checkpointActif == true)
{
x = respawnX;
y = respawnY;
}
else
{
x = map.getBeginX();
y = map.getBeginY();
}
 
//On réinitiliase les coordonnées de la caméra
//si on change de niveau
if (newLevel == true)
{
map.setStartX(map.getBeginX());
map.setStartY(map.getBeginY());
}
 
/* Hauteur et largeur de notre héros */
w = PLAYER_WIDTH;
h = PLAYER_HEIGTH;
 
//Variables nécessaires au fonctionnement de la gestion des collisions
timerMort = 0;
onGround = false;
}

   Vous vous êtiez peut-être demandé à quoi servait notre argument newLevel, jusqu'à présent, puisqu'on ne s'en servait pas ! wink Eh bien, c'est simple, afin que notre petit effet de caméra ne se produise que quand on meurt et pas quand on commence un niveau, il nous faut modifier la fonction, pour qu'elle considére si c'est un nouveau niveau (newLevel) ou pas ! Ainsi, si on passe newLevel à true, on commence un nouveau niveau et on réinitialise la caméra, sinon non (cf. code : if (newLevel == true)). wink

   Passons maintenant à notre fameuse fonction mapCollision().

   Comme cette fonction est longue et complexe (mais aussi assez répétitive cheeky), j'ai préféré la commenter directement dans le code. C'est sans conteste, l'une des fonctions les plus complexes que nous ayions vues jusqu'ici (avec la fonction qui charge la map, quand même ! laugh). Ne vous inquiétez donc pas si vous ne comprenez pas tout du premier coup, nous y reviendrons par la suite. wink

   Pour faire simple, elle décortique notre sprite en blocs correspondant chacun à une tile pour voir quelles tiles le sprite recouvre. Si celles-ci sont traversables (blank), c'est OK, sinon, elle stoppe le sprite en le collant contre les tiles solides.  

   Je vous laisse lire les commentaires :

Fichier : player.cpp : Ajoutez la fonction :

void Player::mapCollision(Map &map)
{
 
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 */
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 (h > TILE_SIZE)
i = TILE_SIZE;
else
i = 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 = (x + dirX) / TILE_SIZE;
x2 = (x + dirX + 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 = (y) / TILE_SIZE;
y2 = (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 (dirX > 0)
{
//On vérifie si les tiles recouvertes sont solides
if (map.getTile(y1, x2) > BLANK_TILE || map.getTile(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).
 
x = x2 * TILE_SIZE;
x -= w + 1;
dirX = 0;
}
}
 
//Même chose à gauche
else if (dirX < 0)
{
if (map.getTile(y1, x1) > BLANK_TILE || map.getTile(y2, x1) > BLANK_TILE)
{
x = (x1 + 1) * TILE_SIZE;
dirX = 0;
}
}
 
}
 
//On sort de la boucle si on a testé toutes les tiles le long de la hauteur du sprite.
if (i == h)
{
break;
}
 
//Sinon, on teste les tiles supérieures en se limitant à la heuteur du sprite.
i += TILE_SIZE;
 
if (i > h)
{
i = h;
}
}
 
 
//On recommence la même chose avec le mouvement vertical (axe des Y)
if (w > TILE_SIZE)
i = TILE_SIZE;
else
i = w;
 
 
for (;;)
{
x1 = (x) / TILE_SIZE;
x2 = (x + i) / TILE_SIZE;
 
y1 = (y + dirY) / TILE_SIZE;
y2 = (y + dirY + h) / TILE_SIZE;
 
if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)
{
if (dirY > 0)
{
// Déplacement en bas
//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
if (map.getTile(y2, x1) > TILE_TRAVERSABLE || map.getTile(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).
y = y2 * TILE_SIZE;
y -= h;
dirY = 0;
onGround = 1;
}
}
 
else if (dirY < 0)
{
// Déplacement vers le haut
if (map.getTile(y1, x1) > BLANK_TILE || map.getTile(y1, x2) > BLANK_TILE)
{
y = (y1 + 1) * TILE_SIZE;
dirY = 0;
}
}
}
 
//On teste la largeur du sprite (même technique que pour la hauteur précédemment)
if (i == w)
{
break;
}
 
i += TILE_SIZE;
 
if (i > w)
{
i = w;
}
}
 
/* Maintenant, on applique les vecteurs de mouvement si le sprite n'est pas bloqué */
x += dirX;
y += dirY;
 
//Et on contraint son déplacement aux limites de l'écran.
if (x < 0)
{
x = 0;
}
 
else if (x + w >= map.getMaxX())
{
//Si on touche le bord droit de l'écran, on passe au niveau sup
map.setLevel(map.getLevel() + 1);
 
//Si on dépasse le niveau max, on annule et on limite le déplacement du joueur
if (map.getLevel() > LEVEL_MAX)
{
map.setLevel(LEVEL_MAX);
x = map.getMaxX() - 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
checkpointActif = 0;
 
map.changeLevel();
initialize(map, true);
}
}
 
//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 (y > map.getMaxY())
{
timerMort = 60;
}
}

   Ouf ! C'était une bonne grosse fonction ! cheeky

   Ne vous inquiétez pas, si vous n'êtes pas encore trop à l'aise avec, car nous allons souvent revenir dessus pour implémenter nos nouvelles tiles spéciales, au fur et à mesure. En effet, pour l'instant, elle ne gère que les tiles traversables, solides et plateformes (qu'on ne teste en fait qu'une fois : dans la direction vers le bas wink).

   Voilà, et on n'oublie pas de rajouter le prototype de notre nouvelle fonction dans le header :

Fichier : player.h : Rajouter :

//Fonctions
void initialize(Map &map, bool newLevel);
void draw(Map &map, sf::RenderWindow &window);
void update(Input &input, Map &map);
void centerScrolling(Map &map);
void mapCollision(Map &map);

    Voilà ! Plus qu'à compiler et à lancer le programme ! wink

   Notre héros peut maintenant se balader dans la map et sauter comme un cabri ! angel Cool ! cool

 

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

                                                                            Jay 

 

 

 

Commentaires   

0 #11 janpin 31-01-2018 17:23
Ou du chapitre 9 , je cherche...

Connectez-vous ou inscrivez-vous pour pouvoir poster.