### 俄罗斯方块

俄罗斯方块对于我们来说，应该算得上是一个家喻户晓的游戏了。它可以锻炼反应能力，也可以提高思维能力。笔者小时候就曾为该游戏所痴迷，梦想长大后也设计一个类似的游戏。如今选择了程序员的职业，自然可以实现这个“梦想”了。下图就是笔者设计的俄罗斯方块。

图 1 俄罗斯方块

在设计俄罗斯方块时，首先需要设计游戏的表格，游戏中的表格由单元格构成。为此，首先需要定义单元格。

class CCell

{

public:

int m_nRowIndex; //单元格行索引

int m_nColIndex; //单元格列索引

BOOL m_bUsed; //是否被使用

BOOL m_bFixed; //是否被固定

CCell()

{

m_nRowIndex = 0;

m_nColIndex = 0;

m_bUsed = FALSE;

m_bFixed = FALSE;

}

};

在单元格中描述了所在表格的行和列索引，单元格是否被使用，单元格是否被固定。所谓单元格被使用是指当一个图像向下移动时，图像所占用的单元格被使用。而单元格被固定是指当图像移动到表格的底部，不能再向下移动了，此时图像所占用的单元格处于固定状态。

有了单元格的描述，就可以设计表格了。

class CGrid

{

public:

int m_nRowCount; //行数

int m_nColCount; //列数

int m_nCellWidth; //单元格宽度

int m_nCellHeight; //单元格高度

CRect m_GridRC; //表格区域

CBitmap m_Bmp; //单元格位图

CBitmap m_BkBmp; //表格背景位图

CCell m_CellGrid[ROW_COUNT][COL_COUNT]; //单元格几集合

public:

BOOL IsAllowChangeType(CPiece& refPiece); //是否允许变形

BOOL IsAllowHorMove(CPiece& refPiece, BOOL bMoveLeft = TRUE); //是否允许水品移动

BOOL GetBottomRowIndex(int nColumn, int nSize, CPiece& refPiece);

CGrid(CRect rc)

{

m_GridRC = rc;

m_nRowCount = ROW_COUNT;

m_nColCount = COL_COUNT;

m_nCellHeight = m_GridRC.Height() / (float)m_nRowCount + 1 ; //单元格高度

m_nCellWidth = m_GridRC.Width() / (float)m_nColCount + 1; //单元格宽度

m_Bmp.LoadBitmap(IDB_CELLBMP);

m_BkBmp.LoadBitmap(IDB_BKBMP);

InitCellGrid();

}

//绘制表格

void DrawGrid(CDC* pDC)

{

//绘制单元格

for (int m=0; m<m_nRowCount; m++)

{

for(int n=0; n<m_nColCount; n++)

{

//计算单元格区域

CRect cellRC;

cellRC.left = m_GridRC.left + n*m_nCellWidth;

cellRC.right = cellRC.left + m_nCellWidth;

cellRC.top = m_GridRC.top + m*m_nCellHeight;

cellRC.bottom = cellRC.top + m_nCellHeight;

CDC memDC;

memDC.CreateCompatibleDC(pDC);

if (m_CellGrid[m][n].m_bUsed == TRUE || m_CellGrid[m][n].m_bFixed == TRUE)

{

//使用位图填充单元格

memDC.SelectObject(&m_Bmp);

pDC->BitBlt(cellRC.left, cellRC.top, cellRC.Width(), cellRC.Height(),

&memDC, 0, 0, SRCCOPY);

}

else

{

//使用位图填充单元格

memDC.SelectObject(&m_BkBmp);

pDC->BitBlt(cellRC.left, cellRC.top, cellRC.Width(), cellRC.Height(),

&memDC, 0, 0, SRCCOPY);

}

memDC.DeleteDC();

}

//绘制边框

CRect clientRC;

clientRC.left = m_GridRC.left;

clientRC.top = m_GridRC.top;

clientRC.right = clientRC.left + m_nCellWidth*(m_nColCount);

clientRC.bottom = clientRC.top + m_nCellHeight*(m_nRowCount);

pDC->FrameRect(clientRC, &CBrush(RGB(0, 0, 255)));

}

}

//初始化单元格

void InitCellGrid()

{

for(int i=0; i<ROW_COUNT; i++)

{

for (int j=0; j<COL_COUNT; j++)

{

m_CellGrid[i][j].m_nRowIndex = i;

m_CellGrid[i][j].m_nColIndex = j;

m_CellGrid[i][j].m_bUsed = FALSE;

m_CellGrid[i][j].m_bFixed = FALSE;

}

}

}

};

