CODEpendent

wx-sdl: A tutorial on combining wxWidgets with SDL
Part 2: Joystick Input

wx-sdl_joy.cc: joystick tutorial

The first tutorial covered using SDL_Surfaces. This tutorial focuses on Joystick input. To keep things simple, we won't use an SDL_Surface for drawing as our only goal here is to illustrate using the SDL_Joystick within a wxWidgets program.

In this program, we are going to create a simple drawing program. It will use the joystick to move the 'trigger', and any button pressed on the joystick will draw a circle at the position of the trigger.

Once again, I'll build class-by-class, but you can download source code and binaries just below.

Download: [ Source Code | Win32 Binary | Linux Binary ]

Windows XP Screenshot
wx-sdl_joy screenshot

Open your favorite editor and create a new file wx-sdl_joy.cc. Just like before, you can change the name and the extention if you like. This is just what I've chosen, and it matches the source code archive. Add the following code to the file.

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <vector>

#include <wx/wxprec.h>

#ifndef WX_PRECOMP
    #include <wx/wx.h>
#endif

#include <wx/dcbuffer.h>

#include "SDL.h"

class SDLApp : public wxApp {
    DECLARE_CLASS(SDLApp)
    
public:
    bool OnInit();
    int OnExit();
};

bool SDLApp::OnInit() {
    // Windows doesn't open a console for GUI apps
    // so redirect stdout/stderr to file to check for error output
    #ifdef __WXMSW__
        std::freopen("stdout.txt", "w", stdout);
        std::freopen("stderr.txt", "w", stderr);
    #endif
    
    // initialize SDL
    if (SDL_Init(SDL_INIT_JOYSTICK) < 0) {
        std::cerr << "unable to init SDL: " << SDL_GetError() << '\n';
        
        return false;
    }
    
    // make sure SDL_Quit is called on exit
    std::atexit(SDL_Quit);
    
    if (SDL_NumJoysticks() == 0) {
        std::cerr << "no joysticks found\n";
        
        return false;
    }
    
    // create the SDLFrame
    SDLFrame *frame;
    
    try {
        frame = new SDLFrame;
    } catch (std::runtime_error &exception) {
        // we weren't able to open the Joystick
        std::cerr << exception.what() << '\n';
        
        return false;
    }
    
    frame->SetClientSize(640, 480);
    frame->Centre();
    frame->Show();
    
    // Our SDLFrame is the Top Window
    SetTopWindow(frame);

    // if we made it here, we are successfully initialized
    return true;
}

int SDLApp::OnExit() {
    // if no errors occurred, delete our Win32 redirect files
    #ifdef __WXMSW__
        std::fclose(stdout);
        std::fclose(stderr);
    
        wxRemoveFile("stdout.txt");
        wxRemoveFile("stderr.txt");
    #endif
    
    return wxApp::OnExit();
}

IMPLEMENT_CLASS(SDLApp, wxApp)
IMPLEMENT_APP(SDLApp)

Just like in the first tutorial, we're going to start with the SDLApp class. It's similar to the first one, but there are some important differences.

Note the absense of the OnRun() method. We don't need to do any idle event generating here, so we don't need an OnRun() method here. Also note that we initialize SDL before we create our SDLFrame this time. This has some important consequences. Let's take a look at the OnInit() method.

    // Windows doesn't open a console for GUI apps
    // so redirect stdout/stderr to file to check for error output
    #ifdef __WXMSW__
        std::freopen("stdout.txt", "w", stdout);
        std::freopen("stderr.txt", "w", stderr);
    #endif

This is something new, although it would have been a good idea to use it in the last tutorial also. (Since I never got any runtime errors while making it, it never occured to me) Windows GUI programs don't open a console, so anything printed to the standard ouput or error streams are never displayed, even if you launch from a terminal like msys or command.com. To overcome this, we'll redirect the stdout and stderr streams to a file on Windows so we can see what the errors are if we get any. This is probably a good argument for log files rather than std stream output for errors, but not for so simple a program as ours.

    // initialize SDL
    if (SDL_Init(SDL_INIT_JOYSTICK) < 0) {
        std::cerr << "unable to init SDL: " << SDL_GetError() << '\n';
        
        return false;
    }
    
    // make sure SDL_Quit is called on exit
    std::atexit(SDL_Quit);
    
    if (SDL_NumJoysticks() == 0) {
        std::cerr << "no joysticks found\n";
        
        return false;
    }

Next we initialize the SDL library. Notice we're not using SDL_INIT_VIDEO because we're not doing any video with SDL. We're just going to use the joystick support.

The second thing of note is that we're going to use atexit to register our SDL_Quit function rather than put it in OnExit(). This is important, because if OnInit() returns false, OnExit() doesn't get called. In the last example, we initialized SDL in the OnRun() method, so OnExit() would always be called if SDL were initialized. Therefore, it was safe to put SDL_Quit in the OnExit() method in the last program, but it's not safe here.

