|
Kindly hosted by
Tetris Tutorial: Part #2
Hello there and welcome to the second tutorial on how to
make a decent Tetris game.
If you haven’t read Tutorial #1, please do so, otherwise you won’t
understand most of this and next tutorials. So, let’s get started.
Last tutorial we go to the place where we could define block positions
in a figure and draw them. This time we will try to make those blocks
move and rotate according to the given input.
Let’s take an example figure: 3 blocks in horizontal line. We want
to be able to rotate it in 90 degrees each time a user presses rotation
key. If user presses Right Key, figure will rotate to right, effectively
becoming a 3 blocks in a column figure. If a user wants to rotate it
again to left or right, it will become what it was at start – 3 blocks
in horizontal line. Pretty simple for start, but let’s add something
extra: we would want to rotate block/figure in a given direction around
a given origin – so, here’s the basic function:
bool Rotate(int direction, int origin_x, int origin_y, bool can_rotate_test);
Explaining:
Direction – the direction
we want to rotate block in (Left or Right)
origin_x & origin_y –
the given origin around which we rotate the block
can_rotate_test – if
it’s true, do the test if we can rotate this block around given origin
in given direction and not bump into boundaries(game window borders).
Otherwise just rotate the block.
The function contents are as follows:
bool CTetrisBlock::Rotate(int dir, int origin_x, int origin_y, bool can_rotate_test)
{
int next_x_pos, next_y_pos;
// Calculate block offset from given origin
int offset_x = m_iPosX - origin_x;
int offset_y = m_iPosY - origin_y;
// If there’s no offset, we’re trying to rotate block around itself, so do nothing
// (we can rotate block around itself, so return true)
if(offset_x ==0 && offset_y == 0)
return true;
// Now, this is the main part of the algorithm – rotation.
// If you would take a look on general rotation in math,
// they use sin(angle) and cos(angle) to find out x/y position of rotated point ‘aingle’ radians.
// Here we use a simplified approach and swap X with Offset of Y, and Y with negative Offset of X.
int X = offset_y;
int Y = -offset_x;
// So, the next position effectively becomes an origin coordinate (x/y) plus rotated XY vector values.
next_x_pos = origin_x + X;
next_y_pos = origin_y + Y;
// If user requests test of possibility of the rotation, do the same thing as above, and..
if(can_rotate_test)
{
int num_blocks_x = WINDOW_WIDTH / (BLOCK_SIZE + BLOCK_SPACING);
int num_blocks_y = WINDOW_HEIGHT / (BLOCK_SIZE + BLOCK_SPACING);
// If block is in any boundary (min/max), return false (e.g ‘cannot rotate’)
// if(next_y_pos == 0 || next_y_pos == num_blocks_y)
return false;
if(next_x_pos == -1|| next_x_pos == num_blocks_x)
return false;
}
// No, user requests simple rotation, so do it – we pre-calculated next x/y position,
// so simply assign values to member variables.
else
{
m_iPosX = next_x_pos;
m_iPosY = next_y_pos;
}
// Return true if user did or didn’t requested testing because we did test before,
// and if we passed it, it means that it’s all OK.
return true;
}
Now, if you don’t understand how the rotation works, I suggest you
go into Debug mode and step thru and see the variables after and before
rotation. At first I was also wondering on how it worked, but then
it just sat there and I accepted it the way it is!
OK, we’re thru with rotation of block, let’s add a function so we
can rotate a figure.
The function name and all parameters are the same, except that we don’t
need origin coordinates, since a figure is simply a wrapper around
an array of blocks:
Here we assume that every’ figure origin block is at 0 index – an
example is the 3 blocks in horizontal line – it’s origin is at relative
position of (0,0), it’s left block is at (-1,0) and it’s right block
is at (1,0). So, each time we do a rotation, we do it around the origin
block.
bool CTetrisFigure::Rotate(int dir, bool can_rotate_test)
{
// If user has requested test for possibility of figure rotation, do it
if(can_rotate_test)
{
for (int i=0; i<GetBlockCount(); i++)
{
if(GetBlock(i) !=NULL)
{
// If any block of the figure cannot be rotated, return false!
if(!GetBlock(i)->Rotate(dir, GetBlock(0)->GetPosX(), GetBlock(0)->GetPosY(), true))
return false;
}
}
}
else
{
for (int i=0; i< GetBlockCount(); i++)
{
if(GetBlock(i) !=NULL)
GetBlock(i)->Rotate(dir, GetBlock(0)->GetPosX(), GetBlock(0)->GetPosY(), false);
}
}
// Return true because the user can rotate the figure!
return true;
}
Pretty simple as well. Figure just tests if each block can be rotated,
and if not, returns false, otherwise it returns true.
Now let’s get into movement of block. This would be similar to rotation,
e.g we are going to have a function that tests if a block can be moved,
but this time we won’t need an origin since when a user presses left
or right key, the block/figure is going to be displaced by only 1 unit
to left or right:
bool CTetrisBlock::Move(int dir, bool can_move_test)
{
// User has requested to test if a block can be moved in a specified direction, so let’s do the test!
if(can_move_test)
{
// Again we’d need boundaries (max) for game window in blocks units
int num_blocks_x = WINDOW_WIDTH/ (BLOCK_SIZE + BLOCK_SPACING);
int num_blocks_y = WINDOW_HEIGHT / (BLOCK_SIZE + BLOCK_SPACING);
int next_x_pos = 0, next_y_pos = 0;
switch(dir)
{
// Remember that the higher the value of position Y in OpenGL, the higher it will be on-screen,
// so if user requests test to move it down,
// substract one from current position.
case DISPLACE_DOWN:
{
next_y_pos = m_iPosY - 1;
next_x_pos = m_iPosX;
}
break;
// Also, in OpenGL the higher value of X position, the more it will be to the right.
case DISPLACE_LEFT:
{
next_x_pos = m_iPosX - 1;
next_y_pos = m_iPosY;
}
break;
case DISPLACE_RIGHT:
{
next_x_pos = m_iPosX + 1;
next_y_pos = m_iPosY;
}
break;
}
// Test if next block position will be bumped into extremities, and if so, return false (e.g ‘cannot move’)
if(next_y_pos == 0 || next_y_pos == num_blocks_y)
return false;
if(next_x_pos == -1|| next_x_pos == num_blocks_x)
return false;
// Next block position is fine, return true (‘can move’)
return true;
}
else // User requested to move this block, so let’s do it
{
switch(dir)
{
case DISPLACE_DOWN:
m_iPosY = m_iPosY - 1;
break;
case DISPLACE_LEFT:
m_iPosX = m_iPosX - 1;
break;
case DISPLACE_RIGHT:
m_iPosX = m_iPosX + 1;
break;
}
}
}
Pretty simple as well. Remember that here, in block Move() procedure,
we do only testing versus boundaries. Let’s see how figure Move procedure
handles it’s movement:
bool CTetrisFigure::Move(int dir, bool can_move_test)
{
// User requested to test if this figure can move in specified direction,
// let’s test each of it’s blocks, and if some of them returns false,
// then user cannot move this figure.
if(can_move_test)
{
for (int i=0; i<GetBlockCount(); i++)
{
if(GetBlock(i) !=NULL)
{
if(!GetBlock(i) ->Move(dir, true))
return false;
}
}
}
// User requested to move this figure in specified direction, let’s do so.
else
{
for (int i=0; i<GetBlockCount(); i++)
{
if(GetBlock(i) !=NULL)
GetBlock(i) ->Move(dir, false);
}
}
// Return true if we can move this figure in specified direction.
return true;
}
Woohoo! Now we can rotate and move our figures! The last thing that
is left is to add functions within CTetrisGame class and do
the tests against the blocks array. So, let’s do it:
bool CTetrisGame::Move (int dir, bool can_move_test)
{
// If there is no current figure, do nothing (return false)
if(m_pCurrentFigure == NULL)
return false;
// User has requested test if the m_pCurrentFigure can be moved, let’s do it
if(can_move_test)
{
// If the m_pCurrentFigure cannot be moved in given direction
// because it’s colliding with boundaries, return false!
if(!m_pCurrentFigure ->Move(dir, true))
return false;
else
// Figure CAN be moved and it doesn’t collide with any boundary!
// Now, test it versus the game blocks array
{
// Create temporary figure and copy blocks positions to it
CTetrisFigure tmp;
tmp.Create(m_pCurrentFigure->GetBlockCount());
for (int i=0; i<m_pCurrentFigure->GetBlockCount(); i++)
{
tmp.GetBlock(i)->SetPosX(m_pCurrentFigure->GetBlock(i)->GetPosX());
tmp.GetBlock(i)->SetPosY(m_pCurrentFigure->GetBlock(i)->GetPosY());
}
// Move the m_pCurrentFigure in specified direction
tmp.Move(dir);
bool all_good = true;
// Now let's see if any of temporary m_pCurrentFigure blocks overlap the game blocks array,
// and if any of them does overlap, m_pCurrentFigure cannot be moved in specified direction.
for (int i=0; i<tmp. GetBlockCount(); i++)
{
int block_at_x = tmp.m_ppBlocks[i]->GetPosX();
int block_at_y = tmp.m_ppBlocks[i]->GetPosY();
if(m_pCurrentFigure ->GetBlock(block_at_x, block_at_y) == NULL)
{
if(m_ppTetrisMatrix[block_at_y][block_at_x] != NULL)
return false;
}
}
// Figure CAN be moved in specified direction!
return true;
}
}
else
// User has requested to move this m_pCurrentFigure , let’s do so.
{
// Un-map that figure from the game blocks array
MapFigure (m_pCurrentFigure , 0);
// Move it (e.g its blocks)
m_pCurrentFigure ->Move(dir);
// Map it back into game blocks array!
MapFigure(m_pCurrentFigure , 1);
// And we’re done!
}
}
Pretty much it! Now, let’s do some rotations in-game and then it’s
time to see the visual results of our hard work!
void CTetrisGame::Rotate(int dir, bool can_rotate_test)
{
if(can_rotate_test)
{
// Do the test if we can rotate this figure and not collide with any boundary.
if(!m_pCurrentFigure->Rotate(dir, true))
return false;
// Now, let’s test this rotation againt already lying blocks in game blocks array…
// Copy and rotate the figure…
CTetrisFigure tmp;
tmp.Create(m_pCurrentFigure->GetBlockCount());
for (int i=0; i<m_pCurrentFigure->GetBlockCount(); i++)
{
tmp.GetBlock(i)->SetPosX(m_pCurrentFigure->GetBlock(i)->GetPosX());
tmp.GetBlock(i)->SetPosY(m_pCurrentFigure->GetBlock(i)->GetPosY());
}
tmp.Rotate(dir);
// Now test each and every block from figure against the game blocks array.
for (int i=0; i<tmp.GetBlockCount(); i++)
{
int block_at_x = tmp. GetBlock(i)->GetPosX();
int block_at_y = tmp. GetBlock(i)->GetPosY();
if(m_pCurrentFigure->GetBlock(block_at_x, block_at_y) == NULL)
{
if(m_ppTetrisMatrix[block_at_y][block_at_x] != NULL)
return;
}
}
}
// User has requested the rotation of this figure, let’s rotate it then!
else
{
// Un-map the figure from game blocks array
MapFigure(m_pCurrentFigure, 0);
// Rotate it
m_pCurrentFigure->Rotate(dir);
// And map it back!
MapFigure (m_pCurrentFigure, 1);
}
return true;
}
Let's add an additional code to WinMain function, so it will know
which keys are used to move and rotate our figure:
We’re done! Now it’s the time to see how it actually works in the
game!
Although its much easier to use MFC’ function OnKeyUp, for now on we
stick with Win32 design, and only at the end we swap to MFC. As an
alternative, I’m presenting here two designs – MFC and Win32 C++ sources.
Personally I think MFC KeyUp is better for movement of the figure,
but everyone has likes/dislikes about both of them.
In Tutorial 3 we will dive into advanced stepping
techniques and maybe some animations! Cyas there!
|