Big Tuto SFML 2 : Rabidja v. 3.0

Annexe 1 : Ajoutons des ombres !

Tutoriel présenté par : Skrool
Relecture et corrections : Jérémie F. Bellanger (Jay81)

Date d'écriture : 22 mars 2015
Dernière mise à jour : 20 mars 2016

      Introduction

   Comment ça ? surprise Ce tutoriel n'est pas écrit par Jay ?! frown 
   Non ! Ne partez pas ! sad Ce n'est pas Jay qui a écrit ce tutoriel, mais il pourra certainement vous intéresser, car aujourd'hui, on parle lumières ! wink 
   Oui, à la fin de ce tutoriel vous aurez votre propre système de lumière dynamique dans votre jeu de plateforme! angel
   Vous êtes prêt ? indecision
   Si vous avez répondu oui de toutes vos forces, alors c'est parti ! wink

 

La vidéo suivante vous donne un aperçu du système de lumières dynamiques, à l'issue de l'annexe 6 

 

   Mais avant de commencer, arrêtons-nous sur un point :

   Qu'appelle-t-on "lumière dynamique"? frown
   Et bien… il n'y a pas de vraie définition pour "lumière dynamique". cheeky C'est comme "open world", ce terme englobe une idée sans définition précise. Bien sûr, lumière dynamique est à associer avec "lumière qui réagit avec son environnement", mais ce n'est toujours pas extrêmement précis. indecision

   Du coup, je vais donner ma propre définition de ce que va être une lumière dynamique dans ce tuto : une lumière éclairant les objets qui l'entourent (et oui laugh ), pouvant se déplacer, être actualisée régulièrement et qui sera arrêtée par les murs. Nos lumières posséderont donc tous ces attributs.

   Mais pour cela, nous allons devoir gérer ces lumières, pour en ajouter, en supprimer, les afficher, etc. Cela va être plus complexe que de gérer un monstre par exemple.

   Nous allons donc devoir créer un gestionnaire de lumières. Après, et seulement après on pourra rajouter nos lumières. On en profitera aussi pour créer un effet d'ombre et d'ambiance à la couleur personnalisable.

 

 

      Ajoutons notre class light !

   Et on est parti ! Tout d'abord, nous allons créer une nouvelle classe Light avec les fichiers light.cpp et light.h.

   Voilà ce à quoi va maintenant ressembler notre header :
 

Fichier : Créer un nouveau fichier light.h et y copier :

//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur du tuto : www.meruvia.fr - Jérémie F. Bellanger
//Copyright / Droits d'auteur du fichier : Skrool, 2015.
 
//Nous allons avoir besoin d'inclure ces bibliothèques
#include <SFML/Graphics.hpp>
#include <iostream>
#include <math.h>
 
//Ainsi que la class Map
class Map;
 
class Light // Class permettant de gérer les lumière
{
public:
//Constructeur et destructeur
Light(void);
 
//Accesseurs
sf::Color getShadowColor(void) const;
sf::Color getTargetShadowColor(void) const;
 
//Mutateurs
void setShadowColor(sf::Color value);
void setTargetShadowColor(sf::Color value);
 
//Fonctions
void draw(sf::RenderWindow *window);
void update(void);
 
 
private:
 
//Tesxture de l'ombre
sf::RenderTexture m_shadowTexture;
 
//Notre couleur d'ombre
sf::Color m_shadowColor, m_targetShadowColor;
 
//Constantes
const int MAX_LIGHT = 15; // <- Notre nombre maximal de lumière.
// Théoriquement, avec notre système on pourra en afficher à l'infini, mais cela risque de nous manger notre CPU, et de réduire nos performances
 
};

 

   L'une des utilités de notre gestionnaire sera de créer un effet d'ombre qui assombrira l'image, mais pas là où il y aura de la lumière pour éclairer ! Comment allons-nous nous y prendre ? Tout simplement en créant une RenderTexture de la taille de la fenêtre, que l'on affichera avec un mode de rendu Multiply.

   « Un mode de rendu ? » Quesako ?!? surprise

   Vous avez bien fait de poser la question. wink Un mode de rendu détermine la façon dont une image s'affiche par-dessus une autre. Il existe plusieurs modes de rendus, parmi eux il y a le mode "Add" et le mode "Multiply". Une image vaut mieux que de longues explications, et vous allez vite comprendre de quoi il s'agit :

 

 

 

   Techniquement, la valeur (R, G, B) des pixels de la texture 2 sont soit ajoutés, soit multipliés à ceux de la texture 1. Du coup, dans le cas du mode de rendu Add, les pixels proches du noir disparaissent, et au contraire, en rendu Multiply, ceux proches du blanc disparaissent.

   Nous allons nous servir plus tard du rendu en mode Add à l'intérieur de nos lumières. wink

   En attendant, nous allons utiliser le mode de rendu Multiply pour afficher la RenderTexture sur laquelle on «collera» ensuite les lumières. Le niveau paraîtra donc sombre, sauf là où il y aura de la lumière. De plus, nous nous servirons plus tard de cette texture pour utiliser un shader de flou.

