Tetris Primer

From GPWiki

Files:GUITutorial_warn.gif The Game Programming Wiki has moved! Files:GUITutorial_warn.gif

The wiki is now hosted by GameDev.NET at wiki.gamedev.net. All gpwiki.org content has been moved to the new server.

However, the GPWiki forums are still active! Come say hello.

Contents

So, you want to make a Tetris clone...

All well-versed and experienced game programmers will recommend that newcomers cut their teeth with something simple before hoping to tackle their grander aspirations. And the absolute classic first game is Tetris. But it is evident by the numerous questions asked by newbies attempting Tetris that it is not as simple as it may seem.

This text is not intended as the end-all and be-all of Tetris game development, and it will only demonstrate one technique to help get you into the mindset of game programming. Going through more than one method at a time could prove confusing to anyone just getting their feet wet.

Where do I begin?

You should start, of course, at the beginning. Here is the minimum of what you'll need:

A language

(If you already know or have chosen a language, you may skip this section.) Choosing a language with which to begin game programming is a difficult and important decision, which falls far outside the scope of this primer. See the article on Picking_a_Language for more information.

Just keep in mind that as a newbie you need a language with whose difficulty you will be able to cope, while not hindering potential progress to other languages in the future by encouraging poor programming practices or needlessly hiding complexity. Also look for a language that is well known and widely used enough that you will not have difficulty finding resources.

This article uses rough C++ for the code snippets, but it should be trivial to convert them to another object oriented language. If your language does not use object orientation, the code will require further adaptation.

A compiler/IDE

(If you already know or have chosen a compiler/IDE, you may skip this section.) Once you know a language, you will need some sort of interface to render useful programs from your code -- a development environment. These vary greatly in advancement, availability, size, and ease of use. Listing potential development environments is again outside the scope of this primer. See the Tools:Programming Programming tools page for more information.

A Library

(If you already know or have chosen a library, you may skip this section.) The vast majority of programming languages do not have in-built functionality for the advanced features commonly required by games: graphics, audio, inut, etc. For this you need a library that is compatible with your language. For more information on potential libraries, see the Libraries libraries page, or the library specific tutorial listings for your language page.

Basic experience

Before starting on a game, you should be familar with all of the basics of your language. You can either get a book (view the resources section of GPWiki), or you can go through a number of tutorials. It all depends on your preferred method of learning.

Things you will need to learn:

  • Your integrated development environment (IDE) or development environment
  • Basic program flow and language constructs
  • File input and output
  • User input: mouse and keyboard, maybe joystick (Accomplished by your library)
  • Simple 2D graphics: Blitting to screen (Accomplished by your library)

An Idea

We already have that idea: a Tetris clone. If you wish to make up your own game, make sure you understand completely the rules (game physics) and the gameplay as you understand those of Tetris.


I know my language. Now what?

Now you design, in as much detail as you can. Think about how your game works. For instance, we know that Tetris is composed of several shapes that are, in turn, composed of squares (or pumpkins or apples), and these squares all fall into a grid. Here is a basic breakdown of a potential design:

  • The main playing board is a grid of empty cells which will be filled with Tetris shapes
  • Each cell can be empty or can contain a piece of a tetris shape (a block).
  • Each tetris shape contains a series of blocks.
  • These blocks fall, rotate, and, when they contact another block or the side of the screen, stop.

Game Structures

Now we need to figure out our basic structures. For this, we are using C++ semantics, but hopefully you will be able to easily translate it to your chosen language. Keep in mind that this code will not compile and is missing several essential elements. It is used only to help you visualize the design.

const int NUM_ROWS = 50,
          NUM_COLS  = 20;
 
class Block
{
	// the image that we assign to this block
	Image m_img; /* class Image is beyond the scope of this article */
};
 
class Shape
{
	// Our shape is composed of a small grid of blocks (or block pointers)
	// if the block pointer is 0, there is no block there.
	Block *m_listOfBlocks[4][4];
};
 
class Cell
{
	// this lets us know if there is a block in this cell
	// it should initially be empty.
	bool m_isEmpty;
	
	// This is the current block that is in this cell
	Block m_block;
};
 
class TetrisGrid
{
	// this is our array of cells. each is initially empty.
	Cell m_listOfCells[NUM_ROWS][NUM_COLS];
};

You should know enough about your language by now to fill in the gaps in the above code and at least get it to compile. You should also create the necessary constructors and destructors.

The Main Loop

Now we need to think about the main loop. This is where we code in the basic programming logic. One technique is to write out the pseudocode of how you think the game logic should flow. One simple design for tetris might look like the following:

Initialize our shapes
	Create the grid
	
	do
	{
		Grab a random shape from our list and put it just above the grid.
		while (shape can drop to the next line)
		{
			drop the shape down a line
			draw the grid and shape
			delay the timer and check for input
		}
		Add the shape to the grid permanantly
	} while (the shape is completely inside the grid [i.e. not sticking above the top]);

Now to convert this to actual code, just go through a line at a time and code in the functionality.

Initialize our shapes

There are many ways to initialize our shapes. The following seems pretty straightforward, so it's the one I chose:

// A list of char strings that define the tetris shapes
// An X designates a slot occupied by a block.
const char *shapeDefs[] = {
	"XXXX"
	"    ",
	
	"XXX "
	"X   ",
 
	"XXX "
	"  X ",
	
	...
};
 
Shape g_Shapes[] = {
	Shape(shapeDefs[0], RGB(0x00, 0x00, 0xFF)),
	Shape(shapeDefs[1], RGB(0x00, 0xFF, 0x00)),
	Shape(shapeDefs[2], RGB(0xFF, 0x00, 0x00)),
	...
};

