CODEpendent

wxNibbles - Event Driven Game Programing with wxWidgets

Part 1: Outline

Here we will create the outline of our program. We will setup our GUI containers (application class, frame, game panel) and the basics of our game engine (the game, timer, and snake).

There will be many source files used in this program, so I suggest you simply grab a source archive before starting. The source archives include the full source and configure script to faciliate compilation. I use MinGW and msys on Windows. You can also download binaries for Windows and Linux (requires GTK+ 2.0)

A quick note about the first four parts, the source will probably only compile in the ANSI build of wxWidgets, not the unicode build. This is because I didn't test the unicode build until the program was finished, and I had to correct some problems. So, if you use the unicode build, then you'll probably need to compile an ANSI build to compile versions 0.1-0.4. Version 0.90 (the one in part 5) will compile under the ANSI and the UNICODE build.

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

You might want to take a look at the archive. There's a lot of stuff in there, but it's less intimidating than it seems. In the source folder, there are three important files: AppConstants.hh, NibblesApp.cc, and NibblesApp.hh. AppConstants.hh hold the global application constants, and the NibblesApp files are the application class.

I won't be describing every line of code. The code is well commented and I'm assuming you all are proficient in C++. I will explain some things I feel are important, but beyond that, the code is filler and can speak for itself. Let's take a look at the OnInit() method, defined in NibblesApp.cc.

bool NibblesApp::OnInit() {
    // create the main application frame
    NibblesFrame *frame = new NibblesFrame;
    frame->SetClientSize(640, 480);
    frame->Centre();
    frame->Show();
    
    // make it our top window
    SetTopWindow(frame);
    
    return true;
}

The OnInit() method is used to initialize the application. We see that our application class creates a NibblesFrame (wxFrame derived) and configures it slightly. The NibblesFrame is our application frame. Pretty standard code, so we'll move on to the NibblesFrame class. It is defined in source/ui/NibblesFrame.cc and source/ui/NibblesFrame.hh. Let's look at the NibblesFrame constructor, defined in NibblesFrame.cc.

NibblesFrame::NibblesFrame() {
    wxString title = *APP_NAME + wxT(' ') + *APP_VERSION;
    
    // create the frame
    Create(NULL, ID_FRAME, title, wxDefaultPosition,
           wxDefaultSize, wxCAPTION | wxSYSTEM_MENU | 
           wxMINIMIZE_BOX | wxCLOSE_BOX);
           
    // create the menu bar
    createMenuBar();
    
    // create the NibblesPanel
    panel = new NibblesPanel(this);
}

It's mostly straightforward. We create the wxFrame and setup our menu bar. It's important to notice a couple things. First, we don't allow frame resizing. Our game will not stretch (or shrink), so we can't allow the frame to change size. Second, our only window in the frame is the NibblesPanel. We need to define a wxPanel derived class to draw our game in, because some platforms don't allow drawing directly on a frame.

There are some other methods defined in NibblesFrame, but we won't be looking to hard at them. Note the event handlers. There is one for when the frame is closed to prevent closing our game while in progress, unless the user confirms. There is also handlers for our menu items, new game, exit, and about. There is a typo at the end of onFileNewGame(), but it didn't affect compilation under gcc, so I didn't notice it till later. You can remove it if you like. Also, there is a bug in onWindowClose(). If the user says no, the event is not actually vetoed, so the frame disappears, but doesn't get destroyed. This, too, will be fixed in a later version. You can fix it here, but it doesn't much matter at this point.

It might also be good to notice the nibbles namespace. All the classes and constants we create for wxNibbles will be defined in this namespace. Let's move on to the NibblesPanel class. This is the heart of our display. Open the NibblesPanel.cc and go to the constructor.

NibblesPanel::NibblesPanel(wxWindow *parent) : wxPanel(parent, ID_PANEL),
                                               game(Game(*this)) {
    // enforce size constraints
    wxSize size(640, 480);
    
    SetMinSize(size);
    SetMaxSize(size);
}

Here, we setup the NibblesPanel. After calling the super constructor, we setup some size constraints. The NibblesPanel must never be any size other than 640x480, so we set this to be the panels minimum and maximum sizes. This is also why we cannot resize the frame. Now let's examine some of the event handlers. We'll start with the EVT_PAINT handler, onPaint().