Mais pourquoi as-tu mis m_ devant tes variables ? surprise Ça ne marche pas, si on ne les met pas? frown
En C++, il y a une convention disant qu'il est préférable de mettre m_ devant une variable déclarée dans une classe pour mieux se repérer dans les fonctions complexes, mais c'est absolument facultatif et nombreux sont ceux qui ne l'aiment pas. indecision Donc libre à vous de choisir de mettre le préfixe m_ devant vos variables ou non. wink

   Revenons au code : nous avons ajoutés la RenderTexture shadowTexture, ainsi qu'une variable sf::Color qui déterminera la couleur de notre ombre.

   Eh oui ! smiley Nous allons pouvoir créer des ombres colorées ! angel Cela peut servir pour créer des ambiances. Par exemple, une ombre rougeâtre permettra de figurer une chaleur intense, comme dans un volcan, une ambiance verte illustrera la présence de poison dans l'air et une ambiance violette illustrera... heuu… frown quelque chose indecision ?

   Une ambiance blanche, quant à elle, correspondra à ne pas avoir d'ombre. En effet, l'ombre va être affichée avec le mode de rendu Multiply, souvenez vous, donc blanc = rien. wink


 

   Notez aussi l'accesseur et le mutateur pour changer la couleur de l'ombre/ambiance. wink

  Passons maintenant au fichier light.cpp :

Fichier : Créer un nouveau fichier light.cpp et y copier :

//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur du tuto : www.meruvia.fr - Jérémie F. Bellanger
//Copyright / Droits d'auteur du fichier : Skrool, 2015.
 
#include "light.h"
#include "map.h"
 
 
/***********************************************************************************************/
/********************************* GestionnaireDeLumiere **********************************/
/************************************************************************************************/
 
Light::Light(void)
{
//Création de notre renderTexture à la taille de notre fenêtre
m_shadowTexture.create(800, 600);
//Initialisation de notre couleur à 250 partout, ce qui correspond au blanc, donc à rien
m_shadowColor = sf::Color(250, 250, 250);
m_targetShadowColor = sf::Color(250, 250, 250);
};
 
 
//Accesseurs
sf::Color Light::getShadowColor(void) const{ return m_shadowColor; }
sf::Color Light::getTargetShadowColor(void) const{ return m_targetShadowColor; }
 
//Mutateurs
void Light::setShadowColor(sf::Color value){ m_shadowColor = value; }
void Light::setTargetShadowColor(sf::Color value){ m_targetShadowColor = value; }
 
 
void Light::draw(sf::RenderWindow *window)
{
// On crée un RenderStates avec le mode de rendu multiply, il nous sera indispensable pour la suite
sf::RenderStates render(sf::BlendMultiply);
 
// On vide notre render texture et le remplit de la couleur de l'ombre
m_shadowTexture.clear(m_shadowColor);
 
// On affiche notre render texture avec le mode multiply
window->draw(sf::Sprite(m_shadowTexture.getTexture()), render);
m_shadowTexture.display();
}
 
