Kindly hosted by
Tetris Tutorial: Part #3
Hello there and welcome to the third tutorial on how to make
a decent Tetris game.
If you haven’t read Tutorial #1 & Tutorial #2, please do so, otherwise
you won’t understand most of this and next tutorials. So, let’s get started.
This time we are going to introduce a falling figure with various time
steps that a user can control. At the end, where the figure won’t be
able to fall anymore, we are going to simply map it’s blocks to game
blocks array and drop some other figure – can’t get any simpler!
So, let’s get started.
If you take a look at a general tetris game, there are only two steps
exist:
Step 1: Drop figure one line down
Step 2: Wait some little time if user considers to move a figure to left
or right so it can fill the empty block space as shown in Picture 1.
---------------Repeat from Step 1---------------
Picture #1
We may also add a third step where user doesn’t actually do anything
– a step where the game do something when eliminating full lines – let’s
call it Step 3. So, here’s our new pipeline:
Step 1: Drop figure one line down
Step 2: Wait some little time if user considers to move a figure to left
or right so it can fill the empty block space.
Step 3: Check if there are any full lines – if none, resume normal game
state
Step 3.5: If there are, eliminate the bottom one neglecting any user
input
Step 3.5.5: Go to step 3
Or in a pseudo-C++ form:
int LineFull = -1;
for (I=0; I<Number_of_lines; I++)
{
if(LineIsFull(I))
{
LineFull = I;
break;
}
}
If(LineFull !=-1)
{
// Do animations for that line…
}
else
{
// Aquire user input
// Etc
}
}
Pretty simple as it seems here, but harder in actual implementation
of the algorithm.
Let’s introduce a function
that will be checking for fullness of the line with given index:
bool CTetrisGame::LineIsFull(unsigned int line_index)
{
// Please note that here we use 'BlockSize' and 'BlockSpacing' for dynamic variables, not constant defines
// We will go into details of loading them later on.
int num_blocks_x = Width(Window) / (BlockSize + BlockSpacing);
for (int x=0; x<num_blocks_x; x++)
{
if(m_ppTetrisMatrix[line_index][x] == NULL)
return false;
}
return true;
}
Also, we have to add a function in which we are going to do all state
checks, line eliminations et cetera. Let’s call it Update() and we are
going to call it before actually drawing the whole thing.
One more idea: if a user would want to pause the game (e.g go to the
menu screen), we are going to have to pause whole update process, but
still draw it to the screen. So, let us introduce a variable into CTetrisGame
class called m_eGameState that will keep track of our current game state.
Also in file Common.h we add a definition for the variable TIME_AQUIRE_USER_INPUT
that is equals to 500 – this variable will be the time (in milliseconds)
that the user have to move figure left or right after the figure has
fell one line down. To actually watch after the time passed and the current
time, we add a variable in CtetrisGame called m_fLastTime, and we can
retrieve the time by firstly declaring a timer variable as CTimer m_Timer
as a member of aforementioned class.
Let’s start with the hardest part - the Update() function:
void CTetrisGame::Update()
{
if(m_eGameState == TetrisGame_Paused)
return;
int num_blocks_x = WINDOW_WIDTH / (BLOCK_SIZE + BLOCK_SPACING);
int num_blocks_y = WINDOW_HEIGHT / (BLOCK_SIZE + BLOCK_SPACING);
// If our game state is normal, proceed..
if(m_eGameState == TetrisGame_Normal)
{
// Check the amount of time passed since last update
double CurrentTime = m_Timer.getAbsoluteTime();
if(CurrentTime - m_fLastTime >= TIME_AQUIRE_USER_INPUT)
{
// If a figure cannot be moved anymore downwards…
if(Move(DISPLACE_DOWN, true) == false)
{
// Check for full lines since there has to be no more user input
for (int i=0; i<num_blocks_y; i++)
{
if(LineIsFull(i))
{
// There is a full line, assign the index and break from the loop
m_iFullLine = i;
m_eGameState = TetrisGame_EliminatingLine;
break;
}
}
// If there’s no full line, drop a new figure
if(m_iFullLine == -1)
DropRandomFigure();
}
else
{
// Our figure CAN be moved down, so move it now
Move(DISPLACE_DOWN, false);
// Update last time
m_fLastTime = CurrentTime;
}
}
}
// If there are any full lines (from the check above) - animate that line until the end
if(m_eGameState == TetrisGame_EliminatingLine)
{
// If AnimateLine() returns true, it means that all blocks have finished their current animation
if(AnimateLine(m_iFullLine))
{
// Line blocks animations are complete, remove this line from game blocks array
for (int i=0; i<num_blocks_x; i++)
m_ppTetrisMatrix[m_iFullLine][i] = NULL;
// Shift above lines down
ShiftLines(m_iFullLine);
// Restore memeber variables for next round
m_eGameState = TetrisGame_Normal;
m_iFullLine = -1;
}
}
}
Pretty hard… Ummm.. Let’s see the way it goes.
Firstly we test if the game is paused, and if it is, we do nothing in
Update() (e.g return – exit). Then we check for the time passed, and
if it exceeds the defined value, we drop the figure one line down –and
here where the trick lies. Since we do not change the game state, it
simply goes again thru the same part it went before for the SAME amount
of time - in our game it is 500 msecs before dropping figure again one
line down.
Pretty tricky, in my old version I’ve used 3 phases (drop figure down,
wait for user input, check for full lines), here we are using only 2!
A much more elegant and simpler approach ?.
Okay, after we’re done with user input and figure dropping, we move onto
lines checking. Here it is very simple – animate current line until all
blocks animations are complete (AnimateLine() returns false), and after
that remove that line blocks from the game blocks array and shift above
lines down and restore game state and line index.
Let’s move on. Now we need to add auxiliary functions: ShiftLines() and
DropRandomFigure(). I’ve copied the code from InitGL() to create a figure
(square) into DropRandomFigure() since that’s about what we need now
- later on we add a random number generator to generate truly random
figures. Let’s see what its contents are:
void CTetrisGame::DropRandomFigure()
{
CTetrisFigure *figure = new CTetrisFigure;
figure->Create(4);
int num_blocks_x = WINDOW_WIDTH / (BLOCK_SIZE + BLOCK_SPACING);
int num_blocks_y = WINDOW_HEIGHT / (BLOCK_SIZE + BLOCK_SPACING);
figure->GetBlock(0)->SetPosX(num_blocks_x / 2);
figure->GetBlock(0)->SetPosY(num_blocks_y - 2);
figure->GetBlock(1)->SetPosX((num_blocks_x / 2) + 1);
figure->GetBlock(1)->SetPosY(num_blocks_y - 2);
figure->GetBlock(2)->SetPosX((num_blocks_x / 2) + 1);
figure->GetBlock(2)->SetPosY(num_blocks_y - 1);
figure->GetBlock(3)->SetPosX(num_blocks_x / 2);
figure->GetBlock(3)->SetPosY(num_blocks_y - 1);
m_pCurrentFigure = figure;
MapFigure(m_pCurrentFigure, true);
}
As I said, a copy & paste task.
Let’s see how ShiftLines() function works:
void CTetrisGame::ShiftLines(unsigned int line_eliminated)
{
int num_blocks_x = WINDOW_WIDTH / (BLOCK_SIZE + BLOCK_SPACING);
int num_blocks_y = WINDOW_HEIGHT / (BLOCK_SIZE + BLOCK_SPACING);
// Remeber, we are shifting lines
// that are above the eliminated one
for (int i=line_eliminated+1; i<num_blocks_x; i++)
{
// Shift this line down
for (int j=0; j<num_blocks_x; j++)
{
// Move this line down with coordinates
if(m_ppTetrisMatrix[i][j] !=NULL)
m_ppTetrisMatrix[i][j]->Move(DISPLACE_DOWN, false);
// Move line down in pointers (inside game blocks array)
m_ppTetrisMatrix[i-1][j] = m_ppTetrisMatrix[i][j];
// Eliminate moved line in pointers (inside game blocks array)
m_ppTetrisMatrix[i][j] = NULL;
}
}
}
And that’s it, we’re done! Now let’s see how our game looks now on the
screen!
Some minor things that are left – add these lines to InitiGL():
gTetrisGame.Create();
gTetrisGame.DropRandomFigure();
In Tutorial 4 we will go into loading our data from XML files.
See ya later!
|