Rogue
Rogue 예제는 이벤트 처리를 위해 Qt 상태 머신을 사용하는 방법을 보여줍니다.
이 예제에서는 간단한 텍스트 기반 게임을 구현합니다. 스크린샷에 @
가 보이시나요? 이것이 바로 로그입니다. #
문자는 벽이고 점은 바닥을 나타냅니다. 실제 게임에서는 고대 용(D
)이나 식량 배급(%
)과 같이 다른 ASCII 문자로 모든 종류의 사물과 생물을 나타낼 수 있습니다. 하지만 너무 흥분하지 마세요. 이 게임에서 도적은 단순히 빈 방에서 뛰어다니는 존재일 뿐입니다.
도적은 키패드(2, 4, 8, 6)로 움직입니다. 그 외에도 플레이어가 q
을 입력하면 트리거되는 quit
명령을 구현했습니다. 그런 다음 플레이어에게 정말 게임을 종료할 것인지 묻습니다.
대부분의 게임에는 두 개 이상의 키를 눌러야 하는 명령이 있습니다(여러 키를 동시에 누르는 것이 아니라 연속적으로 누르는 것을 의미합니다). 이 게임에서는 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 XML 프로퍼티를 설정할 수 있기 때문에 프로퍼티를 사용합니다. 이에 대해서는 나중에 자세히 설명합니다.
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
은 내부적으로 다른 이벤트를 사용하기 때문에 래핑된 이벤트인지 확인해야 합니다. 그 후에는 어떤 키가 눌렸는지 확인하기만 하면 됩니다.
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
에 플레이어를 이동하도록 요청할 수 있습니다.
로그 라이크 전통
게임에 도적이 등장하는 이유가 궁금하셨을 겁니다. 이런 종류의 텍스트 기반 던전 탐험 게임의 역사는 바로 '로그'라는 게임으로 거슬러 올라갑니다. 최신 3D 컴퓨터 게임의 기술에 뒤처지긴 했지만, 로그는 하드코어하고 헌신적인 팬들로 구성된 탄탄한 커뮤니티를 보유하고 있습니다.
이러한 게임을 플레이하는 것은 그래픽이 부족함에도 불구하고 놀라울 정도로 중독성이 있습니다. 가장 잘 알려진 로그라이크 게임인 Angband는 여기 (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.