
Big Tuto SFML 2 / Action-RPG : Legends of Meruvia
Chapitre 11 : Ajoutons des monstres !
Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 26 juin 2016
Date de révision : -
Prologue
Bien, nous avons désormais une grande map, offrant de nombreuses possibilités, mais... comment dire ?
C'est un peu vide ! ![]()
Il est donc temps de peupler nos maps avec quelques monstres ! ![]()
Dans ce chapitre, nous allons réemployer beaucoup de techniques déjà utilisées pour gérer notre joueur. A la différence près, que nos monstres devront se diriger tout seul (on leur programmera donc une Intelligence Artificielle - ou Bêtise Artificielle, c'est comme vous voulez
!) et il n'y en aura pas qu'un (imaginez un jeu avec UN SEUL MONSTRE par map...) ! ![]()
Il va donc falloir gérer un tableau de monstres, qui nous permettra d'afficher jusqu'à 50 monstres à l'écran (pourquoi 50 ?
Parce que c'est déjà pas mal
, mais si vous en voulez des millions, lâchez-vous !!
) ! Bon, je ne vous cache pas que ce chapitre va être un peu long et ardu...
Mais bon, il faut ce qu'il faut ! ![]()
Qui plus est, contrairement au tuto Rabidja, nous allons ajouter ici deux monstres différents, avec des P.V. différents. Et cerise sur le gâteau, nous allons voir comment afficher une jauge de P.V. au-dessous d'eux. ![]()
Classe et spritesheet
Pour nos monstres, nous allons créer une nouvelle classe Monster, très proche de celle du Player. Ajoutez donc une nouvelle classe à votre projet avec les fichiers monster.cpp et monster.h. ![]()
Quoi ?
Le même type de classe que pour notre héros ? ![]()
Mais oui, un monstre est très proche du héros et nécessite beaucoup de variables semblables.
D'ailleurs, ceux qui souhaiteraient parfaire leur maîtrise du C++ pourraient s'amuser à créer des templates et des classes dérivées. ![]()
Voilà, maintenant, avant de commencer, nous aurons besoin des feuilles de sprites de nos monstres. Je vous les redonne ci-dessous, mais normalement, elles devraient déjà figurer dans le dossier graphics de votre projet (notez qu'il existe aussi une variante verte de nos monstres, mais je ne m'en suis pas servi, car ils ne rendent pas très bien sur le gazon...
) :

monster1.png