void Light::update()
{
// On rajoute une partie de la différence entre les valeurs r, g et b des
//deux variables pour avoir un effet de transition en douceur
if (m_shadowColor.r < m_targetShadowColor.r)
m_shadowColor.r++;
else if (m_shadowColor.r > m_targetShadowColor.r)
m_shadowColor.r--;
 
if (m_shadowColor.g < m_targetShadowColor.g)
m_shadowColor.g++;
else if (m_shadowColor.g > m_targetShadowColor.g)
m_shadowColor.g--;
 
if (m_shadowColor.b < m_targetShadowColor.b)
m_shadowColor.b++;
else if (m_shadowColor.b > m_targetShadowColor.b)
m_shadowColor.b--;
}

 

   Ici, rien de bien complexe. wink

   Notez quand même, que nous avons créé deux couleurs (m_shadowColor et m_targetShadowColor), afin que les transitions soient naturelles.

   En effet, dans la fonction update(), nous allons modifier à chaque frame la valeur des trois composantes (r, g, b) de notre couleur shadowColor. En gros, on ajoute à chaque couleur une partie de ce qui manque à shadowColor pour être similaire à targetShadowColor. Du coup, nous avons droit à une magnifique transition ! indecision

   Bien, passons maintenant à la mise à jour du main(). On commence par le header :
 

Fichier : main.h : Rajouter :

 #include "light.h"

 

    Puis au fichier main.cpp : on y instancie notre class Light et on crée un manager (gestionnaire de lumières wink), puis dans la boucle principale, on appele manager.update() et manager.draw().

   Notez que certains prototypes de fonctions comprennent désormais le manager en argument, ne les oubliez pas!wink

 

Fichier : main.cpp : Remplacez la fonction par :

//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 - Chap. 21: Lumieres 1 - Skrool - 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;
Sounds sounds;
Menus menus;
Light manager;
//On instancie autant de classes que de monstres gérables
Monster monster[MONSTRES_MAX];
//On instancie autant de classes que de plateformes gérables
Plateformes plateforme[PLATEFORMES_MAX];
//On instancie autant de classes que de shurikens gérables
Shurikens shuriken[SHURIKEN_MAX];
//On instancie autant de classes que d'explosions gérables
Explosions explosion[EXPLOSIONS_MAX];
 
 
//On lance la musique en boucle
sounds.PlayMusic(true);
 
//On commence au premier niveau (vous pouvez aussi mettre 2 pour tester le 2ème niveau)
map.setLevel(1);
map.changeLevel();
 
//On initialise le player
player.initialize(map, manager, true);
player.setVies(3);
player.setEtoiles(0);
 
//On commence par le menu start
menus.setOnMenu(true, START);
 
