Rogue

Rogue例子显示了如何使用Qt状态机进行事件处理。

此例子实现了一个简单的基于文本的游戏。你在截图中看到了@吗?那就是你,游侠。#字符代表墙壁,点代表地板。在真正的游戏中,其他的ASCII字符将代表各种物体和生物,例如,古老的龙(D)或食物配给(%)。但不让我们走得太多。在这个游戏中,游侠只是在空房间里四处跑。

使用数字键盘(2, 4, 8, 6)移动游侠。除此之外,我们还实现了一个quit命令,如果玩家输入了q就会触发。然后要求玩家确认是否真的想退出。

大多数游戏都有需要多次按键的命令(我们想到的是连续的按键,而不是同时按几个键)。在这个游戏中,只有quit命令属于这个类别,但为了争论,让我们想象有一个完整游戏和丰富的命令集。如果我们通过在keyPressEvent中捕获按键事件来实现这些命令,我们不得不保留大量的类成员变量来跟踪已输入键的序列(或者找到其他方法来推断命令的当前状态)。这很容易导致混乱,这是我们都知道的——不方便。另一方面,通过状态机,不同的状态可以等待单个按键,这使我们的生活变得更加简单。

此示例由两个类组成

  • Window绘制游戏文本显示并设置状态机。窗口在其上方区域(游侠移动的区域)还有一个状态栏。
  • MovementTransition是一个执行游侠单次移动的转换。

在开始代码巡礼之前,我们需要仔细看看机器的设计。以下是一个状态图表,显示了我们的目标

输入状态等待按键以启动新命令。当接收到它识别的按键时,它转接到游戏中的两个命令之一;然而,正如我们将看到的,移动由转换本身处理。退出状态等待玩家在被询问是否真的想退出游戏时回答是或否(通过输入yn)。

图表演示了如何使用一个状态来等待单个按键。接收到的按键可能触发连接到该状态的一个转换。

Window类定义

Window类是一个小部件,它绘制游戏的文本显示。它还设置状态机,即创建和连接机器中的状态。这个小部件的关键事件被机器使用。

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 指定亡灵移动的方向。我们在这个 movePlayer() 方法中使用它,此方法移动亡灵并重新绘制窗口。游戏在亡灵移动区域上方有一行状态栏。该 status 属性包含这一行的文本。我们使用属性是因为在 QState 类中进入时允许设置任何 Qt 属性。稍后我们会详细介绍。

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 是一个包含当前显示字符的数组。我们在 setupMap() 中设置数组,并在亡灵移动时更新它。 pXpY 是亡灵的当前位置,初始设置为 (5, 5)。 WIDTHHEIGHT 是指定地图尺寸的常数。

在此教程中省略了 paintEvent() 函数。我们也没有讨论其他不受状态机影响(如 setupMap()status()setStatus()movePlayer()sizeHint() 函数)的代码。如果您想查看代码,请点击页面顶部 window.cpp 文件的链接。

窗口类实现

以下是 Window 的构造函数

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

在此我们设置了地图和状态机。让我们继续执行 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);

当机器启动时,我们进入 inputState,如果用户想继续游戏,则从 quitState 进入。然后我们将状态设置为一条有用的游戏玩法提示。

首先,将 Movement 转换添加到输入状态。这将允许亡灵使用键盘移动。注意,我们没有为移动转换设置目标状态。这将导致转换被触发(并调用 onTransition() 函数),但机器不会离开 inputState。如果我们设置了 inputState 作为目标状态,我们首先离开,然后再次进入 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);

当进入 quitState 时,我们更新窗口的状态栏。

QKeyEventTransition 是一个实用类,它消除了为 QKeyEvent 实现转换的麻烦。我们只需要指定触发转换的键和转换的目标状态。

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

inputState 的转换允许在玩家输入 q 时触发退出状态。

    machine->setInitialState(inputState);

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

    machine->start();
}

机器已经设置好,现在是启动机器的时候了。

移动转换类

MovementTransition 当玩家请求移动亡灵(按2,4,6或8)时触发,此时机器处于 inputState

class MovementTransition : public QEventTransition
{
    Q_OBJECT

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

在构造函数中,我们告诉 QEventTransition 只将 KeyPress 事件发送到 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;
    }

QStateMachine::WrappedEvent 将按键事件包装起来。必须确认 event 是一个包装事件,因为 Qt 在内部使用其他事件。之后,只是简单地检查按下了哪个键。

接下来,我们来看看 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:
                ;
        }
    }

onTransition() 被调用时,我们知道有一个带有 2、4、6 或 8 的 KeyPress 事件,并可以要求 Window 移动玩家。

黑暗之魂传统

您可能想知道为什么这款游戏中加入了游侠角色。其实,这类基于文本的地牢探险游戏可以追溯到名为“游侠”的早期游戏。尽管在技术上被现代3D计算机游戏所超越,但Rogue-like游戏依然拥有坚实的核心爱好者群体。

玩这类游戏可能会让人出乎意料地着迷(尽管缺乏图形)。这里找到了可能最知名的Rogue-like游戏Angband:[http://rephial.org/](http://rephial.org/)。

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。在此所包含的文档贡献均为各自所有者的版权。本提供的文档是在自由软件基金会发布的《GNU自由文档许可协议》(GNU Free Documentation License version 1.3)许可条款下发布的。Qt及相关标识是Qt公司在芬兰以及其他国家/地区的商标。[了解更多商标信息](https://doc.qt.ac.cn/qt/trademarks.html)。所有其他商标归各自所有者所有。