Tetris clone in an hour with C++.

Started by
148 comments, last by jbadams 12 years ago
I often see people asking "How to make tetris?" or "how should I start my first game?" Well, here's a short and sweet lesson on making a simple tetris clone. No fancy stuff this time, just the basics. This mini-tutorial is strictly for C++. I'll assume at least basic familiarization. You must create this project as a Win32 Application project. It includes only three files, main.cpp, bitmapobject.cpp, and bitmapobject.h. EDIT: MetaCipher converted the code to SDL format, which can be found on page 6. Go have a look! In main.cpp:

//FALLING BLOCK GAME!
//main.cpp
//tell compiler to not include many unneeded header files.
#define WIN32_LEAN_AND_MEAN
//need this for windows stuff.
#include <windows.h>
//need this for srand and rand
#include <stdlib.h>
//now let's include our bitmapobject definitions
#include "bitmapobject.h"
          
That's all you need to start off. Next, let's include some handy-dandy defines that makes editing your program a thousand times easier. Seriously.

//let's give our window a name
#define WINDOWCLASS "FallingBlockGame"
//let's give our window a title...er caption.
#define WINDOWTITLE "A Falling Block Game!"
          
That takes care of Windows. Now let's think ahead. What should we use if we're going to make a tetris clone? Hmm... well, seems to me map tiles would work perfectly! Let's make a size for each tile, then make a size for the map, PLUS an 8-block wide sidebar.

//since we're using square blocks, let's only use a single size.
const int TILESIZE=16;
//now for the map...
const int MAPWIDTH=10;
const int MAPHEIGHT=30;
const int GREY=8;
          
Easy! Now, let's have some (you can use enumerations if you prefer) variables for the future bitmap. We need 9 colors, and 1 Do Not Draw, so 10 in all.

const int TILENODRAW=-1;
const int TILEBLACK=0;
const int TILEGREY=1;
const int TILEBLUE=2;
const int TILERED=3;
const int TILEGREEN=4;
const int TILEYELLOW=5;
const int TILEWHITE=6;
const int TILESTEEL=7;
const int TILEPURPLE=8;
          
Alright, that's done. Let's do some function prototypes. We will need Game Init, Game Loop, Game Done, Draw Tile, Draw Map, New Block, Rotate Block, Collision Test, Move Block, Remove Row, and New Game.

bool GameInit(); // game initialization function
void GameLoop(); //where the game actually takes place
void GameDone(); //clean up! 
void DrawTile(int x, int y, int tile); //coordinates & tile type
void DrawMap(); //draw the whole map.. render function, basically
void NewBlock(); //create a new block!
void RotateBlock(); //rotate a block.. if you can!
void Move(int x, int y); //coordinates to move.
int CollisionTest(int nx, int ny); //test collision of blocks
void RemoveRow(int row); //remove a row.. that would be the 'x'.
void NewGame(); //make a new game!
          
Pretty simple so far, eh? Now, let's throw in some global variables, bwahahahah! Global variables are preferred, because they are FAST. Speed is essential to a game. Thus, when you can, use them. But, be careful with them. The first two we need are an application handle and a main window handle.

HINSTANCE hInstMain=NULL; //main app handle
HWND hWndMain=NULL; //main window handle
          
These are necessary for windows programming. Remember this, always. Next, let's create an array to hold the play area map's tile types. We will add one extra row for Map to make Y-movement collision detection easier.

int Map[MAPWIDTH][MAPHEIGHT+1]; //the game map!
          
Now, let's make a little structure for a game piece, then two variables of that structure; one for the actual piece, and one for the preview piece.

struct Piece {
	int size[4][4];
	int x;
	int y;
};

Piece sPrePiece; //preview piece.
Piece sPiece; //the 's' prefixes indicate this is a 'structure'
          
Simple! Now, something we *REALLY* need is for timing. Let's call it start_time, of the DWORD type. We also need a GAMESTARTED boolean.

DWORD start_time;  //used in timing
bool GAMESTARTED=false; //used by NewBlock()
          
NOW, let's leave main.cpp briefly, and visit bitmapobject.h:

