Application Development in Visual C++ – The Tetris Application

0
146
19 min read

This application supports the single document interface, which implies that we have one document class object and one view class object. The other applications support the multiple document interface, they have one document class object and zero or more view class objects. The following screenshot depicts a classic example of the Tetris Application:

Application Development in Visual C++ - The Tetris Application

  • We start by generating the application’s skeleton code with The Application Wizard. The process is similar to the Ring application code.
  • There is a small class Square holding the position of one square and a class ColorGrid managing the game grid.
  • The document class manages the data of the game and handles the active (falling down) figure and the next (shown to the right of the game grid) figure.
  • The view class accepts input from the keyboard and draws the figures and the game grid.
  • The Figure class manages a single figure. It is responsible for movements and rotations.
  • There are seven kinds of figures. The Figure Info files store information pertaining to their colors and shapes.

The Tetris Files

We start by creating a MFC application with the name Tetris and follow the steps of the Ring application. The classes CTetrisApp, CMainFrame, CTetrisDoc, CTetrisView, and CAboutDlg are then created and added to the project.

There are only two differences. We need to state that we are dealing with a “Single Document Application Type”, that the file extension is “Trs” and that the file type long name is “A Game of Tetris”. Otherwise, we just accept the default settings. Note that in this application we accept the CView base class instead of the CScrollView like we did in the Ring application.

Application Development in Visual C++ - The Tetris Application

 

Application Development in Visual C++ - The Tetris Application

 

Application Development in Visual C++ - The Tetris Application

We add the marked lines below. In all other respects, we leave the file unmodified. We will not need to modify the files Tetris.h, MainFrm.h, MainFrm.cpp, StdAfx.h, StdAfx.cpp, Resource.h, and Tetris.rc.

#include
"stdafx.h"
#include "Square.h"
#include
"Figure.h"
#include "ColorGrid.h"
#include
"Tetris.h"
#include "MainFrm.h"
#include
"TetrisDoc.h"
#include "TetrisView.h"
//
...

The Color Grid Class

The ColorGrid handles the background game grid of twenty rows and twenty columns. Each square can have a color. At the beginning, every square is initialized to the default color white. The Index method is overloaded with a constant version that returns the color of the given square, and a non-constant version that returns a reference to the color. The latter version makes it possible to change the color of a square.

ColorGrid.h

class
Square
{
public:
Square();
Square(int iRow, int iCol);
int Row() const {return m_iRow;}
int Col() const {return m_iCol;}
private:
int m_iRow, m_iCol;
};

There are two Index methods, the second one is intended to be called on a constant object. Both methods check that the given row and position have valid values. The checks are, however, for debugging purposes only. The methods are always called with valid values. Do not forget to include the file StdAfx.h.

ColorGrid.cpp

const int ROWS = 20;
const int COLS = 10;
class
ColorGrid
{
public:
ColorGrid();
void Clear();
COLORREF&
Index(int iRow, int iCol);
const COLORREF Index(int iRow, int iCol)
const;
void Serialize(CArchive&archive);
private:
COLORREF m_buffer[ROWS * COLS];
};

The Document Class

CTetrisDoc is the document class of this application. When created, it overrides OnNewDocument and Serialize from its base class CDocument.

We add to the CTetrisDoc class a number of fields and methods. The field m_activeFigure is active figure, that is the one falling down during the game. The field m_nextFigure is the next figure, that is the one showed in the right part of the game view. They both are copies of the objects in the m_figureArray, which is an array figure object. There is one figure object of each kind (one figure of each color). The integer list m_scoreList holds the ten top list of the game. It is loaded from the file ScoreList.txt by the constructor and saved by the destructor. The integer field m_iScore holds the score of the current game. GetScore, GetScoreList, GetActiveFigure, GetNextFigure, and GetGrid are called by the view class in order to draw the game grid. They simply return the values of the corresponding fields.

