Tutorial 10: Game Interface Design Tetris Game by Osama Hosam


Introduction

Tetris game is famous and easy to play. Also its rules are few, although it has a strong game play. We will cover the implementation of the game from scratch. We will start by game design, then game interface and then game code. The game has blocks that get down from the screen top with a specified speed. The player has to put the blocks over each other in a way not leaving holes between layers. If too much holes between layers the player will loose because the screen will be filled with blocks, if the player completed one row his score will be increased and he will have space to complete other rows. The game interface is shown in Fig.1.


Fig.1 The interface of Tetris game

Game analysis

When you want to code a game, initially you have to understand every tiny detail in the game play. Imagine everything about the game, how the game will be played, the interface of the game, the game sound, etc. Generally speaking you have to do the brainstorm. This technique must be applied to small games, because if done well, it will be easily applied on large scale games. So let’s do brainstorm for our game.

We have entities called block, every block has specified shape. Every block can move right, left or bottom, it can’t move up. The movements of the blocks will be done by keyboard arrows. The left arrow will make the block move left, the right arrow will move the block to the right, the down arrow will move the block quickly to the bottom, the up arrow will rotate or flip the block.

The movements will be inside a border. We need a matrix to hold the border dimensions and hold the block collection data and positions; we called this matrix (Game_Matrix). The movements of the block will be limited to be inside that border. So we will move right until the block reach the last column in the Game_Matrix or collide with an existing block, the block also will move left until it reaches the column number 0 in the Game_Matrix or hit another block. The block will move down until it reaches the last row of the Game_Matrix or hit another block.

If one row of the matrix filled with block items, it will be removed form the matrix and all the above rows will shifted down one row, then the score will be increased.

The speed of the game will be controlled by OpenGL timers, the speed, level and elapsed time will be stored into another entity called Game_settings. See Fig.2 for more details.

Fig.2 Game Entities

Fig.2 shows the three main entities of our game, The Block position will be changed with time and its position will be changed accordingly in the Game_Matrix.

The last thing we want to mention is that the game displays the next block shape which will come from the top of the screen after you settle down the current block. We will assign a small window for this purpose.

Interface design

First we need to create a main menu. The main menu contains, New Game, Settings, Top Score and Exit items, the main menu is shown in Fig.3. To simplify the game design we will just implement the “New Game” screen. The menu is implemented by assigning two types of images for each item, the first image without border and the second image is with border. When the menu item is selected we assign the image with border to the menu item otherwise the image without border will be assigned.

Fig.3 The main menu of the game.

When the new game menu item is selected by using the keyboard arrows and pressing enter, the game screen will be displayed. The game screen is 700×700 pixels, it will be divided into a grid of rectangles every rectangle is 30 pixels wide and 30 pixels high. Fig.4 shows this grid. Also the following code presents the default game settings.

	GameSettings()
	{
		level=0;
		steps_time=300;
		length_unit=30;
		score=0;
		window_width=700;
		window_height=700;
		elapsed_time=0;
		setSpeed();
	}

The rectangle in red representing the Game_Matrix which is 21 rows x 16 columns.

#define MATRIX_ROWS 21
#define MATRIX_COLS 16
Fig.4 The interface of the game.
int single_block[MATRIX_ROWS][MATRIX_COLS];
int block_store[MATRIX_ROWS][MATRIX_COLS];
  • Single_Block matrix this matrix will hold the currently moving block position.
  • Block_Store matrix which holds the entire collection of blocks

The idea is to combine the two matrices to form the overall matrix which will be displayed on the screen. I separated them to easily deal with both matrices, if it was one matrix managing it will be difficult. We assigned numbers to each type of block,

  • 1 is the T shape block
  • 2 is the L shape block
  • 3 is the Z shape block
  • 4 is the I shape block
  • 5 is the O shape block

Every block will be represented by its corresponding number in the single block matrix and block store matrix. The place with no block will be represented by 0. Fig.5 shows both single block and block store matrices.

At any time, the pixel coordinates of every item can be get by multiply the length_unit variable of the GameSettings by the items row, column coordinates of the item in the Game_Matrix.

	Item’s pixel x = item’s column * length_unit
	Item’s pixel y= item’s row * length_unit.
Fig. 5 The single block and block store matrices

Game States

