CODEpendent

wxNibbles - Event Driven Game Programing with wxWidgets

Part 3: Custom Input

In this update to wxNibbles, we will work on customizing the input system, tailoring it to the user. We will add joystick input, and create a customizable input map so the user can select which keys and joystick buttons he wants to use to control the snake.

As before with version 0.2, you will need to download a new source archive for version 0.3. 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 is a couple new classes, all in the source/ui/ folder, but we'll start with the things that have changed from last time first. Open up NibblesFrame.cc and take a look at the createMenuBar() method.

void NibblesFrame::createMenuBar() {
    // create the menu bar
    wxMenuBar *mb = new wxMenuBar;
    
    // create the file menu
    wxMenu *fileMenu = new wxMenu;
    fileMenu->Append(IDM_FILE_NEWGAME, wxT("&New Game\tCtrl-N"));
    
    #ifndef __WXOSX__
        fileMenu->AppendSeparator();
    #endif
    
    fileMenu->Append(wxID_EXIT, wxT("E&xit"));
    
    // append the file menu to the menu bar
    mb->Append(fileMenu, wxT("&File"));
    
    // create the edit menu
    wxMenu *editMenu = new wxMenu;
    editMenu->Append(IDM_EDIT_INPUT, wxT("Input Settings\tF1"));
    
    // append the edit menu to the menu bar
    mb->Append(editMenu, wxT("&Edit"));
    
    // create the help menu
    wxMenu *helpMenu = new wxMenu;
    helpMenu->Append(wxID_ABOUT, wxT("About"));
    
    // append the help menu to the menu bar
    mb->Append(helpMenu, wxT("&Help"));
    
    // set the frame's menu bar
    SetMenuBar(mb);
}

We haven't looked at this before, but we'll take a quick glance now because it has changed since version 0.2. The difference is the new edit menu. As you can see, we create three menus here and add items to them. The important thing for our purpose here is the Input Settings menu item on the edit menu, which triggers the opening of our user customizable input map dialog. Now let's take a look at the NibblesPanel::onKeyDown() EVT_KEY_DOWN handler.

void NibblesPanel::onKeyDown(wxKeyEvent &event) {
    if (input.isButton(BPAUSE, event.m_keyCode)) {
        // (un)pause the game
        game.togglePause();
    } else if (input.isButton(BUP, event.m_keyCode)) {
        // head north
        game.changeDirection(NORTH);
    } else if (input.isButton(BRIGHT, event.m_keyCode)) {
        // head west
        game.changeDirection(EAST);
    } else if (input.isButton(BDOWN, event.m_keyCode)) {
        // head south
        game.changeDirection(SOUTH);
    } else if (input.isButton(BLEFT, event.m_keyCode)) {
        // head west
        game.changeDirection(WEST);
    } else {
        // allow other handlers to process KEY_DOWN events
        event.Skip();
    }
}

Instead of checking for specific keys as we were before, we now check for the five user customizable keys defined in the InputMap class. BPAUSE is the pause button, and the remainder are directional keys. Notice also that we have removed the old direction tests. They won't be fixed till version 0.4, so in this version, it's a lot easier to run into yourself.

Now let's head over to the NibblesPanel::onJoyTimer() method, which is an EVT_TIMER handler. Unfortunately, joystick events on wxMSW 2.6.1 (wxWidgets for Windows) are broken, as they use an old API for handling joystick events which no longer works properly with modern joysticks. Because of this, we will be polling the joystick for input rather than handling joystick events. This leads to some problems, but the only way to solve them is either to fix the underlying wxMSW Joystick event API, or do polling->event translation ourselves. Either are doable, but I was lazy. This will make joystick input worse than keyboard input in our game, unfortunately.