The field m_colorGrid is an object of the class ColorGrid, which we defined in the previous section. It is actually just a matrix holding the colors of the squares of the game grid. Each square is intialized to the color white and a square is considered to be empty as long as it is white.

When the application starts, the constructor calls the C standard library function srand. The name is an abbreviation for sowing a random seed. By calling srand with an integer seed, it will generate a series of random number. In order to find a new seed every time the application starts, the C standard library function time is called, which returns the number of seconds elapsed since January 1, 1970. In order to obtain the actual random number, we call rand that returns a number in the interval from zero to the predefined constant RAND_MAX. The prototypes for these functions are defined in time.h (time) and stdlib.h (rand and srand), respectively.

#include
"StdAfx.h"
COLORREF& ColorGrid::Index(int iRow, int iCol)
{
check((iRow >= 0) && (iRow < ROWS));
check((iCol >= 0) && (iCol < COLS));
return m_buffer[iRow * COLS + iCol];
}
const COLORREF ColorGrid::Index(int iRow, int iCol)
const
{
check((iRow >= 0) && (iRow < ROWS));
check((iCol >= 0) && (iCol < COLS));
return m_buffer[iRow * COLS + iCol];
}

When the user presses the space key and the active figure falls down or when a row is filled and is flashed, we have to slow down the process in order for the user to apprehand the event. There is a Win32 API function Sleep that pauses the application for the given amount of milliseconds.

#include <time.h>
#include <stdlib.h>
time_t
time(time_t *pTimer);
void srand(unsigned int uSeed);
int
rand();

The user can control the horizontal movement and rotation of the falling figures by pressing the arrow keys. Left and right arrow keys move the figure to the left or right. The up and down arrow key rotates the figure clockwise or counter clockwise, respectively. Every time the user presses one of those keys, a message is sent to the view class object and caught by the method OnKeyDown, which in turn calls one of the methods LeftArrowKey, RightArrowKey, UpArrowKey, DownArrowKey to deal with the message. They all work in a similar fashion. They try to execute the movement or rotation in question. If it works, both the old and new area of the figure is repainted by making calls to UpdateAllViews.

The view class also handles a timer that sends a message every second the view is in focus. The message is caught by the view class method OnTimer that in turn calls Timer. It tries to move the active figure one step downwards. If that is possible, the area of the figure is repainted in the same way as in the methods above. However, if it is not possible, the squares of the figure are added to the game grid. The active figure is assigned to the next figure, and the next figure is assigned a copy of a randomly selected figure in m_figureArray. We also check whether any row has been filled. In that case, it will be removed and we will check to see if the game is over.

The user can speed up the game by pressing the space key. The message is caught and sent to SpaceKey. It simply calls OnTimer as many times as possible at intervals of twenty milliseconds in order to make the movement visible to the user.

When a figure has reached its end position and any full rows have been removed, the figure must be valid. That is, its squares are not allowed to occupy any already colored position. If it does, the game is over and GameOver is called. It starts by making the game grid gray and asks the users whether they want to play another game. If they do, the game grid is cleared and set back to colored mode and a new game starts. If they do not, the application exits.

NewGame informs the players whether they made to the top ten list and inquires about another game by displaying a message box. AddToScore examines whether the player has made to the ten top list. If so, the score is added to the list and the ranking is returned, if not, zero is returned.

DeleteFullRows traverses the game grid from top to bottom flashing and removing every full row. IsRowFull traverses the given row and returns true if no square has the default color (white). FlashRow flashes the row by showing it three times in grayscale and color at intervals of twenty milliseconds. DeleteRow removes the row by moving all rows above one step downwards and inserting an empty row (all white squares) at top.

The next figure and the current high score are painted at specific positions on the client area, the rectangle constants NEXT_AREA and SCORE_AREA keep track of those positions.

TetrisDoc.h

void Sleep(int iMilliSeconds);

The field m_figureArray holds seven figure objects, one of each color. When we need a new figure, we just randomly copy one of them.

