Qt 로 만들자: 테트리스

가장 고전적이면서도 지금도 인기를 누리고 있는 게임을 꼽으라면 단연 테트리스일 것이다. 이번에는 이 고전적인 게임 <테트리스> 를 만들어보자.

1. 요구사항

  • 아래, 왼쪽, 오른쪽 이동
  • 한 번에 바닥으로 이동
  • 블럭 회전

2. 코드 분석

2.1 프로젝트 작성

  • 프로젝트 이름 : Tetris
  • 메인 클래스 이름: Tetris
  • 메인 클래스 유형: QMainWindow

2.2 헤더 분석(tetris.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <QMainWindow>

#include <QtWidgets>

class Board;

/**
 * @brief 테트리스
 */

class Tetris : public QMainWindow
{
    Q_OBJECT

public:
    Tetris(QWidget *parent = 0);
    ~Tetris();

private:
    Board *_board;      ///< 테트리스 판

    void initMenus();
    void initWidgets();

private slots:
    void newGame();
};


특별히 분석할 내용은 없다. 보고 넘어가자.

2.3 소스 분석(tetris.cpp)

2.3.1 Tetris 클래스

전체적인 레이아웃과 메뉴만 담당한다.

2.3.1.1 생성자와 소멸자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * @brief 생성자
 * @param parent
 */

Tetris::Tetris(QWidget *parent)
    : QMainWindow(parent)
{
    // 메뉴 초기화
    initMenus();
    // 위젯 초기화
    initWidgets();
}

/**
 * @brief 소멸자
 */

Tetris::~Tetris()
{
}


달라진 내용이 없다. 앞으로 이 부분은 생략해야할 듯.

2.3.1.2 initMenus()

메뉴를 초기화한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * @brief 메뉴를 초기화한다
 */

void Tetris::initMenus()
{
    // '파일' 메뉴 생성
    QMenu *fileMenu = new QMenu(tr("파일(&F)"));
    // '새 게임' 항목 추가
    fileMenu->addAction(tr("새 게임(&N)"), this, SLOT(newGame()),
                        QKeySequence(QKeySequence::New));
    // '끝내기' 항목 추가
    fileMenu->addAction(tr("끝내기(&x)"), this, SLOT(close()),
                        QKeySequence(tr("Ctrl+Q")));

    // '파일' 메뉴 추가
    menuBar()->addMenu(fileMenu);
}


마찬가지. 특별히 달라진 내용은 없다.

2.3.1.3 initWidgets()

위젯들을 초기화하고, 레이아웃을 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * @brief 위젯을 초기화한다
 */

void Tetris::initWidgets()
{
    // 테트리스 판을 센트럴 위젯으로
    _board = new Board;
    setCentralWidget(_board);

    // 메인 창 크기 고정
    layout()->setSizeConstraint(QLayout::SetFixedSize);

    // 테트리스 판에 입력 포커스 설정
    _board->setFocus();
}


11 번째 줄: QLayout::setSizeConstraint() 는 레이아웃의 크기 제한을 설정한다. QLayout::SetFixedSize 는 레이아웃의 크기를 고정한다. 따라서 창 크기가 바뀌지 않는다. 마우스를 창 경계선에 가져다 놓아도 마우스 모양이 크기 변경 형태로 바뀌지 않는다.

14 번째 줄: 실제 입력은 Board 위젯에서 이루어지므로, QWidget::setFocus() 로 입력 포커스를 옮긴다. 이렇게 하지 않으면, 키보드 입력이 Board 위젯으로 전달되지 않는다.

2.3.1.4 newGame()

메뉴의 '새 게임' 항목이나 Ctrl-N 을 눌렀을 때, 새 게임을 시작한다.

1
2
3
4
5
6
7
/**
 * @brief 새 게임을 시작한다
 */

void Tetris::newGame()
{
    _board->newGame();
}


2.3.2 Board 클래스

테트리스의 판을 나타낸다. 테트리스 게임을 실질적으로 구현하고 있다.

2.3.2.1 생성자와 소멸자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * @brief 테트리스 판
 */

class Board : public QWidget
{
    Q_OBJECT

public:
    /**
     * @brief 생성자
     * @param cols 판의 열 수
     * @param rows 판의 행 수
     * @param parent 부모 위젯
     */

    Board(int cols = 10, int rows = 20, QWidget *parent = 0)
        : QWidget(parent)
        , _cols(cols)
        , _rows(rows)
    {


생성자의 선언부이다. 클래스 내부에 Q_OBJECT 가 선언되어 있다. 그리고 이후에 타이머의 시그널을 내부 슬롯에 연결한다. 그런데 이를 그대로 빌드할 경우, 헤더에 선언되어 있는 클래스와 달리 소스(.cpp)에 선언되어 있는 클래스의 경우 문제가 발생한다. 이를 해결하기 위해서는 소스의 마지막 부분에 다음 부분을 추가해야 한다.

1
2
// .cpp 소스 내부의 클래스에서 시그널/슬롯을 쓰기 위해
#include "tetris.moc"


보다 자세한 것은 Qt 이야기: .cpp 안에서 QObject 자식 클래스 사용하기 를 읽어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
        // I 블럭 생성
        _blockI = new Block(1, 4, Qt::red);         // 빨강
        _blockI->mark(0, 0);
        _blockI->mark(0, 1);
        _blockI->mark(0, 2);
        _blockI->mark(0, 3);

        // J 블럭 생성
        _blockJ = new Block(3, 2, Qt::white);       // 하양
        _blockJ->mark(0, 0);
        _blockJ->mark(0, 1); _blockJ->mark(1, 1); _blockJ->mark(2, 1);

        // L 블럭 생성
        _blockL = new Block(3, 2, Qt::magenta);     // 자홍
        _blockL->mark(0, 0); _blockL->mark(1, 0); _blockL->mark(2, 0);
        _blockL->mark(0, 1);

        // O 블럭 생성
        _blockO = new Block(2, 2, Qt::blue);        // 파랑
        _blockO->mark(0, 0); _blockO->mark(1, 0);
        _blockO->mark(0, 1); _blockO->mark(1, 1);

        // S 블럭 생성
        _blockS = new Block(3, 2, Qt::green);       // 녹색
                             _blockS->mark(1, 0); _blockS->mark(2, 0);
        _blockS->mark(0, 1); _blockS->mark(1, 1);

        // T 블럭 생성
        _blockT = new Block(3, 2, "#A52A2A");       // 갈색
        _blockT->mark(0, 0); _blockT->mark(1,0); _blockT->mark(2,0);
                             _blockT->mark(1, 1);

         // Z 블럭 생성
        _blockZ = new Block(3, 2, Qt::cyan);        // 하늘색
        _blockZ->mark(0, 0); _blockZ->mark(1, 0);
                             _blockZ->mark(1,1); _blockZ->mark(2,1);

         // 빈 블럭 생성
        _blockEmpty = new Block(1, 1, Qt::black);   // 검정
        _blockEmpty->mark(0, 0);


테트리스의 블럭들을 생성하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        // 위젯 크기 고정
        setFixedSize(_cols * Block::squareWidth(),
                     _rows * Block::squareHeight());

        // 난수 씨앗 초기화
        qsrand(QTime::currentTime().msecsSinceStartOfDay());

        // 판 초기화
        for (int row = 0; row < _rows; ++row)
        {
            _map.append(QVector<Block *>(_cols));
        }

        // 타이머 연결
        connect(&_timer, &_timer.timeout, this, &this->moveDown);

        // 새 게임 시작
        newGame();
    }


나머지 초기화를 진행하고 있다.
2 번째 줄: QWidget::setFixedSize() 는 위젯의 크기를 주어진 크기로 고정한다.

6 번째 줄: qsrand()srand() 의 Qt 판으로 난수 씨앗을 초기화한다. QTime::currentTime() 은 현재 시간을 돌려주고, QTime::msecsSinceStartOfDay() 는 하루가 시작하고 나서 경과한 시간을 밀리초 단위로 돌려준다.

11 번째 줄: QVector 는 배열을 클래스화한 것으로, 추가는 빠르지만, 삽입은 느린 자료구조이다. QVector::append() 는 주어진 값을 끝에 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    /**
     * @brief 소멸자
     */

    ~Board()
    {
        delete _blockI;
        delete _blockJ;
        delete _blockL;
        delete _blockO;
        delete _blockS;
        delete _blockT;
        delete _blockZ;
        delete _blockEmpty;
    }


생성한 블럭들을 해제한다.

2.3.2.2 newGame()

새 게임을 시작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    /**
     * @brief 새 게임을 시작한다
     */

    void newGame()
    {
        // 게임중...
        _gameOver = false;

        // 판 모두 비움
        for (int row = 0; row < _rows; ++row)
        {
            for (int col = 0; col < _cols; ++col)
            {
                _map[row][col] = _blockEmpty;
            }
        }

        // 새 블럭 생성
        makeNewBlock();

        // 타이머 시작, 1초에 한 번씩 내려감
        _timer.start(1000);

        // 판 갱신
        update();
    }


2.3.2.3 keyPressEvent()

키보드 입력 이벤트를 처리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
protected:
    /**
     * @brief 키보드가 눌리면 발생하는 이벤트를 처리한다
     * @param e 키보드 이벤트
     */

    void keyPressEvent(QKeyEvent *e)
    {
        // 게임이 끝났으면 아무것도 하지 않음
        if (_gameOver)
            return;

        switch(e->key())
        {
        case Qt::Key_Up:    // 위
            rotate();       // 회전
            break;

        case Qt::Key_Down:  // 아래
            moveDown();     // 한 칸 아래로
            break;

        case Qt::Key_Left:  // 왼쪽
            moveLeft();     // 한 칸 왼쪽으로
            break;

        case Qt::Key_Right: // 오른쪽
            moveRight();    // 한 칸 오른쪽으로
            break;

        case Qt::Key_Space:     // 스페이스 바
            moveDownToBottom(); // 바닥으로
            break;

        default:                        // 나머지
            QWidget::keyPressEvent(e);  // 부모 위젯에 전달
            return;
        }
    }


6 번째 줄: QWidget::keyPressEvent() 는 키보드를 눌렀을 때 발생하는 이벤트이다.

12 번째 줄: QKeyEvent::key() 는 현재 눌린 키코드를 알려준다. 키코드는 Qt::Key 에 정의되어 있다.

35 번째 줄: 서브 클래싱한 keyPressEvent() 에서 처리를 하지 않은 이벤트는 반드시 상위 클래스에 전달해야 한다. 이에 따라 QWidget::keyPressEvent() 를 호출한다. 만약 이벤트를 처리하였다면 별다른 조치를 하지 않아도 된다.

2.3.2.4 paintEvent()

블럭 조각 배치도에 따라 블럭 조각을 그린다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    /**
     * @brief 그리기 요청이 있으면 발생하는 이벤트를 처리한다
     * @todo 필요한 부분만 그리기
     */

    void paintEvent(QPaintEvent *)
    {
        QPainter painter;

        painter.begin(this);
        // 판 내부의 블럭 조각을 그림
        for (int row = 0; row < _rows; ++row)
        {
            for (int col = 0; col < _cols; ++col)
            {
                _map.at(row).at(col)->drawSquare(col, row, &painter);
            }
        }
        painter.end();
    }


매번 모든 블럭 조각을 다시 그리지만, Qt 자체에서 더블-버퍼링이 지원되므로 깜빡임 같은 현상은 없다. 하지만, 필요한 부분만 다시 그린다면 더욱 좋을 것이다.

2.3.2.5 makeNewBlock()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private:
    int _cols;  ///< 판의 열 수
    int _rows;  ///< 판의 행 수

    int _col;   ///< 현재 블럭의 열 위치
    int _row;   ///< 현재 블럭의 행 위치

    bool _gameOver; ///< 게임 종료 여부

    Block *_block;      ///< 현재 블럭
    Block *_blockI;     ///< I 블럭
    Block *_blockJ;     ///< J 블럭
    Block *_blockL;     ///< L 블럭
    Block *_blockO;     ///< O 블럭
    Block *_blockS;     ///< S 블럭
    Block *_blockT;     ///< T 블럭
    Block *_blockZ;     ///< Z 블럭
    Block *_blockEmpty; ///< 빈 블럭

    QVector<QVector<Block *> > _map;    ///< 판 내부 블럭 조각 배치도

    QTimer _timer;  ///< 한 칸 아래로 내려가는 시간을 조절하는 타이머

    /**
     * @brief 새로운 생성을 만든다
     */

    void makeNewBlock()
    {
        // 블럭 종류
        static Block *blocks[] =
            {_blockI, _blockJ, _blockL, _blockO, _blockS, _blockT, _blockZ};

        // 새로운 블럭 생성
        int newBlock = qFloor(qrand() / (RAND_MAX + 1.0f) * 7);

        _block = blocks[newBlock];

        // 가로 위치는 화면 중앙에
        _col = (_cols - _block->cols()) / 2;
        // 세로 위치는 판 바로 위에
        _row = -_block->rows();
    }


내부 변수들을 선언하고 있는 부분이 함께 있다. makeNewBlock() 는 새로운 블럭을 생성하고, 블럭의 위치를 초기화한다.

34 번째 줄: qFloor()floor() 의 Qt 판이다. 주어진 값보다 크지 않은 가장 작은 정수를 돌려준다. 간단히 말하면, 버림을 한다. 예를 들어, 47.6 은 47 이 된다. qrand()rand() 의 Qt 판으로 난수를 발생시킨다.

보통 특정 범위의 난수를 발생시킬 때 나머지 연산을 사용하지만, 이보다는 위와 같이 나누기를 사용하는 것이 각 난수를 더 고른 확률로 발생시킬 수 있다.

1.0f 를 더한 것은 부동소숫점으로 바꾸려고 한 의도와 함께, 나누기 값이 1 이 되지 않도록 하기 위해서이다. qrand() 는 0 부터 RAND_MAX 까지 난수를 발생시키기 때문에, RAND_MAX 가 난수로 발생하면, 나누기 값이 1 이 된다. 이렇게 되면 0~6 까지가 아니라, 0~7 까지 발생하여 배열의 범위를 벗어날 수 있다.

2.3.2.6 checkRow() 와 checkCol()

각각 행과 열이 판 내부인지 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    /**
     * @brief 행 위치가 판 내부인지 확인한다
     * @param row 행 위치
     * @return 행 위치가 판 내부이면 참, 아니면 거짓
     */

    bool checkRow(int row)
    {
        return row >= 0 && row < _rows;
    }

    /**
     * @brief 열 위치가 판 내부인지 확인한다
     * @param col 열 위치
     * @return 열 위치가 판 내부이면 참, 아니면 거짓
     */

    bool checkCol(int col)
    {
        return col >= 0 && col < _cols;
    }


2.3.2.7 eraseBlock() 과 putBlock()

eraseBlock() 은 블럭 표시를 지우고, putBlock() 은 블럭을 표시한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    /**
     * @brief 현재 블럭을 지운다
     */

    inline void eraseBlock()
    {
        putBlock(true);
    }

    /**
     * @brief 현재 블럭을 표시한다
     * @param clear 참이면 블럭을 지우고, 거짓이면 블럭을 표시함
     */

    void putBlock(bool clear = false)
    {
        Block *block = clear ? _blockEmpty : _block;

        for (int r = 0, row = _row; r < _block->rows(); ++r, ++row)
        {
            for (int c = 0, col = _col; c < _block->cols(); ++c, ++col)
            {
                if (checkRow(row) && checkCol(col) && _block->marked(c, r))
                {
                    _map[row][col] = block;
                }
            }
        }
    }


eraseBlock() 은 putBlock(true) 를 호출하는 편의함수이다.

2.3.2.8 removeLine()

주어진 행을 지우고, 그 위의 행들을 한 줄씩 아래로 내린다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /**
     * @brief 주어진 행의 줄을 지운다
     * @param row 행 위치
     */

    void removeLine(int row)
    {
        // 행 윗쪽의 내용을 아래로 내림
        for (; row > 0; --row)
        {
            for (int col = 0; col < _cols; ++col)
            {
                _map[row][col] = _map.at(row - 1).at(col);
            }
        }

        // 가장 윗 줄은 빈 줄
        for (int col = 0; col < _cols; ++col)
            _map[0][col] = _blockEmpty;

        // 위젯 다시 그림
        update();
    }


2.3.2.9 checkLine()

블럭이 도달한 위치의 줄이 모두 채워져 있는지 확인하고, 채워져 있으면 해당 줄을 지운다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    /**
     * @brief 블럭이 도달한 위치의 줄이 모두 채워져 있으면 줄을 지운다
     */

    void checkLine()
    {
        for (int row = _row; row < _row + _block->rows(); ++row)
        {
            int col;

            for (col = 0; col < _cols; ++col)
            {
                 if (_map.at(row).at(col) == _blockEmpty)
                     break;
            }

            if (col == _cols)
                removeLine(row);
        }
    }


2.3.2.10 rotate()

블럭을 시계 방향 또는 반시계 방향으로 회전시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    /**
     * @brief 블럭을 회전시킨다
     */

    void rotate()
    {
        // 블럭을 지움
        eraseBlock();
        // 블럭 회전
        _block->rotate();

        // 회전한 블럭이 판 내부 조각들과 충돌하는지 확인
        for (int r = 0, row = _row; r < _block->rows(); ++r, ++row)
        {
            for (int c = 0, col = _col; c < _block->cols(); ++c, ++col)
            {
                if (!checkRow(row) || !checkCol(col)
                        || (_block->marked(c, r)
                                && _map.at(row).at(col) != _blockEmpty))
                {
                    // 충돌했음

                    // 블럭 원래로
                    _block->rotate(false);

                    // 블럭 표시
                    putBlock();

                    return;
                }
            }
        }

        // 충돌하지 않았음

        // 블럭 표시
        putBlock();

        // 다시 그림
        update();
    }


2.3.2.11 moveDown() 과 moveDownToBottm()

moveDown() 은 블럭을 한 칸 아래도 이동시키고, moveDownToBottom() 은 블럭을 바닥으로 이동시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    /**
     * @brief 블럭을 한 칸 아래로
     * @return 아래로 내려갔으면 참, 아니면 거짓
     */

    bool moveDown()
    {
        for (int c = 0; c < _block->cols(); ++c)
        {
            int r;

            // 블럭 가장 아래 조각 찾기
            for (r = _block->rows() - 1; r >= 0; --r)
            {
                if (_block->marked(c, r))
                    break;
            }


충돌 여부를 확인할 블럭 가장 아래 조각을 찾는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
            int col = c + _col;
            int row = r + _row;

            if (row + 1 == _rows        // 가장 아랫줄 ?
                    || (checkRow(row)   // 밑에 조각이 있나 ?
                            && _map.at(row + 1).at(col) != _blockEmpty))
            {
                if (_row < 0)   // 꽉 찼으면
                {
                    // 게임 끝 처리
                    _timer.stop();
                    _gameOver = true;

                    QMessageBox::information(this,
                                             qApp->applicationDisplayName(),
                                             tr("게임이 끝났습니다."));
                }
                else    // 블럭 더 이상 못 내려감
                {
                    // 줄 확인
                    checkLine();
                    // 새 블럭 생성
                    makeNewBlock();
                }

                // 다시 그림
                update();

                // 못 내려갔음
                return false;
            }
        }


아래로 내려갈 수 있는지 없는지 판단하고, 게임의 종료 여부를 판단한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
        // 블럭 지움
        eraseBlock();
        // 한 칸 아래로
        ++_row;
        // 블럭 표시
        putBlock();

        // 다시 그림
        update();

        // 내려갔음
        return true;
    }


막히지 않았으면 블럭을 아래로 이동시킨다.

1
2
3
4
5
6
7
8
    /**
     * @brief 블럭을 바닥으로
     */

    void moveDownToBottom()
    {
        while (moveDown())
            /* nothing */;
    }


막히지 않았으면 블럭을 계속 아래로 이동시킨다.

2.3.2.12 moveLeft()

블럭을 왼쪽으로 한 칸 이동시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    /**
     * @brief 블럭을 한 칸 왼쪽으로
     */

    void moveLeft()
    {
        // 왼쪽 벽이면
        if (_col <= 0)
            return;

        for (int r = 0; r < _block->rows(); ++r)
        {
            int c;

            // 가장 왼쪽 블럭 조각 찾기
            for (c = 0; c < _block->cols(); ++c)
            {
                if (_block->marked(c, r))
                    break;
            }

            int row = r + _row;
            int col = c + _col;

            // 왼쪽이 막혀 있으면
            if (checkRow(row) && _map.at(row).at(col - 1) != _blockEmpty)
                return;
        }

        // 블럭 지움
        eraseBlock();
        // 한 칸 왼쪽으로
        --_col;
        // 블럭 표시
        putBlock();

        // 다시 그림
        update();
    }


2.3.2.13 moveRight()

블럭을 오른쪽으로 한 칸 이동시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    /**
     * @brief 블럭을 한 칸 오른쪽으로
     */

    void moveRight()
    {
        // 오른쪽 벽이면
        if (_col + _block->cols() >= _cols)
            return;

        for (int r = 0; r < _block->rows(); ++r)
        {
            int c;

            // 가장 오른쪽 조각 찾기
            for (c = _block->cols() - 1; c >= 0; --c)
            {
                if (_block->marked(c, r))
                    break;
            }

            int row = r + _row;
            int col = c + _col;

            // 오른쪽이 막혀 있으면
            if (checkRow(row) && _map.at(row).at(col + 1) != _blockEmpty)
                return;
        }

        // 블럭 지움
        eraseBlock();
        // 한 칸 오른쪽으로
        ++_col;
        // 블럭 표시
        putBlock();

        // 다시 그림
        update();
    }
};


2.3.3 Block 클래스

테트리스 블럭을 구현한다.

2.3.3.1 squareWidth() 와 squareHeight()

각각 블럭 조각의 폭과 높이를 돌려주는 정적 멤버 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * @brief 테트리스 블럭
 */

class Block
{
public:
    /**
     * @brief 블럭 조각의 폭을 돌려준다
     * @return 블럭 조각의 폭
     */

    static int squareWidth()
    {
        return 30;
    }

    /**
     * @brief 블럭 조각의 높이를 돌려준다
     * @return 블럭 조각의 높이
     */

    static int squareHeight()
    {
        return 30;
    }


2.3.3.2 생성자와 소멸자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    /**
     * @brief 생성자
     * @param cols 블럭의 열 수
     * @param rows 블럭의 행 수
     * @param color 블럭의 내부 색깔
     */

    Block(int cols, int rows, QColor color)
        : _cols(cols)
        , _rows(rows)
        , _color(color)
    {
        // 블럭 초기화
        for (int row = 0; row < _rows; ++row)
        {
            QVector<bool> v;

            for (int col = 0; col < _cols; ++col)
            {
                v.append(false);
            }

            _map.append(v);
        }

        // 블럭 조각 생성
        _square = new QPixmap(squareWidth(), squareHeight());

        QPainter painter;
        painter.begin(_square);
        painter.fillRect(_square->rect(), color);
        painter.setPen(Qt::darkGray);
        painter.drawRect(0, 0,
                         _square->width() - 1, _square->height() - 1);
        painter.end();

    }


26 번째 줄: QPixmap 은 화면밖(off-screen) 에서  그리기 작업을 지원하는 클래스이다. paintEvent() 가 아니더라도 QPainter 를 쓸 수 있다.

30 번째 줄: QPainter::fillRect() 는 주어진 직사각형을 주어진 색으로 채운다.

32 번째 줄: QPainter::drawRect() 는 주어진 위치와 크기로 직사각형을 그린다. 폭과 높이에서 -1 을 한 것에 유의하자.

1
2
3
4
5
6
7
    /**
     * @brief 소멸자
     */

    virtual ~Block()
    {
        delete _square;
    }


블럭 조각을 해제한다.

2.3.3.3 cols() 와 rows()

각각 블럭의 열 수와 행 수를 돌려준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /**
     * @brief 블럭의 열 수를 돌려준다
     * @return 블럭의 열 수
     */

    int cols() const
    {
        return _cols;
    }

    /**
     * @brief 블럭의 행 수를 돌려준다
     * @return 블럭의 행 수
     */

    int rows() const
    {
        return _rows;
    }


2.3.3.4 marked() 와 mark()

marked() 는 주어진 위치에 조각이 있는지 없는지 알려주고, mark() 는 주어진 위치에 조각이 있다고 표시한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /**
     * @brief 블럭의 특정 위치에 블럭 조각이 있는지 확인한다
     * @param col 블럭 조각의 열 위치
     * @param row 블럭 조각의 행 위치
     * @return 블럭 조각이 있으면 true, 없으면 false
     */

    bool marked(int col, int row) const
    {
        return _map.at(row).at(col);
    }

    /**
     * @brief 블럭의 특정 위치에 블럭 조각이 있는지 표시한다
     * @param col 블럭 조각의 열 위치
     * @param row 블럭 조각의 행 위치
     */

    void mark(int col, int row )
    {
        _map[row][col] = true;
    }


2.3.3.5 rotate()

블럭을 시계 방향 또는 반시계 방향으로 회전시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    /**
     * @brief 블럭을 회전한다
     * @param clockWise 참이면 시계 방향으로, 거짓이면 반시계 방향으로 회전
     */

    void rotate(bool clockWise = true)
    {
        // 새로운 블럭 모양
        QVector<QVector<bool> > newMap;

        // 행/열 바꿈
        int newRows = _cols;
        int newCols = _rows;

        int row;
        int col;

        // 새 블럭 초기화
        for (row = 0; row < newRows; ++row)
        {
            newMap.append(QVector<bool>(newCols));
        }

        if (clockWise)
        {
            // 시계 방향 회전
            for (row = 0; row < _rows; ++row)
            {
                for (col = 0; col < _cols; ++ col)
                {
                    newMap[newRows - 1 - col][row] = marked(col, row);
                }
            }
        }
        else
        {
            // 반시계 방향 회전
            for (row = 0; row < _rows; ++row)
            {
                for (col = 0; col < _cols; ++ col)
                {
                    newMap[col][newCols - 1 - row] = marked(col, row);
                }
            }
        }

        // 회전 시킨 블럭으로 교체
        _rows = newRows;
        _cols = newCols;
        _map = newMap;
    }


2.3.3.6 drawSquare()

블럭 조각을 주어진 위치에 그린다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /**
     * @brief 블럭 조각을 주어진 위치에 그린다
     * @param col 테트리스 판 내부 열 위치
     * @param row 테트리스 판 내부 행 위치
     * @param painter 테트리스 판의 페인터
     */

    void drawSquare(int col, int row, QPainter *painter)
    {
        painter->drawPixmap(col * squareWidth(), row * squareHeight(),
                            *_square);
    }

private:
    int _cols;  ///< 블럭의 열 수
    int _rows;  ///< 블럭의 행 수
    QColor _color;  ///< 블럭 내부의 색깔
    QVector<QVector<bool> > _map;   ///< 블럭 조각 배치도

    QPixmap *_square;   ///< 블럭 조각 모양
};


9 번째 줄: QPainter::drawPixmap() 는 QPixmap 를 화면에 그리는 함수이다.

3. 마무리하면서...

테트리스의 핵심적인 기능을 구현해 보았다. 점수내기라든지 다음 블럭 보여주기 같은 것은 스스로 구현해 보기 바란다.

다음은<테트리스> 의 실행 모습이다.



전체 소스는 여기에서 확인하자.

댓글

이 블로그의 인기 게시물

토렌트: < 왕좌의 게임 > 시즌 1 ~ 시즌 8 완결편 마그넷

토렌트: < 스타워즈 > Ep.1 ~ Ep.6 마그넷

토렌트: NGC < 코스모스 > 우리말 더빙 전편(1편~13편) 마그넷