Finally, we check to make sure there is a joystick attached to the computer. We can't really do a joystick example if there are no joysticks on this computer. If you don't have one, you can't use this program.

    // create the SDLFrame
    SDLFrame *frame;
    
    try {
        frame = new SDLFrame;
    } catch (std::runtime_error &exception) {
        // we weren't able to open the Joystick
        std::cerr << exception.what() << '\n';
        
        return false;
    }
    
    frame->SetClientSize(640, 480);
    frame->Centre();
    frame->Show();
    
    // Our SDLFrame is the Top Window
    SetTopWindow(frame);

Some minor differences in creating the SDLFrame from our first example. First, we're going to catch exceptions, because SDLPanel might throw an exception in the constructor, which we'll see later. Also, since we no longer need access to the SDLFrame outside of the OnInit() method, it is no longer a member variable, but rather a local. Now let's look at the OnExit() method.

    // if no errors occurred, delete our Win32 redirect files
    #ifdef __WXMSW__
        std::fclose(stdout);
        std::fclose(stderr);
    
        wxRemoveFile("stdout.txt");
        wxRemoveFile("stderr.txt");
    #endif

Since OnExit() is not called if OnInit() returns false, we can be sure that if we get to OnExit(), no errors occured during initialization. Therefore, we can delete our empty redirect files. If you use the standard streams elsewhere though, you'll not want to do this. We only use stdout/stderr in the OnInit() method though, so it's safe for us to remove the files here in OnExit().

Time to add another class. Put the following code after the SDL.h header include.

#define ANALOG_THRESHOLD 20000

enum {
    ID_FRAME = 10000,
    ID_PANEL,
    IDM_EDIT_CLEAR
};

class SDLFrame : public wxFrame {
    DECLARE_CLASS(SDLFrame)
    DECLARE_EVENT_TABLE()
    
private:
    SDLPanel *panel;
    
    void onFileExit(wxCommandEvent &event);
    void onEditClear(wxCommandEvent &event);
    void onHelpAbout(wxCommandEvent &event);
    
public:
    SDLFrame();
};

inline void SDLFrame::onFileExit(wxCommandEvent &) { Close(); }
inline void SDLFrame::onEditClear(wxCommandEvent &) { panel->clear(); }

IMPLEMENT_CLASS(SDLFrame, wxFrame)

BEGIN_EVENT_TABLE(SDLFrame, wxFrame)
    EVT_MENU(wxID_EXIT, SDLFrame::onFileExit)
    EVT_MENU(IDM_EDIT_CLEAR, SDLFrame::onEditClear)
    EVT_MENU(wxID_ABOUT, SDLFrame::onHelpAbout)
END_EVENT_TABLE()

SDLFrame::SDLFrame() {
    // Create the SDLFrame
    Create(NULL, ID_FRAME, wxT("Frame Title"), wxDefaultPosition,
           wxDefaultSize, wxCAPTION | wxSYSTEM_MENU | 
           wxMINIMIZE_BOX | wxCLOSE_BOX);

    // create the main menubar
    wxMenuBar *mb = new wxMenuBar;
    
    // create the file menu
    wxMenu *fileMenu = new wxMenu;
    fileMenu->Append(wxID_EXIT, wxT("E&xit"));
    
    // add the file menu to the menu bar
    mb->Append(fileMenu, wxT("&File"));
    
    // create the edit menu
    wxMenu *editMenu = new wxMenu;
    editMenu->Append(IDM_EDIT_CLEAR, wxT("&Clear"));
    
    // add 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"));
    
    // add the help menu to the menu bar
    mb->Append(helpMenu, wxT("&Help"));
    
    // add the menu bar to the SDLFrame
    SetMenuBar(mb);
    
    // create the SDLPanel
    panel = new SDLPanel(this);
}

void SDLFrame::onHelpAbout(wxCommandEvent &) {
    // display standard 'about' box
    wxMessageBox(wxT("wx-sdl tutorial\nCopyright (C) 2005 CODEpendent"),
                 wxT("about wx-sdl tutorial"), wxOK | wxICON_INFORMATION);
}

This class is nearly identical to the last one, so I'll just point out the differences quickly.

This class uses wxID_EXIT and wxID_ABOUT for the menu's exit and about identifiers, respectively. The last program should also have done this, but I forgot. The reason for this is so the Mac menus will look mac native. It won't affect any other platform, so it's just a special hint for wxMac.

There is a third menu, the edit menu. This is used to send clear messages to the panel. We'll see what that's about later.

The method to retrieve the SDLPanel has been removed. It is not needed in this program.

