Tetris tutorial in C++ platform independent focused in game logic for beginners
We are going to learn how to create a Tetris clone from scratch using simple and clean C++. And this will take you less than an hour! This is the perfect tutorial for beginners. Just enjoy it and leave a comment if you want me to explain something better. I know my English sucks, so if you see some mistakes, please, tell me. Let’s go!
Updated! 03/04/2012
Download sourcecode
Here it is the complete sourcecode.
Windows platforms
The sourcecode comes with SDL includes and libs ready to compile in Visual C++ Express Edition 2008. In “Release” folder there is also an executable file just in case you want to try it directly.
Other platforms
Thanks to lmelior and to Javier Santana, there is a Linux version of this tutorial. The sourcecode is platform independent and comes with a “makefile”. However, under Linux, you need libsdl-gfx1.2-dev and libsdl1.2-dev (If you are using Ubuntu you can get them this way: sudo apt-get install libsdl1.2-dev libsdl-gfx1.2-dev)
Keys
ESC | Quit the game |
z | Rotate piece |
x | Drop piece |
Left, Right, Down | I will not offend your intelligence |
Step 0: Introduction
We are going to focus on the game logic, using only rectangle primitives (SDL) for the rendering. All the game logic is isolated from the drawing, so you can expand the tutorial easily. I’m planning making a second tutorial of how to improve this Tetris clone using sprites, background, effects, etc. But right now, let’s focus on the game logic. This is how your prototype will look after you finish the tutorial:
In this tutorial you will learn:
- How to store the pieces and board using matrices (multidimensional arrays).
- How to solve the rotation problem in Tetris, in a really easy way, without using complex maths or anything difficult, just using an intelligent hack.
- How to check collisions between the pieces and the board.
- How the main loop of a Tetris game works.
What you are supposed to already know:
- C++
- A little bit of graphical programming if you want expand the tutorial with improved graphics. Don’t worry about that if you just want to learn the Tetris game logic.
What do you need?
- A compiler or programming IDE. I’ve used Visual C++ Express Edition for this tutorial, that is a free C++ IDE. But you can use the one of your choice, of course.
- Desire to learn
What is the license of the sourcecode?
The sourcecode is under the “Creative Commons – Attribution 3.0 Unported”. That means you can copy, distribute and transmit the work and to adapt it. But you must attribute the work (but not in any way that suggests that they endorse you or your use of the work). The manner of attribution is up to you. You can just mention me (Javier López). A backlink would be also appreciated.
Step 1: The pieces
First, we are going to create a class for storing all the pieces. There are 7 different types of pieces: square, I, L, L-mirrored, N, N-mirrored and T. But, how can we define each piece? Just check out the figure:
As you can see, this piece is defined in a matrix of 5×5 cells. 0 means “no block”, 1 means “normal block” and 2 means “pivot block”. The pivot block is the rotation point: yes, the original Tetris game has a rotation point for each piece
And how can we store that using C++? Easy: using a bidimensional array of 5×5 ints (or bytes, if you are a fanatic of optimization). The previous piece is stored like that:
1
2
3
4
5
|
{0, 0, 0, 0, 0}, {0, 0, 0, 1, 0}, {0, 0, 2, 1, 0}, {0, 0, 1, 0, 0}, {0, 0, 0, 0, 0} |
Now that we already now how to store each piece let’s think about rotations. We can solve the rotation problem in a lot of different ways. In other tutorials, I’ve seen them use complex rotation algebra in order to rotate the piece… but we can solve this problem easily. If we can store each piece… why don’t we just store each piece rotated too? There are four possible rotations for each piece:
As you can see, the longer piece is only 4 block widht. But we are using 5 blocks matrices in order to be able to store all the rotations respeting the pivot block. In a previous version of this tutorial, I was using 4-block matrices, but then it was necessary to store translations of the pivot to the origin. This way, we are using some bytes more but the sourcecode is cleaner. In total we only use 448 bytes to store all the pieces. That’s nothing
So, in order to store all this information we need a 4-dimensional array (wow!), in order to store the 4 possible rotations (matrices of 5×5) of each piece:
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
// Pieces definition char
mPieces [7 /*kind */
][4 /* rotation */
][5 /* horizontal blocks */
][5 /* vertical blocks */
] =
{ // Square {
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
}
},
// I {
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 1},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{1, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
}
}
,
// L {
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 1, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 1, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
},
// L mirrored {
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 1, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 1, 0},
{0, 0, 0, 0, 0}
}
},
// N {
{
{0, 0, 0, 0, 0},
{0, 0, 0, 1, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 0, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 1, 2, 0, 0},
{0, 1, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
},
// N mirrored {
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 0, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 1, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 0, 0, 0},
{0, 1, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 1, 0},
{0, 1, 2, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
},
// T {
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 1, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
}
}; |
Great! Now, in order to rotate a piece we just have to choose the following stored rotated piece.
There is something important that we have to take in count. Each different piece must be correctly positioned every time it is created on the top of the screen. In other words, it needs to be translated to the correct position (in order to show ONLY one row of blocks in the board and to be centered, upper blocks should be OUTSIDE the board). Like each piece is different (some are lower or smaller than others in the matrices), each one needs a different translation every time it is created. We will store these translations in another array, one translation per rotated piece. Take your time to understand this.
The translation are two numbers (horizontal tranlastion, vertical translation) that we have to store for each piece. We will use these numbers later in “Game” class when creating the pieces each time a new piece appears, so it will be initialized in the correct position. This is the array that stores these displacements:
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
51
52
53
|
// Displacement of the piece to the position where it is first drawn in the board when it is created int
mPiecesInitialPosition [7 /*kind */
][4 /* r2otation */
][2 /* position */ ] =
{ /* Square */ {
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -3}
},
/* I */ {
{-2, -2},
{-2, -3},
{-2, -2},
{-2, -3}
},
/* L */ {
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
/* L mirrored */ {
{-2, -3},
{-2, -2},
{-2, -3},
{-2, -3}
},
/* N */ {
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
/* N mirrored */ {
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
/* T */ {
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
}; |
And with that we have solved one of the most tricky parts of this tutorial.
We can now create our Pieces class, this file is called “Pieces.h”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#ifndef _PIECES_ #define _PIECES_ // -------------------------------------------------------------------------------- // Pieces // -------------------------------------------------------------------------------- class
Pieces
{ public :
int
GetBlockType ( int
pPiece, int
pRotation, int
pX, int pY);
int
GetXInitialPosition ( int
pPiece, int
pRotation);
int
GetYInitialPosition ( int
pPiece, int
pRotation);
}; #endif // _PIECES_ |
The 3 methods that you can see in the header returns some information that we will need later. Their implementation is trivial:
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
|
/* ======================================
Return the type of a block (0 = no-block, 1 = normal block, 2 = pivot block) Parameters: >> pPiece: Piece to draw >> pRotation: 1 of the 4 possible rotations >> pX: Horizontal position in blocks >> pY: Vertical position in blocks ======================================
*/ int
Pieces::GetBlockType ( int
pPiece, int pRotation,
int pX,
int pY)
{ return
mPieces [pPiece][pRotation][pX][pY];
} /* ======================================
Returns the horizontal displacement of the piece that has to be applied in order to create it in the correct position. Parameters: >> pPiece: Piece to draw >> pRotation: 1 of the 4 possible rotations ======================================
*/ int
Pieces::GetXInitialPosition ( int
pPiece, int pRotation)
{ return
mPiecesInitialPosition [pPiece][pRotation][0];
} /* ======================================
Returns the vertical displacement of the piece that has to be applied in order to create it in the correct position. Parameters: >> pPiece: Piece to draw >> pRotation: 1 of the 4 possible rotations ======================================
*/ int
Pieces::GetYInitialPosition ( int
pPiece, int pRotation)
{ return
mPiecesInitialPosition [pPiece][pRotation][1];
} |
Step 2: The board
Now we are going to learn how to store the pieces in the board and check collisions. This class stores a bidimensional array of N x N blocks that are initialized to POS_FREE. The pieces will be stored by filling these blocks when they fall down updating the block to POS_FILLED. In this class we need to implement methods in order to store a piece, check if a movement is possible, delete lines, etc. Our board is going to be very flexible, we will be able to choose the amount of horizontal and vertical blocks and the size of each block.
This is the header of the class (“Board.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
#ifndef _BOARD_ #define _BOARD_ // ------ Includes ----- #include "Pieces.h" // ------ Defines ----- #define BOARD_LINE_WIDTH 6 // Width of each of the two lines that delimit the board #define BLOCK_SIZE 16 // Width and Height of each block of a piece #define BOARD_POSITION 320 // Center position of the board from the left of the screen #define BOARD_WIDTH 10 // Board width in blocks
#define BOARD_HEIGHT 20 // Board height in blocks #define MIN_VERTICAL_MARGIN 20 // Minimum vertical margin for the board limit
#define MIN_HORIZONTAL_MARGIN 20 // Minimum horizontal margin for the board limit #define PIECE_BLOCKS 5 // Number of horizontal and vertical blocks of a matrix piece // -------------------------------------------------------------------------------- // Board // -------------------------------------------------------------------------------- class
Board
{ public :
Board (Pieces *pPieces,
int pScreenHeight);
int
GetXPosInPixels ( int
pPos);
int
GetYPosInPixels ( int
pPos);
bool
IsFreeBlock ( int
pX, int
pY);
bool
IsPossibleMovement ( int
pX, int
pY, int pPiece,
int pRotation);
void
StorePiece ( int
pX, int
pY, int pPiece,
int pRotation);
void
DeletePossibleLines ();
bool
IsGameOver ();
private :
enum
{ POS_FREE, POS_FILLED }; // POS_FREE = free position of the board; POS_FILLED = filled position of the board
int
mBoard [BOARD_WIDTH][BOARD_HEIGHT]; // Board that contains the pieces
Pieces *mPieces;
int
mScreenHeight;
void
InitBoard();
void
DeleteLine ( int
pY);
}; #endif // _BOARD_ |
Now, let’s see each different method.
InitBoard method is just a nested loop that initializes all the board blocks to POS_FREE.
1
2
3
4
5
6
7
8
9
10
11
|
/* ======================================
Init the board blocks with free positions ======================================
*/ void
Board::InitBoard()
{ for
( int
i = 0; i < BOARD_WIDTH; i++)
for
( int
j = 0; j < BOARD_HEIGHT; j++)
mBoard[i][j] = POS_FREE;
} |
StorePiece method, just stores a piece in the board by filling the appropriate blocks as POS_FILLED. There is a nested loop that iterates through the piece matrix and store the blocks in the board.
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
|
/* ======================================
Store a piece in the board by filling the blocks Parameters: >> pX: Horizontal position in blocks >> pY: Vertical position in blocks >> pPiece: Piece to draw >> pRotation: 1 of the 4 possible rotations ======================================
*/ void
Board::StorePiece ( int
pX, int
pY, int pPiece,
int pRotation)
{ // Store each block of the piece into the board
for
( int
i1 = pX, i2 = 0; i1 < pX + PIECE_BLOCKS; i1++, i2++)
{
for
( int
j1 = pY, j2 = 0; j1 < pY + PIECE_BLOCKS; j1++, j2++)
{
// Store only the blocks of the piece that are not holes
if
(mPieces->GetBlockType (pPiece, pRotation, j2, i2) != 0)
mBoard[i1][j1] = POS_FILLED;
}
}
} |
IsGameOver checks if there are blocks in the first row. That means the game is over.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* ======================================
Check if the game is over becase a piece have achived the upper position Returns true or false ======================================
*/ bool
Board::IsGameOver()
{ //If the first line has blocks, then, game over
for
( int
i = 0; i < BOARD_WIDTH; i++)
{
if
(mBoard[i][0] == POS_FILLED) return
true ;
}
return
false ;
} |
DeleteLine is the method that erases a line and moves all the blocks of upper positions one row down. It just starts from the line that has to be removed, and then, iterating through the board in a nested loop, moves all the blocks of the upper lines one row done.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/* ======================================
Delete a line of the board by moving all above lines down Parameters: >> pY: Vertical position in blocks of the line to delete ======================================
*/ void
Board::DeleteLine ( int
pY)
{ // Moves all the upper lines one row down
for
( int
j = pY; j > 0; j--)
{
for
( int
i = 0; i < BOARD_WIDTH; i++)
{
mBoard[i][j] = mBoard[i][j-1];
}
}
} |
DeletePossibleLines is a method that removes all the lines that should be erased from the board. It works by first checking which lines should be removed (the ones that have all their horizontal blocks filled). Then, it uses the DeleteLine method in order to erase that line and move all the upper lines one row down.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/* ======================================
Delete all the lines that should be removed ======================================
*/ void
Board::DeletePossibleLines ()
{ for
( int
j = 0; j < BOARD_HEIGHT; j++)
{
int
i = 0;
while
(i < BOARD_WIDTH)
{
if
(mBoard[i][j] != POS_FILLED) break ;
i++;
}
if
(i == BOARD_WIDTH) DeleteLine (j);
}
} |
IsFreeBlock is a trivial method that checks out if a board block is filled or not.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/* ======================================
Returns 1 (true) if the this block of the board is empty, 0 if it is filled Parameters: >> pX: Horizontal position in blocks >> pY: Vertical position in blocks ======================================
*/ bool
Board::IsFreeBlock ( int
pX, int
pY)
{ if
(mBoard [pX][pY] == POS_FREE) return
true ; else
return false ;
} |
Until now we have been always talking about “blocks”. But in order to draw them to the screen we need to specify the position in pixels. So, we need two methods (GetXPosInPixels and GetYPosInPixels ) in order to obtain the horizontal and vertical position in pixels of a given block.
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
|
/* ======================================
Returns the horizontal position (in pixels) of the block given like parameter Parameters: >> pPos: Horizontal position of the block in the board ======================================
*/ int
Board::GetXPosInPixels ( int
pPos)
{ return
( ( BOARD_POSITION - (BLOCK_SIZE * (BOARD_WIDTH / 2)) ) + (pPos * BLOCK_SIZE) );
} /* ======================================
Returns the vertical position (in pixels) of the block given like parameter Parameters: >> pPos: Horizontal position of the block in the board ======================================
*/ int
Board::GetYPosInPixels ( int
pPos)
{ return
( (mScreenHeight - (BLOCK_SIZE * BOARD_HEIGHT)) + (pPos * BLOCK_SIZE) );
} |
IsPossibleMovement is the last and most complex method of Board class. This method will be used later in the main loop to check if the movement of a piece is possible or not. The method compares all the blocks of a piece with the blocks already stored in the board and with the board limits. That comparison is made by iterating through the piece matrix and comparing with the appropriate 5×5 area in the board. If there is a collision that means the movement is not possible, so it returns false. If there is no collision, the movement is possible and it returns true.
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
|
/* ======================================
Check if the piece can be stored at this position without any collision Returns true if the movement is possible, false if it not possible Parameters: >> pX: Horizontal position in blocks >> pY: Vertical position in blocks >> pPiece: Piece to draw >> pRotation: 1 of the 4 possible rotations ======================================
*/ bool
Board::IsPossibleMovement ( int
pX, int
pY, int pPiece,
int pRotation)
{ // Checks collision with pieces already stored in the board or the board limits
// This is just to check the 5x5 blocks of a piece with the appropriate area in the board
for
( int
i1 = pX, i2 = 0; i1 < pX + PIECE_BLOCKS; i1++, i2++)
{
for
( int
j1 = pY, j2 = 0; j1 < pY + PIECE_BLOCKS; j1++, j2++)
{
// Check if the piece is outside the limits of the board
if
( i1 < 0 ||
i1 > BOARD_WIDTH - 1 ||
j1 > BOARD_HEIGHT - 1)
{
if
(mPieces->GetBlockType (pPiece, pRotation, j2, i2) != 0)
return
0;
}
// Check if the piece have collisioned with a block already stored in the map
if
(j1 >= 0)
{
if
((mPieces->GetBlockType (pPiece, pRotation, j2, i2) != 0) &&
(!IsFreeBlock (i1, j1)) )
return
false ;
}
}
}
// No collision
return
true ;
} |
Step 3: The game
Now we are going to implement a general class, called “Game”, that itializes the game, draws the board and pieces by drawing each block as a rectangle (using another class that we will see later called “IO” that uses SDL) and creates new random pieces.
This is the header, “Game.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#ifndef _GAME_ #define _GAME_ // ------ Includes ----- #include "Board.h" #include "Pieces.h" #include "IO.h" #include <time.h> // ------ Defines ----- #define WAIT_TIME 700 // Number of milliseconds that the piece remains before going 1 block down */
// -------------------------------------------------------------------------------- // Game // -------------------------------------------------------------------------------- class
Game
{ public :
Game (Board *pBoard, Pieces *pPieces, IO *pIO,
int pScreenHeight);
void
DrawScene ();
void
CreateNewPiece ();
int
mPosX, mPosY; // Position of the piece that is falling down
int
mPiece, mRotation; // Kind and rotation the piece that is falling down
private :
int
mScreenHeight; // Screen height in pixels
int
mNextPosX, mNextPosY; // Position of the next piece
int
mNextPiece, mNextRotation; // Kind and rotation of the next piece
Board *mBoard;
Pieces *mPieces;
IO *mIO;
int
GetRand ( int
pA, int pB);
void
InitGame();
void
DrawPiece ( int
pX, int
pY, int pPiece,
int pRotation);
void
DrawBoard ();
}; #endif // _GAME_ |
As you can see, the current piece is defined using 4 variables: mPosX, mPosY (the position of the piece in blocks), mPiece (the type of the piece), mRotation (the current matrix that defines the piece, as we have seen, each piece has four matrices, one for each rotation).
Let’s see the implementation of the methods.
GetRand is a trivial method that returns a random number between two boundaries.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* ======================================
Get a random int between to integers Parameters: >> pA: First number >> pB: Second number ======================================
*/ int
Game::GetRand ( int
pA, int pB)
{ return
rand () % (pB - pA + 1) + pA;
} |
InitGame, takes care of the initialization of the game by selecting the first and next piece randomly. The next piece is shown so the player can see which piece will appear next. This method also sets the position in blocks of that pieces. We use two methods that we have seen before in “Pieces” class: GetXInitialPosition and GetYInitialPosition in order to initialize the piece in the correct position.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/* ======================================
Initial parameters of the game ======================================
*/ void
Game::InitGame()
{ // Init random numbers
srand
((unsigned int )
time (NULL));
// First piece
mPiece = GetRand (0, 6);
mRotation = GetRand (0, 3);
mPosX = (BOARD_WIDTH / 2) + mPieces->GetXInitialPosition (mPiece, mRotation);
mPosY = mPieces->GetYInitialPosition (mPiece, mRotation);
// Next piece
mNextPiece = GetRand (0, 6);
mNextRotation = GetRand (0, 3);
mNextPosX = BOARD_WIDTH + 5;
mNextPosY = 5;
} |
CreateNewPiece method sets the “next piece” as the current one and resets its position, then selects a new “next piece”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* ======================================
Create a random piece ======================================
*/ void
Game::CreateNewPiece()
{ // The new piece
mPiece = mNextPiece;
mRotation = mNextRotation;
mPosX = (BOARD_WIDTH / 2) + mPieces->GetXInitialPosition (mPiece, mRotation);
mPosY = mPieces->GetYInitialPosition (mPiece, mRotation);
// Random next piece
mNextPiece = GetRand (0, 6);
mNextRotation = GetRand (0, 3);
} |
DrawPiece is a really easy method that iterates through the piece matrix and draws each block of the piece. It uses green for the normal blocks and blue for the pivot block. For drawing the rectangles it calls to DrawRectangle method of the class “IO” that we will see later.
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
|
/* ======================================
Draw piece Parameters: >> pX: Horizontal position in blocks >> pY: Vertical position in blocks >> pPiece: Piece to draw >> pRotation: 1 of the 4 possible rotations ======================================
*/ void
Game::DrawPiece ( int
pX, int
pY, int pPiece,
int pRotation)
{ color mColor;
// Color of the block
// Obtain the position in pixel in the screen of the block we want to draw
int
mPixelsX = mBoard->GetXPosInPixels (pX);
int
mPixelsY = mBoard->GetYPosInPixels (pY);
// Travel the matrix of blocks of the piece and draw the blocks that are filled
for
( int
i = 0; i < PIECE_BLOCKS; i++)
{
for
( int
j = 0; j < PIECE_BLOCKS; j++)
{
// Get the type of the block and draw it with the correct color
switch
(mPieces->GetBlockType (pPiece, pRotation, j, i))
{
case
1: mColor = GREEN; break ;
// For each block of the piece except the pivot
case
2: mColor = BLUE; break ;
// For the pivot
}
if
(mPieces->GetBlockType (pPiece, pRotation, j, i) != 0)
mIO->DrawRectangle (mPixelsX + i * BLOCK_SIZE,
mPixelsY + j * BLOCK_SIZE,
(mPixelsX + i * BLOCK_SIZE) + BLOCK_SIZE - 1,
(mPixelsY + j * BLOCK_SIZE) + BLOCK_SIZE - 1,
mColor);
}
}
} |
DrawBoard is similiar to the previous method. It draws two blue columns that are used as the limits of the boards. Then draws the board blocks that are flagged as POS_FILLED in a nested loop.
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
|
/* ======================================
Draw board Draw the two lines that delimit the board ======================================
*/ void
Game::DrawBoard ()
{ // Calculate the limits of the board in pixels
int
mX1 = BOARD_POSITION - (BLOCK_SIZE * (BOARD_WIDTH / 2)) - 1;
int
mX2 = BOARD_POSITION + (BLOCK_SIZE * (BOARD_WIDTH / 2));
int
mY = mScreenHeight - (BLOCK_SIZE * BOARD_HEIGHT);
// Check that the vertical margin is not to small
//assert (mY > MIN_VERTICAL_MARGIN);
// Rectangles that delimits the board
mIO->DrawRectangle (mX1 - BOARD_LINE_WIDTH, mY, mX1, mScreenHeight - 1, BLUE);
mIO->DrawRectangle (mX2, mY, mX2 + BOARD_LINE_WIDTH, mScreenHeight - 1, BLUE);
// Check that the horizontal margin is not to small
//assert (mX1 > MIN_HORIZONTAL_MARGIN);
// Drawing the blocks that are already stored in the board
mX1 += 1;
for
( int
i = 0; i < BOARD_WIDTH; i++)
{
for
( int
j = 0; j < BOARD_HEIGHT; j++)
{
// Check if the block is filled, if so, draw it
if
(!mBoard->IsFreeBlock(i, j))
mIO->DrawRectangle ( mX1 + i * BLOCK_SIZE,
mY + j * BLOCK_SIZE,
(mX1 + i * BLOCK_SIZE) + BLOCK_SIZE - 1,
(mY + j * BLOCK_SIZE) + BLOCK_SIZE - 1,
RED);
}
}
} |
DrawScene, just calls the previous methods in order to draw everything.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* ======================================
Draw scene Draw all the objects of the scene ======================================
*/ void
Game::DrawScene ()
{ DrawBoard ();
// Draw the delimitation lines and blocks stored in the board
DrawPiece (mPosX, mPosY, mPiece, mRotation);
// Draw the playing piece
DrawPiece (mNextPosX, mNextPosY, mNextPiece, mNextRotation);
// Draw the next piece
} |
Step 4: Easy drawing, window management and keyboard input using SDL, isolated from the game logic
“IO.cpp” and “IO.h” are the files that implement the “IO” class. It uses SDL in order to create the window, clear it, update the screen and take care of the keyboard input. You can check out “IO.cpp” and “IO.h” files in order to see its implementation. I’m not going to explain the methods that are SDL related. You can change this class in order to use a different renderer (like IndieLib, Allegro, OpenGL, Direct3d, etc).
This is the header (“IO.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
#ifndef _IO_ #define _IO_ // ------ Includes ----- #ifndef LINUX #include "SDL/include/SDL.h"
#include "SDL/SDL_GfxPrimitives/SDL_gfxPrimitives.h" #else #include <SDL/SDL.h>
#include "SDL/SDL_GfxPrimitives/sdl_gfxprimitives.h"
#endif #pragma comment (lib, "SDL/lib/SDL.lib") #pragma comment (lib, "SDL/SDL_GfxPrimitives/SDL_GfxPrimitives_Static.lib") // ------ Enums ----- enum
color {BLACK, RED, GREEN, BLUE, CYAN, MAGENTA, YELLOW, WHITE, COLOR_MAX};
// Colors
// -------------------------------------------------------------------------------- // IO // -------------------------------------------------------------------------------- class
IO
{ public :
IO ();
void
DrawRectangle ( int
pX1, int
pY1, int pX2,
int pY2,
enum color pC);
void
ClearScreen ();
int
GetScreenHeight ();
int
InitGraph ();
int
Pollkey ();
int
Getkey ();
int
IsKeyDown ( int
pKey);
void
UpdateScreen ();
}; #endif // _IO_ |
Step 5: The main loop
The main loop is quite simple. In each frame we draw everything. Later, we use keyboard input in order to move the piece. Before each movement, we first check out if it is possible. We also measure the time in order to move the piece down every n milliseconds. When the piece fall down one block, we check out if that movement is possible, if not, we store the piece in the board. We also check out if there are blocks in the upper row, if so, the game is over.
Let’s see “Main.cpp” step by step:
First, we initialize all the classes. Then, we get the actual milliseconds, which will be used to determine when the piece should move down.
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
|
#include "Game.h" #ifndef LINUX #include <windows.h> #endif /* ================== Main ================== */ int
WINAPI WinMain ( HINSTANCE
hInstance, HINSTANCE
hPrevInstance, LPSTR
lpCmdLine, int
nCmdShow)
{ // ----- Vars -----
// Class for drawing staff, it uses SDL for the rendering. Change the methods of this class
// in order to use a different renderer
IO mIO;
int
mScreenHeight = mIO.GetScreenHeight();
// Pieces
Pieces mPieces;
// Board
Board mBoard (&mPieces, mScreenHeight);
// Game
Game mGame (&mBoard, &mPieces, &mIO, mScreenHeight);
// Get the actual clock milliseconds (SDL)
unsigned
long mTime1 = SDL_GetTicks();
|
This is the main loop. We can exit by pressing ESC. In each frame we clear and update the screen and draw everything.
1
2
3
4
5
6
7
8
9
|
// ----- Main Loop ----- while
(!mIO.IsKeyDown (SDLK_ESCAPE))
{ // ----- Draw -----
mIO.ClearScreen ();
// Clear screen
mGame.DrawScene ();
// Draw staff
mIO.UpdateScreen ();
// Put the graphic context in the screen
|
We start with the input. If we press left, down or right we try to move the piece in that directions. We only move the piece if the movement is possible.
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
|
// ----- Input ----- int
mKey = mIO.Pollkey();
switch
(mKey)
{ case
(SDLK_RIGHT):
{
if
(mBoard.IsPossibleMovement (mGame.mPosX + 1, mGame.mPosY, mGame.mPiece, mGame.mRotation))
mGame.mPosX++;
break ;
}
case
(SDLK_LEFT):
{
if
(mBoard.IsPossibleMovement (mGame.mPosX - 1, mGame.mPosY, mGame.mPiece, mGame.mRotation))
mGame.mPosX--;
break ;
}
case
(SDLK_DOWN):
{
if
(mBoard.IsPossibleMovement (mGame.mPosX, mGame.mPosY + 1, mGame.mPiece, mGame.mRotation))
mGame.mPosY++;
break ;
}
|
By pressing “x”, the piece will fall down directly to the ground. This is really easy to implement by trying to move the piece down until the movement is not possible. Then we store the piece, delete possible lines and check out if the game is over, if not, we create a new piece.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
case
(SDLK_x):
{ // Check collision from up to down
while
(mBoard.IsPossibleMovement(mGame.mPosX, mGame.mPosY, mGame.mPiece, mGame.mRotation)) { mGame.mPosY++; }
mBoard.StorePiece (mGame.mPosX, mGame.mPosY - 1, mGame.mPiece, mGame.mRotation);
mBoard.DeletePossibleLines ();
if
(mBoard.IsGameOver())
{
mIO.Getkey();
exit (0);
}
mGame.CreateNewPiece();
break ;
} |
By pressing “z” we rotate the piece. With the methods that we have already implement this is an easy task. The rotation is in fact to change to the next rotated stored piece. We first should check that the rotated piece will be drawn without colliding, if so, we sets this rotation as the current one.
1
2
3
4
5
6
7
8
|
case
(SDLK_z):
{
if
(mBoard.IsPossibleMovement (mGame.mPosX, mGame.mPosY, mGame.mPiece, (mGame.mRotation + 1) % 4))
mGame.mRotation = (mGame.mRotation + 1) % 4;
break ;
}
} |
If WAIT_TIME passed, the piece should fall down one block. We have to check out if the movement is possible, if not, the piece should be stored and we have to check if we can delete lines. We also see if the game is over, if not, we create a new piece.
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
|
// ----- Vertical movement -----
unsigned
long mTime2 = SDL_GetTicks();
if
((mTime2 - mTime1) > WAIT_TIME)
{
if
(mBoard.IsPossibleMovement (mGame.mPosX, mGame.mPosY + 1, mGame.mPiece, mGame.mRotation))
{
mGame.mPosY++;
}
else
{
mBoard.StorePiece (mGame.mPosX, mGame.mPosY, mGame.mPiece, mGame.mRotation);
mBoard.DeletePossibleLines ();
if
(mBoard.IsGameOver())
{
mIO.Getkey();
exit (0);
}
mGame.CreateNewPiece();
}
mTime1 = SDL_GetTicks();
}
}
return
0;
} |
And that’s all! Please leave a comment if you see some mistakes, language errors or if you have any doubts… or just to say thanks!
Credits
- Javier López
- Special thanks: Imelior, who fixed English mistakes and compiled the tutorial under Linux.
- Special thanks: Javier Santana, who added #ifndef sentences and pointed that was necessary to use libsdl-gfx1.2-dev and libsdl1.2-dev under Linux.
Bonus
Don’t forget to play with the “defines”. Crazy example:
1
2
3
|
#define BLOCK_SIZE 5 // Width and Height of each block of a
#define BOARD_WIDTH 90 // Board width in blocks
#define BOARD_HEIGHT 90 // Board height in blocks |
You should follow me on Twitter
Did you like the tutorial? Then you should follow me on twitter here.