Abstract Menu

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.

Exemple

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.

abstractmenuexemple.jpg

Tutoriel

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 :

  • Play : pour lancer le jeu
  • Option : pour accéder aux options de jeu (dont on ne développera pas sous menu)
  • Crédit : pour rendre à césar ce qui appartient à césar
  • Exit : pour fermer le jeu

On va donc avoir besoin de dériver 3 classes :

  • Menu : qui va hériter d'AbstractPanel et contiendra nos widget.
  • Button : un widget très basique, qui affichera une image de bouton (sf::sprite) et utilisera les WidgetEvent.
  • Cursor : le curseur qui va naviguer de widget en widget.

Menu

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 :

  • Next(), Previous(), Left(), Right(), Use() et Exit() : Ce sont les fonctions qui permettent de naviguer sur le panel. Elles correspondent aux actions auxquelles répondent le panel, à savoir les 4 direction haut, bas, gauche et droite, le bouton d'action qui va nous permettre d'actionner les boutons, et un bouton Exit qui va simplement ramener le curseur sur le bouton de sortie du panel.
  • AddWidget(AbstractWidget *) : La fonction qui va nous permettre d'ajouter des widget au panel.
  • AddChild(AbstractPanel *) : La fonction qui nous permet d'ajouter des sous panels.
  • SetPersistant(bool) : Indique sur le panel reste affiché lors de l'ouverture de l'un de ses panel enfants. Si il n'est pas persistant, le panel appel la fonction Hide() lors de l'ouverture d'un de ses enfants.
  • Focus() et UnFocus() : Permet d'activer le curseur du panel. Un panel unfocus ne répondra pas aux actions, mais transmettra les actions à ses enfants, en suposant que l'un de ses enfant possède le focus.
  • Lock() et UnLock() : Verouille un panel, le panel AINSI QUE ses enfants ne répondrons plus aux actions.

L'héritage d'un AbstractPanel nécessite d'implémententer les fonctions OnHide, OnDisplay, Calculate et Draw.

  • OnHide() : défini le comportement du panel lorsqu'il est fermé/quité,
  • OnDisplay() : défini le comportement du panel lorsqu'il est ouvert/affiché
  • Calculate() : permet d'effectuer des opération de calcul pour la frame en cour. time_interval correspond au temps de la frame (utile si on veut déplacer les boutons ou effectuer des animations)
  • Draw() : affiche simplement le panel et tout ses widget/enfants. alpha correspond à un décallage entre la frame en cour et la frame précédente. C'est une option strictement optionnelle.
Implémentation

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);
}

Button

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 :

  • SetPrevious/Next/Left/RightWidget() : Permet d'indiquer quel widget sera pointé par le curseur à partir du widget actuel, respectivement par les action Previous(), Next(), Left() et Right() du panel.
  • SetUse/SwitchLeft/SwitchRightEvent() : Permet d'attribuer les evenements à actionner respectivement lors de l'appel à l'action Use, Left ou Right. A NOTER qu'un evenement gauche/droite peut être appeler avec un mouvement vers un widget gauche/droite.
  • SetNextPanel() : Si un sous-panel est entré avec cette fonction, alors le bouton permettra de l'ouvrir via l'action Use(). ATTENTION : Le sous-panel doit être stocké dans le panel courant avec la fonction SetChild().
  • SetPanelExit() : A mettre sur True pour que le bouton permette de quitter le panel courant lors d'un appel à l'action Use().
  • OnMove(), OnUse(), etc… : Ces fonctions sont là pour définir le comportement du button lors des evenements accessibles par ces fonctions. Elles ne font rien par défaut, mais vous pouvez les réimplémenter à votre guise. Le bool indique si l'action a été réussi ou non. (Par exemple, un SwitchLeft alors qu'il n'y a aucun event ni widget pour l'action Left() ne donne rien, le bool sera sur false)

L'héritage d'un AbstractWidget nécessite l'implémentation des fonction Calculate et Draw.

Implémentation

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;
}

Cursor

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 :

  • OnMoveNext(), OnMoveRight(), OnUse(), etc… : Comme pour les widget, ces fonctions définissent le comportement du cursor lors des différents évenements. L'utilisation la plus courante pour ces fonction est pour jouer un son lorsqu'on déplace le curseur, tente d'actionner un bouton ou de quitter le panel.
Implémentation

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);
}

Widget Event

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.

Implémentation

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.
		}
};

Le main

Il nous reste à construire un petit programme pour tester notre menu.

Implémentation
#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

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

 
fr/sources/abstract_menu.txt · Last modified: 2010/08/24 18:51 by Spidyy
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki