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







Falling Blocks Part 4: Init(), Shutdown(), and State Functions

Init()

Changes to Init() will actually be fairly minor. We first need to tell SDL_LoadBMP() the name of the file we'll be using. We also need to make a call to srand() to seed our random number generator. After that, all we have to do is initialize our two main blocks. Add the following to Init() in "Main.cpp" and be sure to remove the old call to SDL_LoadBMP():

// Fill our bitmap structure with information
g_Bitmap = SDL_LoadBMP("data/FallingBlocks.bmp");

// Seed our random number generator
srand( time(0) );

// Initialize blocks and set them to their proper locations.
g_FocusBlock = new cBlock( BLOCK_START_X, BLOCK_START_Y, g_Bitmap,
    (BlockType)(rand()%7) );
g_NextBlock  = new cBlock( NEXT_BLOCK_CIRCLE_X, NEXT_BLOCK_CIRCLE_Y,
    g_Bitmap, (BlockType)(rand()%7) );

Notice the statement (BlockType)(rand()%7). We first call rand() to get a random number. We then use the modulo operator (%) to reduce that number to a number between 0 and 6. The modulo operator returns the remainder after dividing a number by another number. If you divide 7 by 7, you get a remainder of 0. If you divide 8 by 7, you get a remainder of 1. Note that you can never get a remainder of 7, so 6 will be the limit. Finally, we cast the number to be of type BlockType because that's what cBlock's constructor takes.

Shutdown()

All we have to add to Shutdown() is some code that deletes our two main blocks. We have to be careful how we do this though. In cBlock, we store an array of pointers to cSquare objects. We don't delete these objects in cBlock's constructor because we usually still need them after deleting our focus block. This is because we add the squares of our focus block to the game area.

In order to delete these objects, we need to get pointers to our blocks' arrays and delete them separately. Add the following code to Shutdown() in "Main.cpp":

// Get pointers to the squares in our focus and next-in-line block so we
// can delete them. We must do this before we delete our blocks so we
// don't lose references to the squares. Note that these are pointers to
// arrays of pointers.

cSquare** temp_array_1 = g_FocusBlock->GetSquares();
cSquare** temp_array_2 = g_NextBlock->GetSquares();

// Delete our blocks
delete g_FocusBlock;
delete g_NextBlock;

// Delete the temporary arrays of squares
for (int i=0; i<4; i++)
{
    delete temp_array_1[i];
    delete temp_array_2[i];
}

One last thing we need to do in Shutdown() is delete the squares in our global vector. Add the following just below the previous code:

// Delete the squares that are in the game area
for (int i=0; i<g_OldSquares.size(); i++)
{
    delete g_OldSquares[i];
}

Menu() and Exit()

When we drew our text in the introduction tutorial, we drew it in the middle of the window. Since we've changed the size of the window, we need to change the location of our text. Replace the previous calls to DisplayText() in Menu() with the following:

DisplayText("Start (G)ame", 120, 120, 12, 255, 255, 255, 0, 0, 0);
DisplayText("(Q)uit Game", 120, 150, 12, 255, 255, 255, 0, 0, 0);

Now do the same thing in Exit():

DisplayText("Quit Game (Y or N)?", 100, 150, 12, 255, 255, 255, 0, 0, 0);

Game()

Our Game() function will need a lot more changes than the other states. We need to move the focus block down at the previously specified interval, give the player time to slide the focus block if it hits something, draw our blocks and squares, and display the current level, score, and the score needed to advance to the next level.

To begin, we need two counter variables. One will count the amount of time that has passed since we moved the focus block down, and the other will keep track of how much time the player has been allowed to slide the focus block. We'll make these variable static because we need to keep their values between frames. Add the following to the top of Game() in "Main.cpp":

static int force_down_counter = 0;
static int slide_counter = SLIDE_TIME;

In every frame we begin by incrementing force_down_counter. We then check to see if it has exceeded our focus block's movement rate (g_FocusBlockSpeed). If it has, we make sure there won't be a collision and move the focus block down. Finally, we reset force_down_counter to zero. Add the following to Game() just below the call to HandleGameInput():

force_down_counter++;

if (force_down_counter >= g_FocusBlockSpeed)
{
    // Always check for collisions before moving anything
    if ( !CheckWallCollisions(g_FocusBlock, DOWN) &&
         !CheckEntityCollisions(g_FocusBlock, DOWN) )
    {
        g_FocusBlock->Move(DOWN); // move the focus block
        force_down_counter = 0; // reset our counter
    }
}

Once we've moved the focus block down, we should check to see if it has reached the bottom of the game area or if it is blocked by any squares. If it is, we start decrementing our slide counter. If there isn't a collision detected, we reset slide_counter. This is because the player might have initially hit something, thus causing our slide counter to start ticking, but then may have moved the focus block out of the way. If we don't reset our slide counter, it will stay at whatever value it was decremented to.

If our slide counter reaches zero, we call HandleBottomCollision() which will change the focus block and check for any full rows of squares. Add the following to Game():

// Check to see if focus block's bottom has hit something.
// If it has, we decrement our counter.

