Qt6 기반의 T-Rex 게임 만들기

개요

이전 게시물인 파이썬기반 T-Rex 게임의 C++ 버전 입니다.

동작방식은 기존의 파이썬 버전 (PyQt) 과 동일하며, Qt 기반으로 작성되어 있습니다.

[ C++로 만든 T-Rex Game ]

 

개발환경

  • Windows 11 Pro 64bit, Qt Creator 7.0 (Qt 6.2.3)

  • C++17, MinGW 11.2.0 64bit 

 

소스코드 구조

Qt Creator를 이용해 제작된 프로젝트 입니다.

아래 소스코드에 포함된 *.pro 파일을 더블 클릭하면 프로젝트가 열립니다.

게임의 진행에 필요한 공룡과 선인장이 포함된 Image 파일은 아래에 링크시켜 두었으며, 프로젝트 경로가 아닌 C++ 목적파일(*.o, *.obj 등) 이 생성되는 경로에 두어야 실행됩니다.

[ 프로젝트 개요 ]
 

  • game.h / cpp

  • game_element.h

  • widget.h / cpp

  • main.cpp

  • 전체소스코드 링크, 이미지 파일 링크

    ( 이미지폴더는 프로젝트 폴더가 아닌 PRJ-Debug\Image 폴더 복사)

[이미지 파일 경로 ( *.obj ) ]


[ 게임에 사용된 캡쳐 이미지, 출처 : Google ]

 

main.cpp 소스코드

#include <QApplication>
#include "widget.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    Widget w;
    w.show();
    return app.exec();
}

메인 함수이며 QApplication의 객체 app, QWidget 에서 상속받은 Widget 객체를 생성하고 앱을 실행.

 

widget.h 소스코드

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class Game;
class QKeyEvent;

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

public:
    void paintEvent(QPaintEvent* e);
    void keyPressEvent(QKeyEvent* e);
    void closeEvent(QCloseEvent* e);

public:
    Game* pGame;

public slots:
    void update(){ QWidget::update(); };
    void gameOver();
};
#endif // WIDGET_H

QWidget에서 상속받은 Widget Class의 선언부.

 

widget.cpp 소스코드

#include "widget.h"
#include "game.h"
#include <QPainter>
#include <QKeyEvent>
#include <QMessageBox>

Widget::Widget(QWidget *parent)
    : QWidget(parent), pGame(nullptr)
{
    setFixedSize(800,300);

    if(!pGame)
        pGame = new Game(this);
}

Widget::~Widget()
{
    if(pGame)
    {
        delete pGame;
        pGame = nullptr;
    }
}

void Widget::paintEvent(QPaintEvent *e)
{
    QPainter qp = QPainter();
    qp.begin(this);
    pGame->draw(qp);
    qp.end();
}

void Widget::keyPressEvent(QKeyEvent *e)
{
    if(pGame)
        pGame->keyDown(e->key());
}

void Widget::closeEvent(QCloseEvent *e)
{
    if(pGame)
        pGame->runGame(false);
}

void Widget::gameOver()
{
    auto result = QMessageBox::information(this, "Game Over!", "Retry(Yes), Exit(No)", QMessageBox::Yes | QMessageBox::No);

    if (result == QMessageBox::Yes)
    {
        if(pGame)
        {
            delete pGame;
            pGame = new Game(this);
        }
    }
    else
    {
        this->close();
    }
}

Widget Class의 구현부.

화면에 보여지는 위젯 생성 역할외 특별한 기능은 없으며,

실제 게임을 담당하는 Game Class의 객체를 동적할당해 생성하고,

QWidget ClasspaintEvent, keyPressEvent, closeEvent Function을 오버라이딩 후 각 이벤트 발생시 처리.

 

game.h 소스코드

#ifndef GAME_H
#define GAME_H

#include <QObject>
#include <thread>
#include <list>
#include "game_element.h"


class Widget;
class QPainter;
class QPixmap;