TetrisDoc.cpp

typedef CList<int>
IntList;
const int FIGURE_ARRAY_SIZE = 7;
class CTetrisDoc :
public
CDocument
{
protected:
CTetrisDoc();
public:
virtual ~CTetrisDoc();
void SaveScoreList();
protected:
DECLARE_MESSAGE_MAP()
DECLARE_DYNCREATE(CTetrisDoc)
public:
virtual void Serialize(CArchive& archive);
int GetScore() const {return m_iScore;}
const IntList* GetScoreList() {return &m_scoreList;}
const ColorGrid* GetGrid() {return &m_colorGrid;}
const Figure& GetActiveFigure() const
{return m_activeFigure;}
const Figure& GetNextFigure() const {return m_nextFigure;}
public:
void LeftArrowKey();
void RightArrowKey();
void UpArrowKey();
void DownArrowKey();
BOOL Timer();
void SpaceKey();
private:
void GameOver();
BOOL NewGame();
int AddScoreToList();
void DeleteFullRows();
BOOL IsRowFull(int iRow);
void FlashRow(int iFlashRow);
void DeleteRow(int iDeleteRow);
private:
ColorGrid m_colorGrid;
Figure m_activeFigure, m_nextFigure;
int m_iScore;
IntList m_scoreList;
const CRect NEXT_AREA, SCORE_AREA;
static Figure m_figureArray[FIGURE_ARRAY_SIZE];
};

When the user presses the left arrow key, the view class object catches the message and calls LeftArrowKey in the document class object. We try to move the active figure one step to the left. It is not for sure that we succeed. The figure may already be located at the left part of the game grid. However, if the movement succeeds, the figure’s position is repainted and true is returned. In that case, we repaint the figure’s old and new graphic areas in order to repaint the figure. Finally, we set the modified flag since the figure has been moved. The method RightArrowKey works in a similar way.

Figure redFigure(NORTH, RED, RedInfo);
Figure brownFigure(EAST, BROWN, BrownInfo);
Figure turquoiseFigure(EAST, TURQUOISE, TurquoiseInfo);
Figure greenFigure(EAST, GREEN, GreenInfo);
Figure blueFigure(SOUTH, BLUE, BlueInfo);
Figure purpleFigure(SOUTH, PURPLE, PurpleInfo);
Figure yellowFigure(SOUTH, YELLOW, YellowInfo);
Figure CTetrisDoc::m_figureArray[] = {redFigure, brownFigure, turquoiseFigure,
greenFigure, yellowFigure, blueFigure, purpleFigure};

Timer is called every time the active figure is to moved one step downwards. That is,each second when the application has focus. If the downwards movement succeeds, then the figure is repainted in a way similar to LeftArrowKey above. However, if the movement does not succeed, the movement of the active figure has come to an end. We call AddToGrid to color the squares of the figure. Then we copy the next figure to the active figure and randomly copy a new next figure. The next figure is the one shown to the right of the game grid.

However, the case may occur that the game grid is full. That is the case if the new active figure is not valid, that is, the squares occupied by the figure are not free. If so, the game is over, and the user is asked whether he wants a new game.

void CTetrisDoc::LeftArrowKey()
{
CRect
rcOldArea = m_activeFigure.GetArea();
if (m_activeFigure.MoveLeft())
{
CRect
rcNewArea = m_activeFigure.GetArea();
UpdateAllViews(NULL, COLOR, (CObject*) &rcOldArea);
UpdateAllViews(NULL, COLOR, (CObject*) &rcNewArea);
SetModifiedFlag();
}
}

If the user presses the space key, the active figure falling will fall faster. The Timer method is called every 20 milliseconds.

 