// Boucle infinie, principale, du jeu
while (window.isOpen())
{
/** GESTION DES INPUTS (CLAVIER, JOYSTICK) **/
input.gestionInputs(window);
 
//Si on n'est pas dans un menu
if (!menus.getOnMenu())
{
/** MISES A JOUR - UPDATES **/
//On met à jour le player : Rabidja
player.update(input, map, sounds, menus, shuriken, manager);
 
//On met à jour le Gestionaire de lumière
manager.update();
 
//On met à jour les monstres un par un
for (int i = 0; i < map.getNombreMonstres(); i++)
{
if (monster[i].update(map, player, sounds) == 2)
{
//Si l'update du monstre renvoie 2, c'est qu'il doit mourir :
 
//On crée une explosion à sa place, si on peut
if (player.getNombreExplosions() < EXPLOSIONS_MAX)
{
explosion[player.getNombreExplosions()].initialize(monster[i].getX(), monster[i].getY());
player.setNombreExplosions(player.getNombreExplosions() + 1);
}
 
//on copie à sa place le dernier monstre avant de retirer un monstre
monster[i].copy(monster[map.getNombreMonstres() - 1]);
map.setNombreMonstres(map.getNombreMonstres() - 1);
}
 
}
 
//On met à jour les plateformes une par une
for (int i = 0; i < map.getNombrePlateformes(); i++)
{
plateforme[i].update(player);
 
//On teste les collisions avec le joueur
plateforme[i].checkCollisions(player);
}
 
//On met à jour les shurikens un par un
for (int i = 0; i < player.getNombreShurikens(); i++)
{
if (shuriken[i].update(player, map) == 1)
{
//On supprime le shuriken s'il sort de l'écran
shuriken[i].copy(shuriken[player.getNombreShurikens() - 1]);
player.setNombreShurikens(player.getNombreShurikens() - 1);
}
else
{
//On teste les collisions avec tous les monstres
for (int j = 0; j < map.getNombreMonstres(); j++)
{
if (shuriken[i].checkCollisions(monster[j]) == 1)
{
//Si la fonction renvoie 2, c'est qu'il y a contact avec le monstre
//Celui-ci doit mourrir et il faut supprimer le shuriken.
 
//On crée une explosion à sa place, si on peut
if (player.getNombreExplosions() < EXPLOSIONS_MAX)
{
explosion[player.getNombreExplosions()].
initialize(monster[j].getX(),
monster[j].getY());
player.setNombreExplosions(player.getNombreExplosions() + 1);
}
 
//On copie à sa place le dernier monstre avant de retirer un monstre
monster[j].copy(monster[map.getNombreMonstres() - 1]);
map.setNombreMonstres(map.getNombreMonstres() - 1);
 
//On supprime le shuriken
shuriken[i].copy(shuriken[player.getNombreShurikens() - 1]);
player.setNombreShurikens(player.getNombreShurikens() - 1);
 
//On joue le sound Fx
sounds.PlaySoundFx(sounds.DESTROY);
}
}
}
}
 
}
else
{
if (menus.getMenuType() == START)
menus.updateStartMenu(input, map, player, manager);
 
else if (menus.getMenuType() == PAUSE)
menus.updatePauseMenu(input);
}
 
/** DESSIN - DRAW **/
//On dessine tout
window.clear();
 
//Si on n'est pas dans un menu, ou si on est en pause, on affiche le jeu
if (!menus.getOnMenu() || (menus.getOnMenu() && menus.getMenuType() == PAUSE))
{
//On affiche le background
map.drawBackground(window);
 
// Affiche la map de tiles : layer 2 (couche du fond)
map.draw(2, window, monster, plateforme, manager);
 
// Affiche la map de tiles : layer 1 (couche active : sol, etc.)
map.draw(1, window, monster, plateforme, manager);
 
// 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);
}
 
//On affiche les plateformes une par une
for (int i = 0; i < map.getNombrePlateformes(); i++)
{
plateforme[i].draw(map, window);
}
 
//On affiche les shurikens un par un
for (int i = 0; i < player.getNombreShurikens(); i++)
{
shuriken[i].draw(map, window);
}
 
// Affiche la map de tiles : layer 3 (couche en foreground / devant)
map.draw(3, window, monster, plateforme, manager);
 
//Gestion des lumières dynamiques par Skrool
manager.draw(&window);
 
//On affiche les explosions une par une
for (int i = 0; i < player.getNombreExplosions(); i++)
{
//Si la fonction de dessin renvoie 1, notre explosion est terminée
//et on doit la supprimer :
if (explosion[i].draw(map, window) == 1)
{
//On copie à sa place la dernière du tableau avant de retirer
//une explosion
explosion[i].copy(explosion[player.getNombreExplosions() - 1]);
player.setNombreExplosions(player.getNombreExplosions() - 1);
}
}
 
//Affichage du HUD
map.drawHud(player, window);
 
//Affichage du menu PAUSE le cas échéant
if (menus.getOnMenu() && menus.getMenuType() == PAUSE)
menus.drawPauseMenu(window, map);
}
//Sinon, on affiche l'écran-titre
else if (menus.getOnMenu() && menus.getMenuType() == START)
menus.drawStartMenu(window, map);
 
window.display();
}
 
// On quitte
return 0;
 
}

   Notez qu'on ajoute notre fonction draw() après la troisième couche de tiles, afin que l'ombre s'affiche partout.

   Cependant, c'est temporaire. En effet, comme les lumières seront affichées sur la RenderTexture, il faudra la mettre entre la couche 1 et la couche 3 de tiles, ce qui n'assombrira pas les tiles du foreground, mais on utilisera une petite feinte pour remédier à cela. wink

 

     Intégration des ombres à notre map

   C'est bien joli tout ça, mais on va pas faire tous nos niveaux de nuit, si !? surprise
   Absolument pas ! angel On va ici raccorder notre système d'ombre avec notre map à tiles. Mais avant tout : changement de tilesets ! indecision

  Hé, mais je n'ai pas envie de remodifier toutes mes maps, pour qu'elles collent avec le nouveaux tileset, moi ! surprise
   Pas de souci, le nouveau tileset ne modifie pas les emplacements des autres tiles, donc pas de problème de compatibilité. wink

   Jetons un oeil à ces nouveaux tilesets (clic droit pour les enregistrer dans le répertoire graphics de votre projet).

   Tilsets à changer dans le jeu :

     

   Tilsets à changer dans le level editor, pour pouvoir voir les tiles lumières :

     

   Vous aurez tout d'abord remarqué qu'il y en a 2 jeux :