monster2.png
Le code
On va commencer par aller dans le fichier map.h, pour vérifier que les variables suivantes s'y trouvent bien. Si vous vous rappelez, je vous avais dit d'ajouter toute une floppée de variables d'un coup, comme cela, nous n'aurions pas besoin de venir les y copier une par une fastidieusement.
Les voici :
- la variable nombreMonstres qui contiendra le nombre de monstres actuellement vivants, et qu'on devra donc gérer et afficher.
- la constante MONSTRES_MAX qui définira le nombre max de monstres qu'on acceptera d'initialiser par niveau (ici 50, mais vous pouvez en mettre plus, ce ne devrait pas être un réel problème en SFML2
). Une fois, ces 50 monstres initialisés, de nouveaux monstres ne seront pas créés, à moins que certains se fassent tuer.
Notez toutefois qu'avec 50 monstres par map, y'a de quoi faire ! ![]()
Fichier : map.h : Vérifiez que ces variables y sont déjà :
|
//Nombre max de monstres à l'écran
int nombreMonstres;
//Et dans la partie CONSTANTES :
//Nombre max de monstres gérés
const int MONSTRES_MAX = 50;
|
La classe Monster
Voilà, c'est dans cette classe que nous allons gérer nos monstres. Oui, mais de quoi va-t-on avoir besoin pour ça ? ![]()
La réponse est plus ou moins dans l'en-tête de la classe ci-dessous (copiez-la dans le nouveau fichier monster.h)
.
Elle ressemble beaucoup à celle du Player, comme je vous l'ai déjà dit, et je vous laisse donc la parcourir avant de revenir dessus. ![]()
Fichier : monster.h : Créer le fichier et y copier :
|
//Legends of Meruvia - C++ / SFML 2 //Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
#ifndef MONSTER_H #define MONSTER_H
#include <SFML/Graphics.hpp> #include <iostream>
class Map; class Player;
class Monster {
public:
//Constructeur Monster();
//Accesseurs int getType(void) const; int getLife(void) const; int getLifeMax(void) const; 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 getFrameNumber(void) const; int getFrameTimer(void) const; int getFrameMax(void) const; int getEtat(void) const; int getDirection(void) const; int getSaveX(void) const; int getSaveY(void) const; int getTimerMort(void) const; int getIATimer(void) const; int getInvincibleTimer(void) const; int getisHurt(void) const; int getHurtDirection(void) const;
//Mutateurs void setLife(int valeur); 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 setTimerMort(int valeur); void setInvincibleTimer(int valeur); void setisHurt(int valeur); void setHurtDirection(int valeur);
//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);
private:
//Variables de la classe en accès privé
// Points de vie/santé + chrono d'invicibilité float life, lifeMax; int invincibleTimer;
//Type de monstre int type;
//Si le monstre est touché int isHurt; int hurtDirection;
// Coordonnées du sprite int x, y;
// Largeur, hauteur du sprite int h, w;
// Checkpoint pour le héros (actif ou non) int 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; //Timer pour l'IA int IATimer;
// 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 sf::Texture Texture[2]; sf::Sprite Sprite[2]; int numberOfMonstersSprites;
//LifeGauge sf::Texture lifeGaugeTexture; sf::Sprite lifeGauge; sf::Texture energyTexture; sf::Sprite energy;
/******************/ /* 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;
/* Constante pour l'animation */ const int TIME_BETWEEN_2_FRAMES_PLAYER = 4;
//Dimensions du monstre const int MONSTER_WIDTH = 42; const int MONSTER_HEIGTH = 42;
//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;
const int IATime = 50; const int TIME_BETWEEN_2_SHOTS = 30; const int MOONWALK_TIMER = 8;
// Taille de la fenêtre : 800x480 pixels const int SCREEN_WIDTH = 800; const int SCREEN_HEIGHT = 480;
}; #endif |
Hum, voyons un peu comment nous allons gérer nos monstres, maintenant. ![]()
On veut qu'ils apparaissent quand on va rencontrer une tile monstre sur la map (que vous avez sans doute déjà placées un peu partout avec le level editor, sinon rassurez-vous, il y en a déjà avec les fichiers téléchargeables
). Il va donc falloir tester dans la fonction map.draw() si on s'apprête à dessiner une tile monstre et si c'est le cas, ne pas le faire, la supprimer et initialiser le monstre à la place grâce à la fonction initialize(). De plus, suivant le numéro de la tile monstre, il faudra aussi initialiser le bon monstre ! ![]()
Une fois notre monstre initialisé, il va falloir le gérer dans une fonction update() puis le dessiner dans une fonction draw() (que c'est original !
).
Ensuite, comme pour le joueur, il va falloir tester les collisions avec la map, pour qu'il puisse se déplacer dessus et il nous faudra aussi tester les collisions avec le joueur pour pouvoir le blesser ou tuer le monstre !
Cool ! ![]()
Je vous donne maintenant ci-dessous le code complet du fichier monster.cpp, que je vous laisse lire et on y revient ensuite
:
Fichier : monster.cpp : Créer le fichier et y copier :
|
//Legends of Meruvia - C++ / SFML 2 //Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
#include "monster.h" #include "player.h" #include "map.h"
using namespace std; using namespace sf;
//Constructeur
Monster::Monster() { numberOfMonstersSprites = 2;
//Chargement des spritesheets des monstres for (int i = 0; i < numberOfMonstersSprites; i++) { if (!Texture[i].loadFromFile("graphics/monster" + to_string(i + 1) + ".png")) { // Erreur cout << "Erreur durant le chargement des spritesheets des monstres." << endl; } else Sprite[i].setTexture(Texture[i]); }
if (!lifeGaugeTexture.loadFromFile("graphics/lifegauge.png")) { // Erreur cout << "Erreur durant le chargement de la jauge de vie des monstres." << endl; } else lifeGauge.setTexture(lifeGaugeTexture);
if (!energyTexture.loadFromFile("graphics/energy2.png")) { // Erreur cout << "Erreur durant le chargement de l'énergie pour la jauge de vie des monstres." << endl; } else energy.setTexture(energyTexture);
//Initialisation des variables : type = 1; dirX = 0; dirY = 0; life = lifeMax = 1; invincibleTimer = 0; x = y = 0; h = w = 42; checkpointActif = respawnX = respawnY = 0; frameNumber = frameTimer = frameMax = 0; etat = direction = 0; timerMort = 0; dirX = dirY = 0; saveX = saveY = 0; IATimer = IATime;
}
//Accesseurs int Monster::getType(void) const { return type; } int Monster::getLife(void) const { return life; } int Monster::getLifeMax(void) const { return lifeMax; } int Monster::getX(void) const { return x; } int Monster::getY(void) const { return y; } int Monster::getW(void) const { return w; } int Monster::getH(void) const { return h; } float Monster::getDirX(void) const { return dirX; } float Monster::getDirY(void) const { return dirY; } int Monster::getFrameNumber(void) const { return frameNumber; } int Monster::getFrameTimer(void) const { return frameTimer; } int Monster::getFrameMax(void) const { return frameMax; } int Monster::getEtat(void) const { return etat; } int Monster::getDirection(void) const { return direction; } int Monster::getSaveX(void) const { return saveX; } int Monster::getSaveY(void) const { return saveY; } int Monster::getTimerMort(void) const { return timerMort; } int Monster::getIATimer(void) const { return IATimer; } int Monster::getInvincibleTimer(void) const { return invincibleTimer; } int Monster::getisHurt(void) const { return isHurt; } int Monster::getHurtDirection(void) const { return hurtDirection; }
//Mutateurs void Monster::setLife(int valeur) { life = valeur; } void Monster::setX(int valeur) { x = valeur; } void Monster::setY(int valeur) { y = valeur; } void Monster::setW(int valeur) { w = valeur; } void Monster::setH(int valeur) { h = valeur; } void Monster::setDirX(float valeur) { dirX = valeur; } void Monster::setDirY(float valeur) { dirY = valeur; } void Monster::setTimerMort(int valeur) { timerMort = valeur; } void Monster::setInvincibleTimer(int valeur) { invincibleTimer = valeur; } void Monster::setisHurt(int valeur) { isHurt = valeur; } void Monster::setHurtDirection(int valeur) { hurtDirection = valeur; }
//Fonctions
void Monster::initialize(int Atype, int x1, int y1) { //On réinitialise la frame et le timer frameNumber = 0; frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER; frameMax = 8;
//On initialise le type type = Atype;
if (type == 1) life = lifeMax = 2; else if (type == 2) life = lifeMax = 3;
invincibleTimer = 0;
//On indique sa direction (il viendra à l'inverse du joueur, logique) direction = LEFT;
/* Ses coordonnées de démarrage seront envoyées par la fonction drawMap() en arguments */ x = x1; y = y1;
/* Hauteur et largeur de notre monstre (rajoutés dans les defs ;) ) */ w = MONSTER_WIDTH; h = MONSTER_HEIGTH;
//Variables nécessaires au fonctionnement de la gestion des collisions comme pour le héros timerMort = 0;
//On le met à l'état 0, étant donné qu'il n'en a qu'un seul (la marche) etat = 0;
isHurt = 0; hurtDirection = 0; }
void Monster::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 frameTimer--;
//Ensuite, on peut passer la main à notre fonction Sprite[type - 1].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 ;)
//Gestion du flip (retournement de l'image selon que le sprite regarde à droite ou à gauche) if (direction == LEFT) { Sprite[type - 1].setTextureRect(sf::IntRect( frameNumber * w, (etat * 3 * h + (direction - 1) * h), w, h)); window.draw(Sprite[type - 1]); } else { Sprite[type - 1].setTextureRect(sf::IntRect( (frameNumber + 1) * w, (etat * 3 * h + direction * h), -w, h)); window.draw(Sprite[type - 1]); }
//Dessin de la jauge de vie au-dessus du monstre int gaugeX = x + (w - 38) / 2 - map.getStartX(); int gaugeY = y + h + 4 - map.getStartY(); lifeGauge.setPosition(Vector2f(gaugeX, gaugeY));
//Calcul et affichage de l'énergie float gaugeEnergy = (life / lifeMax) * 32; for (int i = 0; i < gaugeEnergy; i++) { energy.setPosition(gaugeX + 3 + i, gaugeY + 4); window.draw(energy); } window.draw(lifeGauge);
}
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 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; }
void Monster::mapCollision(Map &map, Player &player) {
int i, x1, x2, y1, y2;
/* 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 //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) == 1 || map.getTile(y2, x2) == 1) { // 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) == 1 || map.getTile(y2, x1) == 1) { 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 if (map.getTile(y2, x1) == 1 || map.getTile(y2, x2) == 1) { //Si la tile est une tile solide, on y colle le monstre y = y2 * TILE_SIZE; y -= (h + 1); dirY = 0; }
}
else if (dirY < 0) {
// Déplacement vers le haut if (map.getTile(y1, x1) == 1 || map.getTile(y1, x2) == 1) { 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()) x = map.getMaxX() - w; else if (y < 0) y = 0; else if (y + h > map.getMaxY()) y = map.getMaxY() - h;
}
bool Monster::collideWithMonsters(Monster &monster) { //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 ((monster.getX() >= x + w) || (monster.getX() + monster.getW() <= x) || (monster.getY() >= y + h) || (monster.getY() + monster.getH() <= y) ) return false; //Sinon, on renvoie true else return true; }
void Monster::copy(Monster &monster) { //Copie des variables nécessaires : type = monster.getType(); life = monster.getLife(); lifeMax = monster.getLifeMax(); dirX = monster.getDirX(); dirY = monster.getDirY(); isHurt = monster.getisHurt(); hurtDirection = monster.getHurtDirection(); x = monster.getX(); y = monster.getY(); h = monster.getH(); w = monster.getW(); frameNumber = monster.getFrameNumber(); frameTimer = monster.getFrameTimer(); frameMax = monster.getFrameMax(); etat = monster.getEtat(); direction = monster.getDirection(); saveX = monster.getSaveX(); saveY = monster.getSaveY(); timerMort = monster.getTimerMort(); IATimer = monster.getIATimer(); invincibleTimer = monster.getInvincibleTimer(); } |
On va ensuite développer un peu plus longuement les fonctions suivantes : ![]()
La fonction update() :
Elle ressemble beaucoup à celle de la classe Player, mais avec des ajouts concernant l'IA en plus. Comme notre monstre peut se déplacer dans 4 directions, les possibilités sont plus complexes que pour un jeu de plateformes (Cf. Tuto Rabidja).
De base, nos monstres ne font que d'avancer, cependant, en plus de tester les collisions avec la map comme pour le joueur, on teste si celui-ci reste coincé (2 tours de suite à la même position, car bloqué contre un mur) et dans ce cas-là, on le fait changer de direction. On teste aussi s'il rencontre un autre monstre, pour éviter qu'ils ne se chevauchent. En effet, si ce n'est pas très gênant dans un jeu de plateformes (on peut penser que les monstres marchent côte à côte), c'est plus difficilement concevable dans un jeu vu du dessus ! ![]()
Concernant les déplacements, on ne peut pas laisser le monstre se déplacer indéfiniment dans la même direction, ça n'aurait pas de sens !
On met donc en place un chrono : IATimer au bout duquel, on changera la direction du monstre au hasard grâce à la formule : direction = rand() % 4; (notez que la même direction pourra être retirée au hasard
).
On gère également le recul du monstre, quand ce dernier sera touché par un coup d'épée, ou par de la magie (à venir dans les chapitres suivants
). En effet, s'il est touché (variable isHurt à true), on ne veut pas que notre monstre continue à foncer droit dans le joueur, mais plutôt qu'il recule ! C'est ce qu'on fait ici, en utilisant la variable hurtDirection qui enregistrera plus tard la direction d'où venait le coup pour pouvoir reculer du bon côté ! ![]()
Enfin, la fonction renvoie normalement 0 (return 0;) sauf si le monstre meurt. Dans ce cas, on renvoie 2, pour indiquer au main() qu'il doit lui régler son compte !
On verra cela en fin de chapitre.
Mais, au fait, on ne gère qu'un seul monstre dans notre classe !
Comment va-t-on faire pour en avoir plusieurs ! ![]()
C'est exact, mais vous oubliez qu'on est en POO, et qu'on peut donc créer autant d'objets Monster que l'on souhaite ! ![]()
Dans notre main(), on va donc créer 50 objets de type Monster, qu'on va ensuite passer en boucle (on effectuera donc l'update de chaque objet à la suite
).
Chaque objet contiendra donc les infos d'un monstre, mais là où cela va se compliquer, c'est justement quand l'un d'eux va mourir. Je vous ai déjà indiqué que la fonction update() allait renvoyer 2 dans ce cas, mais que va en faire le main() ? En effet, on ne peut pas laisser de case vide dans notre tableau d'objets Monster (on aurait pu utiliser une variable : alive ou active éventuellement, mais on aurait perdu de la place dans notre tableau et ça aurait compliqué les choses au final
).
Non, le plus simple, c'est tout simplement de copier le monstre de la dernière case du tableau dans la case du monstre mort et de réduire la taille de notre tableau d'une case (la dernière déjà copiée) en réduisant d'1 la variable nombreMonstres de la classe Map (cf ci-dessus). C'est pour cela que notre classe contient également une fonction copy() qui permet de copier les variables d'un objet Monster à un autre.
Cette fonction s'apparente un peu à un constructeur par copie, à la différence qu'ici notre objet existe déjà. ![]()
Note : Nous avons deux types de monstres différents, cependant leurs différences se situent simplement au niveau du sprite et du nombre de P.V., cela n'a donc pas d'impact sur notre fonction update(). Toutefois, vous pouvez envisager de les faire se déplacer à des vitesses différentes (en utilisant if (type == 1) par exemple). ![]()
La fonction collideWithMonsters() :
C'est elle qui va gérer les collisions entre les monstres.
Son fonctionnement est simple : on teste si les deux sprites ne se touchent pas. Si c'est la cas, on renvoie false, et c'est fini ! ![]()
Sinon, c'est qu'ils se touchent et on renvoie true ! ![]()
La fonction draw() :
Cette fonction est similaire à celle qui affiche le héros. Mais cette fois, on va afficher de méchants monstres !
On n'a pas non plus la partie qui gère le clignotement, mais vous pouvez la rajouter, si vous préférez. ![]()
Sinon, vous remarquerez qu'on affiche le bon sprite, selon notre tableau de sprite en écrivant : window.draw(Sprite[type - 1]);
Pourquoi le -1 ?
Tout simplement parce que le sprite du monstre 1 se trouve dans la case 0 de notre tableau (erreur classique en prog !) ![]()
Vous noterez enfin qu'on affiche une jauge de PV (lifegauge) sous le sprite de notre monstre. On la remplit à l'aide d'un sprite d'1 pixel de large (energy). Pour ce faire, on applique un simple produit en croix : (life / lifeMax) * 32. Mais pourquoi 32 ?
Eh bien simplement car notre jauge fait 32 pixels de long ! ![]()
La fonction mapCollision() :
Elle est identique à celle de la classe Player, de base. Notre monstre ne pourra pas entrer dans des bâtiments ou changer de map !
(Manquerait plus que ça !
)
La fonction copy() :
Comme nous l'avons évoqué ci-dessus, cette fonction permet de copier les variables d'un objet Monster dans un autre, pour supprimer les monstres morts. ![]()
Et voilà pour notre classe Monster qui compose le plus gros de ce chapitre avec ce qui va suivre : la détection des tiles monstres ! On est parti ! ![]()
Détectons les tiles Monster
On retourne dans notre fichier map.cpp pour rajouter d'abord un include vers notre fichier monster.h:
Fichier : map.cpp : Rajouter en haut du fichier :
|
#include "monster.h"
|
Puis on rajoute une fonction detectTilesSpeciales() à notre fichier map.cpp. C'est elle qui va se charger de repérer si la tile à dessiner par la fonction draw() est une tile monstre ou pas. Si tel est le cas, on initialisera un monstre si on n'a pas déjà atteint la limite MONSTRES_MAX et on effacera la tile du tableau en la remplaçant par la tile 0, afin de ne pas créer des monstres en boucle à chaque frame !
Notez que cela n'a aucune incidence sur notre fichier map. ![]()
Pour ceux qui ont suivi les tutos antérieurs, vous noterez aussi que cette fonction faisait généralement partie de la fonction draw() dans ces autres tutos. J'ai choisi de l'en dissocier ici par souci de lisibilité, c'est tout. ![]()
Fichier : map.cpp : Rajouter la fonction detectTilesSpeciales() :
|
void Map::detectTilesSpeciales(int mapY, int mapX, Monster monster[]) { //Si la tile à dessiner n'est pas une tile vide if (tile4[mapY][mapX] != 0) { /*On teste si c'est une tile monstre */ if (tile4[mapY][mapX] >= TILE_MONSTRE_DEBUT && tile4[mapY][mapX] <= TILE_MONSTRE_FIN) { //Si on le peut, on initialise un monstre en envoyant les coordonnées de la tile if (nombreMonstres < MONSTRES_MAX) { monster[nombreMonstres].initialize(tile4[mapY][mapX] - TILE_MONSTRE_DEBUT + 1, mapX * TILE_SIZE, mapY * TILE_SIZE); nombreMonstres++; }
//Et on efface cette tile de notre tableau pour éviter un spawn de monstres //infini ! tile4[mapY][mapX] = 0; }
}
} |
Modifions désormais notre fonction map.draw(). Cela ne va pas être bien compliqué puisqu'il suffira juste d'ajouter l'appel à notre fonction detectTilesSpeciales() dans la boucle de la layer 1 :
Fichier : map.cpp : Remplacer la fonction précédente par :
|
void Map::draw(int layer, RenderWindow &window, Monster monster[]) { int x, y, mapX, x1, x2, mapY, y1, y2, xsource, ysource, a;
/* On initialise mapX à la 1ère colonne qu'on doit blitter. Celle-ci correspond au x de la map (en pixels) divisés par la taille d'une tile (32) pour obtenir la bonne colonne de notre map Exemple : si x du début de la map = 1026, on fait 1026 / 32 et on sait qu'on doit commencer par afficher la 32eme colonne de tiles de notre map */ mapX = startX / TILE_SIZE;
/* Coordonnées de départ pour l'affichage de la map : permet de déterminer à quels coordonnées blitter la 1ère colonne de tiles au pixel près (par exemple, si la 1ère colonne n'est visible qu'en partie, on devra commencer à blitter hors écran, donc avoir des coordonnées négatives - d'où le -1). */ x1 = (startX % TILE_SIZE) * -1;
/* Calcul des coordonnées de la fin de la map : jusqu'où doit-on blitter ? Logiquement, on doit aller à x1 (départ) + SCREEN_WIDTH (la largeur de l'écran). Mais si on a commencé à blitter en dehors de l'écran la première colonne, il va falloir rajouter une autre colonne de tiles sinon on va avoir des pixels blancs. C'est ce que fait : x1 == 0 ? 0 : TILE_SIZE qu'on pourrait traduire par: if(x1 != 0) x2 = x1 + SCREEN_WIDTH + TILE_SIZE , mais forcément, c'est plus long ;)*/ x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);
/* On fait exactement pareil pour calculer y */ mapY = startY / TILE_SIZE; y1 = (startY % TILE_SIZE) * -1; y2 = y1 + SCREEN_HEIGHT + (y1 == 0 ? 0 : TILE_SIZE);
//On met en place un timer pour animer la map if (mapTimer <= 0) { if (tileSetNumber == 0) { tileSetNumber = 1; mapTimer = TIME_BETWEEN_2_FRAMES * 3; } else { tileSetNumber = 0; mapTimer = TIME_BETWEEN_2_FRAMES * 3; }
} else mapTimer--;
/* Dessine la carte en commençant par startX et startY */
/* On dessine ligne par ligne en commençant par y1 (0) jusqu'à y2 (480) A chaque fois, on rajoute TILE_SIZE (donc 32), car on descend d'une ligne de tile (qui fait 32 pixels de hauteur) */ if (layer == 1) { for (y = y1; y < y2; y += TILE_SIZE) { /* A chaque début de ligne, on réinitialise mapX qui contient la colonne (0 au début puisqu'on ne scrolle pas) */ mapX = startX / TILE_SIZE;
/* A chaque colonne de tile, on dessine la bonne tile en allant de x = 0 à x = 640 */ for (x = x1; x < x2; x += TILE_SIZE) { //On détecte les tiles spéciales detectTilesSpeciales(mapY, mapX, monster);
/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro de la tile */ a = tile[mapY][mapX];
/* Calcul pour obtenir son y (pour un tileset de 10 tiles par ligne, d'où le 10 */ ysource = a / 10 * TILE_SIZE; /* Et son x */ xsource = a % 10 * TILE_SIZE;
/* Fonction qui blitte la bonne tile au bon endroit suivant le timer */ if (tileSetNumber == 0) { tileSet1.setPosition(Vector2f(x, y)); tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE)); window.draw(tileSet1); } else { tileSet1B.setPosition(Vector2f(x, y)); tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE)); window.draw(tileSet1B); }
mapX++; }
mapY++; } }
else if (layer == 2) { //Deuxième couche de tiles ;) for (y = y1; y < y2; y += TILE_SIZE) { mapX = startX / TILE_SIZE;
for (x = x1; x < x2; x += TILE_SIZE) { /* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro de la tile */ a = tile2[mapY][mapX];
/* Calcul pour obtenir son y (pour un tileset de 10 tiles par ligne, d'où le 10 */ ysource = a / 10 * TILE_SIZE; /* Et son x */ xsource = a % 10 * TILE_SIZE;
/* Fonction qui blitte la bonne tile au bon endroit suivant le timer */ if (tileSetNumber == 0) { tileSet1.setPosition(Vector2f(x, y)); tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE)); window.draw(tileSet1); } else { tileSet1B.setPosition(Vector2f(x, y)); tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE)); window.draw(tileSet1B); }
mapX++; }
mapY++; } }
else if (layer == 3) { //Troisième couche de tiles ;) for (y = y1; y < y2; y += TILE_SIZE) { mapX = startX / TILE_SIZE;
for (x = x1; x < x2; x += TILE_SIZE) { /* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro de la tile */ a = tile3[mapY][mapX];
/* Calcul pour obtenir son y (pour un tileset de 10 tiles par ligne, d'où le 10 */ ysource = a / 10 * TILE_SIZE; /* Et son x */ xsource = a % 10 * TILE_SIZE;
/* Fonction qui blitte la bonne tile au bon endroit suivant le timer */ if (tileSetNumber == 0) { tileSet1.setPosition(Vector2f(x, y)); tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE)); window.draw(tileSet1); } else { tileSet1B.setPosition(Vector2f(x, y)); tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE)); window.draw(tileSet1B); }
mapX++; }
mapY++; } }
} |
Sans oublier d'adapter le header en rajoutant la class Monster en haut du fichier, en ajoutant les prototypes de detectTilesSpeciales() et en mettant à jour le prototype de draw(). ![]()
Fichier : map.h : Modifier le code suivant :
|
//Fonctions void loadMap(std::string filename); void draw(int layer, sf::RenderWindow &window, Monster monster[]); void changeLevel(void); void testDefilement(void); void detectTilesSpeciales(int mapY, int mapX, Monster monster[]); POINT detectWarpSpe(int number); |
Mise à jour du fichier player.cpp
Mettons désormais à jour notre fichier player.cpp en rajoutant map.setNombreMonstres(0); aux fonctions initialize() et reinitialize() :
Fichier : player.cpp : Mettre à jour les fonctions :
|
void Player::initialize(Map &map) { //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;
x = map.getBeginX(); y = map.getBeginY();
//On recentre la caméra map.setStartX(map.getBeginX() - (SCREEN_WIDTH / 2)); map.setStartY(map.getBeginY() - (SCREEN_HEIGHT / 2));
/* 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;
//On réinitialise le nombre de monstres map.setNombreMonstres(0);
map.setWarpDirection(-1);
}
void Player::reinitialize(Map &map) {
// Coordonnées de démarrage de notre héros selon la direction de warp //Si on n'a pas warpé, alors on commence aux coordonnées de départ de la map if (map.getWarpDirection() == -1) { x = map.getBeginX(); y = map.getBeginY();
//On recentre la caméra map.setStartX(map.getBeginX() - (SCREEN_WIDTH / 2)); map.setStartY(map.getBeginY() - (SCREEN_HEIGHT / 2)); } //Si on a warpé en haut else if (map.getWarpDirection() == UP) { //On change la valeur en y du héros pour qu'il se //trouve en bas de la map y = map.getMaxY() - h - 1;
//On recentre la caméra map.setStartY(map.getMaxY() - SCREEN_HEIGHT); } //Si on a warpé en bas else if (map.getWarpDirection() == DOWN) { //On change la valeur en y du héros pour qu'il se //trouve en haut de la map y = 1;
//On recentre la caméra map.setStartY(0); } //Si on a warpé à gauche else if (map.getWarpDirection() == LEFT) { //On change la valeur en x du héros pour qu'il se //trouve à droite de la map x = map.getMaxX() - w - 1;
//On recentre la caméra map.setStartX(map.getMaxX() - SCREEN_WIDTH); } //Si on a warpé à droite else if (map.getWarpDirection() == RIGHT) { //On change la valeur en x du héros pour qu'il se //trouve à gauche de la map x = 1;
//On recentre la caméra map.setStartX(0); }
//Si on a utilisé une warp spéciale else if (map.getWarpDirection() == 4) { POINT point;
//On détecte la warp spéciale utilisée (1, 2, etc) //et on enregistre ses coordonnées dans point point.x = map.detectWarpSpe(numberSPE + SPE1).x; point.y = map.detectWarpSpe(numberSPE + SPE1).y;
//On place le héros près de cette warp (sans la toucher) //suivant la direction qu'il avait en arrivant : //Ainsi s'il allait vers le haut, on va le faire réapparaître //au-dessus de la warp, et ainsi de suite... if (direction == UP) { x = point.x + 6; y = point.y - h - 6; } else if (direction == DOWN) { x = point.x + 6; y = point.y + TILE_SIZE + 6; } else if (direction == RIGHT) { x = point.x + TILE_SIZE + 6; y = point.y + 6; } else if (direction == LEFT) { x = point.x - w - 6; y = point.y + 6; } else { x = map.getBeginX(); y = map.getBeginY(); }
//On recentre la caméra map.setStartX(x - (SCREEN_WIDTH / 2)); map.setStartY(y - (SCREEN_HEIGHT / 2)); }
//On réinitialise le nombre de monstres map.setNombreMonstres(0);
} |
Branchements dans la boucle principale du jeu
Fichier : main.h : Mettre à jour :
|
//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" #include "monster.h"
using namespace std; using namespace sf;
//Fonctions void update(Input &input, Map &map, Player &player, Monster monster[]); void draw(sf::RenderWindow &window, Map &map, Player &player, Monster monster[]);
// Taille de la fenêtre : 800x480 pixels const int SCREEN_WIDTH = 800; const int SCREEN_HEIGHT = 480;
//Nombre max de monstres à l'écran const int MONSTRES_MAX = 50; |
Et enfin, voilà le code mis à jour de notre fichier main.cpp :
Fichier : main.cpp : Mettre à jour :
|
//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 11 - www.meruvia.fr");
//On active la synchro verticale window.setVerticalSyncEnabled(true);
//Instanciation des classes Input input; Map map; Player player; //On instancie autant de classes que de monstres gérables Monster monster[MONSTRES_MAX];
//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, monster);
// Dessin - draw draw(window, map, player, monster);
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, Monster monster[]) { //On met à jour le player player.update(input, map);
//On met à jour les monstres un par un for (int i = 0; i < map.getNombreMonstres(); i++) { if (monster[i].update(i, map, player, monster) == 2) { //Si l'update du monstre renvoie 2, c'est qu'il doit mourir : //on copie à sa place le dernier monstre avant de retirer un monstre monster[i].copy(monster[map.getNombreMonstres() - 1]); map.setNombreMonstres(map.getNombreMonstres() - 1); }
} }
//Fonction de dessin du jeu : dessine tous les éléments void draw(RenderWindow &window, Map &map, Player &player, Monster monster[]) { //On efface tout window.clear();
// Affiche la map de tiles : layer 2 (couche du fond) map.draw(2, window, monster);
// Affiche la map de tiles : layer 1 (couche active : sol, etc.) map.draw(1, window, monster);
// Affiche le joueur player.draw(map, window);
//On affiche les monstres un par un for (int i = 0; i < map.getNombreMonstres(); i++) { monster[i].draw(map, window); }
// Affiche la map de tiles : layer 3 (couche en foreground / devant) map.draw(3, window, monster);
} |
On y instancie d'abord, toutes nos classes Monster : Monster monster[MONSTRES_MAX];
Il s'agit là de notre tableau de sprites, représentés sous la forme d'un tableau d'objets de type Monster. Il ne faut pas que la notion de tableau de monstres vous effraie. En fait, on crée juste 50 objets Monster qu'on met dans un tableau pour pouvoir les appeler ainsi : monster[1], monster[2], etc... Pratique, non ? ![]()
Dans la boucle principale, maintenant, on met à jour les appels vers draw() et update().
Enfin, on adapte nos fonctions draw() et update() pour qu'elles dessinent et gèrent nos monstres.
Notez que dans la fonction update() si l'update() des monstres renvoie 2, c'est que le monstre en question doit mourir. On appelle alors la fonction de copie (copy) pour copier le dernier monstre du tableau à sa place, et on raccourcit notre tableau en décrémentant la variable nombreMonstres (c'est le plus simple
).
Voilà, plus qu'à compiler et à lancer le programme !
De beaux globulos jaunes et bleus se baladent maintenant gentiment sur la map ! ![]()
Ne sont-ils pas mignons !?! ![]()

@ bientôt pour la suite ! ![]()
Jay.

English
Français 