Débuter dans la création d'interfaces graphiques avec Qt 4

Image non disponible


précédentsommairesuivant

XV. Game Over

t13.rar

Fichiers
  • tutorials/tutorial/t13/cannonfield.cpp
  • tutorials/tutorial/t13/cannonfield.h
  • tutorials/tutorial/t13/gameboard.cpp
  • tutorials/tutorial/t13/gameboard.h
  • tutorials/tutorial/t13/lcdrange.cpp
  • tutorials/tutorial/t13/lcdrange.h
  • tutorials/tutorial/t13/main.cpp
  • tutorials/tutorial/t13/t13.pro
Image non disponible

Avec cet exemple, nous commençons à nous approcher d'un jeu vraiment jouable. Nous donnons aussi à MyWidget un vrai nom (GameBoard) et nous ajoutons quelques slots supplémentaires.

Nous plaçons la définition dans gameboard.h et l'implémentation dans gameboard.cpp.

Le CannonField a désormais un état game over.

Les problèmes d'affichage de LCDRange sont réglés.

XV-A. Analyse du code ligne par ligne

XV-A-1. t13/lcdrange.cpp

 
Sélectionnez
     label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);

Nous déclarons la taille de police du QLabel à (Preferred, Fixed). Le composant vertical s'assure que le QLabel ne bougera pas verticalement ; il va rester à sa taille optimale (sa sizeHint()). Ceci résout le problème d'affichage rencontré au cours du chapitre XIV.

XV-A-2. t13/cannonfield.h

Le CannonField possède, outre un état gameOver, quelques fonctions supplémentaires.

 
Sélectionnez
bool gameOver() const { return gameEnded; }

Cette fonction renvoie true si le jeu est perdu, false s'il continue.

 
Sélectionnez
void setGameOver();
void restartGame();

Voici les deux nouveaux slots : setGameOver() et restartGame().

 
Sélectionnez
void canShoot(bool can);

Ce nouveau signal indique que le CannonField est dans un état dans lequel le slot shoot() a du sens. Nous allons l'utiliser bientôt pour activer ou non le boutton Tirer.

 
Sélectionnez
bool gameEnded;

Cette variable privée contient l'état du jeu ; true signifie que le jeu s'arrête ; false, qu'il continue.

XV-A-3. t13/cannonfield.cpp

 
Sélectionnez
gameEnded = false;

Cette ligne a été ajoutée au constructeur. Initialement, le jeu n'est pas fini (heureusement pour le joueur :-) ).

 
Sélectionnez
void CannonField::shoot()
{
    if (isShooting())
        return;
    timerCount = 0;
    shootAngle = currentAngle;
    shootForce = currentForce;
    autoShootTimer->start(5);
    emit canShoot(false);
}

Nous avons ajouté une nouvelle fonction isShooting(), pour que shoot() l'utilise, au lieu de le tester directement. Ainsi, shoot() nous indique si le CannonFiels peut attaquer ou non.

 
Sélectionnez
void CannonField::setGameOver()
{
    if (gameEnded)
        return;
    if (isShooting())
        autoShootTimer->stop();
    gameEnded = true;
    update();
}

Ce slot finit le jeu. Il doit être appelé en dehors du CannonField, car ce widget ne sait pas quand finir le jeu. Ceci est un principe de design très important dans la programmation par composants. Nous avons choisi de créer un composant aussi flexible que possible pour qu'il puisse se plier à différentes règles (par exemple, une version multijoueur, dans laquelle le joueur gagnant est celui qui le premier touche dix cibles, peut utiliser le CannonField inchangé).

Si le jeu est déjà terminé, nous retournons immédiatement. Si le jeu continue, nous arrêtons le tir, utilisons le drapeau game over et redessinons l'intégralité de ce widget.

 
Sélectionnez
void CannonField::restartGame()
{
    if (isShooting())
        autoShootTimer->stop();
    gameEnded = false;
    update();
    emit canShoot(true);
}

Ce slot commence une nouvelle partie. Si un projectile est en l'air, nous l'arrêtons. Ensuite, nous remettons la variable gameEnded à false et repeignons le widget.

