# Qt-Tetrix
**Repository Path**: bluetooth_car/Qt-Tetrix
## Basic Information
- **Project Name**: Qt-Tetrix
- **Description**: 参照Qt官方示例写的俄罗斯方块,使用Wiki详细记录下自己的思路和理解,希望多交流,多指正。
- **Primary Language**: C++
- **License**: LGPL-3.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 8
- **Created**: 2023-04-17
- **Last Updated**: 2023-04-17
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
####俄罗斯方块简单实现
--------------------
http://git.oschina.net/liujiaxingemail/Tetrix/wikis/home
#####思路:
* [画小方块,2D效果](#one)。
* [方块模型,三维数组](#two)。
* [绘制方块,方向颜色](#three)。
* [方块移动,方向变换](#four)。
* [游戏边框,越界判断](#five)。
* [记录方块,满行判断](#six)。
* [界面布局,效果显示](#seven)。
#####实现(下内容只截取了部分重要的函数做说明,但是整体的思路是完整的。具体的请看代码,代码中也有注释。):
##### 画小方块,2D效果。
1. 创建一个继承于QFrame的类TetrixBoard,创建方法
```C++
void TetrixBoard::drawSquare(QPainter &painter, int x, int y, TetrixShape shape){
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());
//x + 1 y + 1是由于已经存在浅色的线条 -1 表示将像素宽度为1的线条画在方块内部
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);
}
```
* x,y为相对于TetrixBoard左上角(0,0)的位置,TetrixShape为枚举常量,只是用来在Color数组ColorTable中选取颜色。
* 首先让出边框,并实用颜色填充。设置填充颜色为浅色笔画小方块的上面横线和左边竖线,深色画笔画右边竖线和底部横线。
* 这样就可以画出一个宽为SquareWidht,高为SquareHeight的正方型,且看起来有2D效果。
* 定义好常量之后,可以直接重写paintEvent(QPaintEvent *event)方法中实用,并可以直接实例化TetrixBoard看到效果。
##### 方块模型,三维数组。
1. 创建TetrixPiece类,定义一个静态的static const int coordsTable[8][4][4][4]三位数组,具体参照源码,没有复杂的变化与计算,一目了然。1表示有值,0表示无值,有值才画。
2. TetrixShape枚举一 一对应对于三维数组的8种形状,TetrixDirection为每个方块的四种形状
3. ```C++
void TetrixPiece::setShape(TetrixShape tetrixShape,TetrixDirection tetrixDirection){
for(int i = 0; i < 4; i++){
for(int j = 0; j < 4; j++){
coords[i][j] = coordsTable[tetrixShape][tetrixDirection][i][j];
}
}
pieceShape = tetrixShape;
pieceDirection = tetrixDirection;
}
void TetrixPiece::setRandomShape(){
setShape(TetrixShape( qrand() % 7 + 1),TetrixDirection(qrand() % 4));
}
```
* 当前模型数组int coords[4][4],根据形状、方向给其赋值。
* 使用qrand() % 7 可以获取0-6的随机数,piectShape、pieceDirection记录当前属性值。
##### 绘制方块,方向颜色。
1. 在TetrixBoard中创建TetrixPiece对象,并在TetrixBoard的初始化函数中,给TeitixPiece初始化赋值
2. ```C++
//画出当前的形状
for(int i = 0; i < 4; i++){
for(int j = 0; j < 4; j++){
if(currentPiece.value(i,j) == 0){
continue;
}
int x = (j - currentPiece.x() + curX) * SquareWidth + GameBoardBorder;
int y = (i - currentPiece.y() + curY) * SquareHeight + GameBoardBorder;
drawSquare(painter,x,y,currentPiece.shape());
}
}
```
* currentPiece为初始化的TetirxPiece
* value(x,y)函数用来取值如果为1则画方块,如果为0则不画。
* currentPiece.x()与currentPiece.y()分别表示模型中的左边的空列和顶部的空行,在该步骤中可以忽略。如果不减去的画,在移动时会发现无法将方块移动到边缘。
* curX、curY标示的是当前方块的位置,GameBoardBoarder常量主要是边框,在此步骤中,改值可以忽略掉。currentPiece.shape()只为选取颜色而存在。
* 写到此处就可以画出一个无法移动的完整的方块了。
##### 方块移动,方向变换。
1. 方块的移动,就是改变当前位置curX、curY并且调用update()->paintEvent()重新绘制。而方向的改变只是当前模型的变化。
2. ```C++
bool TetrixBoard::tryMove(const TetrixPiece &newPiece, int newX, int newY){
currentPiece = newPiece;
curX = newX;
curY = newY;
update();
return true;
}
```
* 在此步骤下该方法只需要这么多,相对于源码去除的地方只是边界的判断。
2. 事件处理
```C++
void TetrixBoard::keyPressEvent(QKeyEvent *event){
switch(event->key()){
case Qt::Key_Left:
tryMove(currentPiece,curX - 1,curY);
break;
case Qt::Key_Right:
tryMove(currentPiece,curX + 1,curY);
break;
case Qt::Key_Down:
if(!tryMove(currentPiece,curX,curY+1)){
pieceDroped();
}
break;
case Qt::Key_J:
tryMove(currentPiece.rotateLeft(),curX,curY);
break;
case Qt::Key_Up:
tryMove(currentPiece.rotateRight(),curX,curY);
break;
case Qt::Key_Space:
dropDown();
break;
default:
//事件传递
QFrame::keyPressEvent(event);
}
}
```
* currentPiece.rotateLeft(),currentPiece.rotateRight()这两个方法就是用来改变当前方块的方向
* 旋转的实现方式先设置好形状和方向,然后在三维数组中找到需要的二维数组模型。
##### 游戏边框,越界判断。
1. 对于边界判断
```C++
bool TetrixBoard::tryMove(const TetrixPiece &newPiece, int newX, int newY){
//是否越界
if((newPiece.getHeight() + newY) > BoardHeight){
return false;
}else if((newPiece.getWidth() + newX) > BoardWidth || newX < 0){
return false;
}else{
for(int i = 0; i < 4; i++){
for(int j = 0; j < 4; j++){
//如果模型中的值为零,则跳过
if(newPiece.value(i,j) == 0){
continue;
}
if(shapeAt(j - currentPiece.x() + newX , i - currentPiece.y() + newY) != NOShape){
return false;
}
}
}
}
currentPiece = newPiece;
curX = newX;
curY = newY;
update();
return true;
}
```
* BoardHeight、BoardWidth分别是整个游戏的行高和列宽。简单点说就是方块的可移动范围都是在TetrixShape coordsBoard[BoardWidth][BoardHeight];这样一个二维数组中变化。
* 在coordsBoard是否有值的判断
```C++
TetrixShape &shapeAt(int x, int y){return coordsBoard[x][y];}
```
* 该方法主要是根据x,y看coordsboard中是否有值,有值则表示无法移动,返回false
* 而此处调用时的计算,主要是由于currentPiece.x(),currentPiece.y()返回真实的模型位置。
##### 记录方块,满行判断。
* 在无法移动之后,则调用方法
```C++
void TetrixBoard::dropDown(){
int newY = curY;
while(newY < BoardHeight){
if(!tryMove(currentPiece,curX,curY+1)){
break;
}
++newY;
}
pieceDroped();
}
void TetrixBoard::pieceDroped(){
//无法移动,则根据当前位置将方块赋值到board
for(int i = 0; i < 4; i++){
for(int j = 0; j < 4; j++){
if(currentPiece.value(i,j) == 0){
continue;
}
shapeAt(j - currentPiece.x() + curX,i - currentPiece.y() + curY) = currentPiece.shape();
}
}
removeFullLines();
newPiece();
}
```
* dropDown的处理主要是用于瞬间落下的效果,按空格键可触发。
* pieceDroped则是将当前无法移动的每个小放宽赋值到界面数组中
* 得分计算
```C++
void TetrixBoard::removeFullLines(){
int numFullLines = 0;
for(int i = 0; i < BoardHeight; i++){
bool isFullLine = true;
for(int j = 0; j < BoardWidth; j++){
if(shapeAt(j,i) == NOShape){
isFullLine = false;
break;
}
}
if(isFullLine){
//消除当前的满行
++ numFullLines;
for(int j = 0; j < BoardWidth; j++){
shapeAt(j,i) = NOShape;
}
//将上一行的数据向下移动一行
for(int k = i; k > 0; k--){
for(int j = 0; j < BoardWidth; j++){
shapeAt(j,k) = shapeAt(j,k - 1);
}
}
}
}
//关卡分数计算
if(numFullLines > 0){
score += numFullLines;
emit scoreChanged(score);
if( score - (level - 1) * 25 >= 25){
++level;
timer.start(1000/level,this);
emit levelChanged(level);
}
update();
}
}
```
* 第一个外层的for循环是行,里面的是列,首先判断该行是否全部为空,如果不是则将其全部赋值为NOShape。
* 在消除后要将消除上方的方块全部往下移动一行。
* numFullLines只是记录当前消除的行数,根据numFullLines计算出分数,然后在每25分一关的记录。
* emit scoreChanged(score); emit levelChanged(level);是实用信号槽来改变界面上的值。
##### 界面布局,效果显示
* 在写完逻辑代码后,就需要有个主窗体了,这个可以随意使用QMainWindow或者QWidget。
* 省略了很多方法,但是主要的思路就是这些,主要是理解整个思路,然后按照自己的理解开发。
* 界面的逻辑比较单一,主要就是布局好,然后连上信号槽就Ok了。组件我都去除边框,并且固定了大小。希望一起交流。