GraphicsBlast
The Basics | Python | Linux | Command Line

Lesson 2: Becoming Interactive

Becoming Interactive: Let's listen for user input, and close our window only 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 timer in our main function. We'll replace the timer with a game loop, which 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.

5.
window = None
6.
context = None
7.
+ 8.
programRunning = True
9.
windowWidth = 1024
10.
windowHeight = 600

We can then use this to construct a game loop in our main function, replacing our timer from the last lesson:

74.
def main():
75.
76.
    if init() != 0:
77.
        return
78.
+ 79.
    while programRunning:
+ 80.
        draw()
+ 81.
        handleInput()
82.
83.
    close()

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 structure of our program is clean, simple and straight-forward, as it always should be. Compared to before, where we just drew to the screen once, and then left the window alone, we now repeatedly draw to the window, and call a function to check for any events from user input, which we will define in a moment.

But hold on a moment, previously we spoke about input, process, and output. Why have we deviated from that here? Well, currently we have no processing to do, so for brevity I have skipped that function for now. But I have shifted the order slightly, so that we check the input last. 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 in two lesson's time, where we will look at using shaders to draw a triangle. We will also implement dynamic re-loading of the shaders from disk if the user presses a certain key. This game loop order means that if something goes wrong and the shader fails to load properly, our handleEvents function can just set programRunning to false to break us out the loop, and the program can 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 our drawing, likely causing the program to crash and not close cleanly.

Listening to our users

So let's create the function to handle our events:

50.
    sdl2.SDL_DestroyWindow(window)
51.
    sdl2.SDL_Quit()
52.
+ 53.
def handleInput():
+ 54.
    global programRunning
+ 55.
+ 56.
    event = sdl2.SDL_Event()
+ 57.
    while sdl2.SDL_PollEvent(ctypes.byref(event)) != 0:
+ 58.
+ 59.
        if event.type == sdl2.SDL_QUIT:
+ 60.
            programRunning = False
+ 61.
            return
62.
63.
        ...

This new function will of course need to change the state of the programRunning variable in case we want to close our program, so that is declared as a global here.

When our function is called, we create an sdl2.SDL_Event structure to receive and 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 sdl2.SDL_PollEvent and pass in our event structure, SDL removes the oldest event from that queue and populates the structure we passed in with data about what happened.

If any event has happened, sdl2.SDL_PollEvent will return 1, else if not a 0. So if no events have happened at all, we will never enter this 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 (hopefully), so even if the user is making lots of keypresses and mouse clicks, most of the time you call sdl2.SDL_PollEvent there will be nothing in the queue. Therefore you don't need to worry about entering some infinite-loop state here.

As we need to pass the event structure in to the function, like in the previous lesson we must again utilise the ctypes library, in order to pass the structure by reference. This is due to the structure of the underlying SDL library written in C, where this behaviour is optimal for performance.

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.

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

60.
            programRunning = False
61.
            return
62.
+ 63.
        elif event.type == sdl2.SDL_KEYDOWN:
+ 64.
            print("Key pressed down: %s" % (sdl2.SDL_GetKeyName(event.key.keysym.sym).decode()))
+ 65.
            if event.key.keysym.sym == sdl2.SDLK_ESCAPE:
+ 66.
                programRunning = False
+ 67.
                return
68.
69.
def draw():

As we touched on 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_KEYDOWN), or when it is released (SDL_KEYUP).

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

In our case, for closing the window, I'm using the key down event, but the difference between these choices probably won't make much difference here. 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!

In our code, when there is an event of type SDL_KEYDOWN, we print a message to the command line describing which key was pressed. We use sdl2.SDL_GetKeyName to convert the event's key data into a human presentable string. The function actually returns the string in byte form, so we also call decode() to convert it into a proper Python 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.keysym.sym, a property of the event object, equals the key we are interested in. In our case, we test if it's equal to SDL_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 number one 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, and therefore generally more useful.

With this in place, our events handler is complete. If we 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. For now though, this should serve as a nice introduction to how events are handled and how our game-loop operates.