Rogue
Rogue例子显示了如何使用Qt状态机进行事件处理。
此例子实现了一个简单的基于文本的游戏。你在截图中看到了@
吗?那就是你,游侠。#
字符代表墙壁,点代表地板。在真正的游戏中,其他的ASCII字符将代表各种物体和生物,例如,古老的龙(D
)或食物配给(%
)。但不让我们走得太多。在这个游戏中,游侠只是在空房间里四处跑。
使用数字键盘(2, 4, 8, 6)移动游侠。除此之外,我们还实现了一个quit
命令,如果玩家输入了q
就会触发。然后要求玩家确认是否真的想退出。
大多数游戏都有需要多次按键的命令(我们想到的是连续的按键,而不是同时按几个键)。在这个游戏中,只有quit
命令属于这个类别,但为了争论,让我们想象有一个完整游戏和丰富的命令集。如果我们通过在keyPressEvent中捕获按键事件来实现这些命令,我们不得不保留大量的类成员变量来跟踪已输入键的序列(或者找到其他方法来推断命令的当前状态)。这很容易导致混乱,这是我们都知道的——不方便。另一方面,通过状态机,不同的状态可以等待单个按键,这使我们的生活变得更加简单。
此示例由两个类组成
Window
绘制游戏文本显示并设置状态机。窗口在其上方区域(游侠移动的区域)还有一个状态栏。MovementTransition
是一个执行游侠单次移动的转换。
在开始代码巡礼之前,我们需要仔细看看机器的设计。以下是一个状态图表,显示了我们的目标
输入状态等待按键以启动新命令。当接收到它识别的按键时,它转接到游戏中的两个命令之一;然而,正如我们将看到的,移动由转换本身处理。退出状态等待玩家在被询问是否真的想退出游戏时回答是或否(通过输入y
或n
)。
图表演示了如何使用一个状态来等待单个按键。接收到的按键可能触发连接到该状态的一个转换。
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()
中设置数组,并在亡灵移动时更新它。 pX
和 pY
是亡灵的当前位置,初始设置为 (5, 5)。 WIDTH
和 HEIGHT
是指定地图尺寸的常数。
在此教程中省略了 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/)。
© 2024 Qt公司有限公司。在此所包含的文档贡献均为各自所有者的版权。本提供的文档是在自由软件基金会发布的《GNU自由文档许可协议》(GNU Free Documentation License version 1.3)许可条款下发布的。Qt及相关标识是Qt公司在芬兰以及其他国家/地区的商标。[了解更多商标信息](https://doc.qt.ac.cn/qt/trademarks.html)。所有其他商标归各自所有者所有。