En parlant d'écrans, je veux parler des écrans de présentation, de menu, d'options, etc… que l'on trouve dans tous les jeux. Le problème c'est que chaque écran est une petite application en soi : un écran de menu va gérer les flêches afin que l'utilisateur se déplace entre les menus, alors qu'un écran de présentation n'attendra rien d'autre qu'un simple appui sur une touche ou un clic de souris. De même, un écran de jeu utilisera les flêches pour déplacer le personnage par exemple.
Il convient donc de séparer chaque écran afin d'éviter les conflits entre eux. Heureusement, grâce à la SFML, c'est très simple ! Il suffit de déclarer un objet Screen qui représentera chaque écran. Cet objet est complètement virtuel, puisque sans informations du programmeur, il ne sert à rien :
class Screen { public : virtual int Run (sf::RenderWindow &App) = 0; };
Cet objet est volontairement ultra simple pour ne pas perturber le fonctionnement ou l'architecture de l'application qui l'utilise. Run est la fonction appelée depuis le main, et elle renvoie soit le numéro du prochain écran à afficher, soit -1 pour signifier la fin de l'application. Et voilà, avec un objet aussi simple, chacun de nos écrans sera un objet qui possèdera ses propres variables, sa boucle d'évènement, etc…
L'ancienne boucle SFML des events est alors légèrement modifiée :
while (screen >= 0) { screen = screens[screen]->Run(App); }
Il suffit, dès lors, de spécifier le premier écran, et de rentrer dans cette boucle. Chaque objet écran utilisera l'objet sf::RenderWindow pour capturer les évènements, afficher les images, etc…
De plus, les écrans étant des objets créés dans le main, ils conservent les valeurs contenues dans les objets. Cela permet par exemple de faire une pause dans un jeu, et d'afficher le menu de configuration, puis de revenir dans le jeu, là où on l'a laissé.
Voici une application démo qui utilise l'objet cScreen :
- Le main simple au possible
#include <fstream> #include <iostream> #include <sfml/Graphics.hpp> #include "screens.hpp" int main(int argc, char** argv) { //Applications variables std::vector<Screen*> screens; int screen = 0; //Window creation sf::RenderWindow App(sf::VideoMode(640, 480, 32), "SFML Demo 3"); //Mouse cursor no more visible App.ShowMouseCursor(false); //Screens preparations Screen_0 s0; screens.push_back (&s0); Screen_1 s1; screens.push_back (&s1); //Main loop while (screen >= 0) { screen = screens[screen]->Run(App); } return EXIT_SUCCESS; }
- screens.hpp contient un résumé de l'objet, et des différents objets Screen utilisés par le programmeur. Ici, Screen_0 et Screen_1 seront des objets héritants de Screen. Screen_0 correspond au menu, et Screen_1 au jeu. Afin de simplifier le programme, on utilisera aussi un enum pour distinguer les différents écrans :
#ifndef SCREENS_HPP_INCLUDED #define SCREENS_HPP_INCLUDED //Basic Screen Class #include "Screen.hpp" //Including each screen of application #include "Screen_0.hpp" #include "Screen_1.hpp" enum { SCREEN_EXIT = -1, SCREEN_0, // Il sera intéressant de nommer correctement les labels SCREEN_1 // Du type SCREEN_MENU, SCREEN_GAME, SCREEN_OPTIONS, etc... } #endif // SCREENS_HPP_INCLUDED
- Screen_0.hpp contient la définition de l'objet et son code. La boucle Run est comme la boucle “principale” que l'on aurait mise dans le main auparavant. Le constructeur définit les membres de l'objet qui sont, ici, plutôt des variables dont on souhaite conserver la valeur (type statique donc). Cela permet d'afficher l'écran de présentation via un fondu, mais une fois affiché, on ne fait plus de fondu. Une fois affiché, on présente les 2 menus qu'on peut utiliser via les flêches. L'appui sur Play nous fera quitter la fonction Run avec la valeur 1 (le prochain Screen à utiliser donc) et si on appuie sur Echap, on renvoie la valeur -1 (Quitter l'application) :
#include <iostream> #include "Screen.hpp" class Screen_0 : public Screen { private: int alpha_max; int alpha_div; bool playing; public: Screen_0 (void); virtual int Run (sf::RenderWindow &App); }; Screen_0::Screen_0 (void) { alpha_max = 3*255; alpha_div = 3; playing = false; } int Screen_0::Run (sf::RenderWindow &App) { sf::Event Event; bool Running = true; sf::Image Image; sf::Sprite Sprite; int alpha = 0; sf::Font Font; sf::String Menu1; sf::String Menu2; sf::String Menu3; int menu = 0; if (!Image.LoadFromFile("presentation.png")) { std::cerr<<"Error loading presentation.gif"<<std::endl; return (SCREEN_EXIT); } Sprite.SetImage(Image); Sprite.SetColor(sf::Color(255, 255, 255, alpha)); if (!Font.LoadFromFile("verdanab.ttf")) { std::cerr<<"Error loading verdanab.ttf"<<std::endl; return (SCREEN_EXIT); } Menu1.SetFont(Font); Menu1.SetSize(20); Menu1.SetText("Play"); Menu1.SetX(280); Menu1.SetY(160); Menu2.SetFont(Font); Menu2.SetSize(20); Menu2.SetText("Exit"); Menu2.SetX(280); Menu2.SetY(220); Menu3.SetFont(Font); Menu3.SetSize(20); Menu3.SetText("Continue"); Menu3.SetX(280); Menu3.SetY(160); //Clearing screen App.SetBackgroundColor(sf::Color(0, 0, 0, 0)); if (playing) { alpha = alpha_max; } while (Running) { //Verifying events while (App.GetEvent(Event)) { // Window closed if (Event.Type == sf::Event::Closed) { return (SCREEN_EXIT); } //Key pressed if (Event.Type == sf::Event::KeyPressed) { switch (Event.Key.Code) { case sf::Key::Up: menu = 0; break; case sf::Key::Down: menu = 1; break; case sf::Key::Return: if (menu==0) { //Let's get play ! playing = true; return (SCREEN_1); } else { //Let's get work... return (SCREEN_EXIT); } break; default : break; } } } //When getting at alpha_max, we stop modifying the sprite if (alpha<alpha_max) { alpha++; } Sprite.SetColor(sf::Color(255, 255, 255, alpha/alpha_div)); if (menu==0) { Menu1.SetColor(sf::Color(255, 0, 0, 255)); Menu2.SetColor(sf::Color(255, 255, 255, 255)); Menu3.SetColor(sf::Color(255, 0, 0, 255)); } else { Menu1.SetColor(sf::Color(255, 255, 255, 255)); Menu2.SetColor(sf::Color(255, 0, 0, 255)); Menu3.SetColor(sf::Color(255, 255, 255, 255)); } //Drawing App.Draw(Sprite); if (alpha==alpha_max) { if (playing) { App.Draw(Menu3); } else { App.Draw(Menu1); } App.Draw(Menu2); } App.Display(); } //Never reaching this point normally, but just in case, exit the application return (SCREEN_EXIT); }
- Screen_1.hpp contient le jeu lui-même. On ne peut pas quitter l'application directement ici, un appui sur Echap nous fait revenir au menu. Par contre, on voit bien l'intérêt de conserver les valeurs du jeu, lorsqu'on revient du menu on sera situé au même endroit. :
#include <iostream> #include "Screen.hpp" class Screen_1 : public Screen { private: int movement_step; int posx; int posy; sf::Sprite Sprite; public: Screen_1 (void); virtual int Run (sf::RenderWindow &App); }; Screen_1::Screen_1 (void) { movement_step = 5; posx = 320; posy = 240; //Setting sprite Sprite.SetColor(sf::Color(255, 255, 255, 150)); Sprite.SetSubRect(sf::IntRect(0, 0, 10, 10)); } int Screen_1::Run (sf::RenderWindow &App) { sf::Event Event; bool Running = true; //Clearing screen App.SetBackgroundColor(sf::Color(0, 0, 0, 0)); while (Running) { //Verifying events while (App.GetEvent(Event)) { // Window closed if (Event.Type == sf::Event::Closed) { return (SCREEN_EXIT); } //Key pressed if (Event.Type == sf::Event::KeyPressed) { switch (Event.Key.Code) { case sf::Key::Escape: return (SCREEN_0); break; case sf::Key::Up: posy -= movement_step; break; case sf::Key::Down: posy += movement_step; break; case sf::Key::Left: posx -= movement_step; break; case sf::Key::Right: posx += movement_step; break; default: break; } } } //Updating if (posx>630) posx = 630; if (posx<0) posx = 0; if (posy>470) posy = 470; if (posy<0) posy = 0; Sprite.SetPosition(posx, posy); //Drawing App.Draw(Sprite); App.Display(); } //Never reaching this point normally, but just in case, exit the application return SCREEN_EXIT; }
Voilà, en essayant ce code, on a séparé les différentes parties de l'application (jeu ou pas d'ailleurs) sans pour autant trop modifier chacune de ces parties.
Bon courage à vous maintenant !