流氓
Rogue 示例展示了如何使用 Qt State Machine 进行事件处理。
该示例实现了一个简单的文本游戏。你看到截图中的@
吗?那就是你,流氓。#
字符代表墙壁,点代表地板。在真正的游戏中,其他 ASCII 字符将代表各种物体和生物,例如远古巨龙 (D
s) 或口粮 (%
s)。不过,我们还是不要忘乎所以了。在这个游戏中,流氓只是在一个空房间里跑来跑去。
流氓可以通过键盘(2、4、8、6)移动。除此之外,我们还实现了一个quit
命令,如果玩家键入q
,该命令就会触发。然后会询问玩家是否真的想退出。
大多数游戏中的命令都需要按一次以上的键(我们认为是连续按键,而不是同时按几个键)。在本游戏中,只有quit
命令属于这一类,但为了便于论证,让我们想象一下一个拥有丰富命令集的成熟游戏。如果我们通过在keyPressEvent() 中捕捉按键事件来实现这些命令,就必须保留大量的类成员变量来跟踪已键入的按键序列(或寻找其他方法来推断命令的当前状态)。这很容易导致 "意大利面条"(spaghetti),我相信大家都很清楚,这是很令人不快的。另一方面,有了状态机,不同的状态可以等待一个按键的按下,这让我们的生活简单了许多。
本示例由两个类组成:
Window
绘制游戏文本显示和设置状态机。窗口中胭脂红移动的区域上方还有一个状态栏。MovementTransition
是一个执行流氓单次移动的过渡。
在开始代码演练之前,我们有必要仔细了解一下机器的设计。下面的状态图显示了我们想要实现的目标:
输入状态等待按键启动新命令。当接收到可识别的按键时,它会转换到游戏的两个命令之一;不过,正如我们将要看到的,移动是由转换本身处理的。当玩家被问及是否真的想退出游戏时,退出状态会等待玩家回答是或否(键入y
或n
)。
下图演示了我们如何使用一个状态来等待单键按下。接收到的按键可能会触发与该状态相关的某个转换。
窗口类定义
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
指定流氓的移动方向。我们在 中使用它,它会移动流氓并重新绘制窗口。游戏在流氓移动的区域上方有一条状态行。 属性包含该行的文本。我们使用属性是因为 类允许在输入时设置任何 QtmovePlayer()
status
QState 属性。稍后将详细介绍。
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 只向eventTest() 函数发送KeyPress 事件:
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; }
KeyPress 事件会封装在QStateMachine::WrappedEvents 中。由于 Qt XML 在内部使用了其他事件,因此必须确认event
为封装事件。之后,只需检查哪个键被按下即可。
让我们继续访问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
移动玩家。
Roguelike 传统
你可能想知道为什么游戏中会出现流氓。其实,这类基于文本的地牢探索游戏可以追溯到一款名为 "Rogue "的游戏。虽然被现代 3D 电脑游戏的技术所取代,roguelike 仍然拥有一批忠实的铁杆粉丝。
玩这些游戏会让人上瘾(尽管没有画面)。Angband 可能是最著名的rougelike,可在此处找到:http://rephial.org/。
© 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.