//BitMapObject.h
#ifndef BITMAPOBJECT_H
#define BITMAPOBJECT_H
#pragma once
//we need this for windows stuff.
#include <windows.h>
          
Just the usual basics. We need to make a class for the bitmap object.. to store the bitmap, that is. We need a handle to a device context.. that is, an HDC, which refers to an output device, or multiple output devices. We also need a pair of HBITMAP; one old, and one new. We also need a width and height of the bitmap.

class BitMapObject
{
private:
	//memory dc
	HDC hdcMemory;
	//new bitmap!
	HBITMAP hbmNewBitMap;
	//old bitmap.
	HBITMAP hbmOldBitMap;
	//width & height as integers.
	int iWidth;
	int iHeight;
          
Now, let's add some functions. We need, of course, a constructor and destructor, a Load function, a Create function, a Destroy function, functions to return iHeight/iWidth, and a conversion to HDC (bwahah). Then we're done with the .h file.

public:
	//constructor
	BitMapObject();

	//destructor
	~BitMapObject();

	//loads bitmap from a file
	void Load(HDC hdcCompatible,LPCTSTR lpszFilename);

	//creates a blank bitmap
	void Create(HDC hdcCompatible, int width, int height);

	//destroys bitmap and dc
	void Destroy();

	//return width
	int GetWidth();

	//return height
	int GetHeight();

	//converts to HDC
	operator HDC();
};

#endif
          
Now, we're done with that. Let's go on and make the functions for the class in the bitmapobject.cpp file:

//BitMapObject.cpp
#include "bitmapobject.h"
          
So we need the header file. Let's make the constructor, first, then the destructor. We need to set the values of the variables to 0/NULL (I prefer NULL on non-basic variables, and 0 on int/char/long/etc).

BitMapObject::BitMapObject()
{
	hdcMemory=NULL;
	hbmNewBitMap=NULL;
	hbmOldBitMap=NULL;
	iWidth=0;
	iHeight=0;
}

BitMapObject::~BitMapObject()
{
	//if the hdcMemory hasn't been destroyed, do so
	if(hdcMemory)
		Destroy();
}
          
Simple, yes? Now, let's load a bitmap! Fun! It really is easy.

void BitMapObject::Load(HDC hdcCompatible, LPCTSTR lpszFilename)
{
	//if hdcMemory isn't null, make it so captain!
	if(hdcMemory)
		Destroy();

	//create memory dc.
	hdcMemory=CreateCompatibleDC(hdcCompatible);
	//load the bitmap
	hbmNewBitMap=(HBITMAP)LoadImage(NULL,lpszFilename,IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
	//shove the image into the dc
	hbmOldBitMap=(HBITMAP)SelectObject(hdcMemory,hbmNewBitMap);
	//grab the bitmap's properties
	BITMAP bmp;
	GetObject(hbmNewBitMap,sizeof(BITMAP),(LPVOID)&bmp);
	//grab the width & height
	iWidth=bmp.bmWidth;
	iHeight=bmp.bmHeight;
}
          
Not hard. You might look up on MSDN for more info on the above functions, as they explain it better than I could :). Now, let's make that Create function!

void BitMapObject::Create(HDC hdcCompatible, int width, int height)
{
	//if hdcMemory isn't null, blow it up!
	if(hdcMemory)
		Destroy();

	//create the memory dc.
	hdcMemory=CreateCompatibleDC(hdcCompatible);
	//create the bitmap
	hbmNewBitMap=CreateCompatibleBitmap(hdcCompatible, width, height);
	//shove the image into the dc
	hbmOldBitMap=(HBITMAP)SelectObject(hdcMemory, hbmNewBitMap);
	//change the width and height.
	iWidth=width;
	iHeight=height;
}
          
Not much new here. Now let's build that Destroy() function we've used so much!

void BitMapObject::Destroy()
{
	//restore old bitmap.
	SelectObject(hdcMemory, hbmOldBitMap);
	//delete new bitmap.
	DeleteObject(hbmNewBitMap);
	//delete device context.
	DeleteDC(hdcMemory);
	//set members to 0/NULL
	hdcMemory=NULL;
	hbmNewBitMap=NULL;
	hbmOldBitMap=NULL;
	iWidth=0;
	iHeight=0;
}
          
