Ecole Polytechnique Fédérale de Lausanne
Section Sciences de la vie
Programmation Orientée Objet
Tutoriel librairie graphique (SFML)

II. Dessin et affichage d'images

La prochaine étape est de dessiner les éléments pour notre jeu.

Couleur de fond

La couleur de fond peut se régler en passant un paramètre à la fonction clear de sf::RenderWindow.

Les couleurs dans SFML sont représentées par la classe sf::Color (cliquez pour accéder à la documentation). Elles sont codées en RGBA, c'est-à-dire quatre valeurs entières entre 0 et 255 pour rouge, vert, bleu et transparence (red, green, blue, alpha). Pour initialiser un objet de type sf::Color avec des valeurs données, on peut utiliser son constructeur en écrivant par exemple

sf::Color rouge(255,0,0);
sf::Color vert(0,255,0);
sf::Color bleu_transparent(0,0,255,100);

Colorons le fond de notre fenêtre en gris clair. Pour cela, on modifie l'appel à à la fonction clear de sf::RenderWindow par

window.clear(sf::Color(214,214,214));

Rectangles: dessin des raquettes (paddles)

Pour dessiner des rectangles, on utilise la classe sf::RectangleShape (cliquez pour accéder à la documentation). Un exemple d'utilisation est donné ci-dessous:

sf::RectangleShape rectangle;
rectangle.setSize(sf::Vector2f(100, 50)); // Taille (100x50)
rectangle.setOutlineColor(sf::Color(0,255,0)); //Contour
rectangle.setOutlineThickness(5); //Taille du contour
rectangle.setPosition(10, 20); //Position sur l'écran
Attention!
Comme dans beaucoup de librairies graphiques, le axes du système de coordonnées ne suivant pas les conventions habituelles. Ainsi, dans SFML, l'axe vertical pointe vers le bas.

L'envoi du dessin à l'écran se fait en passant le rectangle à la fonction draw de la sf::RenderWindow en question au moment du dessin dans la boucle de rendu. C'est exactement ce qui se passee à la ligne 14 de notre code :

#include <SFML/Graphics.hpp>

int main()
{
    sf::RenderWindow window(sf::VideoMode(800, 600), "Titre");
    
    sf::RectangleShape rectangle;
	rectangle.setSize(sf::Vector2f(100, 50)); // Taille (100x50)
	rectangle.setPosition(10, 20); //Position sur l'écran");

    while (window.isOpen())
    {
	sf::Event event;
        while (window.pollEvent(event)){
          // Les événements seront gérés plus tard ici
        }

        window.clear();
        window.draw(rectangle);
        window.display();
    }

    return EXIT_SUCCESS;
}

Dessinons les "paddles" des joueurs, c'est-à-dire les deux "raquettes", représentées par des rectangles blancs.. Il est bien sûr logique de vouloir créer une classe Paddle que nous instancierons ensuite deux fois, utilisant ainsi les avantages de la programmation orientée objet!

Pour cela, créez dans le dossier src un nouveau dossier Paddle dans lequel vous créerez un fichier Paddle.cpp et un fichier Paddle.h. Il s'agit d'une bonne pratique de créer un dossier par classe (où l'on stockera également plus tard les classes en relation d'héritage, à voir plus tard dans le cours).

Créez dans ces fichiers une classe Paddle avec les attributs et méthodes suivantes:

Attention!
La conception proposée ci-dessus n'est dans l'absolu pas bonne. Plus tard, il vous faudra utiliser une relation d'héritage pour toutes les classes qui sont "affichables" à l'écran.

Ajoutons maintenant cette classe au script de compilation scons. Pour cela, ouvrez le fichier src/SConscript et modifiez la ligne

src_files = []
en
src_files = Glob("Paddle/*.cpp")

Une fois cela fait, instanciez deux Paddle dans main.cpp aux positions voulues et dessinez-les. Votre fichier main.cpp pourrait alors ressembler à cela:

#include <SFML/Graphics.hpp>
#include "Paddle/Paddle.h"

int main()
{
    sf::RenderWindow window(sf::VideoMode(800, 600), "InfoSV Pong");
    
    //Paddles
    Paddle top(10);
    Paddle bottom(580);

    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event)){
           // Les événements seront gérés plus tard ici
        }
        window.clear(sf::Color(214,214,214));
        top.draw(window);
        bottom.draw(window);
        window.display();
    }

    return EXIT_SUCCESS;
}

Vous pouvez télécharger une solution de cette étape.

Cercles

Pour dessiner des cercle, on utilise la classe sf::CircleShape (cliquez pour accéder à la documentation), de manière similaire à sf::RectangleShape Nous vous renvoyons donc à la documentation de SFML.

Images (sprites)