在表格部分包含了表格的行数和列数，表格的客户区域，表格使用的单元格集合。表格还提供了一个非常关键的 DrawGrid 方法，用于绘制表格。绘制的方法是依据遍历表格中的单元格，如果单元格被使用或者被固定，则在该单元格处绘制单元格的位图，否则就绘制默认的背景位图。这样就实现了游戏表格的设计。

接下来还需要设计游戏中的方块。在俄罗斯方块中共有 7 种基本形状，如图 2 所示。

图 2 俄罗斯方块图像

上图中的数字表示的是方块的类型。按照游戏规则，这些基本类型可以按 90°旋转，这样又可以变换出 12 种新的类型，如图 3 所示。

图 3 俄罗斯方块变换图像

为了描述俄罗斯方块图像，笔者定义一个 CPiece 类。

class CPiece

{

public:

int m_nLeftIndex; //图像左边索引

int m_nTopIndex; //图像顶部索引

int m_nType; //类型,对应于

int m_nRowCount; //图像占用行数

int m_nWidth; //图像的宽度，单位为一个单元格

CPoint m_ImagesPT[4]; //图像的构成，表示方块使用的 4 个单元格

BOOL m_bMoving; //移动过程中

//…代码省略

};

在 CPiece 类中，有一个关键的成员 m_ImagesPT，该成员是一个数组，其中包含了 4 个 CPoint 对象。由于罗斯方块图像都占有 4 个单元格，因此使用 4 个 CPoint 对象描述每一个单元格坐标，这里的坐标是单元格的行和列索引。当游戏产生一个新的方块图像时，图像最左边的位置由 m_nLeftIndex 成员表示，顶部位置由 m_nTopIndex 成员表示。依据这两个成员和当前图像的类型，我们可以确定 m_ImagesPT 成员的构成。以图像类型 1 为例，图像由 4 个水平连续排列的单元格构成，那么 m_ImagesPT 成员应为：

for(int i=0; i<4; i++)

{

m_ImagesPT[i].x = m_nLeftIndex + i;

m_ImagesPT[i].y = m_nTopIndex;

}

这样，当游戏中显示方块时，将遍历 CPiece 的 m_ImagesPT 成员，读取每个元素表示的单元格坐标，通过该坐标来定位表格中的单元格 CCell，将单元格 CCell 的 m_bUsed 成员设置为 TRUE，然后调用表格 CGrid 的 DrawGrid 来绘制表格，使得方块显示在表格中。

//显示图像

for(int i=0; i<4; i++)

{

int nX = m_Piece.m_ImagesPT[i].x;

int nY = m_Piece.m_ImagesPT[i].y;

m_pGrid->m_CellGrid[nY][nX].m_bUsed = TRUE;

}

在游戏进行中，默认时方块会向下移动，用户也可以使用左右方向键来移动图像。为了能够移动图像，需要设置 CPiece 类的 m_nLeftIndex 成员和 m_nTopIndex 成员，然后重新设置 CPiece 类的 m_ImagesPT 成员，最后绘制表格就可以实现移动图像的效果了。我们将这些功能封装在 MovePiece 方法中。

void CPiece::MovePiece(int nHor, int nVer)

