CODEpendent

wxNibbles - Event Driven Game Programing with wxWidgets

Part 5: Finishing Touches

This is it, the final part of this tutorial. So far we have made a decent (not great, but not horrible) Nibbles clone. The user can customize the input map, and there is a scoring system with a hi score board. What's left? Well, we're going to add a couple more levels. We're going to serialize (save to disk) the custom input map and the hi score board so we don't lose them when we quit the game. We're going to fix a bug where joystick input in the capture dialog is also registered in the game. We're going to modify the code so that it compiles in the wxWidgets unicode build. And finally, we're going to fix the CaptureDialog so that keyboard events are captured under wxGTK.

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

An added bonus for this final version is that api documentation is available online.

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

There is one new class in this version. We'll start in the NibblesPanel constructor, however.

    // load the HiScore Board
    loadHiScoreBoard();
    
    // load the InputMap
    loadInputMap();

When we first startup, we now load the hi score board and the input map from file. Let's take a look at the loadHiScoreBoard() method.

void NibblesPanel::loadHiScoreBoard() {
    // make the filename
    wxFileName fn(wxStandardPaths::Get().GetUserDataDir(), HISCORE_FILE);
    
    // if the file exists
    if (fn.FileExists()) {
        // create the file stream
        std::fstream input(fn.GetFullPath().mb_str(), std::ios_base::in);
                           
        if (input) {
            // read the HiScore data
            for (int i = 0; i < 5; i++) {
                input >> hiscores[i];
                input.ignore();
            }
            
            // close the file
            input.close();
        }
    }
}

First we use wxStandardPaths to choose the platform-specific directory for user-local program specific data files. If our HISCORE_FILE exists here, then we use a std::fstream to load it. You might notice we have used operator>> to load the hiscore. We'll look at that method soon. Let's move on to the NibblesPanel::loadInputMap() method now.

void NibblesPanel::loadInputMap() {
    // make the filename
    wxFileName fn(wxStandardPaths::Get().GetUserDataDir(), INPUTMAP_FILE);
    
    // if the file exists
    if (fn.FileExists()) {
        // create the file stream
        std::fstream input(fn.GetFullPath().mb_str(), std::ios_base::in);
                           
        if (input) {
            // read the InputMap data
            input >> map;
            
            // close the file
            input.close();
            
            // if the joystick is mapped
            if (map.getJoystick() != -1) {
                // create the joystick
                joystick = new wxJoystick(map.getJoystick());
            
                // start the joystick polling timer
                timer.Start(100);
            }
        }
    }
}

Very similar to the other method, but we have to make sure to start the joystick timer if the input map has a joystick mapped. In the construcor we loaded them, so let's see the destructor.

NibblesPanel::~NibblesPanel() {
    // destroy the InputDialog
    if (inputDialog != NULL) {
        inputDialog->Destroy();
    }
    
    // stop the joystick polling timer
    timer.Stop();
    
    // delete the joystick
    if (joystick != NULL) {
        delete joystick;
    }
    
    // serialize the hiscore board
    saveHiScoreBoard();
    
    // serialize the input map
    saveInputMap();
}

We've never looked at this before, so I posted it in its entirety. But the last two calls are the ones we're interested in.

void NibblesPanel::saveHiScoreBoard() {
    wxFileName dir(wxStandardPaths::Get().GetUserDataDir());
    
    if (!dir.DirExists() && !dir.Mkdir()) {
        // unable to create the directory
        return;
    }
    
    // make the filename
    wxFileName fn(dir.GetFullPath(), HISCORE_FILE);
    
    // create the file stream
    std::fstream output(fn.GetFullPath().mb_str(), std::ios_base::out);
    
    if (output) {
        // write the HiScore data
        for (int i = 0; i < 5; i++) {
            output << hiscores[i] << '\n';
        }
        
        // close the file
        output.close();
    }
}

void NibblesPanel::saveInputMap() {
    wxFileName dir(wxStandardPaths::Get().GetUserDataDir());
    
    if (!dir.DirExists() && !dir.Mkdir()) {
        // unable to create the directory
        return;
    }
    
    // make the filename
    wxFileName fn(dir.GetFullPath(), INPUTMAP_FILE);
    
    // create the file stream
    std::fstream output(fn.GetFullPath().mb_str(), std::ios_base::out);
    
    if (output) {
        // write the InputMap data
        output << map;
        
        // close the file
        output.close();
    }
}