These are the only differences. Very minor as I said. So let's continue with the final class, SDLPanel. Add this code just after the unnamed enumeration.

class SDLPanel : public wxPanel {
    DECLARE_CLASS(SDLPanel)
    DECLARE_EVENT_TABLE()
    
private:
    std::vector<wxPoint> points;
    wxPoint position;
    SDL_Joystick *joystick;

    void onPaint(wxPaintEvent &event);
    void onEraseBackground(wxEraseEvent &event);
    void onIdle(wxIdleEvent &event);
    
public:
    SDLPanel(wxWindow *parent);
    ~SDLPanel();
    void clear();
};

inline void SDLPanel::onEraseBackground(wxEraseEvent &) {}

IMPLEMENT_CLASS(SDLPanel, wxPanel)

BEGIN_EVENT_TABLE(SDLPanel, wxPanel)
    EVT_PAINT(SDLPanel::onPaint)
    EVT_ERASE_BACKGROUND(SDLPanel::onEraseBackground)
    EVT_IDLE(SDLPanel::onIdle)
END_EVENT_TABLE()

SDLPanel::SDLPanel(wxWindow *parent) : wxPanel(parent, ID_PANEL), 
                                       position(320, 240) {
    // open the first Joystick
    if ((joystick = SDL_JoystickOpen(0)) == NULL) {
        throw std::runtime_error("unable to open joystick");
    }
}

SDLPanel::~SDLPanel() {
    // close the joystick
    SDL_JoystickClose(joystick);
}

void SDLPanel::onPaint(wxPaintEvent &) {
    // create the screen buffer
    wxBufferedPaintDC dc(this);
    
    // erase the screen
    dc.SetBackground(*wxWHITE_BRUSH);
    dc.Clear();
    
    // set the drawing colors
    dc.SetPen(*wxBLACK_PEN);
    dc.SetBrush(*wxBLACK_BRUSH);
    
    // draw the point circles
    for (unsigned int i = 0; i < points.size(); i++) {
        dc.DrawCircle(points[i], 10);
    }
    
    // draw the trigger circle
    dc.SetPen(wxPen(wxColour(0, 0, 0xFF)));
    dc.SetBrush(*wxBLUE_BRUSH);
    
    dc.DrawCircle(position, 10);
}

void SDLPanel::onIdle(wxIdleEvent &event) {
    wxPoint old = position;
    bool invalid = false;
    
    // poll the joystick
    SDL_JoystickUpdate();
    
    // get the analog position
    wxInt16 x = SDL_JoystickGetAxis(joystick, 0);
    wxInt16 y = SDL_JoystickGetAxis(joystick, 1);
    
    // get the number of buttons on the joystick
    int buttons = SDL_JoystickNumButtons(joystick);
    
    // if any button is pressed, add a point circle
    for (int i = 0; i < buttons; i++) {
        if (SDL_JoystickGetButton(joystick, i)) {
            points.push_back(position);
            invalid = true;
            
            break;
        }
    }
    
    // check if the trigger has been moved
    if (x > ANALOG_THRESHOLD) {
        ++position.x;
    }
    
    if (x < -ANALOG_THRESHOLD) {
        --position.x;
    }
    
    if (y > ANALOG_THRESHOLD) {
        ++position.y;
    }
    
    if (y < -ANALOG_THRESHOLD) {
        --position.y;
    }
    
    // if we moved the trigger, we need to update the screen
    if ((old.x != position.x) || (old.y != position.y)) {
        invalid = true;
    }
    
    // if the screen is invalid
    if (invalid) {
        // refresh
        Refresh(false);
    } else {
        // otherwise, come back for more input processing
        event.RequestMore();
    }
}

void SDLPanel::clear() {
    // clear the points vector
    points.clear();
    
    // refresh the screen
    Refresh(false);
}

Once again, SDLPanel is the most complicated class. But it only looks daunting. In fact, it's almost as simple as the last program. We'll start with the constructor.

    // open the first Joystick
    if ((joystick = SDL_JoystickOpen(0)) == NULL) {
        throw std::runtime_error("unable to open joystick");
    }

The constructor opens the first joystick SDL can find. If it can't open this joystick, we throw a runtime_error. It would be a better design if we threw a custom exception derived from runtime_error, but for a simple example, I let it go.

Unlike the previous example, we do not need to guarantee any particular size here since we are not using an SDL_Surface. Because of that, we'll just accept whatever SDLFrame::SetClientSize() gives us in the SDLApp::OnInit() method. Let's continue with the onPaint() method.

    // create the screen buffer
    wxBufferedPaintDC dc(this);
    
    // erase the screen
    dc.SetBackground(*wxWHITE_BRUSH);
    dc.Clear();
    
    // set the drawing colors
    dc.SetPen(*wxBLACK_PEN);
    dc.SetBrush(*wxBLACK_BRUSH);
    
    // draw the point circles
    for (unsigned int i = 0; i < points.size(); i++) {
        dc.DrawCircle(points[i], 10);
    }
    
    // draw the trigger circle
    dc.SetPen(wxPen(wxColour(0, 0, 0xFF)));
    dc.SetBrush(*wxBLUE_BRUSH);
    
    dc.DrawCircle(position, 10);