{

if (nHor >= 0 && nHor <= COL_COUNT-m_nWidth)

{

m_nLeftIndex = nHor; //设置左边横坐标

}

m_nTopIndex = nVer; //设置顶部纵坐标

SetPieceType(m_nType); //更新 m_ImagesPT 成员

}

在上面的代码中注意 SetPieceType 方法，该方法的作用是根据方块的不同类型设置相应的 m_ImagesPT 成员数据。

void SetPieceType(int nType)

{

m_nType = nType;

if (nType == 1) //根据不同的方块类型设置相应的行数和列数

{

m_nRowCount = 1; //设置行数,“….”形式占一行

m_nWidth = 4; //设置列数,“….”形式占 4 列

}

else if (nType == 2)

{

m_nRowCount = 2;

m_nWidth = 2;

}

//…代码省略

switch(m_nType) //根据方块的不同类型，以及 m_nLeftIndex 和 m_nTopIndex 成员设置 m_ImagesPT

{

case 1:

{

//****

for(int i=0; i<4; i++)

{

m_ImagesPT[i].x = m_nLeftIndex + i;

m_ImagesPT[i].y = m_nTopIndex;

}

break;

}

//**

//**

case 2:

{

for(int i=0; i<4; i++)

{

if (i / 2 < 1)

{

m_ImagesPT[i].x = m_nLeftIndex + i;

m_ImagesPT[i].y = m_nTopIndex;

}

else

{

m_ImagesPT[i].x = m_nLeftIndex + i-2;

m_ImagesPT[i].y = m_nTopIndex + 1;

}

}

break;

}

//…代码省略

}

}

};

在方块显示过程中，用户可以实现对方块变形。实际上，根据上面的介绍，可以轻而易举地实现实现该功能。因为我们只要根据当期的方块类型确定变形后的方块类型，就可以了，因为直接调用 SetPieceType 方法可以确定一个方块的类型。笔者将实现方块变形的功能封装在 CPiece 类的 GetChangedType 方法中。

int CPiece::GetChangedType(int nSrcType)

{

if (nSrcType == 1) //根据当前的图像来确定变形后的图像类型

{

return 11;

}

else if (nSrcType == 11)

{

return 1;

}

else if (nSrcType == 3)

{

return 33;

}

else if (nSrcType == 33)

{

return 34;

}

else if (nSrcType == 34)

{

return 35;

}

else if (nSrcType == 35)

{

return 3;

}

else if (nSrcType == 4)

{

return 44;

}

else if (nSrcType == 44)

{

return 45;

}

else if (nSrcType == 45)

{

return 46;

}

else if (nSrcType == 46)

{

return 4;

}

else if (nSrcType == 5)

{

return 55;

}

else if (nSrcType >= 55 && nSrcType <= 57 )

{

if(nSrcType == 57)

return 5;

else

return ++nSrcType;

}

else if (nSrcType == 6)

{

return 66;

}

else if (nSrcType == 66)

{

return 6;

}

else if (nSrcType == 7)

{

return 77;

}

else if (nSrcType == 77)

{

return 7;

}

else if (nSrcType == 2) //田字形状的方块不允许变形

{

return 2;

}

}

（1）创建一个基于对话框的工程，工程名称为“RussianGrid”。

（2）向对话框中添加图片、按钮、静态文本和群组框的控件，如图 4 所示。

图 4 对话框资源窗口

（3）向对话框中添加如下成员变量。

CGrid* m_pGrid; //表格对象

CPiece m_Piece; //方块图像

BOOL m_bKeyDown; //是否按键

int m_nKeyDown; //按键的次数

int m_nGrade; //记录游戏分数

int m_nCurPiece;

int m_nNextPiece;

int m_nLevel; //游戏级别

BOOL m_bGameRunning; //游戏进行中

BOOL m_bPaused; //游戏是否暂停

