En esta página

Pícaro

El ejemplo Rogue muestra cómo utilizar la Qt State Machine para el manejo de eventos.

Juego basado en texto en el que el jugador @ navega por una cuadrícula de puntos y paredes #

Este ejemplo implementa un simple juego basado en texto. ¿Ves el @ en la captura de pantalla? Ese eres tú, el pícaro. Los caracteres # son las paredes, y los puntos representan el suelo. En un juego real, otros caracteres ASCII representarían todo tipo de objetos y criaturas, por ejemplo, dragones antiguos (Ds) o raciones de comida (%s). Pero no nos dejemos llevar. En este juego, el pícaro simplemente corre por una habitación vacía.

El pícaro se mueve con el teclado (2, 4, 8, 6). Aparte de eso, hemos implementado un comando quit que se activa si el jugador escribe q. Entonces se pregunta al jugador si realmente quiere abandonar.

La mayoría de los juegos tienen comandos que necesitan que se pulse más de una tecla (pensamos en pulsaciones consecutivas, es decir, no en que se pulsen varias teclas a la vez). En este juego, sólo el comando quit entra en esta categoría, pero por el bien del argumento, imaginemos un juego completo con un rico conjunto de comandos. Si tuviéramos que implementarlos capturando eventos de teclas en keyPressEvent(), tendríamos que mantener un montón de variables miembro de la clase para rastrear la secuencia de teclas ya tecleadas (o encontrar alguna otra forma de deducir el estado actual de un comando). Esto puede dar lugar fácilmente a espaguetis, lo que es -como todos bien sabemos, estoy seguro- desagradable. Con una máquina de estados, en cambio, los estados separados pueden esperar a que se pulse una sola tecla, y eso nos simplifica mucho la vida.

El ejemplo consta de dos clases:

  • Window dibuja la pantalla de texto del juego y configura la máquina de estados. La ventana también tiene una barra de estado sobre el área en la que se mueve el rouge.
  • MovementTransition es una transición que realiza un único movimiento del pícaro.

Antes de embarcarnos en un recorrido por el código, es necesario echar un vistazo más de cerca al diseño de la máquina. He aquí un diagrama de estados que muestra lo que queremos conseguir:

Estado de entrada y estado de salida: q pasa a salida, n vuelve atrás, y sale al estado final

El estado de entrada espera a que se pulse una tecla para iniciar un nuevo comando. Cuando recibe una tecla que reconoce, hace la transición a uno de los dos comandos del juego; aunque, como veremos, el movimiento lo maneja la propia transición. El estado de abandono espera a que el jugador responda sí o no (escribiendo y o n) cuando se le pregunta si realmente quiere abandonar el juego.

El gráfico muestra cómo utilizamos un estado para esperar a que se pulse una sola tecla. La pulsación recibida puede desencadenar una de las transiciones conectadas al estado.

Definición de la clase Window

La clase Window es un widget que dibuja la pantalla de texto del juego. También configura la máquina de estados, es decir, crea y conecta los estados en la máquina. Son los eventos clave de este widget los que utiliza la máquina.

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 especifica la dirección en la que debe moverse el pícaro. Usamos esto en movePlayer(), que mueve al pícaro y repinta la ventana. El juego tiene una línea de estado sobre el área en la que se mueve el pícaro. La propiedad status contiene el texto de esta línea. Usamos una propiedad porque la clase QState permite establecer cualquier propiedad Qt cuando se introduce. Más sobre esto más adelante.

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

El map es un array con los caracteres que se muestran actualmente. Configuramos el array en setupMap(), y lo actualizamos cuando el pícaro se mueve. pX y pY es la posición actual del pícaro, inicialmente establecida en (5, 5). WIDTH y HEIGHT son constantes que especifican las dimensiones del mapa.

La función paintEvent() queda fuera de este recorrido. Tampoco discutimos otro código que no concierne a la máquina de estados (las funciones setupMap(), status(), setStatus(), movePlayer(), y sizeHint() ). Si desea echar un vistazo al código, haga clic en el enlace para el archivo window.cpp en la parte superior de esta página.

Implementación de la clase Window

Aquí está el constructor de Window:

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

Aquí configuramos el mapa y el statemachine. Procedamos con la función 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);

Entramos en inputState cuando se inicia la máquina y desde el quitState si el usuario quiere seguir jugando. A continuación, establecemos el estado a un recordatorio útil de cómo jugar el juego.

En primer lugar, se añade la transición Movement al estado de entrada. Esto permitirá mover al pícaro con el teclado. Fíjate en que no establecemos un estado objetivo para la transición de movimiento. Esto hará que se active la transición (y que se invoque la función onTransition()), pero la máquina no saldrá de inputState. Si hubiéramos establecido inputState como estado objetivo, primero habríamos salido y luego habríamos entrado de nuevo en inputState.

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

Cuando entramos en quitState, actualizamos la barra de estado de la ventana.

QKeyEventTransition es una clase de utilidad que elimina la molestia de implementar transiciones para QKeyEvents. Simplemente tenemos que especificar la tecla en la que la transición debe desencadenar y el estado de destino de la transición.

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

La transición de inputState permite activar el estado de abandono cuando el jugador escribe q.

    machine->setInitialState(inputState);

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

    machine->start();
}

La máquina está preparada, así que es hora de ponerla en marcha.

La clase MovementTransition

MovementTransition se activa cuando el jugador pide que se mueva el pícaro (tecleando 2, 4, 6 u 8) cuando la máquina está en inputState.

class MovementTransition : public QEventTransition
{
    Q_OBJECT

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

En el constructor, le decimos a QEventTransition que sólo envíe eventos KeyPress a la función 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;
    }

Los eventos KeyPress vienen envueltos en QStateMachine::WrappedEvents. Debe confirmarse que event es un evento envuelto porque Qt utiliza otros eventos internamente. Después de eso, es simplemente cuestión de comprobar qué tecla se ha pulsado.

Pasemos a la función 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:
                ;
        }
    }

Cuando se invoca onTransition(), sabemos que tenemos un evento KeyPress con 2, 4, 6 u 8, y podemos pedir a Window que mueva al jugador.

La tradición Roguelike

Puede que te hayas preguntado por qué el juego incluye un pícaro. Pues bien, este tipo de juegos de exploración de mazmorras basados en texto se remontan a un juego llamado, sí, "Rogue". Aunque superados por la tecnología de los modernos juegos de ordenador en 3D, los roguelikes cuentan con una sólida comunidad de seguidores incondicionales y devotos.

Jugar a estos juegos puede ser sorprendentemente adictivo (a pesar de la falta de gráficos). Angband, quizá el rougelike más conocido, se encuentra aquí: http://rephial.org/.

Proyecto de ejemplo @ 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.