void NibblesPanel::onPaint(wxPaintEvent &) {
    wxBufferedPaintDC dc(this);
    
    // clear the background
    dc.SetBackground(*wxGREY_BRUSH);
    dc.Clear();
    
    if (game.isPlaying()) {
        // draw the snake
        drawSnake(dc);
    }
}

The paint handler is pretty simple. We use a wxBufferedPaintDC to achieve double-buffered flicker-free drawing. We clear the background, and if the game is playing, we draw the Snake. Don't worry about what a Game or a Snake is yet. We'll get to those classes soon. Let's see how the Snake gets drawn in drawSnake().

void NibblesPanel::drawSnake(wxDC &dc) {
    const std::vector<wxPoint> &segments = game.getSnake().getSegments();
    
    // start drawing
    dc.BeginDrawing();
    
    // draw the head
    dc.SetPen(*wxGREEN_PEN);
    dc.SetBrush(*wxBLUE_BRUSH);
    
    dc.DrawRectangle(segments[0].x, segments[0].y, BLOCK_SIZE, BLOCK_SIZE);
    
    // draw the body
    dc.SetPen(*wxGREY_PEN);
    dc.SetBrush(*wxGREEN_BRUSH);
    
    for (unsigned int i = 1; i < segments.size(); i++) {
        dc.DrawRectangle(segments[i].x, segments[i].y, BLOCK_SIZE, BLOCK_SIZE);
    }
    
    // end drawing
    dc.EndDrawing();
}

The Snake is defined as a vector of wxPoint. Element 0 is the head, and all the subsequent elements are the body. We simply draw a block for each element in the vector. If you look at the UIConstants.hh file, you'll see the BLOCK_SIZE is defined to be 16, so each block is 16x16. Let's take a look at the EVT_KEY_DOWN handler onKeyDown().

void NibblesPanel::onKeyDown(wxKeyEvent &event) {
    switch (event.m_keyCode) {
        case WXK_PAUSE:
            // (un)pause the game
            game.togglePause();
            break;
        case WXK_UP:
            // up arrow = north
            if (game.getSnake().getDirection() != SOUTH) {
                game.changeDirection(NORTH);
            }
            
            break;
        case WXK_RIGHT:
            // right arrow = west
            if (game.getSnake().getDirection() != WEST) {
                game.changeDirection(EAST);
            }
            
            break;
        case WXK_DOWN:
            // down arrow = south
            if (game.getSnake().getDirection() != NORTH) {
                game.changeDirection(SOUTH);
            }
            
            break;
        case WXK_LEFT:
            // left arrow = west
            if (game.getSnake().getDirection() != EAST) {
                game.changeDirection(WEST);
            }
            
            break;
        default:
            // allow other handlers to process KEY_DOWN events
            event.Skip();
            break;
    }
}

The EVT_KEY_DOWN handler is another simple method. It simply checks for five keys, PAUSE, and the four arrow keys. When one of these keys are hit, we take some action. If none of our keys are hit, we call event.Skip() which allows other event handlers to process this event. I don't know if there are any (we don't define any), but it's good practice if we don't need other handlers to ignore input we don't use explicitly.

I want to say a couple things about the evolution of wxNibbles. I wrote this piece by piece as it came to me. I wrote a nibbles game for the TI-89 and TI-92+ calculator long ago and used it as a template for this, but I still wrote it pretty much as it came to me. This means early versions, like the one we're looking at now, may have bugs or minor problems that are corrected in later versions. A big example here is our false direction check code. I wrote tests in all the directions to make sure the Snake couldn't go backwards and run into its own body. If it did this, it would crash into itself. However, because the user could hit two keys in rapid succession, he could change the direction, and before moving in that direction, change it to go backwards and hit himself. This will eventually be corrected, but I think not until part 4.

On a final note, notice we defined an EVT_BACKGROUND_ERASE handler and defined an empty body (in NibblesPanel.hh). This is important in reducing flicker. Since we draw double-buffered, there may be times when the windowing system thinks the window needs redrawn, even if nothing has changed. Because it draws so fast, but erases before it draws, it might flicker slightly each time an erase event occurs because it erases and then immediately redraws the same thing. To prevent this, we don't erase in EVT_ERASE_BACKGROUND. Our EVT_PAINT handler takes care of clearing the background, as we have already seen.