（4）向对话框中添加 BeginNewPiece 方法，产生一个新的方块图像。

void CRussianGridDlg::BeginNewPiece()

{

m_nNextPiece = GetRandomPiece(); //随机产生一个方块图像

CBitmap bmp;

bmp.LoadBitmap(m_nNextPiece); //加载位图

m_NextPiece.SetBitmap(bmp); //显示下一个方块图像

bmp.DeleteObject();

m_Piece.InitPiece(); //初始化方块

m_Piece.SetPieceType(m_nCurPiece); //设置方块类型

m_nCurPiece = m_nNextPiece;

SetTimer(1, m_nLevel, NULL); //开始一个计时器

}

（5）向对话框中添加 ChangePieceType 方法，对方块进行变形。

void CRussianGridDlg::ChangePieceType(int nSrcType)

{

if (m_Piece.m_bMoving==FALSE) //方块是否移动

{

return ;

}

//获取表格中当前图像占用的列对应的底部行索引

BOOL bRet = m_pGrid->GetBottomRowIndex(m_Piece.m_nLeftIndex, m_Piece.m_nWidth, m_Piece);

if (bRet)

{

return;

}

if (!m_pGrid->IsAllowChangeType(m_Piece)) //图像是否允许变形

{

return;

}

m_Piece.ChangePieceType(); //进行图像变形

HorMovePiece(0); //更新图像

}

（6）向对话框中添加 GameOver 方法，判断游戏是否结束。

BOOL CRussianGridDlg::GameOver()

{

for(int j=0; j<m_pGrid->m_nColCount; j++)

{

if (m_pGrid->m_CellGrid[0][j].m_bFixed) //判断最顶行是否被占用

return TRUE;

}

return FALSE;

}

（7）向对话框中添加 HorMovePiece 方法，当用户按左右方向键时左右移动方块图像。

void CRussianGridDlg::HorMovePiece(int nOffset)

{

if (m_Piece.m_bMoving == FALSE) //方块是否处于移动过程中

{

return;

}

BOOL bLeftMove = (nOffset < 1)? TRUE: FALSE; //防止左右穿透图像

if (!m_pGrid->IsAllowHorMove(m_Piece, bLeftMove))

{

m_bKeyDown = FALSE; //如果不能左右移动，则在 OnTimer 中继续向下移动

return;

}

m_Piece.MovePiece(m_Piece.m_nLeftIndex + nOffset , m_Piece.m_nTopIndex);

for(int i=0; i<4; i++) //显示图像

{

int nX = m_Piece.m_ImagesPT[i].x;

int nY = m_Piece.m_ImagesPT[i].y;

m_pGrid->m_CellGrid[nY][nX].m_bUsed = TRUE;

}

m_pGrid->DrawGrid(GetDC()); //绘制表格

for( i=0; i<4; i++) //在绘制下一次图像时使之前的图像消失

{

int nX = m_Piece.m_ImagesPT[i].x;

int nY = m_Piece.m_ImagesPT[i].y;

m_pGrid->m_CellGrid[nY][nX].m_bUsed = FALSE;

}

//获取当前方块所在列对应的底部行索引

BOOL bRet = m_pGrid->GetBottomRowIndex(m_Piece.m_nLeftIndex, m_Piece.m_nWidth, m_Piece);

if (bRet)

{

for( i=0; i<4; i++)

{

int nX = m_Piece.m_ImagesPT[i].x;

int nY = m_Piece.m_ImagesPT[i].y;

m_pGrid->m_CellGrid[nY][nX].m_bUsed = FALSE;

if (m_nKeyDown > 3)

{

m_pGrid->m_CellGrid[nY][nX].m_bFixed = TRUE;

m_Piece.m_bMoving = FALSE;

GradeHandle(m_Piece); //计算成绩

if (GameOver()) //判断游戏是否结束

{

m_bGameRunning = FALSE;

MessageBox("游戏结束!");

}

}

}

}

}

