Vous trouverez ici un lot de classes utiles pour construire un menu parcourable à l'aide d'un curseur. Ce menu n'est pas prévu pour répondre au click de souris, seulement aux events SFML comme le clavier ou le joystick.
J'utilise ces classes pour mon propre jeu, Excellence (anciennement Eve Shooter)
Ci-dessous, un screen de mon menu d'option, composé de boutons, labels, etc. On peut appercevoir le menu principal en fond (à gauche) qui est lui même un panel. Le menu d'option est un enfant du menu principal.
La composition d'un menu avec les classe abstract est légèrement compliqué à comprendre, mais la composition du menu s'en revèle ensuite très simple. Les classes sont basés sur Qt, dans le sens où il faut créer les objects à partir de l'opérateur new, puis les insérer dans des objets parents.
Au cours du tutoriel, je créer les objets de la manière la plus simple qui soit. A vous ensuite de les étoffer graphiquement pour les rendre plus jolis.
On va passer de suite à la partie pratique. On va créer un menu simple avec 4 bouton :
On va donc avoir besoin de dériver 3 classes :
Le menu est représenté par le panneau qui contiendra nos boutons. On lui fait donc hériter de la classe AbstractPanel.
Un panel est un container qui permet d'afficher les widgets qu'il contient en 1 fois. Il contient aussi les sous-panel. Les sous-panel peuvent être ouvert via un widget qui appel la fonction Show du panel auquel il est lié.
Quelques fonctions du panel à connaitre :
L'héritage d'un AbstractPanel nécessite d'implémententer les fonctions OnHide, OnDisplay, Calculate et Draw.
Menu.h
#ifndef MENU_H #define MENU_H #include "AbstractPanel.h" class Menu : public AbstractPanel { virtual void OnDisplay(bool entering); virtual void OnHide(bool entering); public: Menu(AbstractCursor * cursor, AbstractPanel * parent = 0); // On doit pouvoir faire passer le curseur utilisé pour le panel. virtual void Calculate(sf::RenderTarget & target, float time_interval); virtual void Draw(float alpha = 1.f); }; #endif
Menu.cpp
#include "Menu.h" #include "VideoSystem.h" Menu::Menu(AbstractCursor * cursor, AbstractPanel * parent) : AbstractPanel(cursor, parent) { } void Menu::OnDisplay(bool entering) { SetVisible(true); Focus(entering); } void Menu::OnHide(bool entering) { SetVisible(false); UnFocus(); } // Il est important ici d'appeler CalculateWidgets, CalculateCursor et CalculatePanels, sinon les élements du panel et ses enfants ne seront pas calculés. void Menu::Calculate(float time_interval) { if(IsVisible()) { CalculateWidgets(time_interval); CalculateCursor(time_interval); } CalculatePanels(time_interval); } // Même remarque qu'au dessus pour les fonctions DrawCursor, DrawWidgets et DrawPanels, sinon aucun des élements/enfants n'apparaitrons. void Menu::Draw(sf::RenderTarget & target, float alpha) { if(IsVisible()) // On ne dessine les élements du panel que si ce dernier est visible { DrawCursor(target, alpha); DrawWidgets(target, alpha); } DrawPanels(target, alpha); }
Le button est le widget le plus basique possible pour nos menu. Il reçoit les commandes du curseur et éxecute les events qui lui sont attribués.
Pour être inseré dans le Menu, il hérite de AbstractWidget. Pratiquement toutes les fonctions utiles pour faire fonctionner notre bouton sont définis par AbstractWidget, notre classe button servira donc juste à donner une apparence graphique à notre bouton.
Quelques fonctions des widget à connaitre :
L'héritage d'un AbstractWidget nécessite l'implémentation des fonction Calculate et Draw.
Button.h
#ifndef BUTTON_H
#define BUTTON_H
#include "AbstractWidget.h"
#include <string>
class Button : public AbstractWidget
{
sf::Image m_image; // L'image de notre bouton
sf::Sprite m_sprite; // Le sprite pour afficher l'image
sf::Vector2f m_position; // La position de notre bouton à l'écran
void OnUse(bool success); // Principalement pour montrer l'utilité des fonctions evenement;
public:
Button(const std::string & image_name, const sf::Vector2f & position);
virtual void Calculate(float time_interval);
virtual void Draw(sf::RenderTarget & target, float alpha = 1.f);
const sf::Vector2f & GetPosition() const; // Retourne la position du button, sera utile pour notre cursor
};
#endif
Button.cpp
#include "Button.h" #include <iostream> Button::Button(const std::string & image_name, const sf::Vector2f & position) : m_position(position) { m_image.LoadFromFile(image_name); m_sprite.SetImage(m_image); m_sprite.SetPosition(position); } void Button::Calculate(float time_interval) { } void Button::Draw(sf::RenderTarget & target, float alpha) { target.Draw(m_sprite); } const sf::Vector2f & Button::GetPosition() const { return m_position; } void Button::OnUse(bool success) { if(success) std::cout << "Click !" << std::endl; }
Le cursor va nous permettre de naviguer entre nos boutton. L'AbstractCursor fournis toutes les fonctions utiles pour faire fonctionner notre curseur, notre classe Cursor va donc lui fournir une apparence graphique.
Quelques fonctions à connaitre :
Cursor.h
#ifndef CURSOR_H #define CURSOR_H #include "AbstractCursor.h" class Cursor : public AbstractCursor { sf::Shape shape; public: // Constructeurs Cursor(AbstractWidget * first = 0); // Fonctions de la classe virtual void Calculate(float time_interval); virtual void Draw(sf::RenderTarget & target, float alpha = 1.f); }; #endif
Cursor.cpp
#include "Cursor.h" #include "Button.h" // Constructeurs Cursor::Cursor(AbstractWidget * first) : AbstractCursor(first) { shape = sf::Shape::Rectangle(sf::FloatRect(0, 0, 150.f, 50.f), sf::Color(0,0,0,0), 1, sf::Color::White); shape.SetPointColor(0, sf::Color(0, 128, 255, 128)); shape.SetPointColor(1, sf::Color(0, 128, 255, 0)); shape.SetPointColor(2, sf::Color(0, 128, 255, 0)); shape.SetPointColor(3, sf::Color(0, 128, 255, 128)); } // Fonctions de la classe void Cursor::Calculate(float time_interval) { } void Cursor::Draw(sf::RenderTarget & target, float alpha) { Button * current = static_cast<Button *>(GetCurrent()); sf::Vector2f position = current->GetPosition(); shape.SetPosition(position); target.Draw(shape); }
Il nous reste à ajouter quelques comportements spécifique pour nos boutons. Les WidgetEvent sont des classe très simple, réimplémentant une simple fonction Execute() qui sera appelée par les evements SwitchLeft, SwitchRight et Use du panel.
Je vais juste Implémenter une simple fonction pour switcher un booléen sur sa valeur opposée en guise d'exemple.
WidgetEvent.h
class BoolSwapWidgetEvent : public AbstractWidgetEvent { bool & m_value; public: BoolSwapWidgetEvent(bool & value) : m_value(value){} virtual bool Execute() { value = !value; return true; // Renvoyer true si l'opération s'est bien passée, false en cas contraire. } };
Il nous reste à construire un petit programme pour tester notre menu.
#include <SFML/Graphics.hpp> #include "Menu.h" #include "Button.h" #include "Cursor.h" #include "WidgetEvent.h" int main() { sf::RenderWindow App(sf::VideoMode(800, 600), "Test Menu"); // On créé notre menu avec son cursor Menu my_menu(new Cursor()); // On créé nos 4 boutons Button * play_button = new Button("play_button.png", sf::Vector2f(200, 100)); Button * option_button = new Button("option_button.png", sf::Vector2f(200, 300)); Button * credit_button = new Button("credit_button.png", sf::Vector2f(400, 300)); Button * exit_button = new Button("exit_button.png", sf::Vector2f(200, 500)); // On créé un booléen pour indiquer si on veut jouer ou non bool play = false; // On lie ce booléen à notre bouton play via un widget event play_button->SetUseWidgetEvent(new BoolSwapWidgetEvent(play)); // On lie les widget entre eux pour pouvoir naviguer dans le menu (voir dessin)
play_button->SetNextWidget(option_button); option_button->SetNextWidget(exit_button); option_button->SetPreviousWidget(play_button); option_button->SetRightWidget(credit_button); credit_button->SetLeftWidget(option_button); exit_button->SetPreviousWidget(option_button); // On ajoute les button à notre menu // Le premier widget ajouté est considéré comme le widget d'entré my_menu.addWidget(play_button); my_menu.addWidget(option_button); my_menu.addWidget(credit_button); my_menu.addWidget(exit_button); // On défini le button de sortie pour répondre à l'action Exit() my_menu.SetExitWidget(exit_button); // On défini les bouton play et exit comme des bouton fermant le panel play_button->SetPanelExit(true); exit_button->SetPanelExit(true); // Et on fait apparaitre notre menu my_menu.Display(); // On commence notre boucle // Le test ne comporte que notre menu, donc dés que le menu est fermé, on peut quitter le test while(my_menu->IsVisible()) { sf::Event Event; while(App.GetEvent(Event)) { // On ferme le menu lors de l'event close if(event.Type == sf::Event::Closed) my_menu.Hide(); } // On lie les events SFML à nos actions if(App.GetInput().IsKeyDown(sf::Key::Up)) my_menu.Previous(); if(App.GetInput().IsKeyDown(sf::Key::Down)) my_menu.Next(); if(App.GetInput().IsKeyDown(sf::Key::Left)) my_menu.Left(); if(App.GetInput().IsKeyDown(sf::Key::Right)) my_menu.Right(); if(App.GetInput().IsKeyDown(sf::Key::Enter)) my_menu.Use(); if(App.GetInput().IsKeyDown(sf::Key::Escape)) my_menu.Exit(); // On effectue les calculs (bien qu'on en ai défini aucun dans nos classes) my_menu.Calculate(App.GetFrameTime()); // Et on dessine App.Clear(); my_menu.Draw(App); App.Display(); } // Ici, on peut vérifier si notre bool est sur vrai pour lancer le jeu //if(play) // Game::Play(); return EXIT_SUCCESS; }
Fichiers utilitaires
Les classes ScopedPtr, PtrVector et PtrList sont des classes de conteneur de pointeurs très simple, qui assure la destruction des objets pointés. (sans avoir à utiliser delete) Ces classes sont un substitut aux classes Boost du même nom (boost::scoped_ptr, boost::ptr_vector, boost::ptr_list), il est vivement recommandé de les remplacer par les classes boost. (Attention, les ”(*it)” dans les .cpp des classe Abstract devront être remplacés par “it” si vous passez avec Boost.)
Fichiers du projet