The game state represents which state the game in. Which screen to be displayed? The main menu screen? The game main screen? This question is answered by the game state class. We have assigned the following values for the game state class:

  • 1 refers to the main menu screen
  • 2 refers to the game screen.
  • 3 refers to the Settings screen
  • 4 refers to the Top Score screen

We check to see which state we are in, and then we display the appropriate interface textures. The States class is shown by the following code segment

class States
{
private:
	int state;
public:
	States()
	{
		state=1;
	}
	void setState(int s)
	{
		state=s;
	}
	int getState()
	{
		return state;
	}
}current_state;

The default state of the game is 1 or the main menu. This value can be set and get by the function setState() and getState() respectively.
A question arises here. Why there is no global variable instead of creating a class for that variable? I prefer this way of programming to be easy in the future to update the game States class and add new features if any.

The Paint Class

We have implemented a single class responsible for all the activities of interface design. Let see the game class’s member functions

class Paint
{
private:
	char string [100];
	int menu_number;
	int menu_items_count;
	GLuint texture_id[20];
public:
	Paint();
	void setNextMenuItem();
	void setPrevMenuItem();
	int getMenuItemPos();
	GLuint LoadTexture(char *FileName );
	void DrawBackgroundImage();
	void LoadAllTextures();
	void DrawRectangle(RECTANGLE rect);
	void drawText(char*string,int x,int y, float font_size);
	void printMessageEsc();
	void printMessageGameOver();
	void DrawTexturedRectangle(RECTANGLE rectangle,int item_number);
	void DrawGameMenu();
	void DrawGameSubwindows();

} paint_background;

I’ll explain the class by the order of the execution of its member functions. First the class loads all textures by using LoadAllTextures() function which uses the function LoadTexture() to load a single file . The function takes the path of every image file on the hard disk and load it as a texture to the texture array textur_id[20]. Fig. 6 shows the texture file names and their corresponding positions in the texture_id matrix.

The functions setNextMenuItem and setPrevMenuItem change the current menu item in the menu screen. getMenuItemPos gets the current position of menu item. The implementation is as follows

Paint()
	{
		menu_number=1;
		menu_items_count=4;
	}
	void setNextMenuItem()
	{
		if(menu_number==menu_items_count)
			menu_number =1;
		else
			menu_number++;
	}
	void setPrevMenuItem()
	{
		if(menu_number==1)
			menu_number=menu_items_count;
		else
			menu_number--;
	}
	int getMenuItemPos()
	{
		return menu_number;
	}

Paint() function is the constructor of our class and it sets the menu items to 4 menu items, and set the menu number to 1 means the default menu item is “New Game” menu item, so

  • menu_number = 1 refers to the “New Game” menu item.
  • menu_number=2 refers to the “Settings” item.
  • menu_number=3 refers to the “Top Score” item
  • menu_number=4 refers to the “Exit” item.

When the menu_number is 1 and we need to get the previous menu item we set the menu_number to be 4, also when the menu_number is 4 and we want to get the next menu item we set the menu_number to be 1. So it acts like a circle relating the collection of menu items.

Fig. 6 The textures needed for Tetris game

The function DrawTexturedRectangle takes a rectangle with its positions on the screen and cover it with a texture according to the item_number variables here is the implementation of the DrawTexturedRectangle function

void DrawTexturedRectangle(RECTANGLE rectangle,int item_number)
	{
		switch(item_number)
		{
		case 1:
			if(menu_number == 1)
				glBindTexture(GL_TEXTURE_2D,  texture_id[2]);
			else
				glBindTexture(GL_TEXTURE_2D,  texture_id[3]);
			break;
		case 2:
			if(menu_number == 2)
				glBindTexture(GL_TEXTURE_2D,  texture_id[4]);
			else
				glBindTexture(GL_TEXTURE_2D,  texture_id[5]);
			break;
		case 3:
			if(menu_number == 3)
				glBindTexture(GL_TEXTURE_2D,  texture_id[6]);
			else
				glBindTexture(GL_TEXTURE_2D,  texture_id[7]);
			break;
		case 4:
			if(menu_number == 4)
				glBindTexture(GL_TEXTURE_2D,  texture_id[8]);
			else
				glBindTexture(GL_TEXTURE_2D,  texture_id[9]);
			break;
		case 5:
			glBindTexture(GL_TEXTURE_2D,  texture_id[10]);
			break;
		case 6:
			glBindTexture(GL_TEXTURE_2D,  texture_id[11]);
			break;
		case 7:
			glBindTexture(GL_TEXTURE_2D,  texture_id[12]);
			break;
		case 8:
			glBindTexture(GL_TEXTURE_2D,  texture_id[13]);
			break;
		case 9:
			glBindTexture(GL_TEXTURE_2D,  texture_id[14]);
			break;

		default:
			glBindTexture(GL_TEXTURE_2D,  NULL);
			break;
		}
		//to draw a rectangle
		glBegin(GL_QUADS);

		glTexCoord2f(0.0f,0.0f);
		glVertex2f(rectangle.left,rectangle.bottom );

		glTexCoord2f(1.0f,0.0f);
		glVertex2f(rectangle.right ,rectangle.bottom);
		glTexCoord2f(1.0f,1.0f);
		glVertex2f(rectangle.right,rectangle.top );
		glTexCoord2f(0.0f,1.0f);
		glVertex2f(rectangle.left,rectangle.top);
		glEnd();
	}