restartGame() émet en outre le (nouveau) signal canShoot(true), en même temps que hit() ou miss().

Modifications dans CannonField::paintEvent() :

 
Sélectionnez
void CannonField::paintEvent(QPaintEvent * /* event */)
{
    QPainter painter(this);

    if (gameEnded) {
        painter.setPen(Qt::black);
        painter.setFont(QFont("Courier", 48, QFont::Bold));
        painter.drawText(rect(), Qt::AlignCenter, tr("Game Over"));
    }

L'événement paint a été amélioré pour afficher le texte Game Over, si le jeu est fini (par exemple, si gameEnded est à true). Nous ne nous occupons pas de vérifier la mise à jour du rectangle, car la vitesse n'est pas du tout critique quand le jeu est fini.

Pour dessiner le texte, nous utilisons d'abord un stylo noir ; cette couleur est utilisée quand le texte est dessiné. Ensuite, nous choisissons une police, de la famille Courier, de 48 points de haut et grasse. Finalement, nous dessinons le texte, au centre du widget du rectangle. Malheureusement, sur certains systèmes (spécialement les serveurs X avec des polices Unicode), le chargement d'une police aussi grande peut prendre beaucoup de temps. Mais comme Qt met en cache les polices, vous ne remarquerez ce problème que la première fois que la police est utilisée.

 
Sélectionnez
    paintCannon(painter);
    if (isShooting())
        paintShot(painter);
    if (!gameEnded)
        paintTarget(painter);
}

Nous ne dessinons le projectile que quand on tire ; la cible, seulement quand on joue.

XV-A-4. t13/gameboard.h

Ce fichier est nouveau : il contient la définition de la classe GameBoard, qui correspond au MyWidget des paragraphes précédents.

 
Sélectionnez
class CannonField;

class GameBoard : public QWidget
{
    Q_OBJECT

public:
    GameBoard(QWidget *parent = 0);

protected slots:
    void fire();
    void hit();
    void missed();
    void newGame();

private:
    QLCDNumber *hits;
    QLCDNumber *shotsLeft;
    CannonField *cannonField;
};

Nous avons ajouté quatre slots, qui sont protégés, et utilisés en interne. Nous avons aussi ajouté deux QLCDNumbers (hits et shotsLeft) qui affichent l'état du jeu.

XV-A-5. t13/gameboard.cpp

Ce fichier est nouveau, lui aussi, et contient l'implémentation de la classe.

Nous avons fait quelques changements dans le constructeur.

 
Sélectionnez
cannonField = new CannonField;

cannonField est désormais une variable membre, nous changeons donc le constructeur pour le prendre en compte.

 
Sélectionnez
connect(cannonField, SIGNAL(hit()),
        this, SLOT(hit()));
connect(cannonField, SIGNAL(missed()),
        this, SLOT(missed()));

Cette fois, nous voulons que quelque chose se produise quand le tir atteint ou manque la cible. Ainsi, nous connectons les signaux hit() et missed du CannonField aux deux slots protégés homonymes de la classe.

 
Sélectionnez
connect(shoot, SIGNAL(clicked()),
        this, SLOT(fire()));

Auparavant, nous avons connecté le signal clicked() du bouton directement au slot shoot() du CannonField. Cette fois, voulant garder la trace du nombre de tirs, nous le connectons au slot protégé dans la classe.

Notez la simplicité avec laquelle nous changeons le comportement d'un programme quand nous travaillons avec des composants.

 
Sélectionnez
connect(cannonField, SIGNAL(canShoot(bool)),
        shoot, SLOT(setEnabled(bool)));

Nous utilisons aussi le signal canShoot() pour activer ou non le bouton de tir.

 
Sélectionnez
QPushButton *restart = new QPushButton(tr("&New Game"));
restart->setFont(QFont("Times", 18, QFont::Bold));
connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));

Nous créons, initialisons et connectons le bouton New Game, comme nous l'avons fait pour les autres boutons. Un simple clic sur ce bouton activera le slot newGame() dans ce widget.

 
Sélectionnez
hits = new QLCDNumber(2);
hits->setSegmentStyle(QLCDNumber::Filled);

