Sur cette page

Rogue

L'exemple Rogue montre comment utiliser la Qt State Machine pour la gestion des événements.

Jeu textuel dans lequel le joueur @ se déplace sur une grille composée de points et de murs #

Cet exemple met en œuvre un jeu simple basé sur du texte. Voyez-vous l’ @ e sur la capture d’écran ? C’est vous, le voleur. Les caractères # correspondent aux murs, et les points représentent le sol. Dans un vrai jeu, d’autres caractères ASCII représenteraient toutes sortes d’objets et de créatures, par exemple des dragons anciens (D) ou des rations alimentaires (%). Mais ne nous emballons pas. Dans ce jeu, le voleur se contente de courir dans une pièce vide.

Le personnage se déplace à l’aide du pavé numérique (2, 4, 8, 6). Par ailleurs, nous avons implémenté une commande « quit » qui se déclenche si le joueur tape « q ». On demande alors au joueur s’il souhaite vraiment quitter le jeu.

La plupart des jeux comportent des commandes qui nécessitent d’appuyer sur plusieurs touches (nous pensons ici à des pressions successives, et non à plusieurs touches enfoncées simultanément). Dans ce jeu, seule la commande « quit » entre dans cette catégorie, mais pour les besoins de l’exemple, imaginons un jeu à part entière doté d’un ensemble complet de commandes. Si nous devions les implémenter en interceptant les événements de touche dans ` keyPressEvent()`, nous devrions gérer de nombreuses variables membres de classe pour suivre la séquence des touches déjà saisies (ou trouver un autre moyen de déduire l'état actuel d'une commande). Cela peut facilement conduire à un code en spaghettis, ce qui est – comme nous le savons tous très bien, j’en suis sûr – désagréable. Avec une machine à états, en revanche, des états distincts peuvent attendre la pression d’une seule touche, ce qui nous simplifie grandement la vie.

L’exemple se compose de deux classes :

  • Window affiche le texte du jeu et configure la machine à états. La fenêtre comporte également une barre d’état au-dessus de la zone dans laquelle se déplace le voleur.
  • MovementTransition est une transition qui effectue un seul déplacement du voleur.

Avant de nous lancer dans une analyse du code, il est nécessaire d’examiner de plus près la conception de la machine. Voici un diagramme d’états qui illustre ce que nous souhaitons réaliser :

État d'entrée et état de sortie : q mène à l'état de sortie, n revient en arrière, y mène à l'état final

L’état « input » attend qu’une touche soit enfoncée pour lancer une nouvelle commande. Lorsqu’il reçoit une touche qu’il reconnaît, il passe à l’une des deux commandes du jeu ; cependant, comme nous le verrons, le déplacement est géré par la transition elle-même. L’état « quit » attend que le joueur réponde par oui ou par non (en tapant « y » ou « n ») lorsqu’on lui demande s’il souhaite vraiment quitter le jeu.

Le schéma illustre comment utiliser un état pour attendre la pression d’une seule touche. La touche enfoncée peut déclencher l’une des transitions associées à cet état.

Définition de la classe « Window »

La classe ` Window ` est un widget qui affiche le texte du jeu. Elle configure également la machine à états, c'est-à-dire qu'elle crée et relie les états de la machine. Ce sont les événements de touche provenant de ce widget qui sont utilisés par la machine.

class Window : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(QString status READ status WRITE setStatus)

public:
    enum Direction { Up, Down, Left, Right };

    Window();

    void movePlayer(Direction direction);
    void setStatus(const QString &status);
    QString status() const;

    QSize sizeHint() const override;

protected:
    void paintEvent(QPaintEvent *event) override;

Direction spécifie la direction dans laquelle le personnage doit se déplacer. Nous utilisons cette information dans movePlayer(), qui déplace le personnage et rafraîchit l’affichage de la fenêtre. Le jeu comporte une ligne d’état située au-dessus de la zone dans laquelle le personnage se déplace. La propriété status contient le texte de cette ligne. Nous utilisons une propriété car la classe QState permet de définir n’importe quelle propriété Qt lors de son activation. Nous y reviendrons plus tard.

private:
    void buildMachine();
    void setupMap();

    static constexpr int WIDTH = 35;
    static constexpr int HEIGHT = 20;

    QChar map[WIDTH][HEIGHT];
    int pX = 5;
    int pY = 5;

    QStateMachine *machine;
    QString myStatus;
};

map est un tableau contenant les caractères actuellement affichés. Nous configurons ce tableau dans setupMap() et le mettons à jour lorsque le personnage se déplace. pX et pY correspondent à la position actuelle du personnage, initialement définie à (5, 5). WIDTH et HEIGHT sont des constantes spécifiant les dimensions de la carte.

La fonction ` paintEvent() ` n’est pas abordée dans ce guide. Nous n’évoquons pas non plus les autres parties du code qui ne concernent pas la machine à états (les fonctions ` setupMap()`, ` status()`, ` setStatus()`, ` movePlayer()` et ` sizeHint() `). Si vous souhaitez consulter le code, cliquez sur le lien vers le fichier ` window.cpp ` en haut de cette page.

Implémentation de la classe Window

Voici le constructeur de ` Window` :

Window::Window()
{
    ...
    setupMap();
    buildMachine();
}

Nous configurons ici la carte et la machine à états. Passons à la fonction buildMachine():