- le premier est destiné à remplacer le précédent dans le répertoire graphics de votre jeu : les tiles ombres et lumières y sont transparentes pour ne pas qu'on les voie. wink
- le second est destiné à remplacer celui du level editor : on y voit les tiles ombres et lumières, et ce sera plus pratique pour pouvoir les éditer ! laugh
 
   Maintenant, on y trouve plein de nouvelles tiles ! surprise
   Intéressons-nous d’abord à celles avec un O dessiné dessus. Ce sont les tiles d'ombre. Nous allons modifier la valeur de m_shadowColor grâce à elles.
   Pour les tiles lumières, on verra ça dans un prochain chapitre (chaque chose en son temps wink).
 
   Intégrons maintenant tout ça dans notre class map. On commence par le header :

 

Fichier : map.h : Ajouter / changer les éléments suivants :

//Au début du fichier, rajouter :
class Light;
 
//Modifier le prototype
void draw(int layer, sf::RenderWindow &window, Monster monster[], Plateformes plateforme[], Light &manager);
 
//Nouvelles constantes :
//Tiles pour les ombres par Skrool
const int TILE_OMBRE_DEBUT = 96;
const int TILE_OMBRE_FIN = 99;
const int TILE_OMBRE_AUCUNE = 96;
const int TILE_OMBRE_MOYEN = 97;
const int TILE_OMBRE_BEAUCOUP = 98;
const int TILE_OMBRE_AMBIANCE_ROUGE = 99;
 
const int TILE_LUMIERE_DEBUT = 141;

   Rien de plus à dire. On note les nouvelles constantes pour nos nouvelles tiles. wink 

Fichier : map.cpp : Ajouter / changer les éléments suivants :
//Au début du fichier
#include "light.h"
 
//Modifiez la fonction (ou remplacez-la)
void Map::draw(int layer, RenderWindow &window, Monster monster[], Plateformes plateforme[], Light &manager)
{
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;
}
 
/*On teste si c'est une tile plateforme flottante */
else if (tile[mapY][mapX] >= TILE_PLATEFORME_DEBUT
&& tile[mapY][mapX] <= TILE_PLATEFORME_FIN)
{
//On initialise une plateforme flottante, si on le peut.
//Pour obtenir le type (dernier arg), on enlève le numéro de la 1ère tile plateforme
//(TILE_PLATEFORME_DEBUT) au numéro de la tile en cours (map.tile[mapY][mapX])
//et on rajoute 1 pour que le premier type soit le 1 et pas le 0 ;)
if (nombrePlateformes < PLATEFORMES_MAX)
{
plateforme[nombrePlateformes].initialize(mapX * TILE_SIZE, mapY * TILE_SIZE,
tile[mapY][mapX] - TILE_PLATEFORME_DEBUT + 1);
nombrePlateformes++;
}
 
//Et on efface cette tile de notre tableau pour éviter un spawn de plateformes
//infini !
tile[mapY][mapX] = 0;
}
 
/*On teste si c'est une tile d'ombre */
else if (tile[mapY][mapX] >= TILE_OMBRE_DEBUT && tile[mapY][mapX] <= TILE_OMBRE_FIN)
{
//Si il y a une tile "pas d'ombre", on remet les valeurs à 250
if (tile[mapY][mapX] == TILE_OMBRE_AUCUNE)
manager.setTargetShadowColor(sf::Color(250, 250, 250));
 
//Une tile "ombre moyenne" : on met les valeurs à 100 partout sauf à bleu pour créer un ambiance de nuit
else if (tile[mapY][mapX] == TILE_OMBRE_MOYEN)
manager.setTargetShadowColor(sf::Color(100, 100, 125));
 
//Une tile "ombre forte" : on met les valeurs basses, ce qui crée une ambiance très sombre
else if (tile[mapY][mapX] == TILE_OMBRE_BEAUCOUP)
manager.setTargetShadowColor(sf::Color(60, 60, 70));
 
//Une tile "ombre rougeatre" : on crée une ambiance rouge
else if (tile[mapY][mapX] == TILE_OMBRE_AMBIANCE_ROUGE)
manager.setTargetShadowColor(sf::Color(200, 100, 100));
}
/*On teste si c'est une tile lumière */
else if (tile[mapY][mapX] >= TILE_LUMIERE_DEBUT)
{
//On remplace la tile par du vide, pour l'instant
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;
}
 
