Design Patterns and Video Games

Discover Python and Patterns (10): Keyboard

Thanks to the Pygame library we installed in the previous post, we can draw 2D graphics. In this post, I propose to introduce controls with the keyboard as well as some improvements like window centering and frame rate handling.

This post is part of the Discover Python and Patterns series

Keyboard events

Pygame events allow the capture of keyboard presses:

import pygame

pygame.init()
window = pygame.display.set_mode((640,480))

x = 120
y = 120
running = True
while running:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
            break
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
                break
            elif event.key == pygame.K_RIGHT:
                x += 8
            elif event.key == pygame.K_LEFT:
                x -= 8
            elif event.key == pygame.K_DOWN:
                y += 8
            elif event.key == pygame.K_UP:
                y -= 8

    window.fill((0,0,0))
    pygame.draw.rect(window,(0,0,255),(x,y,400,240))
    pygame.display.update()

pygame.quit()

This program draws a blue rectangle at coordinates (x,y) with a with of 400 pixels and a height of 240 pixels. You can move it using the arrows. You can quit the game using the Espace key or closing the window.

In lines 15-26, there is the handling of the keyboard. It manages key pressing: it is when a key goes from unpressed to pressed. It is different from holding or releasing a key.

Once we know that it is a pygame.KEYDOWN event (line 15), the key attribute of the event contains the value of the pressed key. These values are always something like pygame.K_XXX. In Spyder, if you type pygame.K_, and then hit Ctrl+Space, you will see the list of all key names.

The Espace key quit the game (like the pygame.QUIT event). The arrow keys change the x and y coordinates of the rectangle.

Line 28 calls the function window.fill() with a black color (0,0,0). It fills all the window client area with a color. You can try with a different color, for instance, red (255,0,0), green (0,255,0) or blue (0,0,255).

Exercise: Move with WASD. Update the previous program to move the rectangle with the arrows and the W, A, S, and D keys.

Window centering

When the previous program starts, the window can appear at different locations on the screen. We can force the centering by adding the following commands at the beginning of the program:

import os
os.environ['SDL_VIDEO_CENTERED'] = '1'

The first line imports the standard package os. It contains many functions related to the operating system.

The second line sets the environment variable SDL_VIDEO_CENTERED to "1" (NB: the character "1" not the number). Pygame is based on the SDL library, and this one uses environment variables to define some options like window centering. You can do more hacks like this one, see the SDL document at https://www.libsdl.org.

Clock and frame rate

If you have a look at the CPU usage when the program runs, you can see that it uses 100% of a core. It is because the program renders as many frames as possible, and certainly much more than what a screen can display.

To save computations, we can use a pygame.tick.Clock to limit the frame rate. First of all, we need to create such an object at the beginning of the program:

clock = pygame.time.Clock()

Note that pygame.tick.Clock() looks like a function call, and the line above puts the return value of this function into the variable clock. It is not exactly a function call: it is the creation of an instance of the pygame.time.Clock class. Class instances are advanced objects; for example, they can have several sub-variables called attributes. I introduce them in the next post.

Then, at the end of the while loop, add the following line:

clock.tick(60)

It limits the frame rate to 60 per second. More specifically, it releases the CPU such that the frame rate is 60 per second. For instance, if the beginning of the while took five milliseconds, then it releases the CPU for 11 milliseconds (at 60 frames per second, we have 16 milliseconds per frame).

Considering the function call, it should look strange to you: clock is a variable, not a package like random or pygame. It works because clock refers to a class instance. These objects can contain functions called methods. So, tick() is a method of the pygame.time.Clock class. It has two arguments: clock and 60. The first argument is the class instance, and all the others are in the parentheses.

Window caption and icon

To finish with improvements, I propose to set a window caption (or title) and an icon. For the first one, if we want to see "My game" in the window title, we have to call pygame.display.set_caption() after the creation of the window:

pygame.display.set_caption("My game")

For the window icon, we first need an image. We can load one using the pygame.image.load() function:

iconImage = pygame.image.load("icon.png")

It assumes that the file "icon.png" is in the folder where our program is running. You can download this one here (it is a tank, we'll program a small tank game in the next posts).

Then, the window icon is set using the pygame.display.set_icon() function:

pygame.display.set_icon(iconImage)

Note that we can pack both lines into a single one (the use of a variable is not mandatory):

pygame.display.set_icon(pygame.image.load("icon.png"))

Final program

import os
import pygame

os.environ['SDL_VIDEO_CENTERED'] = '1'

pygame.init()
window = pygame.display.set_mode((640,480))
pygame.display.set_caption("Discover Python & Patterns - https://www.patternsgameprog.com")
pygame.display.set_icon(pygame.image.load("icon.png"))
clock = pygame.time.Clock()

x = 120
y = 120
running = True
while running:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
            break
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
                break
            elif event.key == pygame.K_RIGHT:
                x += 8
            elif event.key == pygame.K_LEFT:
                x -= 8
            elif event.key == pygame.K_DOWN:
                y += 8
            elif event.key == pygame.K_UP:
                y -= 8

    window.fill((0,0,0))
    pygame.draw.rect(window,(0,0,255),(x,y,400,240))
    pygame.display.update()    

    clock.tick(60)

pygame.quit()

In the next post, I will introduce classes, and we will use them to implement the Game Loop pattern.

Solution: Move with WASD

import pygame

pygame.init()
window = pygame.display.set_mode((640,480))

x = 120
y = 120
running = True
while running:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
            break
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
                break
            elif event.key == pygame.K_RIGHT or event.key == pygame.K_d:
                x += 8
            elif event.key == pygame.K_LEFT or event.key == pygame.K_a:
                x -= 8
            elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
                y += 8
            elif event.key == pygame.K_UP or event.key == pygame.K_w:
                y -= 8

    window.fill((0,0,0))
    pygame.draw.rect(window,(0,0,255),(x,y,400,240))
    pygame.display.update()

pygame.quit()