void NibblesPanel::onJoyTimer(wxTimerEvent &) {
    if (joystick != NULL) {
        // poll the joystick
        int bmask = joystick->GetButtonState();
        int buttons = joystick->GetNumberButtons();
        
        // which button(s) are being pressed
        for (int button = 0; button < buttons; button++) {
            if (bmask & (1 << button)) {
                if (input.isButton(BPAUSE, button)) {
                    // (un)pause the game
                    game.togglePause();
                } else if (input.isButton(BUP, button)) {
                    // head north
                    game.changeDirection(NORTH);
                } else if (input.isButton(BRIGHT, button)) {
                    // head east
                    game.changeDirection(EAST);
                } else if (input.isButton(BDOWN, button)) {
                    // head south
                    game.changeDirection(SOUTH);
                } else if (input.isButton(BLEFT, button)) {
                    // head west
                    game.changeDirection(WEST);
                }
            }
        }
        
        // check the xy axis for direction changes
        wxPoint position = joystick->GetPosition();
        
        // set analog threshold values
        int xLow = joystick->GetXMin() + ANALOG_THRESHOLD;
        int yLow = joystick->GetYMin() + ANALOG_THRESHOLD;
        int xHigh = joystick->GetXMax() - ANALOG_THRESHOLD;
        int yHigh = joystick->GetYMax() - ANALOG_THRESHOLD;

        if (position.y < yLow) {
            // head north
            game.changeDirection(NORTH);
        } else if (position.x > xHigh) {
            // head east
            game.changeDirection(EAST);
        } else if (position.y > yHigh) {
            // head south
            game.changeDirection(SOUTH);
        } else if (position.x < xLow) {
            // head west
            game.changeDirection(WEST);
        }
    }
}

Because we have to poll the joystick ourselves, it's more complicated than it would normally be. Every time the joystick timer goes off, which is every 100 ms, we check each button on the joystick. If it's in a down-state, we see if it's a button mapped in the InputMap. If so, we take action based on that. After we check the buttons, we check the analog stick. Because analog has degrees of movement, and we only need four directions, we have to decide how much movement implies a directional change. This is where the ANALOG_THRESHOLD comes into play. Analog sticks have 65,535 positions per axis. We take the extreme values and subtract the ANALOG_THRESHOLD. If we're in that range, we consider it to be a directional change.

Now that we can handle both custom keyboard and joystick input, let's see how the user can customize that input. Move to the the NibblesPanel::setupInput() method.

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

This method creates an InputDialog, which is used by the user to customize the input map for the keyboard and joystick. Because we're doing polling the joystick ourself, we have to take some special considerations for joysticks. If the user adds a joystick, then we need to remove the old one (if we had an old one), and restart the timer for the new joystick.

Now let's take a look at the InputDialog class. We'll start with the constructor, defined in source/ui/InputDialog.cc.

InputDialog::InputDialog(wxWindow *parent, InputMap &input) : captureDialog(NULL),
                                                              input(input) {
    // create the InputDialog
    Create(parent, ID_INPUTDIALOG, wxT("Input Settings"), wxDefaultPosition,
           wxDefaultSize, wxCAPTION | wxSYSTEM_MENU | wxCLOSE_BOX);
           
    // setup the controls
    init();
    
    // find any attached joysticks
    findJoysticks();
    
    // resize and center
    GetSizer()->SetSizeHints(this);
    Centre();
}

The InputDialog class looks daunting, but more than half is setting up the widget controls. In the constructor, we create the wxDialog, initialize the widgets, and resize. We also search for joysticks attached to the computer. So let's go ahead and look at the findJoysticks() method.

void InputDialog::findJoysticks() {
    // get the number of joysticks attached
    int count = wxJoystick().GetNumberJoysticks();
    
    // for each joystick
    for (int i = 0; i < count; i++) {
        wxJoystick joystick(i);
        
        // if it works
        if (joystick.IsOk()) {
            // add it to the choice list
            #if !defined(__WXMSW__) || wxCHECK_VERSION(2, 6, 2)
                joystickChoice->Append(joystick.GetProductName());
            #else
                joystickChoice->Append(wxString::Format("Joystick #%d", i));
            #endif
        }
    }
    
    // select the first Joystick
    if (count > 0) {
        joystickChoice->SetSelection(0);
    }
}

