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

Chapitre 6 : Ajoutons notre héros !

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 11 janvier 2016
Date de révision : 17 avril 2016

      Prologue

   Bon, maintenant que vous avez eu le temps de faire un peu mumuse avec le level editor, indecision retournons aux choses sérieuses ! laugh

   Nous allons maintenant ajouter notre héros, un guerrier en armure avec une cape, pour faire classe ! cool Et, nous allons le créer, grâce à une nouvelle classe : la classe Player (oui, je sais, c'est original ! laugh). 

   Mais pour afficher notre héros, nous allons d'abord avoir besoin d'une spritesheet, que je vais vous redonner, mais normalement vous devriez déjà l'avoir copiée dans votre projet ! cheeky

Mais, c'est quoi une spritesheet ? frown

C'est une feuille de sprites en français, c'est-à-dire un fichier image (au format png de préférence car il est non destructeur, pas comme le jpg) qui reprend toutes les animations de notre héros.
Ce sera ensuite à nous de découper cette feuille dans notre fonction de dessin draw() pour afficher les animations correctement. wink

Si vous avez déjà lu le Big Tuto SFML 2 : Rabidja, vous verrez que la technique reste la même. 

Enfin, si vous voulez en apprendre un peu plus sur le pixel art pour créer votre héros, vous pourrez lire ces quelques chapitres, même si, aujourd'hui, ils datent un peu... cheeky

   Bon, voilà donc notre spritesheet : 

   Si vous avez déjà lu le tuto Rabidja, vous aurez certainement remarqué que notre spritesheet est un peu plus complexe, cette fois. wink

   On peut en effet la découper par tranche de 3 lignes.

   Mais pourquoi 3 lignes ? surprise

   Tout simplement parce que notre héros peut désormais aller dans 4 directions, et plus seulement dans 2. cheeky On garde donc notre vue de profil, qu'on pourra retourner avec un flip, pour aller à droite ou à gauche, mais il nous faut aussi 2 animations pour aller en haut et en bas.

   Nous avons donc ici 4 animations différentes : 

- pour les 3 premières lignes : notre héros est passif, sans bouger (état IDLE),

- pour les 3 lignes suivantes : il marche (état WALK),

- ensuite, nous avons encore 3 lignes qui ressemblent aux premières, mais sans le bras du héros : il s'agit encore de l'état IDLE, mais notre héros sera en train d'attaquer ! Il a donc perdu son bras car celui-ci sera attaché à l'épée. wink

- même chose pour les 3 dernières lignes, où notre héros attaquera en avançant, cette fois.

   Maintenant, avant de passer au code à proprement dit, vous pouvez télécharger les fichiers du projet ci-dessous wink : 

 

      Le code

   Faites donc un clic droit sur le nom de votre projet -> Ajouter -> Classe et créez une nouvelle classe Player ! On commence par le header, player.h :

 

Fichier : player.h : Copiez le code suivant :

//Legends of Meruvia - C++ / SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#ifndef PLAYER_H
#define PLAYER_H
 
#include <SFML/Graphics.hpp>
#include <iostream>
 
class Map;
class Input;
 
 
class Player
{
 
public:
 
//Structures
struct POINT { int x, y; };
 
//Constructeur
Player();
 
//Accesseurs
int getX(void) const;
int getY(void) const;
int getW(void) const;
int getH(void) const;
float getDirX(void) const;
float getDirY(void) const;
int getOnGround(void) const;
int getLife(void) const;
int getGold(void) const;
int getDirection(void) const;
int getMagic(void) const;
int getNombreExplosions(void) const;
float getMP(void) const;
float getMPmax(void) const;
 
//Mutateurs
void setX(int valeur);
void setY(int valeur);
void setW(int valeur);
void setH(int valeur);
void setDirX(float valeur);
void setDirY(float valeur);
void setOnGround(bool valeur);
void setTimerMort(int valeur);
void setGold(int valeur);
void setCheckpoint(bool valeur);
void setMagic(int valeur);
void setNombreExplosions(int valeur);
 
//Fonctions
void initialize(Map &map);
void draw(Map &map, sf::RenderWindow &window);
void update(Input &input, Map &map);
 
 
private:
 
//Variables de la classe en accès privé
 
// Points de vie/santé + chrono d'invicibilité
int life, invincibleTimer;
 
//Magie
float MP, regainTime;
int MPmax;
int gold;
 
// Coordonnées du sprite
int x, y;
 
// Largeur, hauteur du sprite
int h, w;
 
// Checkpoint pour le héros (actif ou non)
bool checkpointActif;
// + coordonnées de respawn (réapparition)
int respawnX, respawnY;
 
// Variables utiles pour l'animation :
// Numéro de la frame (= image) en cours + timer
int frameNumber, frameTimer, frameMax;
// Nombre max de frames, état du sprite et direction
// dans laquelle il se déplace (gauche / droite)
int etat, direction;
int isrunning;
int isAttacking;
 
// Variables utiles pour la gestion des collisions :
//Est-il sur le sol, chrono une fois mort
int timerMort;
 
//Vecteurs de déplacement temporaires avant détection
//des collisions avec la map
float dirX, dirY;
//Sauvegarde des coordonnées de départ
int saveX, saveY;
 
//Spritesheet du héros
sf::Texture heroTexture;
sf::Sprite hero;
sf::Texture swordTexture;
sf::Sprite sword;
 
//Gestion de l'épée
int swordX, swordY;
int swordTimer;
int swordRotation;
 
//Gestion de la magie (boules de feu)
int magicNumber;
 
//Nombre d'explosions à l'écran
int nombreExplosions;
 
//Numéro de la warp spéciale empruntée
int numberSPE;
 
 
 
/******************/
/* Constantes */
/******************/
 
/* Taille maxi de la map : 400 x 150 tiles */
const int MAX_MAP_X = 400;
const int MAX_MAP_Y = 150;
 
/* Taille d'une tile (32 x 32 pixels) */
const int TILE_SIZE = 32;
 
/* Constantes pour l'animation */
const int TIME_BETWEEN_2_FRAMES_PLAYER = 3;
const float TIME_BETWEEN_2_FRAMES_SWORD = 1;
 
/* Taille du sprite de notre héros (largeur = width et hauteur = heigth) */
const int PLAYER_WIDTH = 40;
const int PLAYER_HEIGTH = 48;
 
//Vitesse de déplacement en pixels du sprite
const int PLAYER_SPEED = 3;
 
//Valeurs attribuées aux états/directions
const int IDLE = 0;
const int WALK = 1;
 
const int DEAD = 4;
 
const int DOWN = 0;
const int UP = 1;
const int RIGHT = 2;
const int LEFT = 3;
 
 
// Taille de la fenêtre : 800x480 pixels
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 480;
 
//Constantes pour les limites de la caméra avant scrolling
const int LIMITE_X = 400;
const int LIMITE_Y = 220;
const int LIMITE_W = 100;
const int LIMITE_H = 80;
 
//Enum pour les boutons
const enum{ up, down, right, left, attack, run, enter, magie };
 
//Nombre max de levels
const int LEVEL_MAX = 2;
 
//Une enum pour la gestion du menu.
const enum { START, PAUSE };
 
//Nombre max de shurikens à l'écran
const int MAGIC_MAX = 6;
 
 
/*************************/
/* VALEURS DES TILES */
/************************/
 
const int MUR = 1;
const int SPE1 = 10;
const int SPE2 = 11;
const int SPE3 = 12;
const int SPE4 = 13;
const int SPE5 = 14;
const int SPE6 = 15;
const int SPE7 = 16;
const int SPE8 = 17;
const int SPE9 = 18;
const int SPE10 = 19;
 
 
};
#endif

 

Les fonctions :

   Pour l'instant, on a surtout un constructeur et de nombreux accesseurs / mutateurs pour accéder aux variables expliquées ci-dessous. wink

   On se limitera ainsi à 3 nouvelles fonctions importantes pour ce chapitre : initialize() qui (ré)initialisera notre héros, aussi bien au début du jeu, qu'à chaque fois qu'il mourra ou passera de niveau, draw() qui le dessinera tout en gérant son animation, conformément à sa spritesheet ci-dessus, et update() qui servira à le mettre à jour et le gérer, mais pour l'instant cette fonction restera embryonnaire : on la développera plus tard. wink

 

Les variables :

   Là encore, j'ai choisi d'ajouter la quasi-totalité des variables, même si elles ne nous serviront pas dès ce chapitre. Ce sera moins fastidieux que de les rajouter au fur et à mesure (je vous économise comme ça, et moi aussi !). laugh

   J'ai déjà largement commenté dans le code chacune de ces variables, qui seront pour la plupart assez génériques. wink

   Pour passer en revue les principales, on commence par les points de vie de notre héros (life) et un chrono (timer) qui nous permettra de le rendre invincible pendant un court instant, après s'être fait toucher (sinon, à 60 fps, il perdrait toute sa vie en moins d'une seconde ! surprise Je sais que vos réflexes de gamer sont affutés, mais quand même ! laugh). J'ai aussi rajouté de la magie (RPG oblige ! wink) : le joueur aura donc des MPmax, ses MP en cours (il peut n'avoir qu'un quart de sa magie max, par exemple, et un chrono qui lui permettra d'en regagner au fil du temps, grâce à regainTime. J'ai aussi déjà ajouté les pièces d'or (gold) même si on ne s'en servira pas dans cette 1ère partie du tuto ! cheeky

   Ensuite viennent les coordonnées de notre héros et ses dimensions (que nous avons entrées en defines wink).

   Les variables suivantes nous permettront plus tard de gérer les checkpoints de mi-niveau pour éviter de tout recommencer dès le début à chaque fois qu'on meurt ! Ce sera cool, mais ce n'est pas encore pour tout de suite ! wink

   Les variables d'animation, par contre, vont nous servir dès ce niveau : frameNumber contiendra le numéro de la frame (ou image) affichée en ce moment, frameTimer le temps restant avant le passage à la prochaine frame, et frameMax indiquera le numéro de la dernière frame de l'animation après laquelle on doit revenir à la première. wink

   Etat et direction contiendront les valeurs que nous avons définies en defines et nous permettront de retrouver l'animation en cours (idle, walk, jump, etc...) ainsi que la direction que prend le perso (right / left).

   Les variables isrunning et isAttacking nous permettront de savoir si le joueur est en train de courir ou d'attaquer, pour bien le gérer. wink

   timerMort fera une petite tempo suite à la mort du joueur avant de le réinitialiser (histoire de laisser le temps au joueur de se rendre compte qu'il vient de trépasser comme un... indecision).

   dirX et dirY seront 2 vecteurs qui nous permettront de précalculer le déplacement optimal de notre héros avant détection des collisions. Cette valeur sera ensuite adaptée en conséquence (si notre héros devait se retrouver dans un mur, par exemple, on le collera plutôt contre celui-ci - c'est mieux ! laugh). Mais on verra ça dans un prochain chapitre. wink

   saveX et saveY nous serviront dans certains cas, pour vérifier si le sprite s'est déplacé ou pas depuis la frame précédente, en sauvegardant tout simplement sa position d'avant. On verra à quoi cela pourra bien nous servir. cheeky

   On n'oublie pas non plus la Texture et le Sprite pour notre héros et pour son épée, qui seront deux entités différentes (je vous expliquerai plus tard pourquoi wink) !

   Pour gérer notre épée, nous aurons ensuite besoin de ses coordonnées, de son angle de rotation (car elle va tourner en temps réel, ce ne sera pas une animation ! cool) et d'un chrono (timer).

   Nous avons enfin 3 variables qui contiendront la magie utilisée (magicNumber), le nombre d'explosions (nombreExplosions) et la warp spéciale utilisée pour débarquer dans le niveau (numberSPE), mais nous reviendrons là-dessus bien plus tard, ne vous inquiétez pas. wink

 

   Bon, je pense avoir été assez exhaustif. Je vous rappelle que j'ai choisi de mettre la quasi totalité des variables dès le début pour vous donner une vue d'ensemble sur ce que l'on va faire et sur ce que notre héros sera capable de faire. Bien entendu, il va nous falloir encore quelques chapitres avant d'exploiter toutes ces variables (on va y aller petit à petit wink), et nous en rajouterons même de nouvelles plus tard ! laugh

 

Les constantes :

   Beaucoup de ces constantes sont déjà connues, puisqu'on les a déjà vues dans la classe Map. On en trouve pourtant de nouvelles :

   On a ainsi 2 nouveaux timers (TIME_BETWEEN_2_FRAMES_PLAYER et TIME_BETWEEN_2_FRAMES_SWORD), plus courts, pour calculer l'écart entre 2 frames de l'animation de notre héros ou de son épée. Vous pourrez vous amuser à changer cette valeur pour voir son incidence sur la vitesse d'animation du personnage puis de son épée. wink

   On trouve aussi les dimensions d'un sprite de notre héros qui fait 48 pixels de haut pour 40 pixels de large.

   PLAYER_SPEED nous servira dans un futur chapitre pour calculer la vitesse de déplacement de notre héros. Mais, tant qu'à faire, on le prévoit dès maintenant ! wink

   Ensuite, on trouve des constantes qui permettront de définir l'état de notre héros. Ainsi, l'état du perso aura une valeur (0, 1, ou 4) suivant qu'il sera immobile (IDLE), qu'il marchera (WALK) ou qu'il ser mort (DEAD). Vous remarquerez que les états 2 et 3 de Rabidja ont disparu puisque notre héros ne pourra plus sauter ! wink

   Les 4 valeurs suivantes nous permettront de savoir dans quelle direction se déplace le joueur pour savoir si l'on doit effectuer un flip (retournement) sur nos sprites ou non : pour aller à droite ou à gauche. En effet, si vous avez déjà lu le Big Tuto SDL 1.2 / 2, vous devez vous rappeler que pour chaque anim', on en avait une orientée à droite et une autre à gauche. En SFML2 (comme en SDL 2 d'ailleurs), nous n'en avons plus besoin, car il y a moyen de gérer le flip (horizontal et vertical) autrement. Cela dit, c'est quand même moins évident en SFML 2 qu'en SDL 2...

   Viennent ensuite les dimensions de la fenêtre puis les limites de la caméra : nous utiliserons ces constantes au prochain chapitre quand nous rajouterons la caméra et le scrolling. Mais en attendant, vous saurez déjà où les trouver pour changer le comportement de la caméra. cheeky

   L'enum pour les boutons est la même que celle de classe Input. On s'en servira plus tard pour récupérer nos inputs clavier (puis joystick) et déplacer notre héros.

   Enfin, les constantes concernant les tiles sont les mêmes que celles de la classe Map, et les mêmes que celles que nous avons vues au chapitre précédent avec le level editor. Elles définissent pour l'instant uniquement les murs (tiles bloquantes) et les warps spéciales (jusqu'à 10) pour entrer dans les maisons. On en aura besoin plus tard, également. wink

   Passons maintenant au début du code de notre classe, dans le fichier player.cpp :

Fichier : player.cpp :  

//Legends of Meruvia - C++ / SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#include "player.h"
#include "map.h"
#include "input.h"
 
 
using namespace std;
using namespace sf;
 
 
//Constructeur
 
Player::Player()
{
//Chargement de la spritesheet du héros
if (!heroTexture.loadFromFile("graphics/hero.png"))
{
// Erreur
cout << "Erreur durant le chargement du spritesheet du héros." << endl;
}
else
hero.setTexture(heroTexture);
 
//Chargement du sprite de l'épée
if (!swordTexture.loadFromFile("graphics/sword.png"))
{
// Erreur
cout << "Erreur durant le chargement du sprite de l'épée." << endl;
}
else
sword.setTexture(swordTexture);
 
//Initialisation des variables :
dirX = 0;
dirY = 0;
life = 3;
invincibleTimer = 0;
x = y = h = w = 0;
checkpointActif = false;
respawnX = respawnY = 0;
frameNumber = frameTimer = frameMax = 0;
etat = direction = 0;
timerMort = 0;
dirX = dirY = 0;
saveX = saveY = 0;
 
MP = MPmax = 100;
regainTime = 0.2f;
 
swordTimer = 0;
swordRotation = 0;
}
 
 
//Accesseurs
int Player::getX(void) const { return x; }
int Player::getY(void) const { return y; }
int Player::getW(void) const { return w; }
int Player::getH(void) const { return h; }
float Player::getDirX(void) const { return dirX; }
float Player::getDirY(void) const { return dirY; }
int Player::getLife(void) const { return life; }
int Player::getGold(void) const { return gold; }
int Player::getDirection(void) const { return direction; }
int Player::getMagic(void) const { return magicNumber; }
int Player::getNombreExplosions(void) const { return nombreExplosions; }
float Player::getMP(void) const { return MP; }
float Player::getMPmax(void) const { return MPmax; }
 
 
//Mutateurs
void Player::setX(int valeur) { x = valeur; }
void Player::setY(int valeur) { y = valeur; }
void Player::setW(int valeur) { w = valeur; }
void Player::setH(int valeur) { h = valeur; }
void Player::setDirX(float valeur) { dirX = valeur; }
void Player::setDirY(float valeur) { dirY = valeur; }
void Player::setTimerMort(int valeur) { timerMort = valeur; }
void Player::setGold(int valeur) { gold = valeur; }
void Player::setCheckpoint(bool valeur) { checkpointActif = valeur; }
void Player::setMagic(int valeur) { magicNumber = valeur; }
void Player::setNombreExplosions(int valeur) { nombreExplosions = valeur; }

   Rien de bien sorcier ici, le constructeur charge le sprite de notre héros et celui de son épée puis initialise les variables, tandis que les accesseurs renvoient la variable demandée et les mutateurs la modifie. wink

   C'est assez basique, et je passe rapidement dessus, car le gros du boulot nous attend, les gars ! laugh

   On passe donc maintenant à l'initialisation de notre joueur, qui nous permettra de remettre à défaut ses valeurs, soit au début du jeu, soit quand on passera d'une map à une autre, soit quand on mourra et qu'on recommencera au début de la map :

Fichier : player.cpp : Copier à la suite : 

//Fonctions
void Player::initialize(Map &map)
{
//PV à 3 coeurs
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;
 
//On démarre au début de la map
x = map.getBeginX();
y = map.getBeginY();
 
//On initialise la caméra
map.setStartX(0);
map.setStartY(0);
 
/* 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;
isAttacking = 0;
} 

   Comme vous pouvez le voir, il n'y a rien de bien sorcier là non plus (on fait un RPG sans magie ! surprise Meuh non ! indecision) : on initialise ses Points de Vie à 3 coeurs, on charge l'animation IDLE (ne fait rien) vers la droite, on part de la frame 0, on initialise le timer (chrono) et on définit la frame Max de notre anim' à 8 (car elle fait 8 images cheeky).

   On définit ensuite la position de départ du joueur par rapport aux variables beginX et beginY, qu'on a lues dans le fichier de la map précédemment (cf. chapitre 5) et qu'on appelle à l'aide des fonctions getBeginX() et getBeginY(), sinon on ne pourrait pas y avoir accès wink. Plus tard, quand on aura rajouté la gestion de nos checkpoints, le héros pourra y ressusciter directement sans recommencer le niveau et en s'initialisant aux coordonnées de respawn.

   On initialise ensuite les coordonnées de la map à (0, 0) à l'aide des mutateurs setStartX() et setStartY() pour centrer la caméra au début du niveau, vu qu'on n'a pas encore de caméra intelligente qui scrolle automatiquement. indecision

   On enregistre enfin la taille de notre sprite et on met le timer de résurrection (timerMort) à 0 ainsi que la position isAttacking (car on n'ouvre pas les hostilités dès le début quand même ! cheeky). Cependant, comme on l'a dit auparavant, ces variables ne nous seront pas utiles dès ce chapitre (mais dans le prochain et celui d'après wink).

   Eh voilà ! Vous l'attendiez ! La grosse fonction qui va se charger de dessiner notre sprite à l'écran et de l'animer arrive :

Fichier : player.cpp : Copier à la suite :  

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()));
 
 
//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);
}
}
 
} 

   La première partie de cette fonction (qui est la même que celle de Rabidja wink) s'occupe de gérer le timer ou chrono d'animation.

   Cela peut avoir l'air compliqué, mais en fait, c'est extrêmement simple :

- A chaque tour de boucle, on teste si notre compte à rebours (timer) arrive à 0 :

- Si c'est le cas : on réinitialise le timer à la valeur de la def. 

- Et on augmente la valeur de la frame (image) en cours de 1, pour passer à la suivante.

- Mais si on est rendu au bout de l'anim', on ne va pas blitter du vide ! surprise Donc, on revient à la frame 0wink

- Si ce n'est pas le cas, on décrémente notre compte à rebours de 1, et on continue. 

   C'est la méthode classique pour implémenter un timer et vous pourrez vous en inspirer pour bien d'autres usages, car il faut bien concevoir qu'un jeu vidéo fonctionne un peu comme une horloge et tout doit être savamment minuté pour que l'ensemble ait l'air cohérent. Cela n'en a pas forcément l'air comme ça, car on essaye de faire en sorte que le jeu ait l'air vivant et réaliste / logique, mais tout a été auparavant savamment orchestré par le game designer, le level designer et le programmeur (entre autres wink).

   On passe ensuite au dessin de notre sprite. Pour cela, on va avoir besoin :

- des coordonnées de destination (x, y)  qui correspondent à l'endroit de la map où on va blitter le joueur. Pour cela, on va avoir besoin de ses coordonnées dans le niveau, moins ceux du scrolling de la map (mais on verra ça plus tard, car pour l'instant, on n'a pas encore de scrolling, donc les coordonnées de la map correspondent à celles de l'écran, soit (0,0) wink). 

- d'un rectangle source, sf::IntRect(x, y, w, h) qui correspond à l'endroit de la feuille de sprite que l'on va "découper" (avec la bonne frame de préférence ! laugh).

Pour cela, notre largeur (w) et notre hauteur (h) vont être celles de la taille de notre sprite, soit 40 x 48, il n'y a pas de surprise. wink

Mais là, où il va falloir faire des calculs, c'est pour obtenir les bonnes coordonnées x et y :

- Ainsi, le x de la bonne frame va correspondre au numéro de la frame en cours (frameNumber) multiplié par la largeur d'une frame (40 pixels). Ainsi, la frame 0 sera à 0 x 40 = 0 pixel, la frame 1 à 1 x 40 = 40 pixels, la frame 2 à 2 x40 pixels = 80, etc...

- Et pour trouver le y, on va se servir de notre énum précédente, qu'on va multiplier par 3, car, comme on l'a dit, on a 3 animations différentes par état, et auquel on va rajouter la direction pour que le joueur ne fasse pas du moonwalk ! laugh Notez cependant, que pour LEFT, il faut retrancher 1 à la direction, car il n'y a pas d'animation pour lui, et il faut alors faire un flip, d'où le if séparé. wink Comme vous le voyez, c'est ingénieux mais bien plus compliqué que pour Rabidja...

Je pense que vous avez compris le système. wink Il nous permet ainsi de gagner du temps et des ressources, car il est beaucoup moins coûteux de charger et découper de gros fichiers images que d'en charger plein de petits (croyez-moi, j'ai fait cramer une Xbox 360, comme ça cheeky) ! wink

   Le code, ici, est un peu moins clair en SFML 2 qu'en SDL 2 mais revient exactement au même (vous pouvez jeter un coup d'oeil à la version SDL 2 pour voir la différence wink). Ce qui est le plus perturbant, c'est que le flip, pourtant disponible dans des versions antérieures de la SFML ait disparu. Alors effectivement, on obtient le même résultat avec le code ci-dessus, mais c'est quand même beaucoup moins facile d'accès, surtout quand on débute! cheeky En tout cas, cela va nous permettre de retourner notre sprite horizontalement en fonction de sa direction (RIGHT ou LEFT) et ainsi, on n'aura plus besoin d'avoir deux feuilles de sprites, une pour chaque direction, comme en SDL 1.2 ! cool

   Voilà, une dernière précision sur le clignotement du sprite : quand notre héros va se faire toucher par un monstre ou un piège (et ça va bien lui arriver, faut pas rêver ! laugh), on veut le faire clignoter (comme dans beaucoup de jeux). Pour ça, on regarde d'abord si c'est le cas (sinon on blitte normalement), et si c'est le cas, on ne va blitter qu'une frame sur deux. Mais comment faire ? surprise Eh bien, avec un simple modulo 2 (%2) on va savoir si la frame est paire ou non, et donc blitter que dans l'un des deux cas. wink

 

   Passons maintenant à la fonction update() qui va nous permettre de gérer notre héros. Vous verrez qu'elle reste encore assez embryonnaire pour l'instant, même si elle nous permettra déjà de changer la direction de notre héros et de le faire marcher sur place. cheeky

Fichier : player.cpp : Copier à la suite : 

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;
}
}
 
 
}
 
//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);
 
}
}
} 

 

   On va d'abord réutiliser notre timerMort, largement commenté déjà dans les tutos précédents (notamment Rabidja en SFML 2 et SDL 1.2 / 2) : s'il vaut 0, c'est que tout va bien, sinon, il nous sert de temporisation (timer) avant de réinitialiser notre héros, suite à une mort fâcheuse (et douloureuse !). indecision Vous verrez donc que le code de la fonction est séparé en 2, selon la valeur de ce timer. wink Je vous renvoie aux commentaires pour plus d'infos.

   Globalement, l'ébauche de la fonction reste très semblable à celle de Rabidja, à part qu'on a supprimé tout ce qui était la physique des sauts, ce qui la simplifie pas mal (pour une fois que c'est plus simple de programmer un Action RPG ! cheeky). 

   On gère d'abord le timer d'invincibilité, qui nous servira plus tard, quand notre héros se fera blesser : son fonctionnement est ultra simple : à 0, le joueur n'est pas invincible.

   Pour le rendre invincible, il suffit de passer ce timer à la valeur désirée, et toutes les frames, ce timer est décrémenté de 1, jusqu'à 0, où le joueur ne sera alors à nouveau plus invincible.

   Plus la valeur allouée au timer sera grande, plus l'invincibilité durera longtemps. wink

   On remet ensuite nos vecteurs de déplacement dirX et dirY à 0. Pour l'instant, ceux-ci ne nous serviront à rien, puisque notre héros ne pourra pas encore bouger, mais cela nous sera utile dès le chapitre prochain. wink

   On gère ensuite l'état de la course : isrunning en fonction, de l'appui / ou non sur la touche pour courir.

   Et ensuite, on s'occupe de la gestion des déplacements : suivant la direction choisie (haut, bas, gauche, droite), on incrémente dirX ou dirY d'une valeur fixe (la vitesse de déplacement par frame PLAYER_SPEED, que vous pouvez modifier dans le header) + la valeur de isrunning (0 pixel si on ne court pas / 1 pixel de plus, si on court). On enregistre alors la direction dans laquelle va notre héros et si son état était IDLE, on le passe à WALK et on réinitialise l'animation.

   Enfin, si on n'appuie sur aucune touche, on passe le joueur en état IDLE, s'il ne l'est pas déjà, et on réinitialise l'animation correspondante. wink

   Vous noterez qu'on gère déjà les vecteurs de déplacement de notre joueur (dirX et dirY), mais celui-ci ne bougera pas, car il nous manque la fonction mapCollisions() qui sera chargée de recalculer ces vecteurs en fonction de la map, pour, par exemple, stopper le joueur quand il entrera dans un mur. C'est pourquoi notre héros ne bougera pas pour l'instant. wink

   Enfin, vous verrez que si notre héros meurt, on prévoit déjà de le réinitialiser sur le map, mais cela n'arrivera pas tout de suite, car il ne pourra pas bouger et il n'y aura pas de monstres pour l'instant ! Mais, rassurez-vous, ça va venir ! cheeky

 

   Voilà, c'était donc le plus gros de ce chapitre ! Passons maintenant à l'intégration de notre joueur dans le reste du code ! 

   Pour cela, on rajoute d'abord un include de notre nouvelle classe dans le fichier main.h :

Fichier : main.h : Remplacer par : 

//Legends of Meruvia - C++ / SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#include <cstdlib>
#include <iostream>
#include <SFML/Graphics.hpp>
 
#include "input.h"
#include "map.h"
#include "player.h"
 
using namespace std;
using namespace sf;
 
 
//Fonctions
void update(Input &input, Map &map, Player &player);
void draw(sf::RenderWindow &window, Map &map, Player &player);
 
 
// Taille de la fenêtre : 800x480 pixels
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 480;

 

   On rajoute également Player &player aux fonctions update() et draw() pour pouvoir faire appel à notre classe Player.

   Passons maintenant au main.cpp !

Fichier : main.cpp : Modifier : 

//Legends of Meruvia - C++ / SFML 2.3.2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
 
#include "main.h"
 
 
int main(int argc, char *argv[])
{
// Création d'une fenêtre en SFML
RenderWindow window(VideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32),
"Meruvia - Big Tuto A-RPG/SFML2 - Chapitre 6 - www.meruvia.fr");
 
 
//On active la synchro verticale
window.setVerticalSyncEnabled(true);
 
//Instanciation des classes
Input input;
Map map;
Player player;
 
//On commence au premier niveau
map.setLevel(1);
map.changeLevel();
 
//On initialise le player
player.initialize(map);
player.setGold(100);
 
// Boucle infinie, principale, du jeu
while (window.isOpen())
{
 
// Gestion des inputs
input.gestionInputs(window);
 
//Updates
update(input, map, player);
 
// Dessin - draw
draw(window, map, player);
 
window.display();
}
 
// On quitte
return 0;
 
}
 
 
 
//Fonction de mise à jour du jeu : gère la logique du jeu
void update(Input &input, Map &map, Player &player)
{
//On met à jour le player
player.update(input, map);
}
 
 
 
//Fonction de dessin du jeu : dessine tous les éléments
void draw(RenderWindow &window, Map &map, Player &player)
{
//On efface tout
window.clear();
 
// Affiche la map de tiles : layer 2 (couche du fond)
map.draw(2, window);
 
// Affiche la map de tiles : layer 1 (couche active : sol, etc.)
map.draw(1, window);
 
// Affiche le joueur
player.draw(map, window);
 
// Affiche la map de tiles : layer 3 (couche en foreground / devant)
map.draw(3, window);
}

   Ici, dans le main(), on instancie d'abord la classe Player player; puis on rajoute un appel à notre fonction player.initialize() avant la boucle principale du jeu et on met son nombre de pièces d'or (gold) à 100 : cela ne nous sert à rien, car on ne gèrera pas les pièces d'or dans cette 1ère partie du tuto, mais on aura l'air moins pauvre ! laugh On met enfin à jour l'appel à update() et draw() en rajoutant player.

   Ensuite, dans la fonction update() : On ajoute Player player; dans la déclaration de la fonction, on enlève testDefilement() qui ne nous servira plus à rien et on met à jour le joueur en appelant player.update(inputmap);


   Enfin dans la fonction draw() : On ajoute aussi Player player; dans la déclaration de la fonction et on affiche le joueur.

Attention : on affiche bien le joueur après les couches (layers) 2 (background / fond) et 1 (scène / action) MAIS avant la couche 3 qui est celle du foreground (décor devant), pour qu'il puisse passer dessous. wink

   Notez également, qu'on a retiré l'appel à testDefilement() pour pouvoir bien voir notre lapin s'animer au début de la map. On n'aura désormais plus besoin de cette fonction test, mais vous pouvez la garder en souvenir. cheeky
   Voilà, comme vous pouvez le constater, notre projet commence à prendre du volume, et ce n'est pas fini ! laugh

   Et voilà, plus qu'à compiler et Tadaaaa ! surprise

                                                Notre héros s'affiche et s'anime ! Essayez d'appuyer sur les touches de direction, pour voir ! angel

 

 

   Mais, mais !!?!! frown Il ne bouge pas !! surprise

   Eh oui, c'est normal, comme on l'a vu, on ne l'a pas encore programmé ça ! cheeky Il nous reste encore du chemin à parcourir avant de pouvoir le déplacer et gérer les collisions avec la map ! indecision

   Alors, @ bientôt pour la suite ! angel

                                                Jay.

 

 

 
 

Connexion

CoalaWeb Traffic

Today218
Yesterday297
This week1015
This month4689
Total1743896

26/04/24