/*On teste si c'est une tile plateforme flottante */
else if (tile2[mapY][mapX] >= TILE_PLATEFORME_DEBUT
&& tile2[mapY][mapX] <= TILE_PLATEFORME_FIN)
{
//On initialise une plateforme flottante, si on le peut.
//Pour obtenir le type (dernier arg), on enlève le numéro de la 1ère tile plateforme
//(TILE_PLATEFORME_DEBUT) au numéro de la tile en cours (map.tile[mapY][mapX])
//et on rajoute 1 pour que le premier type soit le 1 et pas le 0 ;)
if (nombrePlateformes < PLATEFORMES_MAX)
{
plateforme[nombrePlateformes].initialize(mapX * TILE_SIZE, mapY * TILE_SIZE,
tile2[mapY][mapX] - TILE_PLATEFORME_DEBUT + 1);
nombrePlateformes++;
}
 
//Et on efface cette tile de notre tableau pour éviter un spawn de plateformes
//infini !
tile2[mapY][mapX] = 0;
}
 
/*On teste si c'est une tile d'ombre */
else if (tile2[mapY][mapX] >= TILE_OMBRE_DEBUT && tile2[mapY][mapX] <= TILE_OMBRE_FIN)
{
//Si il y a une tile "pas d'ombre", on remet les valeurs à 250
if (tile2[mapY][mapX] == TILE_OMBRE_AUCUNE)
manager.setTargetShadowColor(sf::Color(250, 250, 250));
 
//Une tile "ombre moyenne" : on met les valeurs à 75 partout sauf à bleu pour créer un ambiance de nuit
else if (tile2[mapY][mapX] == TILE_OMBRE_MOYEN)
manager.setTargetShadowColor(sf::Color(75, 75, 100));
 
//Une tile "ombre forte" : on met les valeurs basses, ce qui crée une ambiance très sombre
else if (tile2[mapY][mapX] == TILE_OMBRE_BEAUCOUP)
manager.setTargetShadowColor(sf::Color(20, 20, 30));
 
//Une tile "ombre rougeatre" : on crée une ambiance rouge
else if (tile2[mapY][mapX] == TILE_OMBRE_AMBIANCE_ROUGE)
manager.setTargetShadowColor(sf::Color(200, 100, 100));
}
/*On teste si c'est une tile lumière */
else if (tile2[mapY][mapX] >= TILE_LUMIERE_DEBUT)
{
//On remplace la tile par du vide, pour l'instant
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;
}
 
/*On teste si c'est une tile plateforme flottante */
else if (tile3[mapY][mapX] >= TILE_PLATEFORME_DEBUT
&& tile3[mapY][mapX] <= TILE_PLATEFORME_FIN)
{
//On initialise une plateforme flottante, si on le peut.
//Pour obtenir le type (dernier arg), on enlève le numéro de la 1ère tile plateforme
//(TILE_PLATEFORME_DEBUT) au numéro de la tile en cours (map.tile[mapY][mapX])
//et on rajoute 1 pour que le premier type soit le 1 et pas le 0 ;)
if (nombrePlateformes < PLATEFORMES_MAX)
{
plateforme[nombrePlateformes].initialize(mapX * TILE_SIZE, mapY * TILE_SIZE,
tile3[mapY][mapX] - TILE_PLATEFORME_DEBUT + 1);
nombrePlateformes++;
}
 
//Et on efface cette tile de notre tableau pour éviter un spawn de plateformes
//infini !
tile3[mapY][mapX] = 0;
}
 
