Schurke

Das Rogue-Beispiel zeigt, wie man die Qt State Machine für die Ereignisbehandlung verwendet.

Dieses Beispiel implementiert ein einfaches textbasiertes Spiel. Siehst du die @ im Screenshot? Das bist du, der Schurke. Die # Zeichen sind Wände, und die Punkte stehen für den Boden. In einem echten Spiel würden andere ASCII-Zeichen alle möglichen Objekte und Kreaturen darstellen, zum Beispiel alte Drachen (Ds) oder Essensrationen (%s). Aber lassen wir uns nicht hinreißen. In diesem Spiel rennt der Schurke einfach in einem leeren Raum herum.

Der Schurke wird mit der Tastatur (2, 4, 8, 6) bewegt. Außerdem haben wir einen quit Befehl implementiert, der ausgelöst wird, wenn der Spieler q eingibt. Der Spieler wird dann gefragt, ob er/sie wirklich aufhören will.

Die meisten Spiele haben Befehle, die mehr als einen Tastendruck erfordern (wir denken dabei an aufeinanderfolgende Tastendrücke, d. h. nicht an das gleichzeitige Drücken mehrerer Tasten). In diesem Spiel fällt nur der Befehl quit in diese Kategorie, aber stellen wir uns der Einfachheit halber ein vollwertiges Spiel mit einer Vielzahl von Befehlen vor. Wenn wir diese durch das Abfangen von Tastenereignissen in keyPressEvent() implementieren würden, müssten wir eine Menge Klassenvariablen behalten, um die Abfolge der bereits getippten Tasten zu verfolgen (oder einen anderen Weg finden, um den aktuellen Status eines Befehls abzuleiten). Das kann leicht zu Spaghetti führen, was - wie wir sicher alle wissen - unangenehm ist. Bei einem Zustandsautomaten hingegen können verschiedene Zustände auf einen einzigen Tastendruck warten, und das macht unser Leben viel einfacher.

Das Beispiel besteht aus zwei Klassen:

  • Window Zeichnet die Textanzeige des Spiels und richtet den Zustandsautomaten ein. Das Fenster hat außerdem eine Statusleiste über dem Bereich, in dem sich der Rouge bewegt.
  • MovementTransition ist eine Transition, die einen einzelnen Zug des Schurken ausführt.

Bevor wir den Code durchgehen, ist es notwendig, sich den Aufbau des Automaten genauer anzuschauen. Hier ist ein Zustandsdiagramm, das zeigt, was wir erreichen wollen:

Der Eingabezustand wartet auf einen Tastendruck, um einen neuen Befehl zu starten. Sobald eine Taste erkannt wird, geht er zu einem der beiden Befehle des Spiels über; die Bewegung wird jedoch, wie wir sehen werden, durch den Übergang selbst gesteuert. Der Beenden-Zustand wartet darauf, dass der Spieler auf die Frage, ob er/sie das Spiel wirklich beenden möchte, mit Ja oder Nein antwortet (indem er/sie y oder n eingibt).

Das Diagramm zeigt, wie wir einen Zustand verwenden, um auf einen einzigen Tastendruck zu warten. Der empfangene Tastendruck kann einen der mit dem Zustand verbundenen Übergänge auslösen.

Definition der Fensterklasse

Die Klasse Window ist ein Widget, das die Textanzeige des Spiels zeichnet. Sie richtet auch den Zustandsautomaten ein, d.h. sie erstellt und verbindet die Zustände im Automaten. Es sind die Schlüsselereignisse dieses Widgets, die von der Maschine verwendet werden.

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 gibt die Richtung an, in die sich der Schurke bewegen soll. Wir verwenden dies in movePlayer(), das den Schurken bewegt und das Fenster neu färbt. Das Spiel hat eine Statuszeile über dem Bereich, in dem sich der Schurke bewegt. Die Eigenschaft status enthält den Text dieser Zeile. Wir verwenden eine Eigenschaft, weil die Klasse QState es erlaubt, jede Qt-Eigenschaft bei der Eingabe zu setzen. Mehr dazu später.

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 ist ein Array mit den Zeichen, die gerade angezeigt werden. Wir richten das Array in setupMap() ein und aktualisieren es, wenn der Schurke bewegt wird. pX und pY ist die aktuelle Position des Schurken, die anfänglich auf (5, 5) gesetzt ist. WIDTH und HEIGHT sind Konstanten, die die Abmessungen der Karte angeben.

