Big Tuto SFML 2 / Action-RPG : Legends of Meruvia

Chapitre 14 : Gérons les collisions avec les monstres !

 

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 26 juin 2016
Date de révision : -

 

      Prologue

 

   Notre héros est opérationnel, tout comme nos monstres. wink

   Et il peut maintenant dégainer son épée pour pourfendre ses ennemis !... devil

   Manque plus que les collisions ! cheeky Alors, on est parti ! 

 

 

 

      Le code

 

   Comme je l'avais indiqué précédemment, pour tester nos collisions, nous allons dissocier l'épée du sprite du héros. Ainsi si l'épée touche le monstre, c'est lui qui morflle, tandis que si le sprite du héros touche le monstre, les deux vont morfler ! cheeky

   Si vous voulez plus de détails théoriques sur cette technique, je vous invite à lire cet autre tuto.

   Bien, commençons donc par rajouter une fonction qui permettra de gérer les collisions entre le joueur et un monstre.

   Pour cela, on retourne dans le fichier monster.cpp :

 

Fichier : monster.cpp : Ajouter la nouvelle fonction :

bool Monster::collide(Player &player)
{
//Fonction de gestion des collisions
//On teste pour voir s'il n'y a pas collision, si c'est le cas, on renvoie false
if ((player.getX() >= x + w)
|| (player.getX() + player.getW() <= x)
|| (player.getY() >= y + h)
|| (player.getY() + player.getH() <= y)
)
return false;
//Sinon, on renvoie true pour blesser le joueur
else
return true;
} 

 

   Comme vous le voyez, cette fonction est très simple et ressemble beaucoup à celle que nous avions utilisée pour tester les collisions entre les monstres.

   Bien, on va maintenant mettre à jour la fonction update() de nos monstres, en appelant cette fonction ainsi que getCollisionWithSword() que nous rajouterons dans la class Player pour tester les collisions avec l'épée. wink

 

Fichier : monster.cpp  : Mettre à jour la fonction :