if ( CheckWallCollisions(g_FocusBlock, DOWN) ||
     CheckEntityCollisions(g_FocusBlock, DOWN) )
{
    slide_counter--;
}
// If there isn't a collision, we reset our counter.
// This is in case the player moves out of a collision.

else
{
    slide_counter = SLIDE_TIME;
}
// If the counter hits zero, we reset it and call our
// function that handles changing the focus block.

if (slide_counter == 0)
{
    slide_counter = SLIDE_TIME;
    HandleBottomCollision();
}

Because we already wrote drawing functions for our blocks and squares, we can just call them to draw our objects. Add the following to Game(), just below DrawBackground():

// Draw the focus block and next block.
g_FocusBlock->Draw(g_Window);
g_NextBlock->Draw(g_Window);

// Draw the old squares.
for (int i=0; i < g_OldSquares.size(); i++)
{
    g_OldSquares[i]->Draw(g_Window);
}

All we have to do now is display the current level, score, and needed score. DisplayText() takes a string so we need to build three strings to pass to it. To build these strings, we first need to assign them some text ("Score: ", "Needed Score: ", and "Level: "). We then need to add the appropriate values to each string. To do this, we'll use the append() function, which takes a char array and attaches it to the end of a string. We'll convert our numerical values to char arrays with the itoa() function. This function just converts an integer into a char array. Add the following to Game():

// This will be passed to itoa()
char temp[256];

string score = "Score: ";
itoa(g_Score, temp, 10); // the 10 just tells itoa to use decimal notation
score.append( temp );

string nextscore = "Needed Score: ";
itoa(g_Level*POINTS_PER_LEVEL, temp, 10);
nextscore.append(temp);

string level = "Level: ";
itoa(g_Level, temp, 10);
level.append(temp);

DisplayText(score, SCORE_RECT_X, SCORE_RECT_Y, 8, 0, 0, 0, 255, 255, 255);
DisplayText(nextscore, NEEDED_SCORE_RECT_X, NEEDED_SCORE_RECT_Y, 8, 0, 0, 0,
    255, 255, 255);
DisplayText(level, LEVEL_RECT_X, LEVEL_RECT_Y, 8, 0, 0, 0, 255, 255, 255);

GameWon() and GameLost()

Everything in GameWon() and GameLost() has already been covered, so just add the following to "Main.cpp":

// Display a victory message.
void GameWon()
{
    if ( (SDL_GetTicks() - g_Timer) >= FRAME_RATE )
    {
        HandleWinLoseInput();

        ClearScreen();

        DisplayText("You Win!!!", 100, 120, 12, 255, 255, 255, 0, 0, 0);
        DisplayText("Quit Game (Y or N)?", 100, 140, 12, 255, 255, 255, 0, 0, 0);

        SDL_UpdateRect(g_Window, 0, 0, 0, 0);

        g_Timer = SDL_GetTicks();
    }
}

// Display a game over message.
void GameLost()
{
    if ( (SDL_GetTicks() - g_Timer) >= FRAME_RATE )
    {
        HandleWinLoseInput();

        ClearScreen();

        DisplayText("You Lose.", 100, 120, 12, 255, 255, 255, 0, 0, 0);
        DisplayText("Quit Game (Y or N)?", 100, 140, 12, 255, 255, 255, 0, 0, 0);

        SDL_UpdateRect(g_Window, 0, 0, 0, 0);

        g_Timer = SDL_GetTicks();
    }
}

DrawBackground()

Before we get to our new input function, there's a quick change we need to make to DrawBackground(). Previously, we always drew the same background, but in this game our background will change according to the current level. All we need to do to handle this is check to see what the current level is and set our source rectangle to the appropriate location within our bitmap. Replace the current DrawBackground() function with the following:

void DrawBackground()
{
    SDL_Rect source;

    // Set our source rectangle to the current level's background
    switch (g_Level)
    {
        case 1:
        {
            SDL_Rect temp = { LEVEL_ONE_X, LEVEL_ONE_Y, WINDOW_WIDTH,
                WINDOW_HEIGHT };
            source = temp;
        } break;
        case 2:
        {
            SDL_Rect temp = { LEVEL_TWO_X, LEVEL_TWO_Y, WINDOW_WIDTH,
                WINDOW_HEIGHT };
            source = temp;
        } break;
        case 3:
        {
            SDL_Rect temp = { LEVEL_THREE_X, LEVEL_THREE_Y, WINDOW_WIDTH,
                WINDOW_HEIGHT };
            source = temp;
        } break;
        case 4:
        {
            SDL_Rect temp = { LEVEL_FOUR_X, LEVEL_FOUR_Y, WINDOW_WIDTH,
                WINDOW_HEIGHT };
            source = temp;
        } break;
        case 5:
        {
            SDL_Rect temp = { LEVEL_FIVE_X, LEVEL_FIVE_Y, WINDOW_WIDTH,
                WINDOW_HEIGHT };
            source = temp;
        } break;
    }

    SDL_Rect destination = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT };
    
    SDL_BlitSurface(g_Bitmap, &source, g_Window, &destination);
}

