Rogue
「Rogue」のサンプルでは、イベント処理にQt State Machineを使用する方法を紹介しています。

このサンプルでは、シンプルなテキストベースのゲームを実装しています。スクリーンショットに「@ 」という文字が見えますか? それがあなた、つまりローグです。「# 」という文字は壁を表し、ドットは床を表しています。 実際のゲームでは、他の ASCII 文字が、古代のドラゴン (D) や食糧 (%) など、あらゆる種類のオブジェクトや生き物を表すことになるでしょう。しかし、あまり先走らないようにしましょう。このゲームでは、ローグは単に空っぽの部屋の中を走り回っているだけです。
ローグの移動はキーパッド(2、4、8、6)で行います。それとは別に、プレイヤーが「q 」と入力した際に発動するquit コマンドを実装しました。すると、本当に終了したいかどうかがプレイヤーに確認されます。
ほとんどのゲームには、1回以上のキー入力が必要なコマンドがあります(ここでは連続したキー入力を想定しており、複数のキーを同時に押すことは想定していません)。このゲームでは、「quit 」コマンドだけがこのカテゴリーに該当しますが、議論の便宜上、豊富なコマンドセットを備えた本格的なゲームを想定してみましょう。 もしこれらをkeyPressEvent()でキーイベントを捕捉して実装するとすると、すでに入力されたキーの順序を追跡するために多くのクラスメンバ変数を保持しなければなりません(あるいは、コマンドの現在の状態を推測する別の方法を見つける必要があります)。 これは簡単にスパゲッティコードにつながりかねず、皆さんもよくご存知の通り、それは好ましくないことです。一方、ステートマシンを使用すれば、個別のステートが単一のキー入力待ちを行うことができるため、実装がはるかにシンプルになります。
この例は2つのクラスで構成されています:
Windowは、ゲームのテキスト表示を描画し、ステートマシンを設定します。また、このウィンドウには、ローグが移動する領域の上部にステータスバーがあります。MovementTransitionは、ローグの1回の移動を実行する遷移です。
コードの解説に入る前に、このステートマシンの設計について詳しく見ておく必要があります。以下は、私たちが実現したいことを示したステートチャートです:

「入力」状態は、新しいコマンドを開始するためのキー入力を待機します。認識可能なキーが入力されると、ゲームの2つのコマンドのいずれかへ遷移します。ただし、後で見るように、移動処理自体は遷移そのものによって行われます。 「quit」状態は、本当にゲームを終了するかどうかを尋ねられた際、プレイヤーが「はい」または「いいえ」(y またはn と入力)で回答するのを待ちます。
この図は、1つの状態を使用して1回のキー入力を待機する方法を示しています。受け取ったキー入力は、その状態に接続された遷移のいずれかをトリガーする可能性があります。
ウィンドウクラスの定義
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::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 と入力した際にquit状態をトリガーできるようになります。
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でラップされています。Qtは内部で他のイベントを使用しているため、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 にプレイヤーを移動させるよう指示できます。
ローグライクの伝統
なぜこのゲームに「ローグ」が登場するのか、不思議に思われたかもしれません。実は、こうしたテキストベースのダンジョン探索ゲームの起源は、その名も「Rogue」というゲームにまでさかのぼります。現代の3Dコンピュータゲームの技術には及ばないものの、ローグライクには熱心で献身的なファンからなる強固なコミュニティが存在しています。
こうしたゲームは(グラフィックがないにもかかわらず)驚くほど中毒性があります。おそらく最も有名なローグライクである『Angband』は、こちらからプレイできます: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.