Big Tuto SFML 2 : Rabidja v. 3.0

Chapitre 12 : Ajoutons des monstres !

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 20 février 2015
Date de révision : 23 mai 2016

      Prologue

   Bien, notre héros prend maintenant vie ! smiley Il est temps de tester ses réflexes de ninja avec quelques monstres ! Ahah ! laugh

   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 cheeky !) et il n'y en aura pas qu'un (imaginez un jeu de plateformes avec UN SEUL MONSTRE dans tout le niveau...) !

   Il va donc falloir gérer un tableau de monstres, qui nous permettra d'afficher jusqu'à 50 monstres à l'écran (pourquoi 50 ? surprise Parce que c'est déjà pas mal wink, mais si vous en voulez des millions, lâchez-vous !! laugh) ! Bon, je ne vous cache pas que ce chapitre va être un peu long et ardu... frown Mais bon, il faut ce qu'il faut ! angel

 

      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.hwink

   Quoi ? surprise Le même type de classe que pour notre super lapin ? frown

   Mais oui, un monstre est très proche du héros et nécessite beaucoup de variables semblables. wink D'ailleurs, ceux qui souhaiteraient parfaire leur maîtrise du C++ pourraient s'amuser à créer des templates et des classes dérivées. cheeky

   Voilà, maintenant, avant de commencer, nous aurons besoin de la feuille de sprites de notre monstre. Je vous la donne ci-dessous. Vous pouvez l'enregistrer dans le dossier graphics, comme d'habitude :

monster1.png

      Le code

   On va commencer par aller dans le fichier map.h, pour créer une nouvelle variable et une nouvelle constante qui vont nous servir ici:

- la variable nombreMonstres contiendra le nombre de monstres actuellement vivants, et qu'on devra donc gérer et afficher.

- la constante MONSTRES_MAX 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 wink). Une fois, ces 50 monstres initialisés, les nouveaux monstres ne seront pas créés, à moins que certains se fassent tuer. cheeky

   

Fichier : map.h : Rajoutez au bon endroit :

//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 ? frown

   La réponse est plus ou moins dans l'en-tête de la classe ci-dessous (copiez-la dans le nouveau fichier monster.hwink.

   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. cheeky

 

Fichier : monster.h : Créer le fichier et y copier :

//Rabidja 3 - nouvelle version convertie en 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 getX(void) const;
int getY(void) const;
int getW(void) const;
int getH(void) const;
float getDirX(void) const;
float getDirY(void) const;
bool getOnGround(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;
bool getJumping(void) const;
float getDirXmem(void) const;
float getDirYmem(void) const;
int getPosXmem(void) const;
int getPosYmem(void) const;
int getWasOnGround(void) const;
int getWasOnSlope(void) const;
int getTimerMort(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);
 
//Fonctions
void initialize(int x1, int y1);
void draw(Map &map, sf::RenderWindow &window);
int update(Map &map, Player &player);
void mapCollision(Map &map, Player &player);
int checkSlope(Map &map, Player &player);
int collide(Player &player);
int checkFall(Map &map);
void copy(Monster &monster);
 
 
private:
 
//Variables de la classe en accès privé
 
// Points de vie/santé + chrono d'invicibilité
int life, invincibleTimer;
 
// 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;
 
// Variables utiles pour la gestion des collisions :
//Est-il sur le sol, chrono une fois mort
int timerMort;
bool onGround;
 
//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;
 
//Variable pour le double saut
bool jumping;
 
//Gestion des pentes par Stephantasy
float dirXmem, dirYmem;
int posXmem, posYmem;
int wasOnGround;
int wasOnSlope;
 
 
//Spritesheet
sf::Texture Texture;
sf::Sprite Sprite;
 
 
/******************/
/* 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 = 40;
const int MONSTER_HEIGTH = 50;
 
//Valeurs attribuées aux états/directions
const int IDLE = 0;
const int WALK = 1;
const int JUMP1 = 2;
const int JUMP2 = 3;
const int DEAD = 4;
 
const int RIGHT = 1;
const int LEFT = 2;
 
//Constantes définissant la gravité et la vitesse max de chute
const double GRAVITY_SPEED = 0.6;
const int MAX_FALL_SPEED = 15;
const int JUMP_HEIGHT = 10;
 
// Taille de la fenêtre : 800x480 pixels
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 480;
 
 
/*************************/
/* VALEURS DES TILES */
/************************/
 
// Constante définissant le seuil entre les tiles traversables
// (blank) et les tiles solides
const int BLANK_TILE = 99;
 
//Plateformes traversables
const int TILE_TRAVERSABLE = 80;
 
//Tiles Power-ups
const int TILE_POWER_UP_DEBUT = 77;
const int TILE_POWER_UP_FIN = 79;
const int TILE_POWER_UP_COEUR = 78;
 
//Autres Tiles spéciales
const int TILE_RESSORT = 125;
const int TILE_CHECKPOINT = 23;
const int TILE_MONSTRE = 136;
const int TILE_PIKES = 127;
 
//Tiles plateformes mobiles
const int TILE_PLATEFORME_DEBUT = 130;
const int TILE_PLATEFORME_FIN = 131;
 
// Tiles pentes à 26.5° ; BenH = de BAS en HAUT ; HenB = De HAUT en BAS
const int TILE_PENTE_26_BenH_1 = 69;
const int TILE_PENTE_26_BenH_2 = 70;
const int TILE_PENTE_26_HenB_1 = 71;
const int TILE_PENTE_26_HenB_2 = 72;
 
};
#endif

 

   Hum, voyons un peu comment nous allons gérer nos monstres, maintenant. cheeky

   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 wink). 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().

   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 ! cheeky).

   Ensuite, comme pour le joueur, il va falloir tester les collisions avec la map, pour qu'il puisse se déplacer dessus (y compris les pentes), et il nous faudra aussi tester les collisions avec le joueur pour pouvoir le blesser ou tuer le monstre ! wink Cool ! angel

   Je vous donne maintenant ci-dessous le code complet du fichier monster.cpp, que je vous laisse lire :

 

Fichier : monster.cpp : Créer le fichier et y copier :

//Rabidja 3 - nouvelle version intégralement en 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()
{
//Chargement de la spritesheet du monstre
if (!Texture.loadFromFile("graphics/monster1.png"))
{
// Erreur
cout << "Erreur durant le chargement du spritesheet du monstre." << endl;
}
else
Sprite.setTexture(Texture);
 
//Initialisation des variables :
dirX = 0;
dirY = 0;
life = 1;
invincibleTimer = 0;
x = y = h = w = 0;
checkpointActif = respawnX = respawnY = 0;
frameNumber = frameTimer = frameMax = 0;
etat = direction = 0;
timerMort = 0;
onGround = false;
dirX = dirY = 0;
saveX = saveY = 0;
jumping = false;
 
}
 
 
//Accesseurs
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; }
bool Monster::getOnGround(void) const { return onGround; }
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; }
bool Monster::getJumping(void) const { return jumping; }
float Monster::getDirXmem(void) const { return dirXmem; }
float Monster::getDirYmem(void) const { return dirYmem; }
int Monster::getPosXmem(void) const { return posXmem; }
int Monster::getPosYmem(void) const { return posYmem; }
int Monster::getWasOnGround(void) const { return wasOnGround; }
int Monster::getWasOnSlope(void) const { return wasOnSlope; }
int Monster::getTimerMort(void) const { return timerMort; }
 
 
//Mutateurs
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::setOnGround(bool valeur) { onGround = valeur; }
void Monster::setTimerMort(int valeur) { timerMort = valeur; }
 
 
//Fonctions
 
