Qt文档阅读笔记-Tetrix Example解析

本文的俄罗斯方块实例官方的一个经典实例。

俄罗斯方块这个游戏就是从顶部落下方块到底部,把每行都填充满。当一行被填充满,这行就会被移除,玩家就会获取分数。顶部的方块都会依次落下,如果有多行被填充满了,那么会移除多行,获取对应的分数。

键盘左键控制方块向左移动,键盘右键控制方块向右移动,键盘上键使得方块逆时针旋转90°,键盘下键使得方块顺时针旋转90°。

键盘D键使得方块加速落下,键盘空格使得方块立即落下。

本俄罗斯方块实例由3个类构成:

TetrixWindow:构造游戏的整体界面,也就是肉眼看到的东西(不含游戏逻辑)都是这个类负责。

TetrixBoard:包含游戏逻辑、键盘、展示方块、游戏区域。

TetrixPiece:统计分数信息等。

从中可以知道,本实例最复杂的类就是TetrixBoard,包含游戏逻辑和一些界面渲染。TetrixWindow和TetrixPiece就比较简单了。

TetrixWindow类定义

TetrixWindow类用于展示游戏信息并且画出游戏区域:

 class TetrixWindow : public QWidget
 {
     Q_OBJECT

 public:
     TetrixWindow();

 private:
     QLabel *createLabel(const QString &text);

     TetrixBoard *board;
     QLabel *nextPieceLabel;
     QLCDNumber *scoreLcd;
     QLCDNumber *levelLcd;
     QLCDNumber *linesLcd;
     QPushButton *startButton;
     QPushButton *quitButton;
     QPushButton *pauseButton;
 };

在类的private中有几个成员变量,包括前端画线,各种挂机和按钮,按钮有开始游戏,暂停当前游戏和退出。

TetrixWindow继承了QWidget,但QWidget父类不能构造自己想创建的结构,所以,一般用上面这种方式创建程序员想要的界面。

TetrixWindow类声明

构造函数为游戏创建元素:

 TetrixWindow::TetrixWindow()
 {
     board = new TetrixBoard;

在构造函数里面创建TetrixBoard的实例,用于画出游戏区域,标签,下一个方块。标签初始为空。

3个QLCDNumber对象用于展示分数、等级、移除了多少行。程序运行时给他们一个初始值,当游戏开始时,再给数据进行填充。

scoreLcd = new QLCDNumber(5);
scoreLcd->setSegmentStyle(QLCDNumber::Filled);

创建了3个按钮并且绑定了快捷键,用于开始新游戏,暂停当前游戏,退出应用:

startButton = new QPushButton(tr("&Start"));
startButton->setFocusPolicy(Qt::NoFocus);
quitButton = new QPushButton(tr("&Quit"));
quitButton->setFocusPolicy(Qt::NoFocus);
pauseButton = new QPushButton(tr("&Pause"));
pauseButton->setFocusPolicy(Qt::NoFocus);

上面的代码说明3个按钮不接受键盘的聚焦;但他需要和TetrixBoard示例的槽函数关联。虽然设置了Qt::NoFocus,但是如果带上键盘Alt的按键,仍然会收到。

按钮的clicked()信号关联TextrixBoard的Start和Pause,并且关联QCoreApplication::quit()

     connect(startButton, &QPushButton::clicked, board, &TetrixBoard::start);
     connect(quitButton , &QPushButton::clicked, qApp, &QApplication::quit);
     connect(pauseButton, &QPushButton::clicked, board, &TetrixBoard::pause);
 #if __cplusplus >= 201402L
     connect(board, &TetrixBoard::scoreChanged,
             scoreLcd, qOverload<int>(&QLCDNumber::display));
     connect(board, &TetrixBoard::levelChanged,
             levelLcd, qOverload<int>(&QLCDNumber::display));
     connect(board, &TetrixBoard::linesRemovedChanged,
             linesLcd, qOverload<int>(&QLCDNumber::display));
 #else
     connect(board, &TetrixBoard::scoreChanged,
             scoreLcd, QOverload<int>::of(&QLCDNumber::display));
     connect(board, &TetrixBoard::levelChanged,
             levelLcd, QOverload<int>::of(&QLCDNumber::display));
     connect(board, &TetrixBoard::linesRemovedChanged,
             linesLcd, QOverload<int>::of(&QLCDNumber::display));
 #endif

board中有些信号关联了LCD关键的一些槽函数,用于更新分数、等级、游戏区域移除了多少行。

将标签、LCD挂件、board、一些使用createLabel函数创建的label放到QGridLayout:

     QGridLayout *layout = new QGridLayout;
     layout->addWidget(createLabel(tr("NEXT")), 0, 0);
     layout->addWidget(nextPieceLabel, 1, 0);
     layout->addWidget(createLabel(tr("LEVEL")), 2, 0);
     layout->addWidget(levelLcd, 3, 0);
     layout->addWidget(startButton, 4, 0);
     layout->addWidget(board, 0, 1, 6, 1);
     layout->addWidget(createLabel(tr("SCORE")), 0, 2);
     layout->addWidget(scoreLcd, 1, 2);
     layout->addWidget(createLabel(tr("LINES REMOVED")), 2, 2);
     layout->addWidget(linesLcd, 3, 2);
     layout->addWidget(quitButton, 4, 2);
     layout->addWidget(pauseButton, 5, 2);
     setLayout(layout);

     setWindowTitle(tr("Tetrix"));
     resize(550, 370);
 }