Then you just have to create a Shape constructor that parses each string and applies it to it's grid of blocks. I added a color parameter there to also assign each shape a color (passed to the blocks). You could also pass a filename for the block to load, or use some other method for initializing the blocks.

Shape::Shape(const char strShape, int color = 0)
{
    memset(m_listOfBlocks, 0, sizeof(m_listOfBlocks));
 
    // we need to convert 2 rows to 4
    int srcRows = strlen(strShape) / 4;
 
    int offset = 0;
    for (int y = 0; y < srcRows; y++)
        for (int x = 0; x < 4; x++)
            if (strShape[offset++] == 'X')
                m_listOfBlocks[y][x] = new Block(CELL_WIDTH, CELL_HEIGHT, color);
}

Create the grid

This one is pretty easy, assuming you have set up the default constructors to initialize everything as empty.

Grid tetrisGrid();

You could also set it up to accept the ROWS and COLS for the grid size, but that is left as an exersize for the reader.

Grab a random shape

Shape *RandomShape()
{
    Shape *shape = g_Shapes[RandomInt(0, SHAPE_COUNT)];
    shape->Reset(); // Reset our shape's rotation and position
 
    return shape;
}
 
void Shape::Reset()
{
	while (m_rotation > 0)
	{
		RotateCCW();
		m_rotation--;
	}
 
	while (m_rotation < 0)
	{
		RotateCW();
		m_rotation++;
	}
	
        // start in the center, just above the grid
	m_position.x = (NUM_COLS + 4) / 2;
	m_position.y = -2;
}

The Reset method will become apparent later when we look at the rotate method.

While shape can drop

A shape can only drop to the next line if none of the blocks of the shape move into occupied squares.

bool ShapeCanFall(Grid &tetrisGrid, Shape &shape)
{
    int startY = shape.position.y + 1;
    int startX = shape.position.x;
 
    // for each block in our shape
    for (int y = 0; y < 4; y++)
        for (int x = 0; x < 4; x++)
        {
            // if this block exists
            if (shape.m_listOfBlocks[y][x])
                // if at bottom of grid or destination cell is not empty
                if (y+startY >= NUM_COLS ||
                    !tetrisGrid.m_listOfCells[y+startY][x+startX].m_isEmpty)
                    return false;
        }
 
    return true;
}

Drop the shape down

Dropping the shape is just a matter of incrementing its y position.

shape->position.y++;

Draw the grid

First, we draw the grid and the current blocks in the grid, then we draw the shape onto the grid.

...
 
	tetrisGrid.Draw();
	shape->Draw();
 
...
 
void Grid::Draw()
{
        // for each non-empty cell, draw the associated block
        // for each empty cell, draw the null block.
	for (int y = 0; y < NUM_ROWS; y++)
		for (int x = 0; x < NUM_COLS; x++)
			if (!m_listOfCells[y][x].m_isEmpty)
				m_listOfCells[y][x].m_block.Draw(x*CELL_WIDTH, y*CELL_HEIGHT);
			else
				g_NullBlock.Draw(x*CELL_WIDTH,y*CELL_HEIGHT);
			
}
 
void Shape::Draw()
{
        // for each non-null pointer in our block list, blit the block
	for (int y = 0; y < 4; y++)
		for (int x = 0; x < 4; x++)
			if (m_listOfBlocks[y][x])
				m_listOfBlocks[y][x]->Draw(
					(x+m_position.x)*CELL_WIDTH,
					(y+m_position.y)*CELL_HEIGHT
				);
}

The main thing to keep in mind here is that even though a grid cell is empty, we still need to draw something there to overlap any previous block we drew. The shape's blocks, on the other hand, are only drawn if they exist.

Delay and check input

// For increased difficulty, we set delay to a lower value,
// which makes each frame update more quickly.
void DelayAndCheckUserInput(Shape *shape, int delay = 100)
{
	int milliseconds = GetTime();
	while (GetTime() - miliseconds < delay)
	{
		CheckInput(shape);
	}
}
 
void CheckInput(Shape *shape)
{
	switch (GetKey())
	{
		case KEY_CW:
			shape->RotateCW();
			break;
		case KEY_CCW:
			shape->RotateCCW();
			break;
		...
	}
}
 
void Shape::RotateCW()
{
	m_rotation++;
	
	Block *listBlocks[4][4];
	
        // perform the actual rotation
	for (int y = 0; y < 4; y++)
		for (int x = 0; x < 4; x++)
		{
			listBlocks[y][x] = m_listOfBlocks[3-x][y];
		}
		
	ShiftBlocksToUpperLeft();
	
	memcpy(m_listOfBlocks, listBlocks, sizeof(m_listOfBlocks));
}

The definition "Shape::ShiftBlocksToUpperLeft()" was omitted, as well as the definition of "Shape::RotateCCW()". They are left as an exercise for the reader.

Set the shape

Here, we just iterate through each position of the grid and assign each of the shape's active blocks to it.

void Grid::EmbedShape(Shape &shape)
{
    int offsetY = shape.position.y;
    int offsetX = shape.position.x;
 
    for (int y = 0; y < 4; y++)
    {
        if (y+offsetY < 0) continue;
 
        for (int x = 0; x < 4; x++)
            if (shape.m_listOfBlocks[y][x])
                m_listOfCells[y+offsetY][x+offsetX] = *shape.m_listOfBlocks[y][x];
    }
}

While shape is in grid

This one is also pretty easy. We just need to check that the shape's [y] position is not negative.

Do
    {
        ...
    } while(shape->position.y >= 0);