/*On teste si c'est une tile d'ombre */
else if (tile3[mapY][mapX] >= TILE_OMBRE_DEBUT && tile3[mapY][mapX] <= TILE_OMBRE_FIN)
{
//Si il y a une tile "pas d'ombre", on remet les valeurs à 250
if (tile3[mapY][mapX] == TILE_OMBRE_AUCUNE)
manager.setTargetShadowColor(sf::Color(250, 250, 250));
 
//Une tile "ombre moyenne" : on met les valeurs à 75 partout sauf à bleu pour créer un ambiance de nuit
else if (tile3[mapY][mapX] == TILE_OMBRE_MOYEN)
manager.setTargetShadowColor(sf::Color(75, 75, 100));
 
//Une tile "ombre forte" : on met les valeurs basses, ce qui crée une ambiance très sombre
else if (tile3[mapY][mapX] == TILE_OMBRE_BEAUCOUP)
manager.setTargetShadowColor(sf::Color(20, 20, 30));
 
//Une tile "ombre rougeatre" : on crée une ambiance rouge
else if (tile3[mapY][mapX] == TILE_OMBRE_AMBIANCE_ROUGE)
manager.setTargetShadowColor(sf::Color(200, 100, 100));
}
/*On teste si c'est une tile lumière */
else if (tile3[mapY][mapX] >= TILE_LUMIERE_DEBUT)
{
//On remplace la tile par du vide, pour l'instant
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++;
}
}
 
 
}
 
    Ici, c'est la fonction draw() qui évolue et repère désormais nos tiles ombres.
   En fonction du numéro de la tile, on change ainsi notre targetShadowColorpour assurer une transition vers la nouvelle ambiance. wink
   Vous noterez, que contrairement aux monstres, on ne supprime pas la tile ombre. L'effet doit fonctionner même quand on revient en arrière (y compris quand on fait un travelling après être mort). On laisse donc les tiles, mais comme elles sont transparentes sur notre tileset, on ne les voit pas (si vous n'avez pas copié le bon tileset au bon endroit, c'est là, que vous allez vous en rendre compte ! laugh).
 
   Maintenant, quand on meurt, ou qu'on réinitialise le jeu, il faut aussi remettre la luminosité au maximum. Nous allons donc rajouter cela dans la class player.
 
Fichier : player.h : Ajouter / changer les éléments suivants :
//Rajouter au début du fichier
class Light;
 
 
//Mettre à jour les prototypes :
void Player::initialize(Map &map, Light &manager, bool newLevel);
void Player::update(Input &input, Map &map, Sounds &sounds, Menus &menus, Shurikens shuriken[], Light &manager);
void Player::mapCollision(Map &map, Sounds &sounds, Light &manager);
 
 
//Effacer TILE_TRAVERSABLE dans les constantes et le remplacer par :
 
//Plateformes traversables
const int TILE_TRAVERSABLE_DEBUT = 80;
const int TILE_TRAVERSABLE_FIN = 95;

   Il va donc nous falloir mettre à jour la fonction initialize() pour remettre targetShadowColor à blanc. wink

   Et comme son prototype va changer, il va nous falloir modifier plusieurs fonctions / classes en cascade (dont les menus ci-après).

Fichier : player.cpp : Ajouter / changer les éléments suivants dans les fonctions suivantes :