shotsLeft = new QLCDNumber(2);
shotsLeft->setSegmentStyle(QLCDNumber::Filled);

QLabel *hitsLabel = new QLabel(tr("HITS"));
QLabel *shotsLeftLabel = new QLabel(tr("SHOTS LEFT"));

Nous créons quatre nouveaux widgets. Notez que nous ne nous occupons pas de garder les pointeurs vers les QLabel dans la classe, car nous ne voulons plus rien faire avec ceux-là. Qt les supprimera quand le widget GameBoard sera détruit et que les classes qui gèrent l'affichage les redimensionneront de manière appropriée.

 
Sélectionnez
QHBoxLayout *topLayout = new QHBoxLayout;
topLayout->addWidget(shoot);
topLayout->addWidget(hits);
topLayout->addWidget(hitsLabel);
topLayout->addWidget(shotsLeft);
topLayout->addWidget(shotsLeftLabel);
topLayout->addStretch(1);
topLayout->addWidget(restart);

La cellule, en haut à droite du QGridLayout, commence à être encombrée. Nous ajoutons un espace immédiatement à gauche du bouton New Game pour être sûr que ce bouton apparaisse toujours sur le côté droit de la fenêtre.

 
Sélectionnez
newGame();

Nous en avons fini avec la construction du GameBoard ; nous commençons donc à l'utiliser avec newGame(). Même si newGame() est un slot, il peut être utilisé comme une fonction ordinaire.

 
Sélectionnez
void GameBoard::fire()
{
    if (cannonField->gameOver() || cannonField->isShooting())
        return;
    shotsLeft->display(shotsLeft->intValue() - 1);
    cannonField->shoot();
}

Cette fonction déclenche un tir. Si le jeu est fini ou s'il y a déjà un projectile en l'air, nous nous arrêtons immédiatement. Nous décrémentons le nombre de tirs restants et demandons au canon de tirer.

 
Sélectionnez
void GameBoard::hit()
{
    hits->display(hits->intValue() + 1);
    if (shotsLeft->intValue() == 0)
        cannonField->setGameOver();
    else
        cannonField->newTarget();
}

Ce slot est activé quand un tir a touché la cible. Nous incrémentons le nombre de tirs réussis. S'il n'y a plus de tir restant, le jeu est fini. Sinon, nous demandons au CannonField de générer une nouvelle cible.

 
Sélectionnez
void GameBoard::missed()
{
    if (shotsLeft->intValue() == 0)
        cannonField->setGameOver();
}

Ce slot est activé quand un tir a manqué sa cible. S'il n'y a plus de tir restant, le jeu est fini.

 
Sélectionnez
void GameBoard::newGame()
{
    shotsLeft->display(15);
    hits->display(0);
    cannonField->restartGame();
    cannonField->newTarget();
}

Ce slot est activé quand l'utilisateur clique sur le bouton New Game. Il est aussi appelé par le constructeur. Initialement, il autorise quinze tirs. Notez que c'est là le seul emplacement du programme où nous indiquons le nombre de tirs. Changez-le si vous le souhaitez. Ensuite, nous remettons à zéro le nombre de tirs réussis, relançons le jeu et générons une nouvelle cible.

XV-A-6. t13/main.cpp

Ce fichier a subi un "régime amaigrissant" ! MyWidget est parti, il ne reste plus que la fonction main(), inchangée, si ce n'est pour le nom.

XV-B. Exécuter l'application

Le canon peut tirer sur une cible ; une nouvelle cible est automatiquement créée quand la précédente est touchée.

Tirs restants et tirs réussis sont affichés et le programme en garde une trace. Le jeu prend fin, un bouton apparaît, permettant de commencer une nouvelle partie.

XV-C. Exercices

Ajoutez un vent pseudoaléatoire et le montrer à l'utilisateur.

Affichez quelques effets quand la cible est touchée.

Implémentez des cibles multiples.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2009 - 2019 Developpez.com LLC Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.