void Monster::initialize(int x1, int y1)
{
//On réinitialise la frame et le timer
frameNumber = 0;
frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;
frameMax = 8;
 
//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é 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;
onGround = 0;
 
//On le met à l'état 0, étant donné qu'il n'en a qu'un seul (la marche)
etat = 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.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)
{
//On n'a plus de flip auto en SFML, il faut donc tout calculer
Sprite.setTextureRect(sf::IntRect(
(frameNumber + 1) * w,
etat * h,
-w, h));
window.draw(Sprite);
}
else
{
Sprite.setTextureRect(sf::IntRect(
frameNumber * w,
etat * h,
w, h));
window.draw(Sprite);
}
 
}
 
 
int Monster::update(Map &map, Player &player)
{
//Même fonctionnement que pour le joueur
if (timerMort == 0)
{
 
dirX = 0;
dirY += GRAVITY_SPEED;
 
if (dirY >= MAX_FALL_SPEED)
dirY = MAX_FALL_SPEED;
 
//Test de collision dans un mur : si la variable x reste la même, deux tours de boucle
//durant, le monstre est bloqué et on lui fait faire demi-tour.
if (x == saveX || checkFall(map) == 1)
{
if (direction == LEFT)
{
direction = RIGHT;
}
else
{
direction = LEFT;
}
}
 
//Déplacement du monstre selon la direction
if (direction == LEFT)
dirX -= 2;
else
dirX += 2;
 
 
//On sauvegarde les coordonnées du monstre pour gérer le demi-tour
//avant que mapCollision() ne les modifie.
saveX = x;
 
//On détecte les collisions avec la map comme pour le joueur
mapCollision(map, player);
 
//On détecte les collisions avec le joueur
//Si c'est égal à 1, on diminue ses PV
if (collide(player) == 1)
{
if (player.getLife() > 1)
{
//On blesse le joueur
player.playerHurts();
 
//Et on tue le monstre
timerMort = 1;
}
else
{
player.killPlayer();
}
}
 
else if (collide(player) == 2)
{
//On met le timer à 1 pour tuer le monstre intantanément
timerMort = 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;
 
// Récup des infos pour la gestion des pentes (par Stephantasy)
dirXmem = dirX;
wasOnGround = onGround;
dirYmem = dirY;
posXmem = x;
posYmem = y;
 
/* D'abord, on considère le joueur en l'air jusqu'à temps
d'être sûr qu'il touche le sol */
onGround = 0;
 
/* 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 initiés dans updatePlayer
//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) > BLANK_TILE || map.getTile(y2, x2) > BLANK_TILE)
{
// 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) > BLANK_TILE || map.getTile(y2, x1) > BLANK_TILE)
{
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
 
//Gestion des plateformes traversables : elles se situent juste avant
//les tiles bloquantes dans notre tileset (dont la valeur butoire est
//BLANK_TILE). Il suffit donc d'utiliser le numéro de la première tile
//traversable au lieu de BLANK_TILE pour bloquer le joueur,
//seulement quand il tombe dessus (sinon, il passe au-travers
//et le test n'est donc pas effectué dans les autres directions
if (map.getTile(y2, x1) > TILE_TRAVERSABLE || map.getTile(y2, x2) > TILE_TRAVERSABLE)
{
//Si la tile est une plateforme ou une tile solide, on y colle le joueur et
//on le déclare sur le sol (onGround).
y = y2 * TILE_SIZE;
y -= h;
dirY = 0;
onGround = 1;
}
 
}
 
else if (dirY < 0)
{
 
// Déplacement vers le haut
if (map.getTile(y1, x1) > BLANK_TILE || map.getTile(y1, x2) > BLANK_TILE)
{
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;
}
}
 
// Contrôle des pentes
checkSlope(map, player);
 
/* 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())
{
//Si on touche le bord droit de l'écran, on annule
//et on limite le déplacement du joueur
x = map.getMaxX() - w - 1;
}
 
//Maintenant, s'il sort de l'écran par le bas (chute dans un trou sans fond), on lance le timer
//qui gère sa mort et sa réinitialisation (plus tard on gèrera aussi les vies).
if (y > map.getMaxY())
{
timerMort = 60;
}
}
 
 
 
int Monster::checkSlope(Map &map, Player &player)
{
//Adaptation de la fonction écrite par Stephantasy en SFML2
 
/*
* 2014/12/21 by stephantasy
*
* Fonction permettant de placer correctement le Sprite sur une Tile de type "pente".
*
* ATTENTION ! S'assurer que les Tiles "pentes" soient < BLANK_TILE dans le TileSet.
* En effet, "mapCollision" doit considérer que le Sprite peut traverser ces Tiles
* autant sur le plan horizontal que vertical.
*/
 
// Initialisation
int isOnSlope, goOnSlope, goOnSlopeUp, goOnSlopeDown;
isOnSlope = goOnSlope = goOnSlopeUp = goOnSlopeDown = 0;
int diagOffSet = 0;
int yc;
int resetWasOnSlope = 0, checkWasOnSlope = 1;
 
// Si on ne touche plus le sol, on ne se soucis plus de savoir qu'on était sur une pente.
if (wasOnGround == 0)
{
wasOnSlope = 0;
}
 
// On récupère la position du Sprite (à noter qu'on effectue les tests avec le point "en bas au centre" du Sprite)
int posIniX = posXmem + w / 2;
int xa = posIniX / TILE_SIZE;
int posIniY = posYmem + h - 1;
int ya = posIniY / TILE_SIZE;
 
// On récupère la destination du Sprite
int posEndX = posIniX + dirXmem;
int xb = posEndX / TILE_SIZE;
int posEndY = posIniY + 1 + dirYmem;
int yb = posEndY / TILE_SIZE;
 
// Est-ce qu'on est sur une pente ?
if (map.getTile(ya, xa) >= TILE_PENTE_26_BenH_1 && map.getTile(ya, xa) <= TILE_PENTE_26_HenB_2)
{
isOnSlope = map.getTile(ya, xa);
}
 
// Est-ce qu'on va sur une pente ?
if (map.getTile(yb, xb) >= TILE_PENTE_26_BenH_1 && map.getTile(yb, xb) <= TILE_PENTE_26_HenB_2)
{
goOnSlope = map.getTile(yb, xb);
}
 
// Est-ce que la Tile au-dessus de la destination du Sprite est une pente ?
if (map.getTile(yb - 1, xb) >= TILE_PENTE_26_BenH_1 && map.getTile(yb - 1, xb) <= TILE_PENTE_26_HenB_2)
{
goOnSlopeUp = map.getTile(yb - 1, xb);
}
 
// Est-ce que la Tile au-dessous de la destination du Sprite est une pente ?
// La subtilité ici est qu'on est (normalement) déjà sur une pente, mais que le Sprite se
// déplace si vite, qu'on ne voit pas que la Tile suivante est encore une pente !
// En fait, ce n'est pas grave, c'est juste un peu plus réaliste de "coller" le Sprite au sol,
// plutôt que de laisser le Sprite "flotter" dans les airs jusqu'au sol, quelques pixels plus loin...
// (C'est surtout vrai pour les Tiles à pentes raides ou à grande vitesse)
else if (map.getTile(yb + 1, xb) >= TILE_PENTE_26_BenH_1 && map.getTile(yb + 1, xb) <= TILE_PENTE_26_HenB_2)
{
goOnSlopeDown = map.getTile(yb + 1, xb);
}
 
// Si on se dirige vers une pente
if (goOnSlope > 0)
{
double a, b;
 
// On récupère l'équation de la pente
if (!player.slopeEquation(goOnSlope, &a, &b)){ return 0; }
 
// On determine la position en x du Sprite dans la Tile
int xPos = posEndX - xb*TILE_SIZE;
 
// On calcule sa position en y
int yPos = a * xPos + b;
 
// On borne le ypos à 31
if (yPos > 31) { yPos = 31; }
 
// On calcul l'Offset entre le haut de la Tile et le sol de la pente
diagOffSet = TILE_SIZE - yPos;
 
// La Tile "pente" est à la même hauteur que la Tile où se trouve le Sprite
yc = yb;
 
// Le Sprite est à présent sur une pente
wasOnSlope = goOnSlope;
 
// Puisqu'on traite le Sprite sur la pente,
// inutile de traiter le Sprite quittant la pente
checkWasOnSlope = 0;
}
 
// S'il y a une pente au dessus de celle où on va
// (c'est à dire la Tile juste à côté du Sprite, car avec la gravité,
// on "pointe" toujours la Tile en dessous)
else if (goOnSlopeUp > 0)
{
double a, b;
if (!player.slopeEquation(goOnSlopeUp, &a, &b)){ return 0; }
int xPos = posEndX - xb*TILE_SIZE;
int yPos = a * xPos + b;
if (yPos > 31) { yPos = 31; }
diagOffSet = TILE_SIZE - yPos;
 
// La Tile "pente" est 1 Tile au-dessus de la Tile où se trouve le Sprite
yc = yb - 1;
 
wasOnSlope = goOnSlopeUp;
checkWasOnSlope = 0;
}
 
// Si on tombe ici, c'est que le Sprite ne va pas sur une pente mais qu'il est sur une pente.
else if (isOnSlope > 0)
{
// Si on est en l'air,
if (wasOnGround == 0)
{
 
// Il faut vérifier si le Sprite doit être stoppé par la pente.
// Pour cela, on contrôle si la trajectoire du sprite croise le sol de la pente.
// On vérifie donc si ces segments se croisent et si oui, en quel point.
Player::Point segmentD, segmentF;
 
// On récupère le segment de la pente
player.getSlopeSegment(xa, ya, isOnSlope, segmentD, segmentF);
 
// On récupère la position du point de collision entre les segments (s'il y a lieu, sinon -1)
Player::Point point = player.segment2segment(posIniX, posIniY, posEndX, posEndY, segmentD.x, segmentD.y, segmentF.x, segmentF.y);
 
// Pas d'intersection
if (point.x == -1)
{
// On applique les valeurs de départ afin d'éviter d'être repoussé par la Tile
// solide (par mapCollision) lorsqu'on quitte une pente en sautant
x = posXmem;
dirX = dirXmem;
return 0;
}
 
else if (point.x < -1)
{
// Erreur dans la fonction "segment2segment()", on ne doit pas retourner de valeur < -1 !
cout << "ERROR - segment2segment() - Sprite aux coordonnées négatives !\n" << endl;
x = posXmem;
dirX = dirXmem;
return 0;
}
 
// On positionne le Sprite
x = point.x - w / 2;
dirX = 0;
y = point.y;
y -= h;
 
// Si le Sprite est dans la phase ascendante du saut, on le laisse poursuivre
// Sinon, on le stoppe et on l'indique comme étant au sol.
if (dirY > 0)
{
dirY = 0;
onGround = 1;
}
 
wasOnSlope = isOnSlope;
 
return 1;
}
 
// Si on est sur le sol, on vérifie si la Tile suivante, et en desssous, est une pente.
// Dans ce cas, on déplace le Sprite sur la pente,
else
{
 
if (goOnSlopeDown > 0)
{
double a, b;
if (!player.slopeEquation(goOnSlopeDown, &a, &b)){ return 0; }
int xPos = posEndX - xa*TILE_SIZE;
 
//Ici, xPos étant sur la Tile suivante, on retranche une Tile pour avoir le bon yPos
if (dirXmem > 0)
{
xPos -= TILE_SIZE;
}
else
{
xPos += TILE_SIZE;
}
 
int yPos = a * xPos + b;
if (yPos > 31) { yPos = 31; }
diagOffSet = TILE_SIZE - yPos;
yc = yb + 1;
wasOnSlope = isOnSlope;
checkWasOnSlope = 0;
}
 
// sinon on fait la transition en douceur avec "entity->wasOnSlope" ("checkWasOnSlope" restant à true)
}
 
}
 
// Finalement, si on est pas sur une pente, qu'on ne va pas sur une pente
// mais qu'on y était le tour d'avant, on force une sortie en douceur
if (wasOnSlope > 0 && checkWasOnSlope)
{
// Si on quitte une montée
if ((dirXmem > 0 && wasOnSlope == TILE_PENTE_26_BenH_2) ||
(dirXmem < 0 && wasOnSlope == TILE_PENTE_26_HenB_1))
{
yc = ya;
}
 
// Si on quitte une descente
else
{
if ((dirXmem > 0 && wasOnSlope == TILE_PENTE_26_HenB_2) ||
(dirXmem < 0 && wasOnSlope == TILE_PENTE_26_BenH_1))
{
yc = ya + 1;
}
}
 
resetWasOnSlope = 1;
}
 
// Si on "est" ou si on "quitte" une pente (donc que wasOnSlope > 0)
if (wasOnSlope > 0)
{
// On calcul l'écart entre le sol de la pente et la position du Sprite
// Si l'écart est plus grand que la vitesse de chute, on continue de laisser tomber le Sprite
if (wasOnGround == 0)
{
int newPos = yc * TILE_SIZE + diagOffSet;
int ecart = newPos - posIniY;
 
if (ecart > dirYmem)
{
y = posYmem;
dirY = dirYmem;
onGround = 0;
return 0;
}
}
 
// On positionne le Sprite sur la pente
x = posXmem;
dirX = dirXmem;
y = yc * TILE_SIZE + diagOffSet;
y -= h;
dirY = 0;
onGround = 1;
 
// On n'oublie pas de remettre wasOnSlope à 0 si nécéssaire
if (resetWasOnSlope)
{
wasOnSlope = 0;
}
 
return 1;
}
 
return 0;
 
}
 
 
 
int Monster::collide(Player &player)
{
//Fonction de gestion des collisions
//On teste pour voir s'il n'y a pas collision, si c'est le cas, on renvoie 0
if ((player.getX() >= x + w)
|| (player.getX() + player.getW() <= x)
|| (player.getY() >= y + h)
|| (player.getY() + player.getH() <= y)
)
return 0;
 
//Sinon, il y a collision. Si le joueur est au-dessus du monstre (avec une marge
//de 10 pixels pour éviter les frustrations dues au pixel perfect), on renvoie 2.
//On devra alors tuer le monstre et on fera rebondir le joueur.
else if (player.getY() + player.getH() <= y + 10)
{
player.setDirY(-JUMP_HEIGHT);
return 2;
}
 
//Sinon, on renvoie 1 et c'est le joueur qui meurt...
else
return 1;
}
 
 
 
int Monster::checkFall(Map &map)
{
int Fx, Fy;
 
//Fonction qui teste s'il y a du sol sous un monstre
//Retourne 1 s'il doit tomber, 0 sinon
 
//On teste la direction, pour savoir si on doit prendre en compte x ou x + w (cf. schéma)
if (direction == LEFT)
{
//On va à gauche : on calcule là où devrait se trouver le monstre après déplacement.
//S'il sort de la map, on met à jour x et y pour éviter de sortir de notre tableau
//(source d'erreur possible qui peut planter notre jeu...).
Fx = (int)(x + dirX) / TILE_SIZE;
Fy = (int)(y + h - 1) / TILE_SIZE;
 
if (Fy < 0)
Fy = 1;
 
if (Fy > MAX_MAP_Y)
Fy = MAX_MAP_Y;
 
if (Fx < 0)
Fx = 1;
 
if (Fx > MAX_MAP_X)
Fx = MAX_MAP_X;
 
//On teste si la tile sous le monstre est traversable (du vide quoi...)
//Et que ce n'est pas une pente.
//Si c'est le cas, on renvoie 1, sinon 0.
if (map.getTile(Fy + 1, Fx) <= BLANK_TILE - 40)
return 1;
else
return 0;
}
else
{
//Même chose quand on va à droite
Fx = (int)(x + w + dirX) / TILE_SIZE;
Fy = (int)(y + h - 1) / TILE_SIZE;
 
if (Fy <= 0)
Fy = 1;
 
if (Fy >= MAX_MAP_Y)
Fy = MAX_MAP_Y - 1;
 
if (Fx <= 0)
Fx = 1;
 
if (Fx >= MAX_MAP_X)
Fx = MAX_MAP_X - 1;
 
if (map.getTile(Fy + 1, Fx) <= BLANK_TILE - 40)
return 1;
else
return 0;
}
}
 
 
void Monster::copy(Monster &monster)
{
//Copie des variables nécessaires :
dirX = monster.getDirX();
dirY = monster.getDirY();
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();
onGround = monster.getOnGround();
saveX = monster.getSaveX();
saveY = monster.getSaveY();
jumping = monster.getJumping();
timerMort = monster.getTimerMort();
 
//Pour les pentes
dirXmem = monster.getDirXmem();
dirYmem = monster.getDirYmem();
posXmem = monster.getPosXmem();
posYmem = monster.getPosYmem();
wasOnGround = monster.getWasOnGround();
wasOnSlope = monster.getWasOnSlope();
} 

   On va ensuite développer un peu plus longuement les fonctions suivantes : wink

     La fonction update() :

   Comme vous pouvez le voir, elle ressemble beaucoup à celle de la classe Player, mais en moins compliquée.

   De base, notre monstre ne fait 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 lui fait faire demi-tour. On teste aussi s'il arrive au bord d'un trou avec la fonction checkFall() (cf. ci-dessous). Si c'est le cas, au lieu de le laisser tomber dans le trou comme un gros naze, on lui fait faire également demi-tour, wink ça c'est de l'intelligence ! laugh

    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 ! laugh

 

   Mais, au fait, on ne gère qu'un seul monstre dans notre classe ! surprise Comment va-t-on faire pour en avoir plusieurs ! frown

   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 ! angel

   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 wink).

   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 wink).

    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. smiley Cette fonction s'apparente un peu à un constructeur par copie, à la différence qu'ici notre objet existe déjà. wink

 

     La fonction collide() :

   C'est elle qui va gérer les collisions entre le joueur et les monstres. Nous utiliserons ici un système à la Mario (avec bond sur la tête) parce que c'est sans doute le plus simple. Je ne m'attarderai pas sur la théorie de cette technique, car j'ai déjà écrit un autre tuto là-dessus et je vous y renvoie donc si vous ne l'avez pas déjà luwink

   Son fonctionnement est simple : on teste si les deux sprites ne se touchent pas. Si c'est la cas, on renvoie 0, et c'est fini ! wink

   Sinon, c'est qu'ils se touchent ! surprise Mais là, on rajoute un test pour savoir si le joueur est au-dessus du monstre ou pas. Si c'est le cas, il lui rebondit dessus, on renvoie 2, et on devra supprimer le monstre. Sinon, c'est notre lapin qui morfle et on renvoie 1 pour le blesser...  Notons que notre lapin a 3 coeurs (qu'on afiichera plus tard dans le HUD cheeky), et que s'il les perd tous, il meurt ! surprise

   

     La fonction checkFall() :

   Nous avons déjà vu précédemment qu'on pouvait faire faire demi-tour à notre monstre quand il se cognait dans un mur, en testant pour voir si ses coordonnées (x ici) ne changeaient pas. Si c'était le cas, il était coincé et il ne manquait plus qu'à lui faire changer de direction. indecision

   Mais, là, où ça se complique, c'est quand, dans un deuxième temps, vous allez vouloir lui faire faire demi-tour pour éviter de tomber d'une plateforme comme un naze...

   Pas le choix, il va falloir tester la tile qui se situe sous ses pieds pour voir si c'est du sol ou pas (rappelez-vous de notre define BLANK_TILE ! Mais notez toutefois qu'on y retranche 40 pour ne pas inclure les tiles plateformes et pentes dans le lot : on ne veut pas que notre monstre s'arrête avant une plateforme ou une pente, on veut qu'il continue ! laugh). Mais attention , si on peut calculer cette tile en utilisant x quand le monstre va à gauche, il va falloir prendre en compte sa largeur quand il ira à droite. Sinon, on se retrouvera avec un monstre qui marchera à moitié dans le vide à droite mais qui s'arrêtera au bord de la plateforme à gauche (comme dans certains (mauvais) jeux) ! indecision

   C'est donc ce que fait ici notre fonction checkFall() qui va tester si la tile sous le monstre est du vide ou pas. Ainsi, pour ne pas que le monstre marche dans le vide, on va anticiper ses mouvements en prenant en compte son vecteur de déplacement, pour lui faire changer de direction à temps. Ouf ! cheeky
   On va aussi s'assurer par sécurité qu'on ne sort pas du tableau de notre map, pour éviter de planter notre jeu (ce serait plus cool ! laugh).   

 

     La fonction draw() :

   Cette fonction est similaire à celle qui affiche le héros. Mais cette fois, on va afficher de méchants monstres ! laugh On n'a pas non plus la partie qui gère le clignotement car notre monstre n'a qu'un seul PV, mais vous pouvez le rajouter, si vous voulez créer des monstres méga résistants ! laugh

 

     La fonction mapCollision() :

   Elle est rigoureusement identique à celle de la classe Player, pour l'instant. Dans de futurs chapitres, celle de la classe Player va cependant évoluer pour permettre au joueur de ramasser les tiles power-ups et d'interagir avec les tiles spéciales. Mais celle des monstres ne bougera pas. wink

 

     La fonction checkSlope() :

   Elle est aussi quasiment identique à celle de la classe Player, à la différence qu'elle renvoie aux sous-fonctions de la classe Player (segment2segment(), getSlopeSegment() et slopeEquation()), pour éviter d'avoir à les recopier une nouvelle fois. 

 

     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. wink

   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 ! wink

 

      Détectons les tiles Monstre  

   On retourne dans notre fichier map.cpp pour rajouter d'abord deux includes vers nos fichiers player.h et monster.h: 