In this method, we count the number of joysticks attached to the computer, and then add them to the joystickChoice control. There are two problems here. First, Linux uses a new interface for joystick input, which wxWidgets for Unix (wxGTK is what I use), and version 2.6.1 (the most current release) doesn't use that in the GetNumberJoysticks() method. I submitted a patch to fix this, but until it's added, Joysticks may not work for you in Linux. If you know how to apply a patch, you can download mine and try it out. It works great for me. Second, wxMSW used an old method of finding the joystick name which always returns "Microsoft PC-Joystick Driver" under newer versions of Windows (like 2000 and XP). I submitted a patch to fix this (based on code by Ryan Norton), but you'll have to use the 2.6 CVS to get it now. Until it is in version 2.6.2, I have added a test so the joysticks will be named "Joystick #x" in wxMSW <= 2.6.2.

If any joysticks are found, we set the selection to be the first joystick. Back in the NibblesPanel::setupInput() method, it called InputDialog::showModal(), so let's take a look at that method now.

int InputDialog::ShowModal() {
    // clone the current input map
    local = input;
    
    // load the input map
    loadInputMap();
    
    // return default ShowModal() return code
    return wxDialog::ShowModal();
}

Because we may want to ignore changes to the InputMap, we'll change a local copy of the InputMap provided to us in the constructor call. If the user wants to keep the changes, they can use the Apply button. There is also a reset button which will reset the InputMap to its default settings. It might have been nice to have a Revert button also which would go back to the InputMap before any changes were made, but this just occurred to me now. After the input map is cloned, we call loadInputMap() to setup the dialog controls to this map. Let's take a look at that method now.

void InputDialog::loadInputMap() {
    // write the current values to the button labels
    wxString value;
    
    for (int i = 0; i < 5; i++) {
        value.Printf("%ld", local.keymap[i]);
        keyboardButton[i]->SetLabel(value);
        
        value.Printf("%d", local.joymap[i]);
        joystickButton[i]->SetLabel(value);
    }
    
    // if 0 joysticks, we can't do joystick input
    joystickEnableCheck->Enable(joystickChoice->GetCount() > 0);
    
    // enable the proper joystick controls
    joystickEnableCheck->SetValue(local.useJoystick);
    joystickChoice->Enable(local.useJoystick);
    joystickDpadCheck->Enable(local.useJoystick);
    joystickDpadCheck->SetValue(local.useDpad);
    
    for (int i = 0; i < 5; i++) {
        if (i == BPAUSE) {
            joystickButton[i]->Enable(local.useJoystick);
        } else {
            joystickButton[i]->Enable(local.useJoystick && local.useDpad);
        }
    }
}

Here we setup the button labels to match the current InputMap values. We also enable the joystick controls according to the InputMap and whether joysticks have been detected. Now let's take a look at what happens when the user presses one of the keyboard buttons.

void InputDialog::onKeyboardButton(wxCommandEvent &event) {
    // which button are we configuring?
    enum Button button = BPAUSE;
    
    if (event.GetId() == IDB_KBD_UP) {
        button = BUP;
    } else if (event.GetId() == IDB_KBD_RIGHT) {
        button = BRIGHT;
    } else if (event.GetId() == IDB_KBD_DOWN) {
        button = BDOWN;
    } else if (event.GetId() == IDB_KBD_LEFT) {
        button = BLEFT;
    }
    
    // get the capture dialog
    CaptureDialog &dlg = getCaptureDialog();
    dlg.setLabel(wxT("Press any key..."));
    
    // did they OK a value?
    if (dlg.ShowModal() == wxID_OK) {
        // replace the old value in the map
        local.keymap[button] = dlg.getKeycode();
        
        // reset the button label
        keyboardButton[button]->SetLabel(wxString::Format("%ld", local.keymap[button]));
    }
}

Here's where things really get started. When one of the buttons are pressed, we bring up the CaptureDialog to capture the event. This enables us to allow the user to simply press the key he wants to use rather than select from a big list. If the CaptureDialog returns a wxID_OK value from ShowModal(), then we update the InputMap and the button label. Before we check out the CaptureDialog, let's go ahead and see the joystick button handler.

