CODEpendent

wxNibbles - Event Driven Game Programing with wxWidgets

Part 4: Polishing

In this version of the program, we add many features to the game that go beyond the basic concepts of a Nibbles clone. We will add a scoring system for when the snake eats apples and completes levels. We will use this scoring system to add a Hi Score board. (Yes, 'High' is misspelled, but it looked right at the time). We will also add a status display so the user can see how many lives he has, his score, and other useful information. We also fix a few bugs and code issues in this version.

As before with version 0.3, you will need to download a new source archive for version 0.4. Binaries are also available on Windows and Linux with GTK+ 2.0.

Download [ Source (.zip) | Win32 Binary | Source (.tar.bz2) | Linux Binary (GTK+ 2.0) ]

There are some new classes here, but we'll start with what we already know about. Take a look at the NibblesFrame constructor.

    // create the status bar
    CreateStatusBar(4);

We added a line here to create a status bar where our status display will be. There are four segments in the status bar: score, lives, remaining apples, and current level. Now let's look at the changes in createMenuBar().

    // create the game menu
    wxMenu *gameMenu = new wxMenu;
    gameMenu->Append(IDM_GAME_INPUT, wxT("Input Settings\tF1"));
    gameMenu->Append(IDM_GAME_HISCORE, wxT("Display Hi Score Board\tF10"));
    
    // append the game menu to the menu bar
    mb->Append(gameMenu, wxT("&Game"));

The edit menu we created in version 0.3 has been replaced by this new Game menu. We also have a new item on the menu which will display the Hi Score board at any time.

If you look at the EVT_CLOSE handler, you'll notice we fixed the bug where it would still close if you clicked no by vetoing the event. We'll move on to the NibblesPanel class now. We've added the following code to the EVT_PAINT handler onPaint().

        // update the status bar information
        updateStatusBar();

Now while we're drawing, we update the status display. Let's take a look at the updateStatusBar() method.

void NibblesPanel::updateStatusBar() {
    wxFrame &frame = static_cast<wxFrame &>(*GetParent());
    wxString str;
    
    // display the score
    str.Printf("Score: %lu", game.getScore());
    frame.SetStatusText(str, 0);
    
    // display the current level
    str.Printf("Level: %u", (game.getLevel().getLevel() + 1));
    frame.SetStatusText(str, 1);
    
    // display the apples remaining
    str.Printf("Apples: %d", game.getApples());
    frame.SetStatusText(str, 2);
    
    // display the lives remaining
    str.Printf("Lives: %d", game.getSnake().getLives());
    frame.SetStatusText(str, 3);
}

For each segment of the status bar, we simply change the text. It's a very simple status display. It would be nicer if certain parts were graphical, but again, it wasn't a priority for me. Let's take a look at one more change in the onPaint() method.

                // end the Game
                endGame();

This replaces the call to game.end(). We need a special method so we can check to see if the user place on the Hi Score board.

void NibblesPanel::endGame() {
    // if we're playing
    if (game.isPlaying()) {
        // check for a new hi score
        checkForHiScore();
        
        // end the game
        game.end();
    }
}

The endGame() method simply checks to see if the user got a Hi Score, then it calls Game::end(). Let's check out checkForHiScore() now.

void NibblesPanel::checkForHiScore() {
    // which position did the player earn
    int position = findHiScorePosition();
    
    // Top 5: place it on the board
    if ((position >= 0) && (position < 5)) {
        addHiScore(position);
    }
}

We use findHiScorePosition() to see where the user placed. 0-5 is a new Hi Score, and anything else is not.

int NibblesPanel::findHiScorePosition() {
    int pos = 5;
    
    // until we hit the top place
    while (pos > 0) {
        // if the next place higher than us
        if (hiscores[pos - 1] >= game.getScore()) {
            // then break
            break;
        }
        
        // otherwise keep looking
        --pos;
    }
    
    // return our hi score position
    return pos;
}

void NibblesPanel::addHiScore(int position) {
    // Get the player's name and create the HiScore
    wxString name = wxGetTextFromUser(wxString::Format("You Made Position #%d!",
                                                       (position + 1)),
                                      wxT("New Hi Score!"), wxEmptyString,
                                      this);
    HiScore hiscore(name, game.getScore());
    
    // move the other hiscores down
    for (int i = 4; i > position; i--) {
        hiscores[i] = hiscores[i - 1];
    }
    
    // add the new HiScore
    hiscores[position] = hiscore;
    
    // show the HiScore Board
    showHiScoreBoard();
}

findHiScorePosition() just runs through the Hi Score array and sees if the current score outranks any scores on the board. addHiScore() asks for the user to enter his name, and then puts the score on the board. Now let's head over to the updated NibblesPanel::newGame() method.