BOOL
CTetrisDoc::Timer()
{
SetModifiedFlag();
CRect
rcOldArea = m_activeFigure.GetArea();
if (m_activeFigure.MoveDown())
{
CRect
rcNewArea = m_activeFigure.GetArea();
UpdateAllViews(NULL, COLOR, (CObject*) &rcOldArea);
UpdateAllViews(NULL, COLOR, (CObject*) &rcNewArea);
return
TRUE;
}
else
{
m_activeFigure.AddToGrid();
m_activeFigure = m_nextFigure;
CRect rcActiveArea = m_activeFigure.GetArea();
UpdateAllViews(NULL, COLOR, (CObject*) &rcActiveArea);
m_nextFigure = m_figureArray[rand() % FIGURE_ARRAY_SIZE];
UpdateAllViews(NULL, COLOR, (CObject*) &NEXT_AREA);
DeleteFullRows();
if (!m_activeFigure.IsFigureValid())
{
GameOver();
}
return
FALSE;
}
}

When the game is over, the users are asked whether they want a new game. If so, we clear the grid, randomly select the the next active and next figure, and repaint the whole client area.

void CTetrisDoc::SpaceKey()
{
while
(Timer())
{
Sleep(20);
}
}

Each time a figure is moved, one or more rows may be filled. We start by checking the top row and then go through the rows downwards. For each full row, we first flash it and then remove it.