class Game : public QObject
{
    Q_OBJECT


public:
    Game(Widget* p);
    ~Game();

public:
    void draw(QPainter& qp);
    void runGame(bool run) {bRun = run;};
    void keyDown(int key);

private:
    int getRandom(int min, int max);
    void moveGround();
    void moveTrex();
    void createCactus();
    bool moveCactus();
    void threadFunc();
    void threadJump();

private:
    Widget *pWidget;
    QRectF rect;

    // ground
    int gy, gap;
    std::list<int> gr;

    // trex, cactus
    ocs::TRex *pTrex;    
    std::vector<ocs::Cactus> cs;

    std::thread *pThread;
    bool bRun;

    int score;

    ocs::Images trex_img;
    ocs::Images cactus_img;

signals:
    void updateSignal();
    void endSignal();
};

#endif // GAME_H

게임을 실제 가동하는 클래스.

게임의 진행에 필요한 각종 변수 (지면, 공룡, 선인장) 및 이를 지속적으로 관리하는 std::Thread 객체로 구성.

자세한 동작은 *.cpp 참조.

 

game.cpp 소스코드

#include "game.h"
#include "widget.h"
#include <QDebug>
#include <QPainter>
#include <random>
#include <chrono>
#include <Qt>

Game::Game(Widget* p) :
    QObject(), pWidget(p), rect(p->rect()), gy(0), gap(6), pTrex(nullptr), pThread(nullptr), bRun(true), score(0),
    trex_img( QStringList({"rex0.png", "rex1.png"}), 3 ),
    cactus_img( QStringList({"cac0.png", "cac1.png", "cac2.png", "cac3.png"}), 2 )
{
    connect(this, &Game::updateSignal, p, &Widget::update);
    connect(this, &Game::endSignal, p, &Widget::gameOver);

    // create ground
    gy = static_cast<int>(rect.height()*0.8);
    int bt = rect.bottom();

    for (int i=0;i<rect.width()/gap;i++)
    {
        gr.push_back( getRandom(gy, bt) );
    }

    // load image
    trex_img.loadImage();
    cactus_img.loadImage();

    // create trex
    int w   = trex_img.imgs[0].width();
    int h   = trex_img.imgs[0].height();
    int x   = w;
    int y   = gy - h;
    QRectF rect (x, y, w, h);
    pTrex = new ocs::TRex(rect, gy);

    if(!pThread)
        pThread = new std::thread(&Game::threadFunc, this);
}

Game::~Game()
{
    if(pThread)
    {
        if(pThread->joinable())
            pThread->join();

        delete pThread;
        pThread = nullptr;
    }

    if(pTrex)
    {
        delete pTrex;
        pTrex = nullptr;
    }  
}

void Game::draw(QPainter &qp)
{
    //ground
    qp.drawLine(0, gy, rect.width(), gy);
    int x=0;
    std::list<int>::iterator itr;
    for (itr=gr.begin(); itr!=gr.end(); ++itr)
    {
        qp.drawPoint( x*gap, *itr);
        x += 1;
    }

    // trex
    if(pTrex)
        qp.drawPixmap(pTrex->rect, trex_img.imgs[pTrex->idx], trex_img.imgs[pTrex->idx].rect());

    // cactus
    for(auto itr=cs.begin(); itr!=cs.end(); ++itr)
    {
        qp.drawPixmap(itr->rect, cactus_img.imgs[itr->id], cactus_img.imgs[itr->id].rect());
    }

    // score
    QString s = QString("%1").arg(score, 3, 10, QLatin1Char('0'));
    qp.drawText(rect, Qt::AlignTop | Qt::AlignHCenter, s);
}

void Game::keyDown(int key)
{
    if (key == Qt::Key_Space && pTrex && !pTrex->jump)
    {
        qDebug() << "Jump!";
        pTrex->jump = true;
        pTrex->jh = 10;
        auto f = [&]() {threadJump();};
        std::thread t(f);
        t.detach();
    }
}

