Home
Code & Snippets
Tutorials
GUI Docs
Projects
Drawings & Sketches
Links
Bio & Resume

Kindly hosted by


Support This Project


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!