（8）处理“开始游戏”按钮的单击事件，开始新的游戏。

void CRussianGridDlg::OnBeginGame()

{

m_pGrid->InitCellGrid(); //初始化表格

m_nCurPiece = GetRandomPiece(); //产生随机方块

m_Grade.SetWindowText("分数： 0");

m_nGrade = 0;

BeginNewPiece(); //开始新的方块

m_bGameRunning = TRUE;

}

（9）处理对话框的“WM_TIMER”消息，当产生一个新的方块图像，在“WM_TIMER”消息处理函数中不断地调整方块的纵坐标，使其向下移动。

void CRussianGridDlg::OnTimer(UINT nIDEvent)

{

if (m_bKeyDown && m_nKeyDown < 3)

{

return;

}

if (!m_Piece.m_bMoving) //图像已经移动到底部

{

KillTimer(1);

BeginNewPiece();

return;

}

for(int i=0; i<4; i++) //显示图像

{

int nX = m_Piece.m_ImagesPT[i].x;

int nY = m_Piece.m_ImagesPT[i].y;

m_pGrid->m_CellGrid[nY][nX].m_bUsed = TRUE;

}

m_pGrid->DrawGrid(GetDC()); //绘制表格

for( i=0; i<4; i++) //在绘制下一次图像时使之前的图像消失

{

int nX = m_Piece.m_ImagesPT[i].x;

int nY = m_Piece.m_ImagesPT[i].y;

m_pGrid->m_CellGrid[nY][nX].m_bUsed = FALSE;

}

//获取表格中底部的某些列的图像高度

BOOL bRet = m_pGrid->GetBottomRowIndex(m_Piece.m_nLeftIndex, m_Piece.m_nWidth, m_Piece);

if (!bRet) //如果当前图像没有达到所在列的已有图像的高度，继续向下移动图像

{

m_Piece.SetTopIndex(m_Piece.m_nTopIndex + 1);

}

else //图像移动到结尾，在表格底部固定图像

{

for( i=0; i<4; i++)

{

int nX = m_Piece.m_ImagesPT[i].x;

int nY = m_Piece.m_ImagesPT[i].y;

m_pGrid->m_CellGrid[nY][nX].m_bUsed = FALSE;

m_pGrid->m_CellGrid[nY][nX].m_bFixed = TRUE;

}

m_Piece.m_bMoving = FALSE;

KillTimer(1); //结束计时器

GradeHandle(m_Piece); //计算成绩

if (GameOver()) //判断游戏是否结束

{

m_bGameRunning = FALSE;

MessageBox("游戏结束");

}

else

{

BeginNewPiece(); //显示下一个方块

}

}

CDialog::OnTimer(nIDEvent);

}

（10）改写对话框的 PreTranslateMessage 虚方法，如果用户按左右方向键，则移动方块图像，如果用户按上方向键，则对方块进行变形，如果用户按下方向键，则加速。

BOOL CRussianGridDlg::PreTranslateMessage(MSG* pMsg)

{

if (pMsg->message == WM_KEYDOWN )

{

m_bKeyDown = FALSE;

if (pMsg->wParam == VK_LEFT)

{

m_bKeyDown = TRUE;

m_nKeyDown ++;

HorMovePiece(-1); //向左移动

}

else if (pMsg->wParam == VK_RIGHT)

{

m_bKeyDown = TRUE;

m_nKeyDown ++;

HorMovePiece(1); //向右移动

}

else if (pMsg->wParam == VK_UP)

{

ChangePieceType(m_Piece.m_nType); //变形

}

else if (pMsg->wParam == VK_DOWN)

{

Accelerate(); //加速

}

return TRUE;

}

else if (pMsg->message == WM_KEYUP)

{

m_bKeyDown = FALSE;

m_nKeyDown = 0;

}

return CDialog::PreTranslateMessage(pMsg);

}