void Game::threadJump()
{
    while (true)
    {
        if(pTrex)
        {
            pTrex->rect.adjust(0, -pTrex->jh, 0, -pTrex->jh);
            pTrex->jh -= 0.4;

            if (pTrex->rect.bottom() > gy)
            {
                pTrex->rect.moveBottom(gy);
                pTrex->jump = false;
                emit ( updateSignal() );
                break;
            }
        }
        else
            break;

        std::this_thread::sleep_for( std::chrono::milliseconds(10) );
    }
    qDebug() << "End jump!";
}

int Game::getRandom(int min, int max)
{
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dis(min, max);
    return dis(gen);
}

void Game::moveGround()
{
    gr.pop_front();
    gr.push_back( getRandom(gy, rect.bottom())  );
}

void Game::moveTrex()
{
    if (pTrex)
    {
        if(pTrex->cnt%gap==0)
            pTrex->idx = int( ! bool(pTrex->idx) );
        pTrex->cnt+=1;
    }
}

void Game::createCactus()
{
    int n = getRandom(1, 100);
    if (n>99)
    {
        int id = getRandom(0, 3);
        int w = cactus_img.imgs[id].width();
        int h = cactus_img.imgs[id].height();
        int x = rect.right();
        int y = gy-h;

        if (cs.empty())
            cs.push_back( ocs::Cactus(id, QRectF(x, y, w, h)) );
        else
        {
            int i = cs.size()-1;
            if (cs[i].rect.right()<rect.width()*0.7)
                cs.push_back( ocs::Cactus(id, QRectF(x, y, w, h)) );
        }
    }
}

bool Game::moveCactus()
{
    bool ing = true;
    for (auto itr = cs.begin(); itr!=cs.end();)
    {
        if (itr->rect.right()<0)
        {
            itr->dead = true;
            cs.erase(itr);
            score++;
            continue;
        }
        else if (itr->rect.intersects(pTrex->rect))
        {
            ing = false;
            break;
        }
        else
        {
            itr->rect.adjust(-gap, 0, -gap, 0);
            ++itr;
        }
    }
    return ing;
}

void Game::threadFunc()
{
    while (bRun)
    {
        moveGround();
        moveTrex();
        createCactus();
        if(!moveCactus())
        {
            emit( endSignal() );
            break;
        }
        emit ( updateSignal() );
        std::this_thread::sleep_for( std::chrono::milliseconds(10) );
    }
}

 

[라인 9 ~ 40]

Game Class의 생성자 함수.

초기화 리스트를 이용 멤버 변수를 초기화.

화면을 새로 그려주는 updateSignal, 게임종료시 사용할 endSignal 처리.

지면 높이 gy 를 설정하고, 땅이 이동하는 것 처럼 보이게 만들 점의 집합인 gr 리스트 초기화. 이 리스트의 맨처음 Y 값을 제거 (Pop Front) , 마지막에 새 Y값을 추가 (Push Back) 해 지면이 왼쪽으로 이동.

뒤에 소개할 game_element.h 에 선언된 공룡, 선인장클래스의 객체를 생성하고, 초기화.

무한루프로 게임을 동작시킬 Thread 생성.

[게임의 지면, 지면의 점 좌표]

[라인 42 ~ 58]

Game Class 소멸자 함수.

동적할당된 변수를을 삭제.

생성자에서 동적할당 (Dynamic Allocation)을 진행하는 클래스는 복사생성자 (Copy Constructor), 대입연산자 (Assignment Operator)를 따로 만들어 변수끼리 단순 대입이 초래하는 참사(?)를 막을 필요가 있지만 코드에서는 생략.

(좀 더 자세한 내용이 궁금한 분은 Effective C++책 항목 11번을 참조 바랍니다.)

 

[라인 60 ~ 85]

Widget ClasspaintEvent 함수에 의해 호출되는 함수.

위젯에서 생성된 QPainter 객체를 이용해 게임 내 요소 (공룡, 지면이동, 선인장, 점수) 들을 그리는 역할을 담당. 

 

[라인 87 ~ 98]

스페이스 키가 눌러지면 공룡의 점프를 담당.