These methods are very simple. First we create the directory if it doesn't already exist, then we try to write the file using operator<<. Before we check out the overloaded stream operators, let's take a look at the updated setupInput() method.

void NibblesPanel::setupInput() {
    // if the input dialog doesn't exist yet
    if (inputDialog == NULL) {
        // create it
        inputDialog = new InputDialog(GetParent(), map);
    }
    
    // save the old joystick value
    int temp = map.getJoystick();
    
    // if we're polling joystick input, stop for now
    if (timer.IsRunning()) {
        timer.Stop();
    }
    
    // display a modal InputDialog
    inputDialog->ShowModal();
    
    // if the joystick value changed, 
    if (map.getJoystick() != temp) {
        if (joystick != NULL) {
            // delete the old joystick
            delete joystick;
            joystick = NULL;
        }
        
        // if the joystick is mapped
        if (map.getJoystick() != -1) {
            // create the joystick
            joystick = new wxJoystick(map.getJoystick());
        }
    }
    
    if (joystick != NULL) {
        // start the joystick polling timer
        timer.Start(100);
    }
}

It's close to the old one, but we had to make a couple changes. First, we don't want joystick input being polled while we're in the InputDialog. Otherwise the game would take action while we're trying to configre our joystick, which would be bad. So we stop the timer before we open the InputDialog. Second, because the input map is now being loaded from file, there might be a joystick before we ever call this method. So we can't start the timer only if the joystick changed. It needs to be started if we have a joystick, which we might have had even though this method never created one.

Let's take a look at the overloaded stream operators for the InputMap defined in source/ui/InputMap.cc now.

std::ostream &nibbles::operator<<(std::ostream &output, const InputMap &map) {
    // write input map data
    for (int i = 0; i < 5; i++) {
        output << map.keymap[i] << ' ' << map.joymap[i] << ' ';
    }
    
    output << map.joystick << ' ';
    output << map.useJoystick << ' ';
    output << map.useDpad << '\n';
    
    // return the ostream
    return output;
}

std::istream &nibbles::operator>>(std::istream &input, InputMap &map) {
    // read input map data
    for (int i = 0; i < 5; i++) {
        input >> map.keymap[i];
        input >> map.joymap[i];
    }
    
    input >> map.joystick >> map.useJoystick >> map.useDpad;
    
    // return the istream
    return input;
}

These methods are rather straightforward. We overload the stream operators so we can use the iostream classes to read and write them. Let's take a look at the ones in HiScore now.

std::ostream &nibbles::operator<<(std::ostream &output, const HiScore &score) {
    // write score data
    output << score.name.mb_str() << '\n';
    output << score.score << '\n';
    
    // return the ostream
    return output;
}

std::istream &nibbles::operator>>(std::istream &input, HiScore &score) {
    char s[256];
    
    // read the name
    input.getline(s, 255);
    score.name = wxString(s, wxConvLibc);
    
    // read the score
    input >> score.score;
    input.ignore();
    
    // return the istream
    return input;
}

These are similar to the other ones, but because the name might have spaces, we use getline rather than operator>>. This means a name can be no more than 255 characters. I hope no one tries to test this, because I'm not sure what will happen. Thinking about it now, I should have limited the text box with a validator. Well, just don't put in more than 255 characters and you'll be fine.

Adding the new levels just involves adding more level data and changing the LEVELS constant in EngineConstants.hh. Take a look if you're interested, but the code already existed in version 0.2, so there's nothing really to look at. I'm also not going over the changes I made to make it compile in the unicode build. Everywhere it had an error in 0.4, it just needed a wxT() wrapper. That only leaves the fix for wxGTK keyboard input in the CaptureDialog. Let's take a look at the changes in the constructor.

    // add the CapturePanel
    new CapturePanel(this);

Instead of using the CaptureDialog to receive events and draw on, we now use this wxPanel derived CapturePanel instead. To make the change easier, I made CapturePanel a friend of CaptureDialog. You may also have noticed nearly all the methods in CaptureDialog are gone. They were moved into CapturePanel since CaptureDialog's only job was to be an event handler, really. It retains is EVT_CLOSE handler though.

I don't plan to go over anything in the CapturePanel. It's methods are virtually identical to the ones that used to be in CaptureDialog, which we have already gone over. All it does is update the member variables in the CaptureDialog when events happen.

Conclusion

That's it. I hope you learned something useful in this tutorial. If you have questions, feel free to contact me.

I don't plan to write about it, but for all the times I said "I should have done this", or "it would have been better if", I have created an updated version which implements them all.

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

 


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