//Mettre à jour les arguments
void Player::initialize(Map &map, Light &manager, bool newLevel)
{
//Code coupé...
//Rajouter à la fin :
 
 
//On réinitialise les lumières
manager.setTargetShadowColor(sf::Color::White);
}
 
 
//Mettre à jour les arguments
void Player::update(Input &input, Map &map, Sounds &sounds, Menus &menus, Shurikens shuriken[],
Light &manager)
{
//Code coupé...
 
//Mettre à jour l'appel :
mapCollision(map, sounds, manager);
 
 
//Mettre à jour l'appel à initialize :
if (timerMort > 0)
{
timerMort--;
 
if (timerMort == 0)
{
//On perd une vie
vies--;
 
// Si on est mort, on réinitialise le niveau
map.changeLevel();
initialize(map, manager, false);
 
//Sauf si on a plus de vies, on retourne à l'écran-titre
if (vies < 0)
{
//Dans ce cas on retourne au menu start
menus.setOnMenu(true, START);
}
}
}
}

   Enfin, nous allons aussi mettre à jour la fonction mapCollision(), pour ne pas que le jeu considère nos tiles ombres comme des plateformes (d'où la mise à jour de la constante TILE_TRAVERSABLE). wink

Fichier : player.cpp : Ajouter / changer les éléments suivants dans la fonction mapCollision() :

//Mettre à jour les arguments :
void Player::mapCollision(Map &map, Sounds &sounds, Light &manager)
{
 
// Code coupé...
 
//Changer la gestion des plateformes quand on descend :
 
 
/** !! Attention à ne pas oublier le else avant le prochain if, sinon vous resterez
coincés sur les pics et les ressorts !! **/
 
 
//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
else if ((map.getTile(y2, x1) >= TILE_TRAVERSABLE_DEBUT && map.getTile(y2, x1) <= TILE_TRAVERSABLE_FIN)
|| (map.getTile(y2, x2) >= TILE_TRAVERSABLE_DEBUT && map.getTile(y2, x2) <= TILE_TRAVERSABLE_FIN)
|| map.getTile(y2, x1) > BLANK_TILE
|| map.getTile(y2, x2) > BLANK_TILE)
{
//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;
}

   Dans la class Menus, on va maintenant devoir mettre à jour l'appel à la fonction player.initialize(mapmanagertrue);

Fichier : menus.h : Ajouter / changer les éléments suivants :

//Rajouter au début du fichier
class Light;
 
//Mettre à jour le prototype :
void updateStartMenu(Input &input, Map &map, Player &player, Light &manager);

 

Fichier : menus.cpp : Remplacez la fonction :

void Menus::updateStartMenu(Input &input, Map &map, Player &player, Light &manager)
{
//Si on appuie sur BAS
if (input.getButton().down)
{
input.setButton(down, false);
 
//Si choice = O (il est sur start), on le met à 1 (quit)
if (choice == 0)
choice++;
}
 
//Si on appuie sur HAUT
else if (input.getButton().up)
{
input.setButton(up, false);
 
//Si choice = 1 (il est sur Quit), on le met à 0 (Start)
if (choice == 1)
choice--;
}
 
//Choix du level
if (input.getButton().right && tempo <= 0)
{
input.setButton(right, false);
tempo = TIMER;
 
if (map.getLevel() >= LEVEL_MAX)
map.setLevel(1);
else
map.setLevel(map.getLevel() + 1);
}
 
else if (input.getButton().left && tempo <= 0)
{
input.setButton(left, false);
tempo = TIMER;
 
if (map.getLevel() <= 1)
map.setLevel(LEVEL_MAX);
else
map.setLevel(map.getLevel() - 1);
}
 
 
//Si on appuie sur Enter ou A (manette Xbox 360) et qu'on est sur Start, on recharge le jeu et on quitte l'état menu
if (input.getButton().enter || input.getButton().jump)
{
if (choice == 0)
{
player.setCheckpoint(false);
player.initialize(map, manager, true);
map.changeLevel();
 
/* On réinitialise les variables du jeu */
player.setVies(3);
player.setEtoiles(0);
onMenu = false;
}
 
//Sinon, on quitte le jeu
else if (choice == 1)
{
exit(0);
}
 
input.setButton(enter, false);
input.setButton(jump, false);
}
 
//On décrémente notre compte à rebours (Timer)
tempo--;
 
}

   Et enfin, on va mettre à jour notre class Monster, pour que la fonction Monster::mapCollision() ne considère pas, elle non plus, les tiles Ombres comme des plateformes (ce serait gênant des monstres qui marchent dans le vide ! laugh).

Fichier : monster.h : Ajouter / changer les éléments suivants dans la fonction mapCollision() :

//Effacer TILE_TRAVERSABLE et le remplacer par :
 
//Plateformes traversables
const int TILE_TRAVERSABLE_DEBUT = 80;
const int TILE_TRAVERSABLE_FIN = 95;

 

Fichier : monster.cpp : Ajouter / changer les éléments suivants dans la fonction mapCollision() :

void Monster::mapCollision(Map &map, Player &player)
{
// Code coupé... 
//Changer la gestion des plateformes quand on descend :
 
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_DEBUT && map.getTile(y2, x1) <= TILE_TRAVERSABLE_FIN)
|| (map.getTile(y2, x2) >= TILE_TRAVERSABLE_DEBUT && map.getTile(y2, x2) <= TILE_TRAVERSABLE_FIN)
|| map.getTile(y2, x1) > BLANK_TILE
|| map.getTile(y2, x2) > BLANK_TILE)
{
//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;
}

   Et voilà, et pour tester tout ça, je vous ai mis un niveau supplémentaire fait avec les tiles d'ombre, c'est cadeau ! wink

  Arriverez-vous au bout de ce niveau infernal, avant que nous ajoutions nos lumières pour aider notre pauvre lapin?indecision

   Au prochain chapitre, on ajoutera nos lumières, pour de vrai ! C’est promis ! laugh

   @ bientôt pour l'annexe 2 ! angel

                                          Skrool 

 

 

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!