Pretty easy. Lastly to finish up the .cpp, let's do the HDC conversion, and the Height/Width functions.

BitMapObject::operator HDC()
{
	//return hdcMemory.
	return(hdcMemory);
}

int BitMapObject::GetWidth()
{
	//return width
	return(iWidth);
}

int BitMapObject::GetHeight()
{
	//return height
	return(iHeight);
}
          
Beautiful! We're finished with this file, now we can go back to main.cpp, where we left off.. in Global variables. Why did I leave off where I did? Because we need this class. Badly. We need to make a variable with this class to handle the Map, and one to handle the Blocks.

//map for the program
BitMapObject bmoMap;
//block images
BitMapObject bmoBlocks;
          
That's that. Now, let's build a simple message handler. To make that more clear, Windows sends messages to the window all the bloody time. Some of those are useful to us, like a key being pressed or a mouse button being pressed. Most Windows applications have this function. DestroyWindow() beings the shutdown, and PostQuitMessage() tells the program it's exiting. The WM_PAINT might be confusing. It is called a LOT, and basically redraws the window. BitBlt() is the function we'll be using to draw our bitmaps. Yay Windows GDI! Don't have to bother with DirectX, and it still runs fairly well.

LRESULT CALLBACK TheWindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
	//which message did we get?
	switch(uMsg)
	{
	case WM_KEYDOWN:
		{
			//check for escape key
			if(wParam==VK_ESCAPE)
			{
				DestroyWindow(hWndMain);
				return(0);//handled message
			}
			else if(wParam==VK_DOWN) //check for down arrow key
			{
				Move(0,1);
				return(0);//handled message
			}
			else if(wParam==VK_UP) //check for up arrow key
			{
				RotateBlock();
				return(0);//handled message
			}
			else if(wParam==VK_LEFT) //check for left arrow key
			{
				Move(-1,0);
				return(0);//handled message
			}
			else if(wParam==VK_RIGHT) //check for right arrow key
			{
				Move(1,0);
				return(0);//handled message
			}
		}break;
	case WM_DESTROY://the window is being destroyed
		{

			//tell the application we are quitting
			PostQuitMessage(0);

			//handled message, so return 0
			return(0);

		}break;
	case WM_PAINT://the window needs repainting
		{
			//a variable needed for painting information
			PAINTSTRUCT ps;
			
			//start painting
			HDC hdc=BeginPaint(hwnd,&ps);

			//redraw the map
			BitBlt(hdc,0,0,TILESIZE*MAPWIDTH+TILESIZE*GREY,TILESIZE*MAPHEIGHT,bmoMap,0,0,SRCCOPY);

			//end painting
			EndPaint(hwnd,&ps);
					
			//handled message, so return 0
			return(0);
		}break;
	}

	//pass along any other message to default message handler
	return(DefWindowProc(hwnd,uMsg,wParam,lParam));
}
          
Next, we come to the ever-formidable evil mountain that is known as WinMain(). This is where you make your window, and your pact with Bill Gates! Just smile and nod as you sign away your soul. But seriously, once you've made this window, you're going to use pretty much the same framework code for other games. Again, MSDN provides more uh, useful info on it. Yeah.

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd)
{
	//assign instance to global variable
	hInstMain=hInstance;

	//create window class
	WNDCLASSEX wcx;

	//set the size of the structure
	wcx.cbSize=sizeof(WNDCLASSEX);

	//class style
	wcx.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

	//window procedure
	wcx.lpfnWndProc=TheWindowProc;

	//class extra
	wcx.cbClsExtra=0;

	//window extra
	wcx.cbWndExtra=0;

	//application handle
	wcx.hInstance=hInstMain;

	//icon
	wcx.hIcon=LoadIcon(NULL,IDI_APPLICATION);

	//cursor
	wcx.hCursor=LoadCursor(NULL,IDC_ARROW);

	//background color
	wcx.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);

	//menu
	wcx.lpszMenuName=NULL;

	//class name
	wcx.lpszClassName=WINDOWCLASS;

	//small icon
	wcx.hIconSm=NULL;

	//register the window class, return 0 if not successful
	if(!RegisterClassEx(&wcx)) return(0);

	//create main window
	hWndMain=CreateWindowEx(0,WINDOWCLASS,WINDOWTITLE, WS_BORDER | WS_SYSMENU | WS_CAPTION| WS_VISIBLE,0,0,320,240,NULL,NULL,hInstMain,NULL);

	//error check
	if(!hWndMain) return(0);

	//if program initialization failed, then return with 0
	if(!GameInit()) return(0);

	//message structure
	MSG msg;

	//message pump
	for( ; ; )	
	{
		//look for a message
		if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
		{
			//there is a message

			//check that we arent quitting
			if(msg.message==WM_QUIT) break;
			
			//translate message
			TranslateMessage(&msg);

			//dispatch message
			DispatchMessage(&msg);
		}

		//run main game loop
		GameLoop();
		
	}
	
	//clean up program data
	GameDone();

	//return the wparam from the WM_QUIT message
	return(msg.wParam);
}
          