void InputDialog::onJoystickButton(wxCommandEvent &event) {
    // which button are we configuring?
    enum Button button = BPAUSE;
    
    if (event.GetId() == IDB_JOY_UP) {
        button = BUP;
    } else if (event.GetId() == IDB_JOY_RIGHT) {
        button = BRIGHT;
    } else if (event.GetId() == IDB_JOY_DOWN) {
        button = BDOWN;
    } else if (event.GetId() == IDB_JOY_LEFT) {
        button = BLEFT;
    }
    
    // get the capture dialog
    CaptureDialog &dlg = getCaptureDialog();
    dlg.setLabel(wxT("Press any button..."));
    dlg.setJoystick(joystickChoice->GetSelection());
    
    // did they OK a value?
    if (dlg.ShowModal() == wxID_OK) {
        // replace the old value in the map
        local.joymap[button] = dlg.getButton();
        
        // reset the button label
        joystickButton[button]->SetLabel(wxString::Format("%d", local.joymap[button]));
    }
}

This method is very similar to the keyboard button handler. I would probably have used the same method if I hadn't wanted a different label on the CaptureDialog. Speaking of the CaptureDialog, let's look at that. Since the CaptureDialog's only job is to wait for input, we'll take a look at the event handlers, starting with the EVT_KEY_DOWN handler, onKeyDown().

void CaptureDialog::onKeyDown(wxKeyEvent &event) {
    if (joystick != NULL) {
        // we are looking for joystick events
        // so ignore key events
        return;
    }
    
    // save the keycode
    keycode = event.m_keyCode;
    
    // return wxID_OK
    EndModal(wxID_OK);
}

We ignore EVT_KEY_EVENTS if we're waiting for joystick input. Note however that we never call event.Skip(). We don't want the input being processed anywhere else. If we're not waiting for joystick input, then we save the keycode and end the modal dialog.

There is a critical error in this code under wxGTK. I didn't know at the time that top level windows (frames and dialogs) can't receive keyboard events. This means the InputDialog will not be able to configure the keyboard if you're using wxGTK. This problem is fixed in a later version. Sorry, but I didn't test the program under Linux until it was completed.

Now we'll look at joystick input. Instead of using a wxTimer like we do in the NibblesPanel, we're going to poll the joystick in idle time instead. This would normally be a bad idea since the program will use 100% of the CPU, but since the CaptureDialog is only used for a short period of time, it's easier to do it this way using idle events.

void CaptureDialog::onIdle(wxIdleEvent &event) {
    // if we are looking for joystick events
    if (joystick != NULL) {
        // poll the joystick
        int bmask = joystick->GetButtonState();
        int buttons = joystick->GetNumberButtons();
        
        // if a button is down
        if (bmask != 0) {
            // save the button
            for (int i = 0; i < buttons; i++) {
                if (bmask & (1 << i)) {
                    button = i;
                }
            }
            
            // release the joystick
            releaseJoystick();
            
            // return wxID_OK
            EndModal(wxID_OK);
        } else {
            // otherwise, come back for more polling
            event.RequestMore();
        }
    }
}

You'll notice this is very similar to the NibblesPanel::onJoyTimer() method. In this method, when a button is pressed, we record the button and cancel the modal dialog. We keep requesting idle events until a button is pressed.

I want to briefly touch on the InputDialog::onResetButton() and InputDialog::onApplyButton() handlers.

void InputDialog::onApplyButton(wxCommandEvent &) {
    // copy the local settings to the input map
    input = local;
}

void InputDialog::onResetButton(wxCommandEvent &) {
    // reset the input map to standard
    local.reset();
    
    // adjust the controls
    loadInputMap();
}

Two very simple handlers. If the Apply button is clicked, we copy the local InputMap to the InputMap we were given in the constructor call. If the Reset button is pressed, we reset the local InputMap to the defaults and reload it using loadInputMap().

I'm not going to go over anything in the InputMap class. It is a very simple map which has settings to help the InputDialog handle its controls. You can glance at it if you want to know more about the methods we've been calling. There is nothing else new in this version, so, we'll pick it up next time.

Conclusion

It's got a nicer interface now. Joystick input and customizable input controls. You're no longer forced to use arrow keys and the pause button if you don't want to. If you're using Windows, feel free to try the binary. You can try out joystick input in Linux, but custom keyboard input won't work in this version.

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