最后将grid放到widget中,并设置程序的标题,应用程序的大小。

createLabel()这个函数在堆区创建label,并且将label居中,最后返回label的指针:

 QLabel *TetrixWindow::createLabel(const QString &text)
 {
     QLabel *label = new QLabel(text);
     label->setAlignment(Qt::AlignHCenter | Qt::AlignBottom);
     return label;
 }

在此所有的label都放到了widget的布局中了,TetrixWindow变成了主界面,他会随着窗口的关闭而销毁。

TetrixPiece类定义

TetrixPiece类为游戏区域创建俄罗斯方块,包括形状、位置、方块在游戏区域的位置范围。

 class TetrixPiece
 {
 public:
     TetrixPiece() { setShape(NoShape); }

     void setRandomShape();
     void setShape(TetrixShape shape);

     TetrixShape shape() const { return pieceShape; }
     int x(int index) const { return coords[index][0]; }
     int y(int index) const { return coords[index][1]; }
     int minX() const;
     int maxX() const;
     int minY() const;
     int maxY() const;
     TetrixPiece rotatedLeft() const;
     TetrixPiece rotatedRight() const;

 private:
     void setX(int index, int x) { coords[index][0] = x; }
     void setY(int index, int y) { coords[index][1] = y; }

     TetrixShape pieceShape;
     int coords[4][2];
 };

每个方块包含4个形状,这些都存在私有变量coords中,每个块又有pieceShape变量,用于描述当前方块的内部信息。

构造函数采用内联编写,并且每个piece都初始化个无形状的快。shape()函数返回pieceShape变量,x()和y()返回块的x和y轴坐标。

TetrixPiece类声明

setRandomShape()函数随机创建一个块:

 void TetrixPiece::setRandomShape()
 {
     setShape(TetrixShape(QRandomGenerator::global()->bounded(7) + 1));
 }

使用setShape()函数选择块的位置,TetrixShape的构造函数需要传入枚举。

enum TetrixShape { NoShape, ZShape, SShape, LineShape, TShape, SquareShape,
                   LShape, MirroredLShape };