This is the onPaint() method. It is rather simple. We keep a vector of wxPoint objects in the SDLPanel. For each point in this vector, we draw a filled circle centered at that point with radius 10 in black. We also keep a wxPoint position which is the current location of our trigger. At this location, we draw a circle in blue. If you're curious about why we didn't use wxBLUE_PEN, it's because for some reason, wxWidgets pre-defines a blue brush, but not a blue pen. Simple double-buffered drawing. Let's check out the onIdle() method.

    wxPoint old = position;
    bool invalid = false;

We start by defining two variables which will be used to determine if the screen needs to be refreshed.

    // poll the joystick
    SDL_JoystickUpdate();
    
    // get the analog position
    wxInt16 x = SDL_JoystickGetAxis(joystick, 0);
    wxInt16 y = SDL_JoystickGetAxis(joystick, 1);
    
    // get the number of buttons on the joystick
    int buttons = SDL_JoystickNumButtons(joystick);
    
    // if any button is pressed, add a point circle
    for (int i = 0; i < buttons; i++) {
        if (SDL_JoystickGetButton(joystick, i)) {
            points.push_back(position);
            invalid = true;
            
            break;
        }
    }
    
    // check if the trigger has been moved
    if (x > ANALOG_THRESHOLD) {
        ++position.x;
    }
    
    if (x < -ANALOG_THRESHOLD) {
        --position.x;
    }
    
    if (y > ANALOG_THRESHOLD) {
        ++position.y;
    }
    
    if (y < -ANALOG_THRESHOLD) {
        --position.y;
    }

Next, we read the joystick. If we have any buttons pressed, we add the location of the trigger to our point list. If the joystick has moved, we move the trigger in the direction of the joystick move. Because analog joysticks give values ranging from -32768 to 32767, we use ANALOG_THRESHOLD to determine if the movement is considered a full movement in a direction. This could be modified to allow analog joysticks to move slow or fast depending upon what degree of motion is used. I was too lazy to do that here though, and this isn't really a joystick tutorial so much as a tutorial for using SDL_Joystick with a wxWidgets program.

    // if we moved the trigger, we need to update the screen
    if ((old.x != position.x) || (old.y != position.y)) {
        invalid = true;
    }
    
    // if the screen is invalid
    if (invalid) {
        // refresh
        Refresh(false);
    } else {
        // otherwise, come back for more input processing
        event.RequestMore();
    }

Finally, we check to see if we need to repaint the screen. We must repaint either a) if the trigger was moved, or b) if a new circle point was added. We already checked for the circle points, so here we see if our old position differs from our new position.

We could simply always repaint the screen. However, this is inefficient and would waste time that could be used by background processes instead. If you profile your CPU while this is running, you will notice it uses 100% of the CPU. This may seem bad, but it's really not because we are only polling the joystick in idle time. In other words, if any background tasks are running, they will receive the idle time instead. However, if we always repainted the screen, they would not. So, despite the fact that it seems to be using 100% of the CPU, it really only does that when there is nothing else for the CPU to do. We could solve the 100% CPU usage 'problem' by using a timer callback instead of polling in idle time.

If we don't repaint the screen, we need to come back to onIdle() so we can keep polling the joystick. So we tell the idle event to request more idle events.

There is a huge flaw in this program. Because joystick polling is done in idle time, the more circles we have to draw, the slower the trigger will move. This is because every time we redraw the trigger, we have to redraw all the circles, too. If we have to draw a few thousand circles and fill them everytime we move the trigger a single pixel, there will be less and less idle time to poll for joystick events to move the trigger. This could be solved by using a background buffer for the circles and only redrawing the trigger in the onPaint() handler.

There is one more flaw in the program, though much less significant. The trigger can be moved outside the panel. This could be fixed by adding boundary checks to the trigger movement.

Finally, we'll take a look at the clear() method.

    // clear the points vector
    points.clear();
    
    // refresh the screen
    Refresh(false);

The clear() method is very simple. We just clear the points vector and refresh the screen. Now for the destructor.

    // close the joystick
    SDL_JoystickClose(joystick);

All the destructor needs to do is close our SDL_Joystick. I've ignored the onEraseBackground() method because it's identical to the one in the last tutorial.

Conclusion

That's basically all there is to using SDL Joystick input in a wxWidgets program. I'll leave you with some ideas for exercises.

 


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