Fichier : map.cpp : Rajouter en haut du fichier :

#include "player.h"
#include "monster.h"

   Puis on rajoute un accesseur getNombreMonstres() et un mutateur setNombreMonstres() :

Fichier : map.cpp : Rajouter les fonctions getNombreMonstres() et setNombreMonstres() :

//Accesseurs
int Map::getBeginX(void) const { return beginx; }
int Map::getBeginY(void) const { return beginy; }
int Map::getStartX(void) const { return startX; }
int Map::getStartY(void) const { return startY; }
int Map::getMaxX(void) const { return maxX; }
int Map::getMaxY(void) const { return maxY; }
int Map::getTile(int y, int x) const { return tile[y][x]; }
int Map::getLevel(void) const { return level; }
int Map::getNombreMonstres(void) const { return nombreMonstres; }
 
 
//Mutateurs
void Map::setLevel(int valeur) { level = valeur; }
void Map::setStartX(int valeur) { startX = valeur; }
void Map::setStartY(int valeur) { startY = valeur; }
void Map::setTile(int y, int x, int valeur) { tile[y][x] = valeur; }
void Map::setNombreMonstres(int valeur) { nombreMonstres = valeur; }

  

    Modifions désormais notre fonction map.draw(). Cela ne va pas être bien compliqué, mais redondant, car il va falloir écrire presque la même chose pour chaque couche de tiles (ou layer). En effet, on veut pouvoir placer une tile monstre et l'initialiser depuis n'importe quelle couche. wink

   La technique est simple : au moment de dessiner la tile, on teste si c'est une tile monstre. Si c'est le cas, on ne l'affiche pas, mais on la supprime (en la remplaçant par la tile 0) et on initialise un monstre (si c'est possible et qu'on n'a pas dépassé le nombre de MONSTRES_MAX) en renvoyant les coordonnées x et y (correspondant à la tile monstre) à la fonction initialize() de la classe Monsterwink On initialise le monstre correspondant à la variable nombreMonstres et on l'incrémente pour augmenter notre tableau de sprites et indiquer qu'un nouveau monstre est de la partie ! laugh

   C'est long mais pas bien compliqué. Je vous laisse lire :

 

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 (chapitre 19)
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)
{
 
//Si la tile à dessiner n'est pas une tile vide
if (tile[mapY][mapX] != 0)
{
/*On teste si c'est une tile monstre */
if (tile[mapY][mapX] == TILE_MONSTRE)
{
//Si on le peut, on initialise un monstre en envoyant les coordonnées de la tile
if (nombreMonstres < MONSTRES_MAX)
{
monster[nombreMonstres].initialize(mapX * TILE_SIZE, mapY * TILE_SIZE);
nombreMonstres++;
}
 
//Et on efface cette tile de notre tableau pour éviter un spawn de monstres
//infini !
tile[mapY][mapX] = 0;
}
}
 
/* 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)
{
 
//Si la tile à dessiner n'est pas une tile vide
if (tile2[mapY][mapX] != 0)
{
/*On teste si c'est une tile monstre */
if (tile2[mapY][mapX] == TILE_MONSTRE)
{
//Si on le peut, on initialise un monstre en envoyant les coordonnées de la tile
if (nombreMonstres < MONSTRES_MAX)
{
monster[nombreMonstres].initialize(mapX * TILE_SIZE, mapY * TILE_SIZE);
nombreMonstres++;
}
 
//Et on efface cette tile de notre tableau pour éviter un spawn de monstres
//infini !
tile2[mapY][mapX] = 0;
}
}
 
