GraphicsBlast
The Basics | C++ | Linux | Command Line

Lesson 2: Becoming Interactive

Becoming Interactive: Let's listen for inputs and close our window when asked

A window that just opens and closes itself isn't very interesting is it?

Let's look at how our window can interact with the user. In this lesson, we'll look at some basic event handling, learning how to keep our window open until the user closes it, or presses escape on the keyboard.

Staying Alive

To start with, let's have a look at replacing the two second exit delay in our main function. We'll replace the delay with a game loop, which we talked about in the last lesson. This loop will keep running, keep checking for any user inputs or events, and keep drawing to the screen until something breaks us out this loop and we close our program.

So let's begin by creating a boolean variable called programRunning. We will initialise it to true, and if anything in our code changes the state of this variable to false, that will be our cue to break out of the loop and end our program:

10.
SDL_Window* window = NULL;
11.
SDL_GLContext context = NULL;
12.
+ 13.
bool programRunning = true;
14.
15.
bool init()
16.
{

We can then use this to construct a game loop in our main function:

114.
int main(int argc, char* argv[])
115.
{
116.
    if(!init())
117.
    {
118.
        close();
119.
        return -1;
120.
    }
121.
+ 122.
    while(programRunning)
+ 123.
    {
+ 124.
        update();
+ 125.
        draw();
+ 126.
        handleEvents();
+ 127.
    }
128.
129.
    close();
130.
131.
    return 0;
132.
}

NOTE: For clarity, the line numbers in the code above correspond to those found in the finished code for this lesson, available at the bottom of this page. If you are progressively following along, note that there may be some discrepancy.

The main function of our program is clean, simple and straight-forward, as it always should be. Previously we simply drew to the window once, waited, then exited. Now we keep calling update() to update any dynamic variables in our scene, draw() to redraw to our window, and then use handleEvents() to check for any events (user inputs) before jumping back to the start of the loop and calling these again.

But hold on a moment, previously we spoke about input, process, and output. Why have we deviated from that here? Here, we've implemented it as process, output, and then input. The reason for this is that once we're in the loop, the user input function is the thing that will cause us to want to exit the program. It is the function that changes programRunning to false. Therefore if there is a command to quit, we will immediately break out of our loop without performing any additional renders.

To give you an example of why this is important, consider what happens later on in Lesson 4, where we will look at using shaders to draw a triangle. We will implement dynamic re-loading of the shaders from disk if the user presses a certain key. This game loop ordering means that if something goes wrong and the shader fails to load properly when the keypress is detected, our handleEvents function can just set programRunning to false to break us out the loop, and after the function finishes the program will terminate cleanly. If our loop was handleEvents and then draw, even if we changed programRunning to false, our loop would try to use our broken shader to perform a draw before it could exit, likely causing the program to crash and not close cleanly.

Listening to our users

So let's create the handleEvents function to deal with user inputs:

77.
    SDL_DestroyWindow(window);
78.
    SDL_Quit();
79.
}
80.
+ 81.
void handleEvents()
+ 82.
{
+ 83.
    SDL_Event event;
+ 84.
    while(SDL_PollEvent(&event))
+ 85.
    {
+ 86.
        if(event.type == SDL_EVENT_QUIT)
+ 87.
        {
+ 88.
            programRunning = false;
+ 89.
            return;
+ 90.
        }
+ 91.
        ...

When our new function is called, we create an SDL_Event structure to store information about any events that have happened. The operating system maintains a queue of events for our window, and every time we make a call to SDL_PollEvent and pass in our event structure, SDL removes the oldest event from that queue and populates our structure with information about what happened.

If any event has happened, SDL_PollEvent will return 1, else if the queue was completely empty a 0. So if no events have happened at all, we will never enter our loop. However if something has happened, our loop will keep calling SDL_PollEvent until all events have been handled and the queue is empty. When this happens, the function will return 0 so the while loop will stop, and exit the handleEvents function. Bear in mind that your game loop may well be looping at hundreds or thousands of times per second, so even if the user is making lots of keypresses and mouse clicks, most of the time you call SDL_PollEvent there will be nothing in the queue, so you don't need to worry about entering some infinite-loop state here.

TIP: Have you ever wondered how an operating system knows a program is not responding? This happens if a program fails to ask the operating system if any events have happened for a while (say a few seconds, a huge amount of time for a computer!) This could be innocent in badly-designed programs that are really busy doing something else, but it could also indicate that something may have gone wrong. This is why the operating system normally offers to let you wait, in case it is just busy, or give you a way to force the program to close.

Before we go on, I would just like to talk briefly about an alternative approach here. As it stands, our program will be checking for input and drawing to the window as fast as it can. Why? Because code is executed as fast as the processor can run it. But if you are considering building an application like a text editor for example, we really don't want it to be maxing out the CPU non-stop just to draw some static text on screen.

In this scenario, it would be better to change SDL_PollEvent(&event) to SDL_WaitEvent(&event). This will cause your CPU usage to drop to pretty much zero. It waits for a user event to happen and only then will the code move past this function. So it's not suited to applications like games where the screen needs to be constantly updated, but in some applications it's a better choice.

Anyway, as we mentioned before the event structure we created and passed to SDL_PollEvent contains information about whatever event happened. These can be that the user maximised the window, which maybe indicates to use that we should adjust our interface to make use of the extra room, but also includes:

Additionally, as in our case, it could be that the user has tried to close the window. The operating system sends this event to our window if the user presses Alt-F4, clicks the cross in the title bar, and in a few other situations. It's worth mentioning however that just because the operating system sent us a close request, we don't actually have to exit our program! We might open a dialog box asking if the user really wants to quit, and give them the option to click cancel. You can find a full list of all the possible event types to catch at the SDL wiki.

In our case, we will just close our window and exit our program if we receive this message. We check if the type of event is a close request, and if we receive it we set programRunning to false, and return.

Let's do the same when the user presses the escape key:

86.
        if(event.type == SDL_EVENT_QUIT)
87.
        {
88.
            programRunning = false;
89.
            return;
90.
        }
+ 91.
        else if(event.type == SDL_EVENT_KEY_DOWN)
+ 92.
        {
+ 93.
            printf("Key pressed down: %s\n", SDL_GetKeyName(event.key.key));
+ 94.
            if(event.key.key == SDLK_ESCAPE)
+ 95.
            {
+ 96.
                programRunning = false;
+ 97.
                return;
+ 98.
            }
+ 99.
        }
+ 100.
    }
+ 101.
}