Die Funktion paintEvent() wird in dieser Anleitung nicht behandelt. Wir gehen auch nicht auf anderen Code ein, der nicht den Zustandsautomaten betrifft (die Funktionen setupMap(), status(), setStatus(), movePlayer() und sizeHint() ). Wenn Sie einen Blick auf den Code werfen möchten, klicken Sie auf den Link für die Datei window.cpp oben auf dieser Seite.

Implementierung der Fensterklasse

Hier ist der Konstruktor von Window:

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

Hier richten wir die Karte und die Statemachine ein. Fahren wir mit der Funktion buildMachine() fort:

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

Wir geben inputState ein, wenn die Maschine gestartet wird, und von quitState aus, wenn der Benutzer weiterspielen möchte. Dann setzen wir den Status auf eine hilfreiche Erinnerung, wie das Spiel zu spielen ist.

Zunächst wird der Übergang Movement zum Eingabezustand hinzugefügt. Dadurch kann der Schurke mit der Tastatur bewegt werden. Beachten Sie, dass wir keinen Zielzustand für den Bewegungsübergang festlegen. Dadurch wird zwar der Übergang ausgelöst (und die Funktion onTransition() aufgerufen), aber die Maschine verlässt den inputState nicht. Hätten wir inputState als Zielzustand festgelegt, hätten wir ihn erst verlassen und dann erneut den inputState betreten.

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

Wenn wir quitState betreten, aktualisieren wir die Statusleiste des Fensters.

QKeyEventTransition ist eine Hilfsklasse, die uns die Implementierung von Übergängen für QKeyEventabnimmt. Wir müssen lediglich die Taste, bei der der Übergang ausgelöst werden soll, und den Zielzustand des Übergangs angeben.

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

Der Übergang von inputState ermöglicht das Auslösen des Beenden-Zustands, wenn der Spieler q eingibt.

    machine->setInitialState(inputState);

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

    machine->start();
}

Die Maschine ist eingerichtet, also ist es an der Zeit, sie zu starten.

Die MovementTransition-Klasse

MovementTransition wird ausgelöst, wenn der Spieler den Schurken auffordert, sich zu bewegen (indem er 2, 4, 6 oder 8 eintippt), wenn sich die Maschine im inputState befindet.

class MovementTransition : public QEventTransition
{
    Q_OBJECT

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

Im Konstruktor weisen wir QEventTransition an, nur KeyPress Ereignisse an die Funktion eventTest() zu senden:

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

Die KeyPress-Ereignisse werden in QStateMachine::WrappedEvents verpackt. event muss bestätigt werden, dass es sich um ein verpacktes Ereignis handelt, da Qt intern andere Ereignisse verwendet. Danach ist es nur noch eine Frage der Überprüfung, welche Taste gedrückt worden ist.

Kommen wir nun zur Funktion 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:
                ;
        }
    }

Wenn onTransition() aufgerufen wird, wissen wir, dass wir ein KeyPress -Ereignis mit 2, 4, 6 oder 8 haben, und können Window bitten, den Spieler zu bewegen.

Die Roguelike-Tradition

Sie haben sich vielleicht gefragt, warum das Spiel einen Schurken enthält. Nun, diese Art von textbasierten Dungeon-Erkundungsspielen geht auf ein Spiel namens, ja, "Rogue" zurück. Obwohl sie von der Technologie moderner 3D-Computerspiele überflügelt wurden, haben Roguelikes eine solide Gemeinschaft von hartgesottenen, treuen Anhängern.

Das Spielen dieser Spiele kann erstaunlich süchtig machen (trotz der fehlenden Grafik). Angband, das vielleicht bekannteste Roguelike, finden Sie hier: http://rephial.org/.

Beispielprojekt @ code.qt.io

© 2025 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.