Now we get to the beginnings of the actual game! WHEE! First, Initialization is the key to victory. We use a temporar rectangle (RECT rcTemp) to clear out the window. Then, we create the map image, and similiarly clear it out. Lastly, we load the blocks bitmap file (which you will have to create on your own, more on that at the bottom of this mini-tutorial). Let's also put in the GameDone().. it has no code in it, since there's no need for it currently. Hey, you can use the framework for your own games :)!

bool GameInit()
{
	//set the client area size
	RECT rcTemp;
	SetRect(&rcTemp,0,0,MAPWIDTH*TILESIZE+TILESIZE*GREY,MAPHEIGHT*TILESIZE);//160x480 client area
	AdjustWindowRect(&rcTemp,WS_BORDER | WS_SYSMENU | WS_CAPTION| WS_VISIBLE,FALSE);//adjust the window size based on desired client area
	SetWindowPos(hWndMain,NULL,0,0,rcTemp.right-rcTemp.left,rcTemp.bottom-rcTemp.top,SWP_NOMOVE);//set the window width and height

	//create map image
	HDC hdc=GetDC(hWndMain);
	bmoMap.Create(hdc,MAPWIDTH*TILESIZE+TILESIZE*GREY,MAPHEIGHT*TILESIZE);
	FillRect(bmoMap,&rcTemp,(HBRUSH)GetStockObject(BLACK_BRUSH));
	ReleaseDC(hWndMain,hdc);

	bmoBlocks.Load(NULL,"blocks.bmp");
	NewGame();

	return(true);//return success
}

void GameDone()
{
	//clean up code goes here
}
          
Piece of cake. Actually, I'm having a cookie, but anyway, let's make the Game Loop. I bet you think this part will be hard? HAH! Well, we need to do timing, but that's about it. GetTickCount() runs in milliseconds, thus this locks the game to 1fps, and 1-second movement. You can change this in various ways, such as 1000 to 33 (for 30fps hard lock) and changing the automatic Move(0,1) on a timer (ex: if(timer==30) {timer=0; Move(0,1); } timer++;)

void GameLoop()
{
	if( (GetTickCount() - start_time) > 1000)
	{
		Move(0,1);
		start_time=GetTickCount();
	}

}
          
See? SEE? I bet you didn't believe me, did you? Bah! Anyway, now to build the NewGame() function. We set up the start_time, and we initialize the Map array, then tell it to put in a new block & new

void NewGame()
{
	start_time=GetTickCount();
	GAMESTARTED=false;

	//start out the map
	for(int x=0;x< MAPWIDTH;x++)
	{
		for(int y=0;y< MAPHEIGHT+1;y++)
		{
			if(y==MAPHEIGHT) //makes Y-collision easier.
				Map[x][y]=TILEGREY;
			else
				Map[x][y]=TILEBLACK;
		}
	}
	NewBlock();
	
	DrawMap();
}
          