Pour afficher des images, on utilise deux classes: sf::Texture (qui représente une image comme ressource en mémoire) et sf::Sprite (qui représente un objet ayant une texture, qui pourra être dessiné à l'écran).

Cette distinction est notamment faite pour des raisons d'optimisation: si la même image est affichée plusieurs fois, il serait stupide de la charger plusieurs fois en mémoire.

Ces deux classes peuvent s'utiliser de la façon suivante:

//création d'une texture à partie d'un fichier
sf::Texture texture;
texture.loadFromFile("texture.png");
//Création d'un sprite à partir de cette texture
sf::Sprite sprite(texture);
sprite.setPosition(100, 25);

Puis on envoie le dessin à l'écran comme on le faisait pour un sf::RectangleShape (voir ci-dessus), c'est-à-dire en passant le sprite à la fonction draw de la sf::RenderWindow en question au moment du dessin dans la boucle de rendu.

Dessinons la balle du jeu à partir de l'image d'une molécule d'eau.

Faites un clic droit sur l'image ci-dessus, sélectionne "Enregistrer l'image sous" et enregistrez-la dans votre dossier res sous le nom ball.png.

Comme pour les Paddle, il est pratique et opportun de créer une classe Ball, ayant comme attributs:

Implémentez cette classe, puis instanciez un objet de type Ball dans votre application. Appelez la fonction de dessin dans la boucle de rendu.

Pour pouvoir compiler, n'oubliez pas d'ajouter la classe Ball au fichier SConscript, c'est-à-dire modifiez la ligne

src_files = Glob("Paddle/*.cpp")
de celui-ci en
src_files = [Glob("Paddle/*.cpp"), Glob("Ball/*.cpp")] 

Vous devriez arriver au résultat suivant:

Vous pouvez télécharger une solution de cette étape.

Texte

Pour afficher du texte, on utilise les classes sf::Font (qui représente une police d'écriture) et sf::Text (qui représente un texte d'une police d'écriture donnée). La première n'est pas contenue dans la seconde pour les mêmes raisons que précédemment.

Un exemple d'utilisation est le suivant:

//Chargement police
sf::Font font;
font.loadFromFile("arial.ttf");
 
//Texte avec police "font" de taille 30
sf::Text text("Blabla", font, 30);
text.setStyle(sf::Text::Bold); //En gras
text.setColor(sf::Color::Red); //Couleur

//Changer le texte en cours de route.
text.setString("Blublu");

Puis on envoie le texte à l'écran comme on le faisait pour un sf::RectangleShape (voir ci-dessus), c'est-à-dire en passant le texte à la fonction draw de la sf::RenderWindow en question au moment du dessin dans la boucle de rendu.

Ajoutons l'affichage des scores à notre application.

A nouveau, nous allons adopter une conception orientée objet et créer une classe Score, dotée des attributs suivant:

Attention!
A nouveau, cette conception n'est dans l'absolu pas idéale. Vous verrez bientôt au cours que toutes les classes "dessinables" devraient être regroupées sous une relation d'héritage.

Implémentez cette classe. Pour convertir un int en string, vous pouvez utiliser le code suivant

int i(2); // exemple d'entier initialisé à 2 
std::stringstream oss;
oss<<i;
std::string s(oss.str());

après avoir inclus la librairie standard sstream (i.e. #include <sstream>).

ou plus simplement:
 int i(2);
std::string s(std::to_string(i));
// ici, s contient "2" 

Pour pouvoir compiler, n'oubliez pas d'ajouter la classe Score au fichier SConscript, c'est-à-dire modifiez la ligne

src_files = [Glob("Paddle/*.cpp"), Glob("Ball/*.cpp")] 
de celui-ci en
src_files = [Glob("Paddle/*.cpp"), Glob("Ball/*.cpp"), Glob("Score/*.cpp")] 

Intégrons maintenant cette classe à notre programme principal.

  1. Téléchargez la police arial.ttf dans votre dossier res en faisant un clic droit, puis "Enregistrer sous". Avant la boucle de rendu, chargez-la dans un nouvel objet sf::Font.

  2. Créez un tableau statique de deux Score avant la boucle de rendu et initialisez-les (méthode init) aux deux positions de votre souhait et avec la police précédemment chargée.
  3. Faites appel aux fonctions draw des Score dans la boucle de rendu pour les dessiner.

Vous devriez obtenir le résultat suivant:

Si vous voulez vous amuser un peu, vous pouvez essayer de rajouter le titre de notre jeu en transparence au fond comme ceci:

Si vous regardez un peu la documentation de sf::Text, vous verrez que les objets de ce type disposent d'une méthode setRotation qui permettra de faire subir une rotation au texte.

Vous pouvez télécharger une solution de cette étape.

Tutorial SFML
Corentin Perret, Jamila Sam - 2013-2019