int Monster::update(int monsterNumber, Map &map, Player &player, Monster monster[])
{
//Même fonctionnement que pour le joueur
if (timerMort == 0)
{
//On diminue le timer d'invincibilité et de recul
if (invincibleTimer > 0)
invincibleTimer--;
 
if (isHurt > 0)
isHurt--;
 
dirX = 0;
dirY = 0;
 
//Test de collision dans un mur : si la variable x ou y reste la même, deux tours de boucle
//durant, le monstre est bloqué et on lui fait faire demi-tour.
if (x == saveX && isHurt == 0)
{
if (direction == LEFT)
{
direction = RIGHT;
 
//Sécurité : système d'éjection du monstre pour
//l'évacuer s'il reste coincé dans un obstacle
if (y == saveY && direction == UP)
y += 1;
else if (y == saveY && direction == DOWN)
y -= 1;
}
 
else if (direction == RIGHT)
{
direction = LEFT;
 
//Sécurité : système d'éjection du monstre pour
//l'évacuer s'il reste coincé dans un obstacle
if (y == saveY && direction == UP)
y += 1;
else if (y == saveY && direction == DOWN)
y -= 1;
}
 
}
 
else if (y == saveY)
{
if (direction == UP)
{
direction = DOWN;
 
//Sécurité : système d'éjection du monstre pour
//l'évacuer s'il reste coincé dans un obstacle
if (x == saveX && direction == LEFT)
x += 1;
else if (x == saveX && direction == RIGHT)
x -= 1;
}
 
else if (direction == DOWN)
{
direction = UP;
 
//Sécurité : système d'éjection du monstre pour
//l'évacuer s'il reste coincé dans un obstacle
if (x == saveX && direction == LEFT)
x += 1;
else if (x == saveX && direction == RIGHT)
x -= 1;
}
 
}
 
//Si le timer de l'IA descend en-dessous de 0, il est temps
//de changer de direction au hasard.
if (IATimer <= 0 && isHurt == 0)
{
direction = rand() % 4;
IATimer = IATime;
}
else
IATimer--;
 
//Déplacement du monstre selon la direction
//s'il est touché
if (isHurt > 0)
{
if (hurtDirection == LEFT)
dirX -= 10;
else if (hurtDirection == RIGHT)
dirX += 10;
else if (hurtDirection == UP)
dirY -= 10;
else if (hurtDirection == DOWN)
dirY += 10;
}
else
{
if (direction == LEFT)
dirX -= 1;
else if (direction == RIGHT)
dirX += 1;
else if (direction == UP)
dirY -= 1;
else if (direction == DOWN)
dirY += 1;
}
 
 
 
//On sauvegarde les coordonnées du monstre pour gérer le demi-tour
//avant que mapCollision() ne les modifie.
saveX = x;
saveY = y;
 
//On détecte les collisions avec la map comme pour le joueur
mapCollision(map, player);
 
}
 
//On détecte les collisions avec le joueur
//Si c'est égal à 1, on diminue ses PV
if (collide(player))
{
if (player.getLife() > 1)
{
//On blesse le joueur
player.playerHurts();
 
//Et on blesse le monstre s'il n'est pas invincible
if (invincibleTimer == 0)
{
life--;
if (life <= 0)
timerMort = 1;
invincibleTimer = TIME_BETWEEN_2_SHOTS;
}
 
//On le déclare touché, pour qu'il recule en arrière
isHurt = MOONWALK_TIMER;
 
//On détermine sa direction de recul, par rapport au placement
//de l'ennemi
//S'il vient d'en haut
if (player.getDirection() == DOWN)
hurtDirection = DOWN;
else if (player.getDirection() == UP)
hurtDirection = UP;
else if (player.getDirection() == RIGHT)
hurtDirection = RIGHT;
else if (player.getDirection() == LEFT)
hurtDirection = LEFT;
 
}
else
{
player.killPlayer();
}
}
 
 
//On détecte les collisions avec l'épée
//Si c'est le cas, on diminue ses PV
if (player.getCollisionWithSword(x, y, w, h))
{
//Et on blesse le monstre s'il n'est pas invincible
if (invincibleTimer == 0)
{
life--;
if (life <= 0)
timerMort = 1;
invincibleTimer = TIME_BETWEEN_2_SHOTS;
}
 
//On le déclare touché, pour qu'il recule en arrière
isHurt = MOONWALK_TIMER;
 
//On détermine sa direction de recul, par rapport au placement
//de l'ennemi
//S'il vient d'en haut
if (player.getDirection() == DOWN)
hurtDirection = DOWN;
else if (player.getDirection() == UP)
hurtDirection = UP;
else if (player.getDirection() == RIGHT)
hurtDirection = RIGHT;
else if (player.getDirection() == LEFT)
hurtDirection = LEFT;
}
 
 
//On détecte les collisions avec les autres monstres
for (int i = 0; i < map.getNombreMonstres(); i++)
{
//On ne reteste pas le monstre avec lui-même, sinon
//gare à la cata !!
if (i != monsterNumber)
if (collideWithMonsters(monster[i]))
{
if (direction == UP)
{
direction = DOWN;
y = monster[i].getY() + monster[i].getH() + 1;
}
else if (direction == DOWN)
{
direction = UP;
y = monster[i].getY() - h - 1;
}
else if (direction == RIGHT)
{
direction = LEFT;
x = monster[i].getX() - w - 1;
}
else if (direction == LEFT)
{
direction = RIGHT;
x = monster[i].getX() + monster[i].getW() + 1;
}
 
}
 
 
}
 
//Si le monstre meurt, on active une tempo
if (timerMort > 0)
{
timerMort--;
 
/* Si le monstre meurt, on renvoie 2 pour le remplace simplement par le dernier
monstre du tableau dans le main puis on rétrécit le tableau d'une case
(on ne peut pas laisser de case vide), sinon on renvoie 0 */
if (timerMort == 0)
return 2;
else
return 0;
}
else
return 0;
} 

 

   Voilà, on a juste rajouté un appel à collide() pour tester les collisions avec le joueur et un appel à getCollisionWithSword() pour les collisions avec l'épée. Après, tout est assez transparent et bien commenté. cheeky

   Dans le cas où le joueur est touché, on appelle playerHurts() pour lui enlever un coeur ou killPlayer() pour le tuer s'il n'a plus de coeur. 

   On trouve l'équivalent pour notre monstre, quand il se prend un coup d'épée. wink

   Notez qu'on fait également reculer le monstre pour montrer qu'il encaisse un choc. Pour cela, on utilise la direction de "charge" du héros pour déplacer le monstre dans la même direction. En effet, si le joueur rentre dans le monstre en allant vers la droite, il paraît logique que celui-ci recule vers la droite. cheeky

   Bien, avant de passer à la class Player, n'oublions pas de rajouter le prototype de drawSword() dans player.h :

 

Fichier : player.h : Ajouter le prototype de drawSword() :