void
CTetrisDoc::GameOver()
{
UpdateAllViews(NULL, GRAY);
if (NewGame())
{
m_colorGrid.Clear();
m_activeFigure = m_figureArray[rand() %FIGURE_ARRAY_SIZE];
m_nextFigure = m_figureArray[rand() % FIGURE_ARRAY_SIZE];
UpdateAllViews(NULL, COLOR);
else
{
SaveScoreList();
exit(0);
}
}

When a row is completely filled, it will flash before it is removed. The flash effect is executed by redrawing the row in color and in grayscale three times with an interval of 50 milliseconds.

void CTetrisDoc::DeleteFullRows()
{
int iRow = ROWS - 1;
while (iRow >= 0)
{
if
(IsRowFull(iRow))
{
FlashRow(iRow);
DeleteRow(iRow);
++m_iScore;
UpdateAllViews(NULL, COLOR, (CObject*) &SCORE_AREA);
}
else
{
--iRow;
}
}
}

When a row is removed, we do not really remove it. If we did, the game grid would shrink. Instead, we copy the squares above it and clear the top row.

void
CTetrisDoc::FlashRow(int iRow)
{
for (int iCount = 0; iCount < 3; ++iCount)
{
CRect rcRowArea(0, iRow, COLS, iRow + 1);
UpdateAllViews(NULL, GRAY, (CObject*) &rcRowArea);
Sleep(50);
CRect rcRowArea2(0, iRow, COLS, iRow + 1);
UpdateAllViews(NULL, COLOR, (CObject*) &rcRowArea2);
Sleep(50);
}
}

The View Class

CTetrisView is the view class of the application. It receives system messages and (completely or partly) redraws the client area.

The field m_iColorStatus holds the painting status of the view. Its status can be either color or grayscale. The color status is the normal mode, m_iColorStatus is initialized to color in the constructor. The grayscale is used to flash rows and to set the game grid in grayscale while asking the user for another game.

OnCreate is called after the view has been created but before it is shown. The field m_pTetrisDoc is set to point at the document class object. It is also confirmed to be valid. OnSize is called each time the size of the view is changed. It sets the global variables g_iRowHeight and g_iColWidth (defi ned in Figure.h), which are used by method of the Figure and ColorGrid classes to paint the squares of the figures and the grid.

OnSetFocus and OnKillFocus are called when the view receives and loses the input focus. Its task is to handle the timer. The idea is that the timer shall continue to send timer messages every second as long as the view has the input focus. Therefore, OnSetFocus sets the timer and OnKillFocus kills it. This arrangement implies that OnTimer is called each second the view has input focus.

In Windows, the timer cannot be turned off temporarily; instead, we have to set and kill it. The base class of the view, CWnd, has two methods: SetTimer that initializes a timer and KillTimer that stops the timer. The first parameter is a unique identifier to distinguish this particular timer from any other one. The second parameter gives the time interval of the timer, in milliseconds. When we send a null pointer as the third parameter, the timer message will be sent to the view and caught by OnTimer. KillTimer simply takes the identity of the timer to finish.

void CTetrisDoc::DeleteRow(int iMarkedRow)
{
for (int iRow = iMarkedRow; iRow > 0; --iRow)
{
for (int iCol = 0; iCol < COLS; ++iCol)
{
m_colorGrid.Index(iRow, iCol) = m_colorGrid.Index(iRow - 1, iCol);
}
}
for (int iCol = 0; iCol < COLS; ++iCol)
{
m_colorGrid.Index(0, iCol) = WHITE;
}
CRect rcArea(0, 0, COLS, iMarkedRow + 1);
UpdateAllViews(NULL, COLOR, (CObject*) &rcArea);
}

OnKeyDown is called every time the user presses a key on the keyboard. It analizes the pressed key and calls suitable methods in the document class if the left, right, up, or down arrow key or the space key is pressed.

When a method of the document class calls UpdateAllViews, OnUpdate of the view class object connected to the document object is called. As this is a single view application, the application has only one view object on which OnUpdate is called. UpdateAllViews takes two extra parameters, hints, which are sent to OnUpdate. The first hint tells us whether the next repainting shall be done in color or in grayscale, the second hint is a pointer to a rectangle holding the area that is to be repainted. If the pointer is not null, we calculate the area and repaint it. If it is null, the whole client area is repainted.

OnUpdate is also called by OnInitialUpdate of the base class CView with both hints set to zero. That is not a problem because the COLOR constant is set to zero. The effect of this call is that the whole view is painted in color.

OnUpdate calls UpdateWindow in CView that in turn calls OnPaint and OnDraw with a device context. OnPaint is also called by the system when the view (partly or completely) needs to be repainted. OnDraw loads the device context with a black pen and then draws the grid, the score list, and´the active and next figures.

TetrisView.h

UINT_PTR SetTimer(UINT_PTR iIDEvent, UINT iElapse, void (CALLBACK* lpfnTimer)
(HWND, UINT, UINT_PTR, DWORD));
BOOL KillTimer(UINT_PTR nIDEvent);

TetrisView.cpp

This application catches the messsages WM_CREATE, WM_SIZE, WM_SETFOCUS, WM_KILLFOCUS, WM_TIMER, and WM_KEYDOWN.

const int
TIMER_ID = 0;
enum {COLOR = 0, GRAY = 1};
class CTetrisDoc;
COLORREF GrayScale(COLORREF rfColor);
class CTetrisView : public CView
{
protected:
CTetrisView();
DECLARE_DYNCREATE(CTetrisView)
DECLARE_MESSAGE_MAP()
public:
afx_msg
int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnSize(UINT nType, int iClientWidth, int iClientHeight);
afx_msg void OnSetFocus(CWnd* pOldWnd);
afx_msg void OnKillFocus(CWnd* pNewWnd);
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
afx_msg void OnTimer(UINT nIDEvent);
void OnUpdate(CView* /* pSender */, LPARAM lHint, CObject* pHint);
void OnDraw(CDC* pDC);
private:
void DrawGrid(CDC* pDC);
void DrawScoreAndScoreList(CDC* pDC);
void DrawActiveAndNextFigure(CDC* pDC);
private:
CTetrisDoc* m_pTetrisDoc;
int m_iColorStatus;
};

When the view object is created, is connected to the document object by the pointer m_pTetrisDoc.

BEGIN_MESSAGE_MAP(CTetrisView, CView)
ON_WM_CREATE()
ON_WM_SIZE()
ON_WM_SETFOCUS()
ON_WM_KILLFOCUS()
ON_WM_TIMER()
ON_WM_KEYDOWN()
END_MESSAGE_MAP()

The game grid is dimensioned by the constants ROWS and COLS. Each time the user changes the size of the application window, the global variables g_iRowHeight and g_iColWidth, which are defined in Figure.h, store the height and width of one square in pixels.

int CTetrisView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
// We check that the view has been correctly created.
if (CView::OnCreate(lpCreateStruct) == -1)
{
return -1;
}
m_pTetrisDoc = (CTetrisDoc*) m_pDocument;
check(m_pTetrisDoc != NULL);
ASSERT_VALID(m_pTetrisDoc);
return 0;
}