setShape中有个静态变量coordsTable用于记录所有形状的位置:

 void TetrixPiece::setShape(TetrixShape shape)
 {
     static const int coordsTable[8][4][2] = {
         { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
         { { 0, -1 },  { 0, 0 },   { -1, 0 },  { -1, 1 } },
         { { 0, -1 },  { 0, 0 },   { 1, 0 },   { 1, 1 } },
         { { 0, -1 },  { 0, 0 },   { 0, 1 },   { 0, 2 } },
         { { -1, 0 },  { 0, 0 },   { 1, 0 },   { 0, 1 } },
         { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
         { { -1, -1 }, { 0, -1 },  { 0, 0 },   { 0, 1 } },
         { { 1, -1 },  { 0, -1 },  { 0, 0 },   { 0, 1 } }
     };

     for (int i = 0; i < 4 ; i++) {
         for (int j = 0; j < 2; ++j)
             coords[i][j] = coordsTable[shape][i][j];
     }
     pieceShape = shape;
 }

coords的数据是从coordsTable这个表中获取的,每一组数据代表一个形状。

int x(int index) const { return coords[index][0]; }
int y(int index) const { return coords[index][1]; }

x()和y()是内联函数,返回当前coords的横纵坐标,他们的范围是-2到2,虽然在setShape中的coordsTable范围水平是-1到1,垂直范围是-1到2,但形状会旋转,90,180,270度,所以coords的横纵范围会变为-2到2。

minX()和maxX()函数返回当前块X轴数据的最小值和最大值:

 int TetrixPiece::minX() const
 {
     int min = coords[0][0];
     for (int i = 1; i < 4; ++i)
         min = qMin(min, coords[i][0]);
     return min;
 }

 int TetrixPiece::maxX() const
 {
     int max = coords[0][0];
     for (int i = 1; i < 4; ++i)
         max = qMax(max, coords[i][0]);
     return max;
 }

同样minY() 和maxY()返回当前块Y轴数据的最小值和最大值:

 int TetrixPiece::minY() const
 {
     int min = coords[0][1];
     for (int i = 1; i < 4; ++i)
         min = qMin(min, coords[i][1]);
     return min;
 }

 int TetrixPiece::maxY() const
 {
     int max = coords[0][1];
     for (int i = 1; i < 4; ++i)
         max = qMax(max, coords[i][1]);
     return max;
 }

 rotatedLeft()当前方块逆时针旋转90°。

 TetrixPiece TetrixPiece::rotatedLeft() const
 {
     if (pieceShape == SquareShape)
         return *this;

     TetrixPiece result;
     result.pieceShape = pieceShape;
     for (int i = 0; i < 4; ++i) {
         result.setX(i, y(i));
         result.setY(i, -x(i));
     }

rotatedRight()当前方块顺时针旋转90°。

 TetrixPiece TetrixPiece::rotatedRight() const
 {
     if (pieceShape == SquareShape)
         return *this;

     TetrixPiece result;
     result.pieceShape = pieceShape;
     for (int i = 0; i < 4; ++i) {
         result.setX(i, -y(i));
         result.setY(i, x(i));
     }

TetrixBoard类定义

TetrixBoard类继承了QFrame此类包含了游戏逻辑和一些展示数据:

 class TetrixBoard : public QFrame
 {
     Q_OBJECT

 public:
     TetrixBoard(QWidget *parent = 0);

     void setNextPieceLabel(QLabel *label);
     QSize sizeHint() const override;
     QSize minimumSizeHint() const override;

 public slots:
     void start();
     void pause();

 signals:
     void scoreChanged(int score);
     void levelChanged(int level);
     void linesRemovedChanged(int numLines);

 protected:
     void paintEvent(QPaintEvent *event) override;
     void keyPressEvent(QKeyEvent *event) override;
     void timerEvent(QTimerEvent *event) override;

槽函数有start()和pause()。TetrixBoard重载了QFrame的sizeHint()和minimumSizeHint()方法,并且还有3个信号与TetrixWindow实例关联。

下面是TetrixBoard类中的private变量和函数。
 

 private:
     enum { BoardWidth = 10, BoardHeight = 22 };

     TetrixShape &shapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }
     int timeoutTime() { return 1000 / (1 + level); }
     int squareWidth() { return contentsRect().width() / BoardWidth; }
     int squareHeight() { return contentsRect().height() / BoardHeight; }
     void clearBoard();
     void dropDown();
     void oneLineDown();
     void pieceDropped(int dropHeight);
     void removeFullLines();
     void newPiece();
     void showNextPiece();
     bool tryMove(const TetrixPiece &newPiece, int newX, int newY);
     void drawSquare(QPainter &painter, int x, int y, TetrixShape shape);

     QBasicTimer timer;
     QPointer<QLabel> nextPieceLabel;
     bool isStarted;
     bool isPaused;
     bool isWaitingAfterLine;
     TetrixPiece curPiece;
     TetrixPiece nextPiece;
     int curX;
     int curY;
     int numLinesRemoved;
     int numPiecesDropped;
     int score;
     int level;
     TetrixShape board[BoardWidth * BoardHeight];
 };

面板的大小被private中enum的BoardWidth和BoardHeight固定,TetrixBoard类中包含TetrixShape数组其大小为BoardWidth*BoardHeight。

使用QBasicTimer去控制方块在区域下落的速度。此时需要重写timerEvent()用于更新界面。

TetrixBoard类声明

在构造函数中,设置了widget的框架风格,并且设置了Qt::StrongFocus用于接收键盘按键,并且初始化了状态:

 TetrixBoard::TetrixBoard(QWidget *parent)
     : QFrame(parent)
 {
     setFrameStyle(QFrame::Panel | QFrame::Sunken);
     setFocusPolicy(Qt::StrongFocus);
     isStarted = false;
     isPaused = false;
     clearBoard();

     nextPiece.setRandomShape();
 }

首块(下一块)都随机设置一个形状。

setNextPieceLabel()函数用于设置下一个块:

 void TetrixBoard::setNextPieceLabel(QLabel *label)
 {
     nextPieceLabel = label;
 }

设置hint,包括一般的hint()和最小的hint()大小

 QSize TetrixBoard::sizeHint() const
 {
     return QSize(BoardWidth * 15 + frameWidth() * 2,
                  BoardHeight * 15 + frameWidth() * 2);
 }

 QSize TetrixBoard::minimumSizeHint() const
 {
     return QSize(BoardWidth * 5 + frameWidth() * 2,
                  BoardHeight * 5 + frameWidth() * 2);
 }

设置最小的Hint()尺寸目的是当主窗口足够小时防止Hint()被隐藏。

当start()槽函数被调用时代表要开始游戏了。首先重置游戏状态,包括分数、等级、游戏区域内容:

 void TetrixBoard::start()
 {
     if (isPaused)
         return;

     isStarted = true;
     isWaitingAfterLine = false;
     numLinesRemoved = 0;
     numPiecesDropped = 0;
     score = 0;
     level = 1;
     clearBoard();

     emit linesRemovedChanged(numLinesRemoved);
     emit scoreChanged(score);
     emit levelChanged(level);

     newPiece();
     timer.start(timeoutTime(), this);
 }

这个函数也发送linesRemovedChanged、scoreChanged、levelChanged信号给其他组件。

pause()槽函数用于暂停当前**:

 void TetrixBoard::pause()
 {
     if (!isStarted)
         return;

     isPaused = !isPaused;
     if (isPaused) {
         timer.stop();
     } else {
         timer.start(timeoutTime(), this);
     }
     update();
 }

首先检测游戏是否已经开始,只有**开始才能暂停。

重写paintEvent()函数,使用QPainter画面板:

 void TetrixBoard::paintEvent(QPaintEvent *event)
 {
     QFrame::paintEvent(event);

     QPainter painter(this);
     QRect rect = contentsRect();

tetrixBoard是QFrame的子类,使用contentsRect()获取当前可绘画的区域。

如果游戏暂停,将隐藏目前面板的状态,并且展示出一些文字信息。就在此函数绘画出文字并在widget上展示。

通过内部矩形底部减去面板上小方块的总高度,可以找到板顶部位置,调用drawSquare()函数在指定位置画出block,就从这个位置开始画。

     int boardTop = rect.bottom() - BoardHeight*squareHeight();

     for (int i = 0; i < BoardHeight; ++i) {
         for (int j = 0; j < BoardWidth; ++j) {
             TetrixShape shape = shapeAt(j, BoardHeight - i - 1);
             if (shape != NoShape)
                 drawSquare(painter, rect.left() + j * squareWidth(),
                            boardTop + i * squareHeight(), shape);
         }
     }

未被占用的块设置为空白。

与面板上存在的块不同,当前块是逐渐下落的:

     if (curPiece.shape() != NoShape) {
         for (int i = 0; i < 4; ++i) {
             int x = curX + curPiece.x(i);
             int y = curY - curPiece.y(i);
             drawSquare(painter, rect.left() + x * squareWidth(),
                        boardTop + (BoardHeight - y - 1) * squareHeight(),
                        curPiece.shape());
         }
     }
 }

TetrixBoard界面上通过keyPressEvent()处理玩家键盘事件。

 void TetrixBoard::keyPressEvent(QKeyEvent *event)
 {
     if (!isStarted || isPaused || curPiece.shape() == NoShape) {
         QFrame::keyPressEvent(event);
         return;
     }

只有**开始时,键盘事件才会正常接收。

监听的键盘事件有Left、Right、Down、Up、Space、D

     switch (event->key()) {
     case Qt::Key_Left:
         tryMove(curPiece, curX - 1, curY);
         break;
     case Qt::Key_Right:
         tryMove(curPiece, curX + 1, curY);
         break;
     case Qt::Key_Down:
         tryMove(curPiece.rotatedRight(), curX, curY);
         break;
     case Qt::Key_Up:
         tryMove(curPiece.rotatedLeft(), curX, curY);
         break;
     case Qt::Key_Space:
         dropDown();
         break;
     case Qt::Key_D:
         oneLineDown();
         break;
     default:
         QFrame::keyPressEvent(event);
     }

对于其他键盘事件可以直接将事件回给父类QFrame::keyPressEvent(event)。

当事件超时timerEvent()函数会调用QBasicTimer实例。

 void TetrixBoard::timerEvent(QTimerEvent *event)
 {
     if (event->timerId() == timer.timerId()) {
         if (isWaitingAfterLine) {
             isWaitingAfterLine = false;
             newPiece();
             timer.start(timeoutTime(), this);
         } else {
             oneLineDown();
         }
     } else {
         QFrame::timerEvent(event);
     }
 }

当一行刚好被填充满,创建一个新的块,并重置时间;

clearBoard()函数将面板上所有的小方块设置为NoShape:
 

 void TetrixBoard::clearBoard()
 {
     for (int i = 0; i < BoardHeight * BoardWidth; ++i)
         board[i] = NoShape;
 }

dropDown()负责将当前块下落,当落到底部或者落到其他块顶部时,停止下落。

void TetrixBoard::dropDown()
 {
     int dropHeight = 0;
     int newY = curY;
     while (newY > 0) {
         if (!tryMove(curPiece, curX, newY - 1))
             break;
         --newY;
         ++dropHeight;
     }
     pieceDropped(dropHeight);
 }

dropDown()函数最终会调用到pieceDropped(),这个函数更新*家分数和下落后的一些处理。

oneLineDown()函数使得当前块下降1,当玩家按D时会快速下落:

 void TetrixBoard::oneLineDown()
 {
     if (!tryMove(curPiece, curX, curY - 1))
         pieceDropped(0);
 }

当块不能下落的时候,会调用pieceDropped()函数,并且参数为0,表示不能继续下落了。

pieceDropped()函设置当前块下落,并且里面有些判断和信号发出,最终会创建一个新块出来:

 void TetrixBoard::pieceDropped(int dropHeight)
 {
     for (int i = 0; i < 4; ++i) {
         int x = curX + curPiece.x(i);
         int y = curY - curPiece.y(i);
         shapeAt(x, y) = curPiece.shape();
     }

     ++numPiecesDropped;
     if (numPiecesDropped % 25 == 0) {
         ++level;
         timer.start(timeoutTime(), this);
         emit levelChanged(level);
     }

     score += dropHeight + 7;
     emit scoreChanged(score);
     removeFullLines();

     if (!isWaitingAfterLine)
         newPiece();
 }

removeFullLines()函数当方块下落后,从底部到顶部判断有没有满行的。满了就全部置为空:

void TetrixBoard::removeFullLines()
 {
     int numFullLines = 0;

     for (int i = BoardHeight - 1; i >= 0; --i) {
         bool lineIsFull = true;

         for (int j = 0; j < BoardWidth; ++j) {
             if (shapeAt(j, i) == NoShape) {
                 lineIsFull = false;
                 break;
             }
         }

         if (lineIsFull) {
             ++numFullLines;
             for (int k = i; k < BoardHeight - 1; ++k) {
                 for (int j = 0; j < BoardWidth; ++j)
                     shapeAt(j, k) = shapeAt(j, k + 1);
             }
             for (int j = 0; j < BoardWidth; ++j)
                 shapeAt(j, BoardHeight - 1) = NoShape;
         }
     }

如果一行都是满的,将上面的行依次往下拷贝。最顶部的块清空,如何发送消除行新增信号。

     if (numFullLines > 0) {
         numLinesRemoved += numFullLines;
         score += 10 * numFullLines;
         emit linesRemovedChanged(numLinesRemoved);
         emit scoreChanged(score);

         timer.start(500, this);
         isWaitingAfterLine = true;
         curPiece.setShape(NoShape);
         update();
     }
 }

如果行被移除,*家的分数,消除行等信息都会被更新。发出linesRemoved()和scoreChanged()信号更新界面。

除此之外,timer也会停下来isWaitingAfterLine标志位表名那一行是否被移除。随后timerEvent()继续调用**继续运行。

newPiece()函数从最顶部创建一个新块,并且这个新块是随机创建的:

 void TetrixBoard::newPiece()
 {
     curPiece = nextPiece;
     nextPiece.setRandomShape();
     showNextPiece();
     curX = BoardWidth / 2 + 1;
     curY = BoardHeight - 1 + curPiece.minY();

     if (!tryMove(curPiece, curX, curY)) {
         curPiece.setShape(NoShape);
         timer.stop();
         isStarted = false;
     }
 }

新块从面板顶部被创建,当新块不能移动时,游戏结束,并设置curPiece的状态,暂停timer,并把isStarted设置为false。

showNextPiece()函数更新label,这个label代表下一个块:

void TetrixBoard::showNextPiece()
 {
     if (!nextPieceLabel)
         return;

     int dx = nextPiece.maxX() - nextPiece.minX() + 1;
     int dy = nextPiece.maxY() - nextPiece.minY() + 1;

     QPixmap pixmap(dx * squareWidth(), dy * squareHeight());
     QPainter painter(&pixmap);
     painter.fillRect(pixmap.rect(), nextPieceLabel->palette().background());

     for (int i = 0; i < 4; ++i) {
         int x = nextPiece.x(i) - nextPiece.minX();
         int y = nextPiece.y(i) - nextPiece.minY();
         drawSquare(painter, x * squareWidth(), y * squareHeight(),
                    nextPiece.shape());
     }
     nextPieceLabel->setPixmap(pixmap);
 }

将块的数据放到pixmap中,并将pixmap放到label上。

tryMove()判断块能否在坐标系上下移。

 bool TetrixBoard::tryMove(const TetrixPiece &newPiece, int newX, int newY)
 {
     for (int i = 0; i < 4; ++i) {
         int x = newX + newPiece.x(i);
         int y = newY - newPiece.y(i);
         if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
             return false;
         if (shapeAt(x, y) != NoShape)
             return false;
     }

先检测面板上是否有下落的空间,如果有就占了那块空间,否则就下落失败。

     curPiece = newPiece;
     curX = newX;
     curY = newY;
     update();
     return true;
 }

drawSquare()函数画不同的块以及填充不同的颜色:

 void TetrixBoard::drawSquare(QPainter &painter, int x, int y, TetrixShape shape)
 {
     static const QRgb colorTable[8] = {
         0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
         0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00
     };

     QColor color = colorTable[int(shape)];
     painter.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2,
                      color);

     painter.setPen(color.light());
     painter.drawLine(x, y + squareHeight() - 1, x, y);
     painter.drawLine(x, y, x + squareWidth() - 1, y);

     painter.setPen(color.dark());
     painter.drawLine(x + 1, y + squareHeight() - 1,
                      x + squareWidth() - 1, y + squareHeight() - 1);
     painter.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
                      x + squareWidth() - 1, y + 1);
 }

从colorTable中获取不同的颜色,再用这个颜色去设置painter,最终在坐标轴上画出来。

猜你喜欢

转载自blog.csdn.net/qq78442761/article/details/129850352