점프값 (jh) 을 설정하고, 쓰레드가 호출할 람다함수를 생성하고 실행.

쓰레드 객체의 detach() 함수는 쓰레드를 실행하고 제어하기 위해 붙잡고 있는 손을 놓는다고 생각하면 이해가 쉬움.


[라인 100 ~ 123]

바로 전 점프 쓰레드 실행시 호출되는 함수.

공룡 객체의 사각형 Y좌표를 조금씩 감소 시키며 점프기능을 구현.

(화면 좌표계의 기본설정은 Y좌표가 감소되면 위로, 증가되면 아래로)

처음엔 공룡의 Y값이 크게 감소되지만 점차 작아지며 (위로 올라감), 0보다 작아지면 부호가 바뀌며 아래쪽으로 이동.

공룡의 Y 값이 지면보다 내려가면 점프 종료.


[라인 125 ~ 131]

무작위의 수 범위 (최소, 최대) 를 입력받아 랜덤값을 돌려주는 함수.


[라인 133 ~ 137]

지면 Y 좌표가 저장된 std::List 객체에서 맨 왼쪽 값을 제거 (Pop Front).

새로운 지면 Y 좌표를 해당 리스트의 맨 오른쪽에 추가 (Push Back).


[라인 139 ~ 147]

2장의 공룡 이미지를 번갈아 표시하며 뛰는 것처럼 보이기 위해 카운팅이 N번 진행되면 이미지 인덱스를 바꾸어주는 역할.

 

[라인 149 ~ 169]

랜덤한 확률로 선인장을 생성하는 함수.

선인장이 겹쳐서 어려개 생성되지 않도록 조절해 선인장 벡터에 저장. 


[라인 171 ~ 195]

선인장을 이동시키는 함수.

화면을 벗어난 선인장은 삭제, 공룡과 선인장들의 충돌을 감지하고 처리.


[라인 197 ~ 212]

전체 게임을 진행하는 쓰레드 함수.

지면 이동, 공룡이동, 선인장 생성 및 이동 등을 수행하는 함수를 지속적으로 호출.


game_element.h 소스코드

#ifndef GAME_ELEMENT_H
#define GAME_ELEMENT_H

#endif // GAME_ELEMENT_H

#include <QRectF>
#include <QPixmap>
#include <vector>
#include <QApplication>

namespace ocs
{
    class Images
    {
    public:
        Images(const QStringList& sl, int r) : list(sl), ratio(r) {}
        ~Images(){};
    public:
        void loadImage()
        {
            for (int i=0;i<list.size();i++)
            {
                QString fn (list[i]);
                QString path = QApplication::applicationDirPath();
                path = path + "/image/" + fn;
                QPixmap img = QPixmap(path);
                img = img.scaled(img.width()/ratio, img.height()/ratio, Qt::KeepAspectRatio, Qt::SmoothTransformation);
                imgs.push_back(img);
            }
        }
    private:
        QStringList list;
        int ratio;
    public:
        std::vector<QPixmap> imgs;
    };


    class TRex
    {
    public:
        TRex(QRectF r, int gy) :
            rect(r), jump(false), jh(0), cnt(0), idx(0)
        {          
        }
        ~TRex() {}

    public:
        QRectF rect;
        bool jump;
        double jh;
        int cnt, idx;
    };


    class Cactus
    {
    public:
        Cactus (int i, QRectF r) :
            id(i), rect(r), dead(false)
        {
        }

        ~Cactus() {}

    public:
        int id;
        QRectF rect;
        bool dead;        
    };
}


[라인 13 ~ 36]

게임에 사용되는 공룡, 선인장 이미지를 관리하는 클래스.


[라인 39 ~ 53]

공룡 클래스, 공룡 사각형 위치,  점프상태, 이미지 인덱스 등 정보를 관리.


[라인 56 ~ 70]

선인장 클래스, 선인장 사각형 위치, 종류, 화면을 나갔는지 등 정보를 관리.


이상으로 모든 설명을 마칩니다.

감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기