//Fonctions
void initialize(int Atype, int x1, int y1);
void draw(Map &map, sf::RenderWindow &window);
int update(int monsterNumber, Map &map, Player &player, Monster monster[]);
void mapCollision(Map &map, Player &player);
bool collideWithMonsters(Monster &monster);
void copy(Monster &monster);
bool collide(Player &player);

 

   On va maintenant rajouter nos fonctions playerHurts(), killPlayer() et getCollisionWithSword(), évoquées ci-dessus dans le fichier player.cpp

 

Fichier : player.cpp  : Mettre à jour la fonction :

void Player::killPlayer()
{
//On met le timer à 1 pour tuer le joueur intantanément
timerMort = 1;
}
 
 
void Player::playerHurts()
{
//Si le timer d'invincibilité est à 0
//on perd un coeur
if (invincibleTimer == 0)
{
life--;
invincibleTimer = 80;
}
}
 
 
bool Player::getCollisionWithSword(int Ax, int Ay, int Aw, int Ah)
{
//Fonction de gestion des collisions avec l'épée
//Quand le joueur est en attaque, on va considérer une zone définie (hitbox)
//autour du joueur qui sera considérée comme la zone d'action de l'épée.
//Celle-ci va varier selon la direction de notre héros
int hitboxX, hitboxY, hitboxW, hitboxH;
 
 
//Si on attaque
if (isAttacking)
{
//On adapte la hitbox selon la direction du joueur
if (direction == UP)
{
hitboxX = x - 10;
hitboxY = y - 28;
hitboxW = w + 20;
hitboxH = 28;
}
else if (direction == DOWN)
{
hitboxX = x - 10;
hitboxY = y + h;
hitboxW = w + 20;
hitboxH = 28;
}
if (direction == RIGHT)
{
hitboxX = x + w;
hitboxY = y - 10;
hitboxW = 28;
hitboxH = h + 20;
}
else if (direction == LEFT)
{
hitboxX = x - 28;
hitboxY = y - 10;
hitboxW = 28;
hitboxH = h + 20;
}
 
//On teste les collisions.
//On renvoie true, s'il y a contact
if ((hitboxX >= Ax + Aw)
|| (hitboxX + hitboxW <= Ax)
|| (hitboxY >= Ay + Ah)
|| (hitboxY + hitboxH <= Ay)
)
return false;
//Sinon, on renvoie true pour blesser le joueur
else
return true;
}
else
return false;
} 
 

   Il n'y a pas grand chose à dire des 2 premières fonctions. wink

   En ce qui concerne la fonction getCollisionWithSword() par contre, vous aurez sans doute remarqué que la fin consiste en notre fonction de détection des collisions classique, en revanche, auparavant, on se charge de calculer une hitbox pour notre épée, et cela par rapport à la position de notre héros et à sa direction. En effet, comme notre sprite d'épée subit des rotations, il n'est pas aisé de s'en servir pour calculer des collisions. Nous avons donc décidé de créer un rectangle virtuel situé devant le héros qui constitue la zone de frappe dans l'épée. Si un monstre passe dans ce rectangle invisible alors que notre héros attaque (isAttacking == 1), il est touché. Après, c'est à vous de calculer ce rectangle au plus juste pour votre jeu, afin qu'il ne soit pas trop difficile de toucher un monstre, sans se faire toucher, mais sans tuer les monstres à 2km de distance non plus ! laugh

   On peut aussi envisager de rajouter d'autres armes, par la suite, avec des portées et donc des hitboxes différentes. wink

 

   Rajoutons désormais les prototypes de playerHurts(), killPlayer() et getCollisionWithSword() dans le fichier player.h et le tour est joué ! 

 

Fichier : player.h : Ajouter les prototypes manquants :

//Fonctions
void initialize(Map &map);
void reinitialize(Map &map);
void draw(Map &map, sf::RenderWindow &window);
void update(Input &input, Map &map);
void centerScrolling(Map &map);
void mapCollision(Map &map);
void drawSword(Map &map, sf::RenderWindow &window);
void killPlayer();
void playerHurts();
bool getCollisionWithSword(int Ax, int Ay, int Aw, int Ah);

 

   Voilà, on compile et on lance le programme ! wink   

   Maintenant, à vous la joie d'occir tous ces monstres ! cheeky

   P.S. : Si vous mourez, votre perso sera réinitialisé aux coordonnées par défaut de la map. wink

 

 

   Je vous dis donc à bientôt pour le chapitre 15 ! 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!