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 on, 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<4; i++)
for(j=0; j<4; j++)
sPiece.size[j]=sPrePiece.size[j];
}
newblock=rand()%7;
for(i=0; i<4; i++)
for(j=0; j<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<4; i++)
for(j=0; j<4; j++)
temp[3-j]=sPiece.size[j];
//check collision of the temporary array with map borders
for(i=0; i<4; i++)
for(j=0; j<4; j++)
if(temp[j] != TILENODRAW)
if(sPiece.x + i < 0 || sPiece.x + i > MAPWIDTH - 1 ||
sPiece.y + j < 0 || sPiece.y + j > MAPHEIGHT - 1)
return;
//check collision of the temporary array with the blocks on the map
for(int x=0; x< MAPWIDTH; x++)
for(int y=0; y< MAPHEIGHT; y++)
if(x >= sPiece.x && x < sPiece.x + 4)
if(y >= sPiece.y && y < 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<4; i++)
for(j=0; j<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<1)
{
//you lose! new game.
NewGame();
}
else
{
bool killblock=false;
int i,j;
//new block time! add this one to the list!
for(i=0; i<4; i++)
for(j=0; j<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< MAPHEIGHT; j++)
{
bool filled=true;
for(i=0; i< MAPWIDTH; i++)
if(Map[j] == TILEBLACK)
filled=false;
if(filled)
{
RemoveRow(j);
killblock=true;
}
}
if(killblock)
{
for(i=0; i<4; i++)
for(j=0; j<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< 4; i++)
for(j=0; j< 4; j++)
if(sPiece.size[j] != TILENODRAW)
if(newx + i < 0 || newx + i > MAPWIDTH - 1 ||
newy + j < 0 || newy + j > MAPHEIGHT - 1)
return 1;
for(x=0; x< MAPWIDTH; x++)
for(y=0; y< MAPHEIGHT; y++)
if(x >= newx && x < newx + 4)
if(y >= newy && y < 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< MAPWIDTH; x++)
for(y=row; y>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–>