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.
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.