
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. ![]()
Et il peut maintenant dégainer son épée pour pourfendre ses ennemis !... ![]()
Manque plus que les collisions !
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 ! ![]()
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. ![]()
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é. ![]()
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. ![]()
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. ![]()
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. ![]()
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 ! ![]()
On peut aussi envisager de rajouter d'autres armes, par la suite, avec des portées et donc des hitboxes différentes. ![]()
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 !
Maintenant, à vous la joie d'occir tous ces monstres ! ![]()
P.S. : Si vous mourez, votre perso sera réinitialisé aux coordonnées par défaut de la map. ![]()

Je vous dis donc à bientôt pour le chapitre 15 ! ![]()
Jay

English
Français 