
Big Tuto SFML 2 / Action-RPG : Legends of Meruvia
Chapitre 13 : Ajoutons l'épée du héros !
Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 26 juin 2016
Date de révision : -
Prologue
Nous allons maintenant rajouter l'épée de notre héros lorsqu'il attaque.
Pour cela, nous allons utiiser le fichier sword.png, normalement déjà présent dans le dosier graphics :

sword.png
Nous avons choisi de dissocier l'arme du héros de son sprite, comme dans beaucoup de jeux, et cela pour 2 raisons :
1. il est plus facile de gérer les collisions ainsi : collision joueur / monstre = joueur touché tandis que collision monstre / épée = monstre touché. Bien entendu, en cas d'attaque un peu trop rentre dedans, les deux peuvent être touchés, comme dans la réalité. ![]()
2. on peut facilement changer d'arme, en changeant simplement le sprite. ![]()
Afin de simuler les coups d'épée, on aurait pu utiliser une animation comme dans beaucoup de jeux, mais comme on a envie de se compliquer la vie, on va calculer le déplacement et la rotation de notre épée en temps réel !
Mais non, ça va aller !... ![]()
Le code
On va commencer par ajouter notre nouvelle fonction drawSword() dans le fichier player.cpp :
Fichier : player.cpp : Ajouter la nouvelle fonction :
|
void Player::drawSword(Map &map, RenderWindow &window)
{
//On place l'épée correctement sur la map, par rapport au joueur
if (direction == DOWN)
{
swordX = x + 20 - map.getStartX();
swordY = y + 24 - map.getStartY();
}
else if (direction == UP)
{
swordX = x + 24 - map.getStartX();
swordY = y + 28 - map.getStartY();
}
else if (direction == RIGHT)
{
swordX = x + 20 - map.getStartX();
swordY = y + 44 - map.getStartY();
}
else if (direction == LEFT)
{
swordX = x + 20 - map.getStartX();
swordY = y + 24 - map.getStartY();
}
sword.setPosition(swordX, swordY);
//Gestion de la rotation, en fonction de la direction
sword.setOrigin(0, 0);
if (direction == DOWN)
sword.setRotation(swordRotation);
else if (direction == UP)
sword.setRotation(swordRotation - 200);
else if (direction == RIGHT)
sword.setRotation(swordRotation - 135);
else if (direction == LEFT)
sword.setRotation(swordRotation + 45);
window.draw(sword);
/* Gestion du timer */
// Si notre timer (un compte à rebours en fait) arrive à zéro
if (swordTimer <= 0)
{
//On le réinitialise
swordTimer = TIME_BETWEEN_2_FRAMES_SWORD;
//Et on augmente la rotation de l'épée
swordRotation += 10;
//Mais si on dépasse 90°, on stoppe l'attaque :
if (swordRotation > 80)
{
swordRotation = 0;
isAttacking = 0;
}
}
//Sinon, on décrémente notre timer
else
swordTimer--;
}
|
Voilà, ce sera la seule vraie nouveauté de ce chapitre : c'est cette simple petite fonction qui va se charger d'afficher notre épée et de l'animer en temps réel. ![]()
Revenons sur son fonctionnement :
- la 1ère chose à faire, c'est de placer correctement l'épée par rapport au sprite de notre héros. Bien entendu, comme ce sprite change selon sa direction, il faut s'adapter.
Maintenant, vous allez sûrement vous demander comment j'en suis arrivé à ces valeurs (+20, +24, etc). Eh bien, tout simplement par tâtonnements, en déplaçant l'épée de quelques pixels par rapport au sprite de notre héros, jusqu'à ce que cela rende bien.
(Notez qu'on peut aussi utiliser un logiciel de dessin pour calculer ce décalage plus facilement qu'en modifiant le code / recompilant à chaque fois
)
- on va ensuite s'occuper de la rotation de notre épée. Celle-ci va se faire grâce à la variable swordRotation que l'on va incrémenter de 10 degrés par intervalle (grâce à un chrono ou timer, pour que le mouvement ne soit pas trop rapide) sur un mouvement d'épée allant à 90 degrés. Bien entendu, là encore, selon la direction de notre personnage, la rotation de l'épée ne va pas partir du même point de rotation, d'où l'ajout de 45 degrés quand on va vers la gauche, par exemple
). Vous pouvez maintenant vous amuser à changer ces valeurs pour modifier le swing de l'épée et mieux comprendre comment cela fonctionne. ![]()
- Notez enfin, qu'une fois le mouvement de rotation de l'épée à 90 degrés terminé, on repasse isAttacking à 0 afin d'arrêter l'attaque. ![]()
Voilà, il nous faut maintenant mettre à jour notre fonction draw() en appelant notre nouvelle fonction drawSword().
Fichier : player.cpp : Mettre à jour la fonction :
|
void Player::draw(Map &map, RenderWindow &window)
{
/* Gestion du timer */
// Si notre timer (un compte à rebours en fait) arrive à zéro
if (frameTimer <= 0)
{
//On le réinitialise
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
//Et on incrémente notre variable qui compte les frames de 1 pour passer à la suivante
frameNumber++;
//Mais si on dépasse la frame max, il faut revenir à la première :
if (frameNumber >= frameMax)
frameNumber = 0;
}
//Sinon, on décrémente notre timer
else
{
if (etat != IDLE)
frameTimer -= 1 + (isrunning * 2);
else
frameTimer--;
}
//On place le joueur correctement sur la map
hero.setPosition(Vector2f(x - map.getStartX(), y - map.getStartY()));
//Si on attaque, il faut aussi dessiner l'épée.
//On la dessine après le joueur dans toutes les directions, sauf quand il regarde vers le haut
if (isAttacking == 1 && direction == UP)
drawSword(map, window);
//Pour connaître le X de la bonne frame à dessiner, il suffit de multiplier
//la largeur du sprite par le numéro de la frame à afficher -> 0 = 0; 1 = 40; 2 = 80...
//On calcule le Y de la bonne frame à dessiner, selon la valeur de l'état du héros :
//Aucun Mouvement (Idle) = 0, marche (walk) = 1, etc...
//Tout cela en accord avec notre spritesheet, of course ;)
//Si on a été touché et qu'on est invincible
if (invincibleTimer > 0)
{
//On fait clignoter le héros une frame sur deux
//Pour ça, on calcule si le numéro de la frame en
//cours est un multiple de deux
if (frameNumber % 2 == 0)
{
//Gestion du flip (retournement de l'image selon que le sprite regarde à droite ou à gauche)
if (direction == LEFT)
{
hero.setTextureRect(sf::IntRect(
frameNumber * w,
(etat * 3 * h + (direction - 1) * h + isAttacking * 6 * h),
w, h));
window.draw(hero);
}
else
{
//On n'a plus de flip auto en SFML, il faut donc tout calculer
hero.setTextureRect(sf::IntRect(
(frameNumber + 1) * w,
(etat * 3 * h + direction * h + isAttacking * 6 * h),
-w, h));
window.draw(hero);
}
}
//Sinon, on ne dessine rien, pour le faire clignoter
}
//Sinon, on dessine normalement
else
{
//Gestion du flip (retournement de l'image selon que le sprite regarde à droite ou à gauche)
if (direction == LEFT)
{
hero.setTextureRect(sf::IntRect(
frameNumber * w,
(etat * 3 * h + (direction - 1) * h + isAttacking * 6 * h),
w, h));
window.draw(hero);
}
else
{
//On n'a plus de flip auto en SFML, il faut donc tout calculer
hero.setTextureRect(sf::IntRect(
(frameNumber + 1) * w,
(etat * 3 * h + direction * h + isAttacking * 6 * h),
-w, h));
window.draw(hero);
}
}
//Si on attaque, il faut aussi dessiner l'épée.
//On la dessine après le joueur dans toutes les directions, sauf quand il regarde vers le haut
if (isAttacking == 1 && direction != UP)
drawSword(map, window);
}
|
Vous aurez remarqué qu'on peut appeler la fonction drawSword() à 2 endroits différents dans le fichier.
Bon, tout d'abord, on ne l'appelle que si notre héros est en train d'attaquer (isAttacking doit valoir 1), ce qui paraît logique ! ![]()
Après la subtilité vient de la direction vers laquelle se dirige notre personnage. En effet, quand il se dirige vers le haut (UP) notre héros apparaît de dos. Or si on blitte l'épée par-dessus, comme pour les autres directions, cela va avoir l'air très bizarre (vous pouvez essayer pour rigoler !
). C'est pour ça qu'on doit blitter l'épée avant le sprite du héros s'il va vers le haut, tandis qu'on doit la blitter après pour toutes les autres directions. ![]()
On passe maintenant à la mise à jour de la fonction update() pour détecter l'attaque quand on appuie sur la touche d'attaque :
Fichier : player.cpp : Mettre à jour la fonction :
|
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 nos vecteurs de déplacement, 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;
dirY = 0;
//Gestion de la course en appuyant sur la touche courir
if (input.getButton().run)
isrunning = 1;
else
isrunning = 0;
//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 + isrunning;
//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 :
if (etat != WALK)
{
//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 + isrunning;
//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
if (etat != WALK)
{
//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 haut
else if (input.getButton().up == true)
{
//On augmente les coordonnées en x du joueur
dirY -= PLAYER_SPEED + isrunning;
//Et on indique qu'il va à droite (pour le flip
//de l'affichage, rappelez-vous).
direction = UP;
//Si ce n'était pas son état auparavant
if (etat != WALK)
{
//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 bas
else if (input.getButton().down == true)
{
//On augmente les coordonnées en x du joueur
dirY += PLAYER_SPEED + isrunning;
//Et on indique qu'il va à droite (pour le flip
//de l'affichage, rappelez-vous).
direction = DOWN;
//Si ce n'était pas son état auparavant
if (etat != WALK)
{
//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, on charge l'animation marquant l'inactivité (Idle)
else if (input.getButton().right == false && input.getButton().left == false &&
input.getButton().up == false && input.getButton().down == false)
{
//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;
}
}
//On gère l'attaque
if (input.getButton().attack)
{
isAttacking = 1;
input.setButton(attack, false);
}
//On rajoute notre fonction de détection des collisions qui va mettre à
//jour les coordonnées de notre héros.
mapCollision(map);
//On gère le scrolling (fonction ci-dessous)
centerScrolling(map);
}
//Gestion de la mort :
//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)
{
// Si on est mort, on réinitialise le niveau
map.changeLevel();
initialize(map);
}
}
}
|
Comme vous le voyez, le code ajouté est vraiment très simple : on passe simplement la variable isAttacking à 1. ![]()
Rajoutons désormais le prototype de drawSword() dans le fichier player.h et le tour est joué !
Fichier : player.h : Ajouter le prototype de drawSword() :
|
//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);
|
Voilà, on compile et on lance le programme !
En appuyant sur la touche attaque, notre épée s'affiche désormais et s'anime en temps réel, à l'aide de notre simple système de rotation. ![]()
Cependant, vous ne pourrez pas encore tuer de monstres avec !
Il va d'abord nous falloir rajouter la gestion des collisions. ![]()

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

English
Français 