Let's move on to the Snake class now. It's defined in the source/engine/Snake.cc and source/engine/Snake.hh files. We'll start in the constructor, defined in Snake.cc.

Snake::Snake(unsigned int lives) : direction(NORTH), lives(lives), alive(true) {
    // center the Snake vertically in the middle of the screen
    wxPoint point((640 / 2) - BLOCK_SIZE, (480 / 2) - (2 * BLOCK_SIZE));
    
    for (int i = 0; i < 4; i++) {
        segments.push_back(point);
        point.y += BLOCK_SIZE;
    }
}

The constructor sets up the Snake. The initial direction is north, and is centered in a vertical layout. Now let's look at the move() method.

void Snake::move() {
    // move the body towards the head
    for (unsigned int i = segments.size() - 1; i >= 1; --i) {
        segments[i].x = segments[i - 1].x;
        segments[i].y = segments[i - 1].y;
    }
    
    // move the head in the current direction
    if (direction == NORTH) {
        segments[0].y -= BLOCK_SIZE;
    } else if (direction == EAST) {
        segments[0].x += BLOCK_SIZE;
    } else if (direction == SOUTH) {
        segments[0].y += BLOCK_SIZE;
    } else {
        segments[0].x -= BLOCK_SIZE;
    }
}

The move() method is called whenever the Snake is supposed to move. The Snake simply heads in a direction and lets its body follow. The remainder of the mehtods in Snake are all one line, so I won't go over them. Mow let's take a look at the Game class, defined in source/engine/Game.cc and source/engine/Game.hh. We'll start in the constructor.

Game::Game(NibblesPanel &panel) : snake(NULL), panel(panel), 
                                  difficulty(AVERAGE), level(0), 
                                  playing(false), paused(true) {
    // create the game Timer
    timer = new Timer(*this);
}

The Game class keeps track of the all the elements of the Game. It holds the Snake, the game settings, and has a wxTimer to act as a game clock. In the constructor, we setup the basic properties and create the Timer class. Let's take a look at some of the Game methods. We'll start with start().

void Game::start(enum Difficulty difficulty) {
    // end the current Game
    end();
    
    // setup the game parameters
    this->difficulty = difficulty;
    level = 0;
    
    // create the Snake
    int temp = static_cast<int>(difficulty);
    snake = new Snake(LIVES[temp]);
    
    // now we are playing
    playing = true;
}

When new game is called from the file menu, this method is eventually called by the NibblesPanel. Any old game is ended and we setup the game properties. We create a Snake and set playing to true. Let's take a look at the counterpart end() method.

void Game::end() {
    // if we are playing
    if (playing) {
        // stop playing
        playing = false;
        
        // stop the Timer
        timer->Stop();
        
        // remove the Snake
        delete snake;
    }
}

Three things to do here. Set playing to false, stop the Timer, and delete the Snake. Let's look at the togglePause() method now.

void Game::togglePause() {
    // toggle pause value
    paused = !paused;
    
    if (paused) {
        // stop the timer
        timer->Stop();
    } else {
        // (re)start the timer
        timer->Start(SPEEDS[difficulty]);
    }
}

togglePause() just starts or stops the Timer, as indicated by the new value of paused. This is where our game actually is started, when the user hits pause to unpause. The important method is tick(), which is called every time the Timer goes off.

void Game::tick() {
    // move the Snake
    snake->move();
    
    // refresh the panel
    panel.Refresh();
}

Right now, it's pretty simple. Each time the game timer goes off, we move the Snake and refresh the NibblesPanel. It will become more complicated as the game gets more complete.

I'm not going to go over the Timer class. It is just a wxTimer class with a defined Notify method which calls Game::tick().

Conclusion

Well, it's a start. Keep in mind that evolution thing I was talking about, because many things in this outline will change by the time we're finished, either due to style or necessity. I'm a firm believer in design principles, and I like my code to look good, too. I suggest running the binary to see what it's like. Keep in mind the Snake can currently run off screen. This didn't seem to cause a problem in Windows, but I didn't test on any other platforms, so watch out.

Take a look at part 2 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