/* 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)
{
 
//Si la tile à dessiner n'est pas une tile vide
if (tile3[mapY][mapX] != 0)
{
/*On teste si c'est une tile monstre */
if (tile3[mapY][mapX] == TILE_MONSTRE)
{
//Si on le peut, on initialise un monstre en envoyant les coordonnées de la tile
if (nombreMonstres < MONSTRES_MAX)
{
monster[nombreMonstres].initialize(mapX * TILE_SIZE, mapY * TILE_SIZE);
nombreMonstres++;
}
 
//Et on efface cette tile de notre tableau pour éviter un spawn de monstres
//infini !
tile3[mapY][mapX] = 0;
}
}
 
/* 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 les classes Monster et Player en haut du fichier, en ajoutant les prototypes de getNombreMonstres() et setNombreMonstres() et en mettant à jour le prototype de draw()wink

Fichier : map.h : Modifier le code suivant :

#include <SFML/Graphics.hpp>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
 
class Player;
class Monster;
 
class Map
{
public:
 
//Constructeur
Map();
 
//Accesseurs
int getBeginX(void) const;
int getBeginY(void) const;
int getStartX(void) const;
int getStartY(void) const;
int getMaxX(void) const;
int getMaxY(void) const;
int getTile(int y, int x) const;
int getLevel(void) const;
int getNombreMonstres(void) const;
 
//Mutateurs
void setLevel(int valeur);
void setStartX(int valeur);
void setStartY(int valeur);
void setTile(int y, int x, int valeur);
void setNombreMonstres(int valeur);
 
//Fonctions
void drawBackground(sf::RenderWindow &window);
void loadMap(std::string filename);
void draw(int layer, sf::RenderWindow &window, Monster monster[]);
void changeLevel(void);
void testDefilement(void);

 

 

     Mise à jour du fichier player.cpp

   Mettons désormais à jour notre fichier player.cpp avec 2 nouvelles fonctions basiques :

- killPlayer() qui tue le joueur en mettant son timerMort à 1 (comme quand il tombe dans un troutrou ! laugh).

- playerHurts() qui enlève un coeur au joueur, le fait rebondir de douleur et le rend temporairement invincible.

 

Fichier : player.cpp : Ajouter les fonctions :

void Player::killPlayer()
{
//On met le timer à 1 pour tuer le joueur intantanément
timerMort = 1;
}
 
void Player::playerHurts()
{
//Si le timer d'invincibilité est à 0
//on perd un coeur
if (invincibleTimer == 0)
{
life--;
invincibleTimer = 80;
//et on fait sauter le joueur
dirY = -JUMP_HEIGHT;
}
}

   On n'oublie pas de rajouter les prototypes au fichier player.h :

Fichier : player.h : Modifier la partie du code :

//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 setVies(int valeur);
void setEtoiles(int valeur);
void setCheckpoint(bool valeur);
void killPlayer();
void playerHurts();

   Puis, dans notre fonction player.initialize(), on fait un reset du nombre de monstres, pour réinitialiser le niveau (sinon les anciens monstres resteraient ou passeraient d'un niveau à l'autre ! surprise) :

Fichier : player.cpp : initialize() - rajouter à la fin de la fonction :

//On réinitialise le nombre de monstres
map.setNombreMonstres(0);

 

     Branchements dans la boucle principale du jeu   

   Commençons d'abord par l'en-tête main.h : On y rajoute la constante MONSTRES_MAX ainsi que l'include de monster.h :

Fichier : main.h : Mettre à jour :

//Rabidja 3 - nouvelle version convertie en 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;
 
// 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 main() :

Fichier : main.cpp : Mettre à jour :

//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
//Big Tuto C++/SFML 2.2 - Février 2015 - Mise à jour 1.2
 
#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),
"Rabidja 3.0 - Chapitre 12 : Monstres - Big Tuto SFML2 - www.meruvia.fr");
 
//Limite les fps à 60 images / seconde
window.setFramerateLimit(60);
 
//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 (vous pouvez aussi mettre 2 pour tester le 2ème niveau)
map.setLevel(2);
map.changeLevel();
 
//On initialise le player
player.initialize(map, true);
player.setVies(3);
player.setEtoiles(0);
 
// Boucle infinie, principale, du jeu
while (window.isOpen())
{
/** GESTION DES INPUTS (CLAVIER, JOYSTICK) **/
input.gestionInputs(window);
 
