Home
2D Game Tutorials
Arcade Game   Tutorials
Miscellaneous   Tutorials
Code Dump
Forum
Links







Falling Blocks Part 6: Changing the Focus Block

When the focus block can no longer move, we need to add its squares to the game area, check for any completed rows, increase the player's score and level if necessary, increase the focus block's speed if the level changes, change the focus block, and check to see if the player has won or lost. We'll handle these processes in five serparate functions.

HandleBottomCollision()

When the focus block can't move anymore, this is the function that gets called. It will handle all of the functionality we just discussed. Some of the processes will be delegated to other functions, but we'll call them from here.

We'll start with a call to a function that checks for completed lines. If any lines are completed, we'll increase the player's score and check to see if it's time to change levels. If the level changes, we'll increase the focus block's speed and check to see if the player has finished the last level.

When we change the focus block, we set it back to the top of the game area. This is a good time to see if the player has lost because if the focus block can't move now, we know that the squares have built up to the top of the game area.

Add the following to "Main.cpp":

void HandleBottomCollision()
{
    ChangeFocusBlock();

    // Check for completed lines and store the number of lines completed
    int num_lines = CheckCompletedLines();

    if ( num_lines > 0 )
    {
        // Increase player's score according to number of lines completed
        g_Score += POINTS_PER_LINE * num_lines;

        // Check to see if it's time for a new level
        if (g_Score >= g_Level * POINTS_PER_LEVEL)
        {
            g_Level++;
            CheckWin(); // check for a win after increasing the level
            g_FocusBlockSpeed -= SPEED_CHANGE; // shorten the focus blocks movement interval
        }
    }

    // Now would be a good time to check to see if the player has lost
    CheckLoss();
}

ChangeFocusBlock()

Before we change the focus block, we need to get a pointer to its array of squares and add them to the squares in the game area. We then need to delete the current focus block and assign the next block in line as the focus block. Because the next block in line is in the bottom right of the screen, we need to call SetupSquares() to move it to the top of the game area.

After that, we just assign our next block in line to a new, randomly choosen block.

Add the following to "Main.cpp":

// Add the squares of the focus block to g_OldSquares
// and set the next block as the focus block.

void ChangeFocusBlock()
{
    // Get an array of pointers to the focus block squares
    cSquare** square_array = g_FocusBlock->GetSquares();

    // Add focus block squares to g_OldSquares
    for (int i=0; i<4; i++)
    {
        g_OldSquares.push_back(square_array[i]);
    }

    delete g_FocusBlock; // delete the current focus block
    g_FocusBlock = g_NextBlock; // set the focus block to the next block
    g_FocusBlock->SetupSquares(BLOCK_START_X, BLOCK_START_Y, g_Bitmap);

    // Set the next block to a new block of random type
    g_NextBlock = new cBlock(NEXT_BLOCK_CIRCLE_X, NEXT_BLOCK_CIRCLE_Y,
                             g_Bitmap, (BlockType)(rand()%7));
}

CheckCompletedLines()

To check for completed lines, we'll create an array that stores the number of squares in each row. There are 13 rows so we make our array that size. We then iterate through our vector of squares and increment the appropriate index in our array. Note that when we intialize our array, we want all of its indices set to zero. We have to do this manually because the compiler will just fill it with junk values if we don't.

Determining what row a certain square is in takes a bit of math. First, we need to know the size of one row. This is the same as the size of one block, SQUARE_MEDIAN*2. We then need to know where the bottom row is. Remember that we store our squares by their centers so we should find the location of the center of the bottom row. This is accomplished by subtracting SQUARE_MEDIAN from the location of the bottom of the game area (GAME_AREA_BOTTOM).

To understand how we determine which row a square is in, we need an example. Let's assume that our rows are 20 units in height and that the center of the first row is located at 10 units from the top of the screen. If a square's Y value is 10 and we divide it by 20 we get zero. This is because we are using integers so the remainder gets chopped off. Zero is the index of our first row because we always start counting at zero. If the square's Y value is 30 and we divide it by 20, we get 1. If the square's Y value is 50 and we divide it by 20 we get 2.

This seems to be working perfectly but there is one problem. Our top row doesn't start at 10 pixels from the top of the window. The game area actually starts a bit further down. To get the location of the top row, we subtract the location of the bottom row by one less than the number of rows times the size of each row. What this does is move us up by 12 rows, which takes us to the top.

Now, to figure out which row a given square is in, we first subtract the location of the top of the game area from the location of the square. This effectively eliminates the part of the background that comes before the game area. We then just divide by the size of a single row.

Our function returns the number of lines that have been completed so we'll have a variable to keep track of that. We first iterate through the entire vector of squares, incrementing the indices of our counter array that correspond to the rows in which our squares are in.