The function switch the item number, if it is menu item, it make sure that it is the currently selected item or not by checking the value of menu_number variable, if it is the currently selected item, it shows the corresponding image with border around it. The corresponding texture is bind to memory and then the rectangle is covered by it.

The DrawBackgroundImage() draws either the background image of the menu screen or the background image of the game screen.
The PrintMessageEsc() and PrintMessageGameOver() functions use the DrawText function to draw a text on the screen in a specified position.
The function DrawGameMenu() is straight forward and it uses the function DrawTexturedRectangle to draw the four items of the menu.
The function DrawGameSubwindows() is little complicated, but if you understood well the grid division shown before in Fig.4 it will be an easy task for you, let us see the implementation of this function

	void DrawGameSubwindows()
	{
		int unit=game_settings.getGameRectLength();
		// the rectangle which includes the next block shape
		RECTANGLE left_border_up_b={unit,unit,unit+4*unit,unit+4*unit};
		// the rectangle which include the score
		RECTANGLE left_border_down_b={unit,18*unit,unit+4*unit,18*unit+4*unit};
		//the recangle which include the game blocks
		RECTANGLE game_screen={unit+4*unit+unit,unit,6*unit+16*unit,unit+4*unit+
		13*unit+4*unit};

		//draw
		DrawRectangle(game_screen);
		DrawRectangle(left_border_up_b);
		DrawRectangle(left_border_down_b);

		//put text score
		glColor3f(1.0f,1.0f,1.0f);
		sprintf(string,"SCORE %d ",pcResult); 
		drawText(string,unit+0.5*unit,18*unit+unit,1.5);

		//put the speed text
		sprintf(string,"SPEED %d ",game_settings.getSpeed()); 
		drawText(string,unit+0.5*unit,18*unit+(1.75)*unit,1.5);

		//put level text
		sprintf(string,"LEVEL %d ",game_settings.getLevel()); 
		drawText(string,unit+0.5*unit,18*unit+(2.5)*unit,1.5);

		//put elpsed time
		sprintf(string,"TIME %d ",game_settings.getElapsedTime()); 
		drawText(string,unit+0.5*unit,18*unit+(3.25)*unit,1.5);

		//put the shape text
		drawText("NEXT",unit+unit,unit+unit,2);

		//draw the next shape rectangles(block)
		RECTANGLE first_rect;
		RECTANGLE second_rect;
		RECTANGLE third_rect;
		RECTANGLE fourth_rect;

		int next_shape=current_moving_block.getNextshape();
		int hlf=unit/2;
		switch(next_shape)
		{
		case 1:
			//T shape

			first_rect.left=unit+hlf;   first_rect.top=unit+2*unit;  
			first_rect.right=2*unit+hlf;  first_rect.bottom=3*unit+unit;
			second_rect.left=2*unit+hlf;  second_rect.top=unit+2*unit; 
			second_rect.right=3*unit+hlf; second_rect.bottom=3*unit+unit;
			third_rect.left=3*unit+hlf; third_rect.top=unit+2*unit;  
			third_rect.right=4*unit+hlf;  third_rect.bottom=3*unit+unit;
			fourth_rect.left=2*unit+hlf;fourth_rect.top=unit+3*unit; 
			fourth_rect.right=3*unit+hlf; fourth_rect.bottom=4*unit+unit;
			break;
		case 2:
			// L shape
			first_rect.left=unit+hlf;   first_rect.top=unit+2*unit;  
			first_rect.right=2*unit+hlf;  first_rect.bottom=3*unit+unit;
			second_rect.left=2*unit+hlf;  second_rect.top=unit+2*unit; 
			second_rect.right=3*unit+hlf; second_rect.bottom=3*unit+unit;
			third_rect.left=3*unit+hlf; third_rect.top=unit+2*unit;  
			third_rect.right=4*unit+hlf;  third_rect.bottom=3*unit+unit;
			fourth_rect.left=unit+hlf;   fourth_rect.top=unit+3*unit;  
			fourth_rect.right=2*unit+hlf;  fourth_rect.bottom=4*unit+unit;

			break;
		case 3:
			//Z shape
			first_rect.left=unit+hlf;   first_rect.top=unit+3*unit;  
			first_rect.right=2*unit+hlf;  first_rect.bottom=4*unit+unit;
			second_rect.left=2*unit+hlf;  second_rect.top=unit+2*unit; 
			second_rect.right=3*unit+hlf; second_rect.bottom=3*unit+unit;
			third_rect.left=3*unit+hlf; third_rect.top=unit+2*unit;  
			third_rect.right=4*unit+hlf;  third_rect.bottom=3*unit+unit;
			fourth_rect.left=2*unit+hlf;fourth_rect.top=unit+3*unit; 
			fourth_rect.right=3*unit+hlf; fourth_rect.bottom=4*unit+unit;
			break;
		case 4:
			//I shape
			first_rect.left=unit;        first_rect.top=3*unit;  
			first_rect.right=unit+unit;  first_rect.bottom=3*unit+unit;
			second_rect.left=unit+unit;  second_rect.top=3*unit; 
			second_rect.right=unit+2*unit; second_rect.bottom=3*unit+unit;
			third_rect.left=unit+2*unit; third_rect.top=3*unit;  
			third_rect.right=unit+3*unit;  third_rect.bottom=3*unit+unit;
			fourth_rect.left=unit+3*unit;fourth_rect.top=3*unit; 
			fourth_rect.right=unit+4*unit; fourth_rect.bottom=3*unit+unit;

			break;
		case 5:
			//O shape
			first_rect.left=unit+unit;   first_rect.top=unit+3*unit;  
			first_rect.right=unit+2*unit;  first_rect.bottom=4*unit+unit;
			second_rect.left=unit+unit;  second_rect.top=3*unit; 
			second_rect.right=unit+2*unit; second_rect.bottom=3*unit+unit;
			third_rect.left=unit+2*unit; third_rect.top=3*unit;  
			third_rect.right=unit+3*unit;  third_rect.bottom=3*unit+unit;
			fourth_rect.left=unit+2*unit;fourth_rect.top=unit+3*unit; 
			fourth_rect.right=unit+3*unit; fourth_rect.bottom=4*unit+unit;

			break;
		default:
			break;
		}

		DrawTexturedRectangle(first_rect,next_shape+4);
		DrawTexturedRectangle(second_rect,next_shape+4);
		DrawTexturedRectangle(third_rect,next_shape+4);
		DrawTexturedRectangle(fourth_rect,next_shape+4);
	}

The left_border_up_b represents the rectangle in which the next shape will be drawn. It will be drawn one unit from top and one unit from left, with width = 4 units and height = 4 units. The unit = 30 pixels. The Left_border_down_b represents the rectangle which holds the game data, the score, the level, the elapsed time, and the speed. The function Draw text will be used to draw the text needed on the screen. The game_screen rectangle is the rectangle in which the game will be played and holds the collection of the blocks and the current moving block.

Four rectangles will be defined; first_rect, second_rect, third_rect, and fourth rectangle. These rectangles represent the four rectangles which are the elements of the next block to be displayed. Although this is tedious way to draw them on the screen by defining their coordinates rectangle by rectangle, but this way will not be used when it is time to draw the blocks in the game screen.
The DrawTexturedRectangle function is adopted to draw each block. We have passed the value next_shape+4 to skip the 4 elements of the main menu screen.

Next tutorial:

In the next tutorial we will continue explaining the Tetris game. We will implement the game logic.

Source Code

Click here to download the source code of the Tetris game.