/** MISES A JOUR - UPDATES **/
//On met à jour le player : Rabidja
player.update(input, map);
 
//On met à jour les monstres un par un
for (int i = 0; i < map.getNombreMonstres(); i++)
{
if (monster[i].update(map, player) == 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);
}
}
 
/** DESSIN - DRAW **/
//On dessine tout
window.clear();
 
//On affiche le background
map.drawBackground(window);
 
// 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);
 
window.display();
}
 
// On quitte
return 0;
 
}

 

   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 ? wink

   Dans la boucle principale, maintenant :

- On met à jour (update) tous nos monstres, à l'aide d'une boucle for et si la fonction nous renvoie 2, c'est qu'il 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 wink).
- On dessine chaque monstre en appelant sa fonction draw() dans une nouvelle boucle for.
- On modifie l'appel à map.draw() pour qu'elles prennent désormais nos monstres en argument (3x).

   Voilà, plus qu'à compiler et à lancer le programme ! wink   

   De beaux zombies s'éveillent maintenant à la vie, prêts à en découdre ! indecision

   Argh !! Mais qu'avons-nous fait !?! surprise

   Je vous laisse maintenant souffler et je vous dis à bientôt pour le chapitre 13 ! angel

                                                                                                                               Jay 

 

 

 
 

This site uses cookies to enable you to log in. We do not store or sell any personal data. By continuing to use this website, you agree to their use. Thanks!