void NibblesPanel::newGame() {
    // confirm ending the current game before starting a new one
    if (game.isPlaying()) {
        int result = wxMessageBox(wxT("Quit current game?"), 
                                  wxT("Warning: Game in Progress"), 
                                  wxYES_NO | wxICON_QUESTION, this);
        
        if (result == wxNO) {
            return;
        }
    }
    
    // end any old game
    endGame();
    
    // query for difficulty level
    const wxString choices[] = { wxT("Novice"), wxT("Average"), wxT("Expert") };
    
    int result = wxGetSingleChoiceIndex(wxT("Please select a difficulty level"),
                                        wxT("Difficulty Level Selection"),
                                        3, choices, this);
               
    // get the focus back
    SetFocus();
    
    if (result != -1) {
        enum Difficulty difficulty = static_cast<enum Difficulty>(result);
        
        // start a new game
        game.start(difficulty);

        // refresh the screen
        Refresh();
    }
}

This is a little more complex than what we used to do. For starters, we moved the game in progress question from NibblesFrame::onNewGame() here. After we're sure the user wants a new game, we ask them to choose a difficulty level. A better control here would be nice, but I was too lazy to go custom. After the user has selected, we start the game. Finally, let's take a look at the showHiScoreBoard() method.

void NibblesPanel::showHiScoreBoard() {
    wxString board, temp;
 
    // build the Hi Score Board string
    for (int i = 0; i < 5; i++) {
        temp.Printf("%d: %lu %s\n", (i + 1), hiscores[i].getScore(),
                    hiscores[i].getName().c_str());
        board += temp;
    }
    
    // display the scores
    wxMessageBox(board, wxT("Hi Score Board"), wxOK);
}

This method just creates a big wxString which we display in a message box. This would be another place where a custom control would be a little nicer, but again, I was too lazy for that. Let's move on to the Game class and the Game::start() method.

    // reset the level
    level.setLevel(0);
    
    // reset the score
    score = 0;
    
    // reset the remaining apples
    apples = APPLES[difficulty];

We have added these lines since the 0.3 version. We reset the level, score, and apples. We also reset the apples in Game::restart(). Moving on to the Game::tick() method.

    // did the snake eat the apple?
    if (snake->getSegments()[0] == apple) {
        // decrement apples remaining
        --apples;
        
        // add apple points
        score += APPLE_POINTS * BONUS[difficulty];
        
        if (apples > 0) {
            // grow the snake
            snake->addSegment(3);
        } else {
            // pause the game
            togglePause();
            
            // add level completion points
            score += LEVEL_POINTS * BONUS[difficulty];
            
            // advance to the next level
            level.nextLevel();
            
            // reincarnate the snake
            snake->reincarnate();
            
            // reset the apples
            apples = APPLES[difficulty];
        }
        
        // randomize the apple position
        moveApple();
    }

Eating the apple now earns points for the player, and the possibility of level completion. So, we decrement the apple count, add the points for eating the apple, and then check if we've eaten enough apples for this level. If not, the snake grows. Otherwise, we pause the game, add points for level completion, move to the next level, reset the apple count, and finally reincarnate the snake. This version finally fixes our direction problem, so let's take a look at the new Snake::changeDirection() method.

void Snake::setDirection(enum Direction direction) {
    // make sure we don't run into ourself
    const wxPoint &head = segments[0];
    const wxPoint &body = segments[1];
    
    wxPoint temp;
    
    if (direction == NORTH) {
        // old position is south
        temp = wxPoint(head.x, head.y - BLOCK_SIZE);
    } else if (direction == EAST) {
        // old position is west
        temp = wxPoint(head.x + BLOCK_SIZE, head.y);
    } else if (direction == SOUTH) {
        // old position is north
        temp = wxPoint(head.x, head.y + BLOCK_SIZE);
    } else {
        // old position is east
        temp = wxPoint(head.x - BLOCK_SIZE, head.y);
    }
    
    // if we won't crash, set the direction
    if (temp != body) {
        this->direction = direction;
    }
}

Before, we simply changed the direction to whatever was set. This forced the caller to ensure that the direction was valid, and was a bad design. Our new method doesn't change direction unless it's not suicidal for the snake. As HiScore is an extremely simple class, I won't be going over anything in it.

Conclusion

The game's starting to be really nice now. It has status display, level completion, a point system, and a hi score board. Only one part remains in our tutorial.

Take a look at part 5 when you're ready to continue. Feel free to contact me if you have questions.

 


Need to contact us? We can be reached by email or via our online feedback form.


Copyright © 2005 CODEpendent
All Rights Reserved

Get Firefox!    Valid HTML 4.01!    Made with jEdit