OnUpdate is called by the system when the window needs to be (partly or completely) repainted. In that case, the parameter pHint is zero and the whole client area is repainted. However, this method is also indirectly called when the document class calls UpdateAllView. In that case, lHint has the value color or gray, depending on whether the client area shall be repainted in color or in a grayscale.

If pHint is non-zero, it stores the coordinates of the area to be repainted. The coordinates are given in grid coordinates that have to be translated into pixel coordinates before the area is invalidated.

The method first calls Invalidate or InvalidateRect to define the area to be repainted, then the call to UpdateWindow does the actual repainting by calling OnPaint in CView, which in turn calls OnDraw below.

void CTetrisView::OnSize(UINT /* uType */,int iClientWidth, int iClientHeight)
{
g_iRowHeight = iClientHeight / ROWS;
g_iColWidth = (iClientWidth / 2) / COLS;
}

OnDraw is called when the client area needs to be repainted, by the system or by UpdateWindow in OnUpdate. It draws a vertical line in the middle of the client area, and then draws the game grid, the high score list, and the current figures.

void
CTetrisView::OnUpdate(CView* /* pSender */, LPARAM lHint, CObject*pHint)
{
m_iColorStatus = (int) lHint;
if (pHint != NULL)
{
CRect rcArea = *(CRect*) pHint;
rcArea.left *= g_iColWidth;
rcArea.right *= g_iColWidth;
rcArea.top *= g_iRowHeight;
rcArea.bottom *= g_iRowHeight;
InvalidateRect(&rcArea);
}
else
{
Invalidate();
}
UpdateWindow();
}

DrawGrid traverses through the game grid and paints each non-white square. If a square is not occupied, it has the color white and it not painted. The field m_iColorStatus decides whether the game grid shall be painted in color or in grayscale.

void CTetrisView::OnDraw(CDC* pDC)
{
CPen pen(PS_SOLID, 0, BLACK);
CPen* pOldPen = pDC->SelectObject(&pen);
pDC->MoveTo(COLS * g_iColWidth, 0);
pDC->LineTo(COLS * g_iColWidth, ROWS * g_iRowHeight);
DrawGrid(pDC);
DrawScoreAndScoreList(pDC);
DrawActiveAndNextFigure(pDC);
pDC->SelectObject(&pOldPen);
}

GrayScale returns the grayscale of the given color, which is obtained by mixing the average of the red, blue, and green component of the color.

void
CTetrisView::DrawGrid(CDC* pDC)
{
const ColorGrid* pGrid = m_pTetrisDoc->GetGrid();
for (int iRow = 0; iRow < ROWS; ++iRow)
{
for (int iCol = 0; iCol < COLS; ++iCol)
{
COLORREF rfColor = pGrid->Index(iRow, iCol);
if (rfColor != WHITE)
{
CBrush
brush((m_iColorStatus == COLOR) ? rfColor:GrayScale(rfColor));
CBrush* pOldBrush = pDC->SelectObject(&brush);
DrawSquare(iRow, iCol, pDC);
pDC->SelectObject(pOldBrush);
}
}
}
}

The active figure (m_activeFigure) is the figure falling down on the game grid.The next figure (m_nextFigure) is the figure announced at the right side of the client area. In order for it to be painted at the right-hand side, we alter the origin to the middle of the client area, and one row under the upper border by calling SetWindowOrg.

LEAVE A REPLY

Please enter your comment!
Please enter your name here