We then search through our counter array and see if any rows have been completed. When we find a completed row, we increment our line counter and erase the squares in the completed row.

After erasing any lines, we have to move all of the squares down that were above those lines.

Add the following to "Main.cpp":

// Return amount of lines cleared or zero if no lines were cleared
int CheckCompletedLines()
{
    // Store the amount of squares in each row in an array
    int squares_per_row[13];

    // The compiler will fill the array with junk values if we don't do this
    for (int index=0; index<13; index++)
        squares_per_row[index] = 0;

    int row_size = SQUARE_MEDIAN * 2; // pixel size of one row
    int bottom = GAME_AREA_BOTTOM - SQUARE_MEDIAN; // center of bottom row
    int top = bottom - (12 * row_size); // center of top row

    int num_lines = 0; // number of lines cleared
    int row; // multipurpose variable

    // Check for full lines
    for (int i=0; i<g_OldSquares.size(); i++)
    {
        // Get the row the current square is in
        row = (g_OldSquares[i]->GetCenterY() - top) / row_size;

        // Increment the appropriate row counter
        squares_per_row[row]++;
    }

    // Erase any full lines
    for (int line=0; line<13; line++)
    {
        // Check for completed lines
        if (squares_per_row[line] == SQUARES_PER_ROW)
        {
            // Keep track of how many lines have been completed
            num_lines++;

            // Find any squares in current row and remove them
            for (int index=0; index<g_OldSquares.size(); index++)
            {
                if ( ( (g_OldSquares[index]->GetCenterY() - top) / row_size ) == line )
                {
                    // delete the square
                    delete g_OldSquares[index];
                    // remove it from the vector
                    g_OldSquares.erase(g_OldSquares.begin() + index);

                    // When we delete a square, the next square in the vector takes
                    // its place. We have to be sure to stay at the current index so
                    // we don't skip any squares. For example, if we delete the first
                    // square, the second square now becomes the first. We have to
                    // stay at the current (first) index so we can check the second
                    // square (now the first).

                    index--;
                }
            }
        }
    }

    // Move squares above cleared line down
    for (int index=0; index<g_OldSquares.size(); index++)
    {
        for (int line=0; line<13; line++)
        {
            // Determine if this row was filled
            if (squares_per_row[line] == SQUARES_PER_ROW)
            {
                // If it was, get the location of it within the game area
                row = (g_OldSquares[index]->GetCenterY() - top) / row_size;

                // Now move any squares above that row down one
                if ( row < line )
                {
                    g_OldSquares[index]->Move(DOWN);
                }
            }
        }
    }

    return num_lines;
}

CheckWin()

To check to see if the player has beat the game, we just see if the current level is beyond the maximum number of levels. If it is, we pop all of the states off our stack and push the game won state on. We also need to clear the old squares array in case the user decides to have another game. Add the following to "Main.cpp":

// Check to see if player has won. Handle winning condition if needed.
void CheckWin()
{
    // If current level is greater than number of levels, player has won
    if (g_Level > NUM_LEVELS)
    {
        // Pop all states
        while (!g_StateStack.empty())
        {
            g_StateStack.pop();
        }

        // Push the victory state onto the stack
        StateStruct win;
        win.StatePointer = GameWon;

        g_StateStack.push(win);
    }
}

CheckLoss()

CheckLoss() will only ever be called when the focus block is at the top of the game area. We just have to check to see if the focus block can move down. If it can't, we know that the squares have built up too high and the game is over. We then do the same thing that we did for CheckWin(). Add the following to "Main.cpp":

void CheckLoss()
{
    // We call this function when the focus block is at the top of that
    // game area. If the focus block is stuck now, the game is over.

    if ( CheckEntityCollisions(g_FocusBlock, DOWN) )
    {
        // Clear the old squares vector
        for (int i=0; i<g_OldSquares.size(); i++)
        {
            delete g_OldSquares[i];
        }
        g_OldSquares.clear();

        // Pop all states
        while (!g_StateStack.empty())
        {
            g_StateStack.pop();
        }

        // Push the losing state onto the stack
        StateStruct lose;
        lose.StatePointer = GameLost;

        g_StateStack.push(lose);
    }
}

Conclusion

I hope you've enjoyed this tutorial and that you're now comfortable with how a game project works. The next two tutorials will be much shorter and easier. I strongly suggest you read through them and try some similarly simple game projects on your own. There's no way you can go on to more complex projects until you master this stuff. Trust me, I tried skipping this stuff and ended up wasting months and months on projects I wasn't ready for.

 

This site is © Copyright Aaron Cox 2004-2005, All Rights Reserved.
Website templates