Easy. Now, let's build the DrawTile() function. Simple enough, take a pair of coordinates and place the tile (block) of the specified type.. err color... er you get the picture. edit: I want to explain BitBlt() more. The first variable it needs is an a DESTINATION handle to the device context (remember the operator HDC() function? bmoMap returns that). The second two are starting X and Y coordinates, respectively, and the upper left corner of a window is 0,0, and the bottom right just depends on the size you have; in this case, 160,480. The next two determine how MUCH of the bitmap is sent to bmoMap, that is, they are the width and height respectively (since we have a square, we're using one size: TILESIZE). After that, it's another HDC, this time bmoBlocks, which is the SOURCE. Then, we have another pair of coordinates; these two determine where on the bitmap you are starting at... that is, if you wanted to be able to draw the whole bitmap, you'd use 0,0, but if you wanted to draw only the bottom right of the bitmap, you would use (for 16x16) 8,8. Lastly, you can use either SRCAND, SRCPAINT, or SRCCOPY, and these determine HOW the hmoBlocks are copied to hmoMap: SRCCOPY is a straight copy of the source that overwrites whatever's in the destination in that area, SRCAND copies the source to the destination using the AND operation (and we use it for bitmasking), while SRCPAINT copies the source to the destination using the XOR operation.

void DrawTile(int x,int y,int tile)//put a tile
{
	//mask first
	BitBlt(bmoMap,x*TILESIZE,y*TILESIZE,TILESIZE,TILESIZE,bmoBlocks,tile*TILESIZE,TILESIZE,SRCAND);
	//then image
	BitBlt(bmoMap,x*TILESIZE,y*TILESIZE,TILESIZE,TILESIZE,bmoBlocks,tile*TILESIZE,0,SRCPAINT);
}
          