Notice that we used a temporary variable when creating our source rectangle. This is just so we can use initializer lists. We can't do this:

int source;
source = { LEVEL_FOUR_X, LEVEL_FOUR_Y, WINDOW_WIDTH, WINDOW_HEIGHT };

because source has already been defined. You can only just initalizer lists when initializing something.

HandleGameInput()

When playing a game like Tetris, it can become very tedious if you have to press an arrow key every time you want the block to move. A better approach is to allow the player to hold down the arrow keys. To do this, we'll declare three boolean variables at the top of our function to represent the left, right, and down arrow keys. As far as detecting input goes, all we'll do is set the appropriate variable to true when the user presses a key, and back to false when the user releases the key. After that, all we need to do is check the boolean variables each time our input function gets called and move the focus block in the appropriate direction. The rest is easy, so here's the code:

void HandleGameInput()
{
    // These variables allow the user to hold the arrow keys down
    static bool down_pressed = false;
    static bool left_pressed = false;
    static bool right_pressed = false;

    // Fill our event structure with event information.
    if ( SDL_PollEvent(&g_Event) )
    {
        // Handle user manually closing game window
        if (g_Event.type == SDL_QUIT)
        {
            // While state stack isn't empty, pop
            while (!g_StateStack.empty())
            {
                g_StateStack.pop();
            }

            return; // game is over, exit the function
        }

        // Handle keyboard input here
        if (g_Event.type == SDL_KEYDOWN)
        {
            if (g_Event.key.keysym.sym == SDLK_ESCAPE)
            {
                g_StateStack.pop();

                return; // this state is done, exit the function
            }

            if (g_Event.key.keysym.sym == SDLK_UP)
            {
                // Check collisions before rotating
                if (!CheckRotationCollisions(g_FocusBlock))
                {
                    g_FocusBlock->Rotate();
                }
            }

            // For the left, right, and down arrow keys, we just set a bool variable
            if (g_Event.key.keysym.sym == SDLK_LEFT)
            {
                left_pressed = true;
            }
            if (g_Event.key.keysym.sym == SDLK_RIGHT)
            {
                right_pressed = true;
            }
            if (g_Event.key.keysym.sym == SDLK_DOWN)
            {
                down_pressed = true;
            }
        }

        // If player lifts key, set bool variable to false
        if (g_Event.type == SDL_KEYUP)
        {
            if (g_Event.key.keysym.sym == SDLK_LEFT)
            {
                left_pressed = false;
            }
            if (g_Event.key.keysym.sym == SDLK_RIGHT)
            {
                right_pressed = false;
            }
            if (g_Event.key.keysym.sym == SDLK_DOWN)
            {
                down_pressed = false;
            }
        }
    }

    // Now we handle the arrow keys, making sure to check for collisions
    if (down_pressed)
    {
        if ( !CheckWallCollisions(g_FocusBlock, DOWN) &&
            !CheckEntityCollisions(g_FocusBlock, DOWN) )
        {
            g_FocusBlock->Move(DOWN);
        }
    }
    if (left_pressed)
    {
        if ( !CheckWallCollisions(g_FocusBlock, LEFT) &&
            !CheckEntityCollisions(g_FocusBlock, LEFT) )
        {
            g_FocusBlock->Move(LEFT);
        }
    }
    if (right_pressed)
    {
        if ( !CheckWallCollisions(g_FocusBlock, RIGHT) &&
            !CheckEntityCollisions(g_FocusBlock, RIGHT) )
        {
            g_FocusBlock->Move(RIGHT);
        }
    }
}

HandleWinLoseInput()

Before pushing our win or lose states onto our stack, we'll first clear the stack (covered more when we get to HandleBottomCollision()). This is so the player can press 'escape' to quit the game while in the win/lose state. If the player chooses to continue the game though, we need to push our exit and menu states back onto the stack. Aside from that, this function is just like our other input functions. Add the following to "Main.cpp":

// Input handling for win/lose screens.
void HandleWinLoseInput()
{
    if ( SDL_PollEvent(&g_Event) )
    {
        // Handle user manually closing game window
        if (g_Event.type == SDL_QUIT)
        {
            // While state stack isn't empty, pop
            while (!g_StateStack.empty())
            {
                g_StateStack.pop();
            }

            return;
        }

        // Handle keyboard input here
        if (g_Event.type == SDL_KEYDOWN)
        {
            if (g_Event.key.keysym.sym == SDLK_ESCAPE)
            {
                g_StateStack.pop();

                return;
            }
            if (g_Event.key.keysym.sym == SDLK_y)
            {
                g_StateStack.pop();
                return;
            }
            // If player chooses to continue playing, we pop off
            // current state and push exit and menu states back on.

            if (g_Event.key.keysym.sym == SDLK_n)
            {
                g_StateStack.pop();

                StateStruct temp;
                temp.StatePointer = Exit;
                g_StateStack.push(temp);

                temp.StatePointer = Menu;
                g_StateStack.push(temp);
                return;
            }
        }
    }
}

Falling Blocks Part 5: Collision Detection

 

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