ローグ

Rogue のサンプルでは、Qt のステートマシンを使ったイベント処理の方法を紹介しています。

この例では、シンプルなテキストベースのゲームを実装しています。スクリーンショットの@ が見えますか?それがあなた、ローグです。# の文字は壁で、ドットは床を表しています。実際のゲームでは、他のASCII文字があらゆる種類のオブジェクトやクリーチャー、例えば古代のドラゴン(Ds)や食料配給(%s)を表すでしょう。しかし、浮かれてはいけない。このゲームでは、ローグは何もない部屋を走り回るだけだ。

ローグはキーパッド(2、4、8、6)で移動する。それはさておき、プレイヤーがq と入力すると発動するquit コマンドを実装しました。すると、プレイヤーは本当に辞めたいかどうか聞かれる。

たいていのゲームには、複数のキーを押す必要があるコマンドがある(複数のキーを同時に押すのではなく、連続して押すことを想定している)。このゲームでは、quit のコマンドだけがこれに該当するが、議論のために、豊富なコマンドセットを持つ本格的なゲームを想像してみよう。keyPressEvent() でキーイベントをキャッチしてこれらを実装するとしたら、すでに入力されたキーのシーケンスを追跡するために(あるいは、コマンドの現在の状態を推測する他の方法を見つけるために)、多くのクラス・メンバー変数を保持しなければならないだろう。これはスパゲッティになりやすく、不愉快であることは皆さんよくご存知の通りだ。一方、ステートマシンを使えば、キーを1回押すだけで、別々の状態を待つことができる。

この例は、2つのクラスで構成されている:

  • Window はゲームのテキスト表示を描画し、ステートマシンをセットアップする。ウィンドウには、ルージュが動く領域の上にステータス・バーもある。
  • MovementTransition は、ローグの一手を実行するトランジションである。

コードのウォークスルーに入る前に、マシンの設計を詳しく見ておく必要がある。以下は、我々が実現したいことを示す状態図である:

入力状態は、新しいコマンドを開始するためにキーが押されるのを待つ。認識したキーを受け取ると、ゲームの2つのコマンドのうちの1つに遷移する。ただし、後述するように、移動は遷移自体が処理する。quitステートは、プレイヤーが本当にゲームを終了したいかどうかを尋ねられたときに、(y またはn と入力して)イエスかノーで答えるのを待ちます。

この図は、キーが1回押されるのを待つために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 は、ローグが移動する方向を指定します。ローグを動かし、ウィンドウを再描画する 。ゲームでは、ローグが移動する領域の上にステータス行が表示されます。 プロパティには、この行のテキストが含まれます。プロパティを使用するのは、 クラスが、入力時に任意の 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() で設定し、ローグが移動したときに更新します。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クラス

MovementTransition は、マシンが にあるときに、プレイヤーがローグを移動させるように要求(2、4、6、または8をタイプ)するとトリガーされます。inputState

class MovementTransition : public QEventTransition
{
    Q_OBJECT

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

コンストラクタでは、KeyPress イベントのみをeventTest() 関数に送信するようにQEventTransition に指示します:

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でラップされます。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() が呼び出されたら、KeyPress イベントが 2, 4, 6, 8 であることがわかるので、Window にプレイヤーを動かすよう依頼することができます。

ローグライクの伝統

なぜこのゲームにローグが登場するのか、不思議に思ったかもしれない。この種のテキストベースのダンジョン探索ゲームは、そう、「ローグ」というゲームにさかのぼる。現代の3Dコンピュータ・ゲームの技術に押され気味ではあるが、ローグライクには熱心な信者のコミュニティが存在する。

これらのゲームをプレイすると、(グラフィックがないにもかかわらず)驚くほど病みつきになることがある。おそらく最も有名なローグライクであるAngbandは、http://rephial.org/。

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。