Now we have a simple tile drawing function in place! Works great. First, it masks the spot with a background image (so you can do like, see-through numbers or whatever. It prints an image from bmoBlocks to bmoMap (which basically functions as a buffer). Now let's do our main rendering function; DrawMap()! Real easy, just draw the toolbar, then the preview block on top of that, then draw the background blocks and the moving piece that the player controls on that!

void DrawMap()//draw screen
{
	int xmy, ymx;

	//place the toolbar
	for(xmy=MAPWIDTH; xmy< MAPWIDTH+GREY; xmy++)
		for(ymx=0; ymx< MAPHEIGHT; ymx++)
			DrawTile(xmy, ymx, TILEGREY);

	//draw preview block
	for(xmy=0; xmy<4; xmy++)
		for(ymx=0; ymx<4; ymx++)
			if(sPrePiece.size[xmy][ymx] != TILENODRAW)
				DrawTile(sPrePiece.x+xmy, sPrePiece.y+ymx, sPrePiece.size[xmy][ymx]);
	
	//draw the map
	//loop through the positions
	for(xmy=0;xmy< MAPWIDTH;xmy++)
		for(ymx=0;ymx< MAPHEIGHT;ymx++)
				DrawTile(xmy,ymx,Map[xmy][ymx]);

	//draw moving block
	for(xmy=0; xmy<4; xmy++)
		for(ymx=0; ymx<4; ymx++)
			if(sPiece.size[xmy][ymx] != TILENODRAW)
				DrawTile(sPiece.x+xmy, sPiece.y+ymx, sPiece.size[xmy][ymx]);

	//invalidate the window rect
	InvalidateRect(hWndMain,NULL,FALSE);
}
          
We're doing good! Now, let's tackle the NewBlock() function. This is where we generate our little blocks. Let's assume the default shapes that Tetris(TM) fans are familiar with. We'll need to generate a preview piece, then replace the piece the player uses when s/he's finished with the current one. We'll also need to do a special case generation for the initial startup of the game.

void NewBlock()
{
	int newblock;
	int i,j;
	//  0   1   2   3   4    5   6    
	//   X                             These
	//   X   XX   X  XX   XX  XX   XX  are
	//   X   XX  XXX  XX XX    X   X   block
	//   X                     X   X   types

	//begin game! make generate a block and then one in preview.

	srand(GetTickCount());


	//initialize the piece to all blank.
	for(i=0; i<4; i++)
		for(j=0; j<4; j++)
			sPiece.size[j]=TILENODRAW;

	sPiece.x=MAPWIDTH/2-2;
	sPiece.y=-1;

	//let's see if the game's started yet
	if(GAMESTARTED == false)
	{
		//guess not..  
		//Generate a piece right off.
		//From now &#111;n, use previous preview block.
		GAMESTARTED=true;

		newblock=rand()%7;

		switch (newblock)
		{
		case 0: //Tower!
			{
				sPiece.size[1][0]=TILERED;
				sPiece.size[1][1]=TILERED;
				sPiece.size[1][2]=TILERED;
				sPiece.size[1][3]=TILERED;
				sPiece.y=0;
			}break;
		case 1: //Box!
			{
				sPiece.size[1][1]=TILEBLUE;
				sPiece.size[1][2]=TILEBLUE;
				sPiece.size[2][1]=TILEBLUE;
				sPiece.size[2][2]=TILEBLUE;
			}break;
		case 2: //Pyramid!
			{
				sPiece.size[1][1]=TILESTEEL;
				sPiece.size[0][2]=TILESTEEL;
				sPiece.size[1][2]=TILESTEEL;
				sPiece.size[2][2]=TILESTEEL;
			}break;
		case 3://Left Leaner
			{
				sPiece.size[0][1]=TILEYELLOW;
				sPiece.size[1][1]=TILEYELLOW;
				sPiece.size[1][2]=TILEYELLOW;
				sPiece.size[2][2]=TILEYELLOW;
			}break;
		case 4://Right Leaner
			{
				sPiece.size[2][1]=TILEGREEN;
				sPiece.size[1][1]=TILEGREEN;
				sPiece.size[1][2]=TILEGREEN;
				sPiece.size[0][2]=TILEGREEN;
			}break;
		case 5://Left Knight
			{
				sPiece.size[1][1]=TILEWHITE;
				sPiece.size[2][1]=TILEWHITE;
				sPiece.size[2][2]=TILEWHITE;
				sPiece.size[2][3]=TILEWHITE;
			}break;
		case 6://Right Knight
			{
				sPiece.size[2][1]=TILEPURPLE;
				sPiece.size[1][1]=TILEPURPLE;
				sPiece.size[1][2]=TILEPURPLE;
				sPiece.size[1][3]=TILEPURPLE;
			}break;
		}
	}
	else
	{
		for(i=0; i&lt;4; i++)
			for(j=0; j&lt;4; j++)
				sPiece.size[j]=sPrePiece.size[j];

	}

	newblock=rand()%7;

	for(i=0; i&lt;4; i++)
		for(j=0; j&lt;4; j++)
			sPrePiece.size[j]=TILENODRAW;

	sPrePiece.x=MAPWIDTH+GREY/4;
	sPrePiece.y=GREY/4;

	switch (newblock)
	{
		case 0: //Tower!
			{
				sPrePiece.size[1][0]=TILERED;
				sPrePiece.size[1][1]=TILERED;
				sPrePiece.size[1][2]=TILERED;
				sPrePiece.size[1][3]=TILERED;
			}break;
		case 1: //Box!
			{
				sPrePiece.size[1][1]=TILEBLUE;
				sPrePiece.size[1][2]=TILEBLUE;
				sPrePiece.size[2][1]=TILEBLUE;
				sPrePiece.size[2][2]=TILEBLUE;
			}break;
		case 2: //Pyramid!
			{
				sPrePiece.size[1][1]=TILESTEEL;
				sPrePiece.size[0][2]=TILESTEEL;
				sPrePiece.size[1][2]=TILESTEEL;
				sPrePiece.size[2][2]=TILESTEEL;
			}break;
		case 3://Left Leaner
			{
				sPrePiece.size[0][1]=TILEYELLOW;
				sPrePiece.size[1][1]=TILEYELLOW;
				sPrePiece.size[1][2]=TILEYELLOW;
				sPrePiece.size[2][2]=TILEYELLOW;
			}break;
		case 4://Right Leaner
			{
				sPrePiece.size[2][1]=TILEGREEN;
				sPrePiece.size[1][1]=TILEGREEN;
				sPrePiece.size[1][2]=TILEGREEN;
				sPrePiece.size[0][2]=TILEGREEN;
			}break;
		case 5://Left Knight
			{
				sPrePiece.size[1][1]=TILEWHITE;
				sPrePiece.size[2][1]=TILEWHITE;
				sPrePiece.size[2][2]=TILEWHITE;
				sPrePiece.size[2][3]=TILEWHITE;
			}break;
		case 6://Right Knight
			{
				sPrePiece.size[2][1]=TILEPURPLE;
				sPrePiece.size[1][1]=TILEPURPLE;
				sPrePiece.size[1][2]=TILEPURPLE;
				sPrePiece.size[1][3]=TILEPURPLE;
			}break;
	}

	DrawMap();
}
          </pre>          

Ah, randomization.  You could adjust these values to a more weighted approach, but these are straight, equal chance randomizations.

Now, let's build our rotation function, RotateBlock(). Simple and easy: copy & rotate to a temporary, then check collisions, then copy it back to the original.  Like switching two variables, sorta.

<pre>
void RotateBlock()
{
	int i, j, temp[4][4];

	//copy &rotate the piece to the temporary array
	for(i=0; i&lt;4; i++)
		for(j=0; j&lt;4; j++)
			temp[3-j]=sPiece.size[j];

	//check collision of the temporary array with map borders
	for(i=0; i&lt;4; i++)
		for(j=0; j&lt;4; j++)
			if(temp[j] != TILENODRAW)
				if(sPiece.x + i &lt; 0 || sPiece.x + i &gt; MAPWIDTH - 1 ||
					sPiece.y + j &lt; 0 || sPiece.y + j &gt; MAPHEIGHT - 1)
					return;

	//check collision of the temporary array with the blocks &#111;n the map
	for(int x=0; x&lt; MAPWIDTH; x++)
		for(int y=0; y&lt; MAPHEIGHT; y++)
			if(x &gt;= sPiece.x && x &lt; sPiece.x + 4)
				if(y &gt;= sPiece.y && y &lt; sPiece.y +4)
					if(Map[x][y] != TILEBLACK)
						if(temp[x - sPiece.x][y - sPiece.y] != TILENODRAW)
							return;

	//end collision check

	//successful!  copy the rotated temporary array to the original piece
	for(i=0; i&lt;4; i++)
		for(j=0; j&lt;4; j++)
			sPiece.size[j]=temp[j];
	
	DrawMap();

	return;
}
          </pre>          

And you thought game programming was hard.  Well, it can be.  Just not Tetris-clones.  Now, let's build our Move() function.  Check for a collision, then move, or put piece to map / remove row / start a new game if you're too high.  

<pre>
void Move(int x, int y)
{
	if(CollisionTest(x, y))
	{
		if(y == 1)
		{
			if(sPiece.y&lt;1)
			{
				//you lose!  new game.
				NewGame();
			}
			else
			{
				bool killblock=false;
				int i,j;
				//new block time! add this &#111;ne to the list!
				for(i=0; i&lt;4; i++)
					for(j=0; j&lt;4; j++)
						if(sPiece.size[j] != TILENODRAW)
							Map[sPiece.x+i][sPiece.y+j] = sPiece.size[j];

				//check for cleared row!
				for(j=0; j&lt; MAPHEIGHT; j++)
				{
					bool filled=true;
					for(i=0; i&lt; MAPWIDTH; i++)
						if(Map[j] == TILEBLACK)
							filled=false;

					if(filled)
					{
						RemoveRow(j);
						killblock=true;
					}
				}

				if(killblock)
				{
					for(i=0; i&lt;4; i++)
						for(j=0; j&lt;4; j++)
							sPiece.size[j]=TILENODRAW;
				}
				NewBlock();
			}
		}

	}
	else
	{
		sPiece.x+=x;
		sPiece.y+=y;
	}

	DrawMap();
}
          </pre>          

Now, let's put in the CollisionTest() function.  Really easy, just testing bounds and if it its another block.

<pre>
int CollisionTest(int nx, int ny)
{
	int newx=sPiece.x+nx;
	int newy=sPiece.y+ny;

	int i,j,x,y;

	for(i=0; i&lt; 4; i++)
		for(j=0; j&lt; 4; j++)
			if(sPiece.size[j] != TILENODRAW)
				if(newx + i &lt; 0 || newx + i &gt; MAPWIDTH - 1 ||
					newy + j &lt; 0 || newy + j &gt; MAPHEIGHT - 1)
					return 1;

	for(x=0; x&lt; MAPWIDTH; x++)
		for(y=0; y&lt; MAPHEIGHT; y++)
			if(x &gt;= newx && x &lt; newx + 4)
				if(y &gt;= newy && y &lt; newy +4)
					if(Map[x][y] != TILEBLACK)
						if(sPiece.size[x - newx][y - newy] != TILENODRAW)
							return 1;
	return 0;
}
    </pre>    

Simple, yes?  Now, the RemoveRow() function.  Easy stuff here, just deleting a row and moving the rest down.

<pre>
void RemoveRow(int row)
{
	int x,y;
	int counter=0;

	for(x=0; x&lt; MAPWIDTH; x++)
		for(y=row; y&gt;0; y–)
			Map[x][y]=Map[x][y-1];

}
          </pre>          

And that's it!  You've got the code, now you just need the graphics.  To make the graphics, create a new bitmap of a size TILESIZE*9 width, and TILESIZE*2 height.

Now, divide this into 16x16 squares, or whatever TILESIZE you have, and you'll have two rows of 9 columns.  Make the bottom row totally black (0 r,0 g,0 b).  Make each column of the top row a different color.  In order, make the columns Black, Grey, Blue, Red, Green, Yellow, White, Steel, and Purple.  Black and Grey should be totally 1 color, while (for a good look!) the other colors should be in this format:

<pre>
x x x x x x x x x x x x x x x x
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
x o o o o o o o o o o o o o o m
m m m m m m m m m m m m m m m m
    </pre>          

Where o = normal shade of a color, x = lighter shade of the color, and m = darker shade of the color.

Enjoy.
-Greven

edit: annoying smilies.
and the italic code. Gr.
and the weird vanishing code. Double Gr.

edit2:
Re-added CollisionTest() function.  Sorry about that.  I had had it before, but somehow I had deleted it when trying to fix the vanishing code / italic code problems.  My appologies.

edit3:
another annoying forum/code problem.
added more detailed description of BitBlt() and timing

edit4:
changed the description of #define WIN32_LEAN_AND_MEAN, per correction from James Gregory

edit5:
Thanks for the SDL version, MetaCipher!

<!–EDIT–><span class=editedby><!–/EDIT–>[Edited by - Evil_Greven on July 13, 2004 12:13:46 AM]<!–EDIT–></span><!–/EDIT–>
Advertisement
Nice job on this, I hope it helps many!

daveandrews.org - a Christian Programmer''s Weblog | Dusty Engine - a task engine using Irrlicht
i think that is quite possibly the longest post i''ve seen on the gamedev forums... why not html-ize it and send it in as an article?
"I never let schooling interfere with my education" - Mark Twain
Hi,

Have you left out the CollisionTest(int, int) function?
I can see the prototype but not the actual function(and neither can my compiler ).

Thanks for an excellent tutorial.

Tronn.
Oh, hell, I did, didn''t I?

Crap! Here, I''ll put it in, I must have forgotten to copy & paste.

-Greven
Evil_Greven, perhaps you should put this in a HTML file, rather than on a topic at GameDev.net. Or, you could go here: http://gamedev.net/info/writers.asp


Quick Clickys: [ WiseElben.com | My Journal | nMagic | My Profile ]
"Give a man a fish and he will eat for a day. Teach a man how to fish and he will eat for a life time."
-Chinese Proverb
Excellent. It now compiles and runs.:D

I did have to change this line:

wcx.nonononononononostyle=CS_OWNDC | CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

To this:

wcx.nostyle=CS_OWNDC | CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

After that it compiled fine apart from a couple of warnings.

Now I just need to go through it with fine toothed comb and work out how it all works.:D

Tronn
Oops. I changed the line to:

wcx.nostyle=CS_OWNDC | CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

Tronn.
Thats strange. The line I changed says ''wcx.style'' but every time I post it it changes to ''wcx.nostyle''.

Tronn
rite, sorry amaeur - love the post, but do you create the bitmap with the class? im sorry this may sound really really bad, or in paint for example. im just trying to learn off the tutorial and study the code. thnxs again for the topic.
nufc forever ppl....

This topic is closed to new replies.

Advertisement