Rogue
“Rogue”示例演示了如何使用 Qt State Machine 进行事件处理。

本示例实现了一个简单的文字冒险游戏。你看到截图中的@ 了吗?那就是你——这位“Rogue”。# 字符代表墙壁,而点则代表地面。 在真正的游戏中,其他 ASCII 字符会代表各种各样的物体和生物,例如远古巨龙(D)或口粮(%)。但我们不要想得太远。在这款游戏中,Rogue 只是在空荡荡的房间里四处奔跑。
角色通过方向键(2、4、8、6)移动。除此之外,我们还实现了一个quit 命令,当玩家输入q 时,该命令会被触发。随后系统会询问玩家是否真的要退出。
大多数游戏都有需要按下多个键才能执行的命令(这里指的是连续按键,而非同时按下多个键)。在本游戏中,只有quit 命令属于这一类,但为了便于说明,让我们设想一款拥有丰富命令集的完整游戏。 如果我们要通过在 `keyPressEvent()` 中捕获键盘事件来实现这些功能,就必须维护大量类成员变量来追踪已输入的按键序列(或者找到其他方法来推断命令的当前状态)。 这很容易导致代码变得像意大利面一样乱七八糟,而——我相信大家都深有体会——这种情况令人不快。另一方面,借助状态机,不同的状态可以分别等待单个按键的触发,这大大简化了我们的工作。
该示例包含两个类:
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 指定了角色移动的方向。我们在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 类
MovementTransition 当机器处于inputState 状态时,若玩家请求移动盗贼(通过输入2、4、6或8),该类就会被触发。
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;
}KeyPress 事件被包装在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 来移动玩家。
Roguelike 的传统
你可能一直在想,为什么游戏中会出现一个“rogue”。其实,这类基于文本的地牢探索游戏可以追溯到一款名为“Rogue”的游戏。尽管在技术上已被现代 3D 电脑游戏所超越,但roguelike 游戏仍拥有一群铁杆且忠实的拥趸。
玩这类游戏可能会让人出乎意料地上瘾(尽管没有画面)。Angband 或许是最著名的 Roguelike 游戏之一,可在此处找到:http://rephial.org/。
© 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.