As we mentioned earlier, there are actually two options to choose from when dealing with keyboard events. You can either perform an action when a key is pressed down (SDL_EVENT_KEY_DOWN), or when it is released (SDL_EVENT_KEY_UP).

Of these two options, performing an action on the "key down" event generally makes the action feel more responsive. If you have a shooting game, there will be a noticeable lag if you wait until the key is released before firing a gun, which may frustrate users. On the other hand, using the "key up" event is useful for situations where you want to give the user a split-second chance to cancel the input command which they are currently doing. Even just the half second between pressing and releasing a key can be enough for them to react and perhaps move their cursor elsewhere in order not to perform an action.

In our case, for closing the window, I'm using the key down event, but the difference between these choices probably won't matter much in this context. But in many other scenarios it can have an affect on the user's interactions and perceptions, so I want you to at least be aware of this when writing your event handling code!

Looking back at our code, when there is an event of type SDL_EVENT_KEY_DOWN, we print a message to the command line describing which key was pressed. We use SDL_GetKeyName to convert the event's key data into a human presentable string. I've put this here as it may be useful for you as debugging information, although probably you will remove it before releasing your finished program.

The actual test to see which key was pressed is relatively simple. We compare if event.key.key, a property of the event object, equals the key we are interested in. In our case, we test if it's equal to SDLK_ESCAPE, and if so set our programRunning variable to false and return like we did when the close button is pressed.

A full list of keys and their variable names are available on the SDL wiki. For example SDLK_1 can be used to check if the numerical "1" key was pressed, SDLK_G for the letter "g", or even SDLK_BRIGHTNESSUP if for some reason you want to see if they adjusted their brightness.

Notice here that I am using the "Key code" designations from the SDL wiki, hence the "SDLK" prefix. You can also compare against the "Scan code", which is the "raw" keyboard position, whereas the "Key code" is adjusted according to the user's regional keyboard layout.

With this in place, our event handling code is complete for now!

Update

Just before we finish up this lesson, we also need to define our update() function we called from the game loop.

As we talked about the main idea here is that this is the function you will use to update your variables moment by moment. Perhaps to update an object's position to make it fall with gravity for example. This clearly doesn't belong in the rendering code, and doesn't belong in the input handling either. So the update function is where this kind of code belongs.

Right now, we don't actually have any variables to update, so I'm simply going to leave this function blank and keep it as a placeholder for later. I considered just not implementing this function for this tutorial and saving it for a later lesson where it's actually needed, but I think having it here gives you a clearer idea in your head about the input, process and output nature of the main game loop.

So behold:

96.
                programRunning = false;
97.
                return;
98.
            }
99.
        }
100.
    }
101.
}
102.
+ 103.
void update()
+ 104.
{
+ 105.
+ 106.
}
107.
108.
void draw()
109.
{
110.
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

...and with that our three functions are complete!

Conclusion

If we compile and run our code now, we will see an interactive window which remains open until either the user presses the escape key, or clicks on the window's close button.

It's still simple, but definitely progress! In the future, we will cover more advanced input, like handling mouse and joystick events. For now though, this should serve as a nice introduction to how events are handled and how our game-loop operates.