이 페이지에서

Rogue

Rogue 예제는 이벤트 처리를 위해 Qt State Machine를 사용하는 방법을 보여줍니다.

플레이어 @가 점과 벽으로 이루어진 격자 위를 이동하는 텍스트 기반 게임 #

이 예제는 간단한 텍스트 기반 게임을 구현합니다. 스크린샷에서 ‘ @ ’라는 문구를 보시나요? 바로 여러분, 즉 ‘도적(Rogue)’입니다. ‘ # ’ 문자는 벽을, 점들은 바닥을 나타냅니다. 실제 게임에서는 다른 ASCII 문자들이 온갖 종류의 물체와 생물, 예를 들어 고대 드래곤(Ds)이나 식량 배급(%s)을 나타낼 것입니다. 하지만 너무 앞서가지 맙시다. 이 게임에서 도적은 그저 텅 빈 방을 이리저리 뛰어다니고 있을 뿐입니다.

로그는 방향키(2, 4, 8, 6)로 이동합니다. 그 외에도, 플레이어가 ` q`를 입력하면 실행되는 ` quit ` 명령어를 구현했습니다. 그러면 플레이어에게 정말로 게임을 종료할지 묻는 메시지가 표시됩니다.

대부분의 게임에는 한 번 이상의 키 입력이 필요한 명령어가 있습니다(여기서 말하는 것은 연속적인 키 입력이며, 여러 키를 동시에 누르는 것은 아닙니다). 이 게임에서는 ‘ quit ’ 명령어만이 이 범주에 속하지만, 논의를 위해 다양한 명령어가 포함된 본격적인 게임을 가정해 봅시다. keyPressEvent()에서 키 이벤트를 처리하여 이를 구현하려면, 이미 입력된 키의 순서를 추적하기 위해 많은 클래스 멤버 변수를 유지해야 하거나(또는 명령어의 현재 상태를 추론할 다른 방법을 찾아야 합니다). 이는 쉽게 스파게티 코드로 이어질 수 있는데, 우리 모두가 잘 알고 있듯이 이는 결코 바람직하지 않습니다. 반면, 상태 머신을 사용하면 각 상태가 하나의 키 입력을 기다릴 수 있으므로 코딩이 훨씬 간단해집니다.

이 예제는 두 개의 클래스로 구성되어 있습니다:

  • Window 게임의 텍스트 화면을 그리며 상태 머신을 설정합니다. 또한 이 창에는 도적이 이동하는 영역 위에 상태 표시줄이 있습니다.
  • MovementTransition 는 루그의 단일 이동을 수행하는 전환입니다.

코드 분석에 들어가기 전에, 상태 기계의 설계를 좀 더 자세히 살펴볼 필요가 있습니다. 다음은 우리가 달성하고자 하는 바를 보여주는 상태 차트입니다:

입력 상태 및 종료 상태: q는 종료 상태로 전환되고, n은 되돌아가며, y는 최종 상태로 종료된다.

입력 상태는 새로운 명령을 시작하기 위해 키 입력을 기다립니다. 인식 가능한 키를 수신하면 게임의 두 명령 중 하나로 전환되지만, 나중에 보게 되겠지만 이동은 전환 자체에 의해 처리됩니다. 종료 상태는 플레이어에게 게임을 정말로 종료할지 묻는 메시지가 표시되었을 때, 플레이어가 ‘예’ 또는 ‘아니오’( 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::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객체로 감싸져 전달됩니다. 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)’가 등장하는지 궁금하셨을 수도 있습니다. 사실, 이러한 텍스트 기반 던전 탐험 게임의 기원은 바로 “Rogue”라는 게임으로 거슬러 올라갑니다. 현대 3D 컴퓨터 게임의 기술에 밀려났음에도 불구하고, 로그라이크 장르에는 열성적이고 헌신적인 팬들로 구성된 탄탄한 커뮤니티가 있습니다.

(그래픽이 부족함에도 불구하고) 이런 게임을 하다 보면 의외로 중독성이 강할 수 있습니다. 아마도 가장 잘 알려진 로그라이크인 ‘앙밴드(Angband)’는 여기에서 찾아볼 수 있습니다: http://rephial.org/.

code.qt.io의 예제 프로젝트

© 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.