void Window::buildMachine()
{
    machine = new QStateMachine;

    auto inputState = new QState(machine);
    inputState->assignProperty(this, "status", "Move the rogue with 2, 4, 6, and 8");

    auto transition = new MovementTransition(this);
    inputState->addTransition(transition);

On entre dans la fonction ` inputState ` au démarrage de la machine et à partir de la fonction ` quitState ` si l’utilisateur souhaite continuer à jouer. On définit ensuite l’état sur un rappel utile expliquant comment jouer au jeu.

Tout d’abord, la transition « Movement » est ajoutée à l’état « input ». Cela permettra de déplacer le voleur à l’aide du clavier. Notez que nous ne définissons pas d’état cible pour la transition de mouvement. La transition sera ainsi déclenchée (et la fonction onTransition() appelée), mais la machine ne quittera pas l’état « inputState ». Si nous avions défini « inputState » comme état cible, nous aurions d’abord quitté l’état « inputState », puis y serions retournés.

    auto quitState = new QState(machine);
    quitState->assignProperty(this, "status", "Really quit(y/n)?");

    auto yesTransition = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_Y);
    yesTransition->setTargetState(new QFinalState(machine));
    quitState->addTransition(yesTransition);

    auto noTransition = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_N);
    noTransition->setTargetState(inputState);
    quitState->addTransition(noTransition);

Lorsque nous entrons dans quitState, nous mettons à jour la barre d’état de la fenêtre.

QKeyEventTransition est une classe utilitaire qui évite d’avoir à implémenter manuellement les transitions pour les QKeyEvents. Il suffit simplement de spécifier la touche qui doit déclencher la transition et l’état cible de celle-ci.

    auto quitTransition = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_Q);
    quitTransition->setTargetState(quitState);
    inputState->addTransition(quitTransition);

La transition de l’ inputState permet de déclencher l’état « quit » lorsque le joueur tape « q ».

    machine->setInitialState(inputState);

    connect(machine, &QStateMachine::finished, qApp, &QApplication::quit);

    machine->start();
}

La machine est configurée, il est donc temps de la démarrer.

La classe `MovementTransition`

MovementTransition est déclenchée lorsque le joueur demande au voleur de se déplacer (en tapant 2, 4, 6 ou 8) alors que la machine se trouve dans l’ inputState.

class MovementTransition : public QEventTransition
{
    Q_OBJECT

public:
    explicit MovementTransition(Window *window)
        : QEventTransition(window, QEvent::KeyPress), window(window)
    {
    }

Dans le constructeur, nous indiquons à ` QEventTransition ` de n’envoyer les événements ` KeyPress ` qu’à la fonction ` eventTest()` :

protected:
    bool eventTest(QEvent *event) override {
        if (event->type() == QEvent::StateMachineWrapped &&
            static_cast<QStateMachine::WrappedEvent *>(event)->event()->type() == QEvent::KeyPress) {
            auto wrappedEvent = static_cast<QStateMachine::WrappedEvent *>(event)->event();

            auto keyEvent = static_cast<QKeyEvent *>(wrappedEvent);
            int key = keyEvent->key();

            return key == Qt::Key_2 || key == Qt::Key_8 || key == Qt::Key_6 ||
                   key == Qt::Key_4 || key == Qt::Key_Down || key == Qt::Key_Up ||
                   key == Qt::Key_Right || key == Qt::Key_Left;
        }
        return false;
    }

Les événements KeyPress sont encapsulés dans des objets ` QStateMachine::WrappedEvent`. Il faut s’assurer que ` event ` correspond bien à un événement encapsulé, car Qt utilise d’autres événements en interne. Il suffit ensuite de vérifier quelle touche a été enfoncée.

Passons maintenant à la fonction ` onTransition() ` :

    void onTransition(QEvent *event) override {
        auto keyEvent = static_cast<QKeyEvent *>(
                static_cast<QStateMachine::WrappedEvent *>(event)->event());

        int key = keyEvent->key();
        switch (key) {
            case Qt::Key_Left:
            case Qt::Key_4:
                window->movePlayer(Window::Left);
                break;
            case Qt::Key_Up:
            case Qt::Key_8:
                window->movePlayer(Window::Up);
                break;
            case Qt::Key_Right:
            case Qt::Key_6:
                window->movePlayer(Window::Right);
                break;
            case Qt::Key_Down:
            case Qt::Key_2:
                window->movePlayer(Window::Down);
                break;
            default:
                ;
        }
    }

Lorsque onTransition() est appelée, nous savons que nous avons un événement KeyPress avec 2, 4, 6 ou 8, et nous pouvons demander à Window de déplacer le joueur.

La tradition des jeux de type « roguelike »

Vous vous êtes peut-être demandé pourquoi le jeu met en scène un « rogue ». Eh bien, ce type de jeux d’exploration de donjons en mode texte remonte à un jeu intitulé, justement, « Rogue ». Bien qu’éclipsés par la technologie des jeux vidéo 3D modernes, les roguelikes bénéficient d’une solide communauté de fans inconditionnels et dévoués.

Jouer à ces jeux peut s’avérer étonnamment addictif (malgré l’absence de graphismes). Angband, sans doute le roguelike le plus connu, est disponible ici : http://rephial.org/.

Exemple de projet sur code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.