Beginning Python Games Development With Pygame (2015)

CHAPTER 6

image

Accepting User Input

There are a variety of ways that the player can interact with a game, and this chapter covers the various input devices in detail. In addition to retrieving information from the devices, we will also explore how to translate what the player does into meaningful events in the game. This is extremely important for any game—regardless of how good a game looks and sounds, it must also be easy to interact with.

Controlling the Game

On a typical home computer we can pretty much rely on there being a keyboard and mouse. This venerable setup is preferred by first-person shooter fans who like to control head movement (i.e., looking about) with the mouse and keep one hand on the keyboard for directional controls and firing. Keyboards can be used for motion by assigning a key for the four basic directions: up, down, left, and right. A further four directions can be indicated by pressing these keys in combination (up + right, down + right, down + left, up + left). But having eight directions is still very limiting for most games, so they aren’t ideally suited for games that need a little finesse to play.

Most players prefer an analog device such as the mouse. A standard mouse is ideal for a directional control because it can accurately detect anything from tiny adjustments to quick sweeps in any direction. If you have ever played a first-person shooter with just keyboard control, you will appreciate the difference. Pressing the “rotate left” key causes the player to rotate like a robot with a constant speed, which is not nearly as useful as being able to quickly turn around to shoot a monster approaching from the side.

The keyboard and mouse work well for games, which is perhaps a little ironic because neither device was designed with games in mind. Joysticks and joypads, however, were designed purely for games and have evolved alongside the games they are used to play. The first joysticks were modeled after controls used in aircraft and had simple directional sticks with a single button. They were popular with the game consoles of the day, but players found it uncomfortable to have to grip the joystick base with one hand while moving the stick with the other. This led to the development of joypads, which could be held in both hands yet still gave the player easy access to the controls with fingers and thumbs. The first joypads had a directional control on one side and trigger buttons on the other. Nowadays joypads have numerous buttons located everywhere there is a spare finger to press them, along with several sticks. The classic directional pads are still found on joypads, but most also have analog sticks that can detect fine adjustments. Many also have force feedback features, which can add an extra dimension to games by making the joypad shake or rumble in response to events on the screen. No doubt there will be other features added to joypads in the future that will further enhance the gaming experience.

There are other devices that can be used for games, but most of them mimic the standard input devices. So if your game can be played with the mouse, it will also be playable with these mouse-like devices. Game input devices are constantly being improved; consider the current status of virtual reality, such as with the Oculus Rift, which simulates a few that you would expect when you operate the mouse in “free view” in games. The difference here, however, is that you now have mouse-like devices, keyboards, and then the Oculus Rift on top of that for even more input for games to consider. I think you’re convinced that input and handling input matters, let’s talk about keyboards!

Understanding Keyboard Control

Most keyboards in use today are QWERTY keyboards, so called because the initial six letters on the first letter row spell out QWERTY. There are variations between brands; keys can be slightly different shapes and sizes, but they tend to be in approximately the same position on the keyboard. This is a good thing, since computer users don’t want to have to relearn how to type every time they buy a new keyboard! All keyboards I have used have had five rows for standard typing keys: one row for function keys F1–F12 and four cursor keys for moving a caret around the screen. They also have a “numpad,” which is a block of keys for entering numbers and doing sums, and a few other miscellaneous keys. The numpad is often omitted on laptop keyboards to save space, because the number keys are duplicated on the main part of the keyboard. We can detect all these keys with the pygame.key module.

Image Note  Although it is the most common, QWERTY isn’t the only keyboard layout; there are other keyboards such as AZERTY and Dvorak. The same keyboard constants may be used, but the keys may be at a different location on the keyboard. If you give the player the option of selecting their own keys for use in the game, they can select the controls that are most appropriate for their keyboard.

Detecting Key Presses

There are two ways to detect a key press in Pygame. One way is to handle KEYDOWN events, which are issued when a key is pressed, and KEYUP events, which are issued when the key is released. This is great for typing text because we will always get keyboard events even if a key has been pressed and released in the time since the last frame. Events will also capture very quick taps of the key for fire buttons. But when we use keyboard input for movement, we simply need to know if the key is pressed or not before we draw the next frame. In this case, we can use thepygame.key module more directly.

Every key on the keyboard has a key constant associated with it, which is a value we can use to identify the key in code. Each constant begins with K_. There are letters (K_a to K_z), numbers (K_0 to K_9), and many other constants such as K_f1, K_LEFT, and K_RETURN. See the Pygame documentation for a complete list (https://www.pygame.org/docs/ref/key.html). Because there are constants for K_a to K_z, you may expect there to be equivalent uppercase versions—but there aren’t any. The reason for this is that capital letters are the result of combining keys (Shift + key). If you need to detect capital letters, or other Shifted keys, use the Unicode parameter in key events that contain the result of such key combinations.

We can use the pygame.key.get_pressed function to detect whether a key is pressed. It returns a list of booleans (True or False values), one for each of the key constants. To look up a particular key, use its constant as an index into the pressed list. For example, if we were using the spacebar as a fire button, we might write the trigger code like this:

pressed_keys = pygame.key.get_pressed()
if pressed_keys[K_SPACE]:
    # Space key has been pressed
    fire()

It should be discussed that various keyboards have various rules regarding how many simultaneous keypresses they support. If you look at gaming keyboards, you will find that they almost always list, as a selling-point, how many simultaneous keypresses they support. Many cheap keyboards support as few as 3-5 simultaneous keypresses, while gaming keyboards can support mid-twenties or more.

In many games, you may have a key to crouch, move forward, strafe a bit, and move slowly. Maybe while you’re doing this diagonal-crouched-slow-strafe, you want to switch to your grenade by pressing the number 5. You may find that nothing happens when you press 5! The horror! If only you had bought the Pro Gamer X ”Over 9000” keyboard!

Not only should you consider the fact that your players may not have the best keyboard, keep in mind they only have ten total fingers anyway, but usually only five to really work with because one hand is likely on a mouse. There are plenty of gamers, however, that do make use of pressing two or more keys with one finger.

Also, you may want to consider those who use gaming pads, where the number of buttons might be severely limited.

With all of this in mind, let’s write a script to experiment with the keyboard. Listing 6-1 uses the get_pressed function to detect any pressed keys and displays a list of them on the screen.

Listing 6-1. Testing Pressed Keys (keydemo.py)

import pygame
from pygame.locals import *
from sys import exit

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

font = pygame.font.SysFont("arial", 32);
font_height = font.get_linesize()

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

    screen.fill((255, 255, 255))

    pressed_key_text = []
    pressed_keys = pygame.key.get_pressed()
    y = font_height

    for key_constant, pressed in enumerate(pressed_keys):
        if pressed:
            key_name = pygame.key.name(key_constant)
            text_surface = font.render(key_name+" pressed", True, (0,0,0))
            screen.blit(text_surface, (8, y))
            y+= font_height

        pygame.display.update()

After Listing 6-1 gets the pressed keys as a list of booleans, it enters a for loop that iterates through each value. You’ll notice that the loop iterates over the pressed keys indirectly by calling enumerate, which is a built-in function that returns a tuple of the index (the first value is 0, second is 1, and so on) and the value from the iterated list. If the iterated value (pressed) is True, then that key has been pressed, and we enter a small code block to display it on screen. This code block uses another function in the keyboard module, pygame.key.name, which takes a key constant and returns a string with a description of the key (i.e., it turns K_SPACE into "space").

Here, you can test your keyboard—how many keys does it support? You may find there are scenarios where you can support up to 8 or more, but other times as few as 4, or something like that. This is due to how keyboards work (see:http://www.sjbaker.org/wiki/index.php?title=Keyboards_Are_Evil for more information).I have a keyboard from 2005 that only supports four keys with Listing 6-1, but my main keyboard is a gaming keyboard and I had no problem registering as many keys as I could press. I did not attempt to use my feet, however.

You may notice that, when you run Listing 6-1, it displayed numlock pressed even though you were not touching it at the time. This is because numlock is a special key that switches a state in the keyboard. When it’s on, the numpad can be used to enter numbers. But when it is off, the numpad is used for other keys that do scrolling and navigating text. Another key that works like this is the Caps Lock key. If you tap Caps Lock, Listing 6-1 will display caps lock pressed even when it has been released. Tap it again to disable the Caps Lock state. Yet another key that does this is Scroll Lock (on a PC keyboard), which doesn’t get used much these days. These three keys shouldn’t be used as triggers because Pygame can’t change this behavior.

Let’s go over the pygame.key module in more detail:

·     key.get_focused—A Pygame window only receives key events when the window is focused, usually by clicking on the window title bar. This is true of all top-level windows. The get_focused function returns True if the window has focus and can receive key events; otherwise, it returns False. When Pygame is running in full-screen mode, it will always have focus because it doesn’t have to share the screen with other applications.

·     key.get_pressed—Returns a list of boolean values for each key. If any of the values are set to True, the key for that index is pressed.

·     key.get_mods—Returns a single value that indicates which of the modifier keys are pressed. Modifier keys are keys such as Shift, Alt, and Ctrl that are used in combination with other keys. To check whether a modifier key is pressed, use the bitwise AND operator (&) with one of the KMOD_ constants. For example, to check whether the left Shift key is pressed, you would use pygame.key.get_mods() & KMOD_LSHIFT.

·     pygame.key.set_mods—You can also set one of the modifier keys to mimic the effect of a key being pressed. To set one or more modifier keys, combine the KMOD_ constants with the bitwise OR operator (|). For example, to set the Shift and Alt keys you could use pygame.key.set_mods(KMOD_SHIFT | KMOD_ALT).

·     pygame.key.set_repeat—If you open your favorite text editor and hold down a letter key, you will see that after a short delay the key starts repeating, which is useful if you want to enter a character several times without pressing and releasing many times. You can ask Pygame to send you repeated KEY_DOWN events with the set_repeat function, which takes a value for the initial delay before a key repeats and a value for the delay between repeated keys. Both values are in milliseconds (1,000 milliseconds in a second). You can disable key repeat by calling set_repeat with no parameters.

·     pygame.key.name—This function takes a KEY_ constant and returns a descriptive string for that value. It is useful for debugging because when the code is running we can only see the value for a key constant and not its name. For instance, if I get a KEY_DOWNevent for a key with a value of 103, I can use key.name to print out the name of the key (which in this case is “g”).

Directional Movement with Keys

You can move a sprite on the screen with the keyboard by assigning a key for up, down, left, and right. Any keys may be used for directional movement, but the most obvious keys to use are the cursor keys, because they are designed for directional movement and are positioned in just the right place to operate with one hand. First-person shooter fans are also accustomed to using the W, A, S, and D keys to move about.

So how do we turn key presses into directional movement? As with most types of movement, we need to create a heading vector that points in the direction we want to go. If only one of the four direction keys is pressed, the heading vector is quite simple. Table 6-1 lists the four basic direction vectors.

Table 6-1. Simple Direction Vectors

Direction

Vector

Left

-1, 0

Right

+1, 0

Up

0, -1

Down

0, 1

In addition to horizontal and vertical movement, we want the user to be able to move diagonally by pressing two keys at the same time. For example, if the Up and Right keys are pressed, the sprite should move diagonally toward the top right of the screen. We can create this diagonal vector by adding two of the simple vectors. If we add Up (0.0, –1.0) and Right (1.0, 0,0), we get (1.0, –1.0), which points up and right, but we can’t use this as a heading vector because it is no longer a unit vector (length 1). If we were to use it as a heading vector, we would find that our sprite moves faster diagonally than it does either vertically or horizontally, which is not that useful.

Before we use our calculated heading, we should turn it back into a unit vector by normalizing it, which gives us a heading of approximately (0.707, –0.707). See Figure 6-1 for a visual depiction of the diagonal vectors that are calculated from simple vectors.

9781484209714_Fig06-01

Figure 6-1. Diagonal vectors by combining simple vectors

Listing 6-2 implements this directional movement. When you run it, you see a sprite that can be moved horizontally or vertically by pressing any of the cursor keys, or moved diagonally by pressing two cursor keys in combination.

If you run Listing 6-2 without modifying our vector2.py file, you find an error that is generated by the attempt to divide something by zero. You can handle this within your normalize method:

def normalize(self):
        magnitude = self.get_magnitude()

        try:
            self.x /= magnitude
            self.y /= magnitude
        except ZeroDivisionError:
            self.x = 0
            self.y = 0

If you are attempting to divide by zero, then x and y must be zero anyway. Thus, you can solve it very easily as in the preceding example. Now we’re ready to make something move:

Listing 6-2. Simple Directional Movement (keymovement.py)

import pygame
from pygame.locals import *
from sys import exit
from gameobjects.vector2 import Vector2

background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)
background = pygame.image.load(background_image_filename).convert()
sprite = pygame.image.load(sprite_image_filename).convert_alpha()

clock = pygame.time.Clock()

sprite_pos = Vector2(200, 150)
sprite_speed = 300

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

    pressed_keys = pygame.key.get_pressed()

    key_direction = Vector2(0, 0)

    if pressed_keys[K_LEFT]:
        key_direction.x = -1
    elif pressed_keys[K_RIGHT]:
         key_direction.x = +1
    if pressed_keys[K_UP]:
        key_direction.y = -1
    elif pressed_keys[K_DOWN]:
        key_direction.y = +1

    key_direction.normalize()

    screen.blit(background, (0,0))
    screen.blit(sprite, (sprite_pos.x,sprite_pos.y))

    time_passed = clock.tick(30)
    time_passed_seconds = time_passed / 1000.0

    sprite_pos += key_direction * sprite_speed * time_passed_seconds

    pygame.display.update()

Listing 6-2 cheats a little in calculating the direction vector. It sets the x component to –1 or +1 if K_LEFT or K_RIGHT is pressed and the y component to –1 or +1 if K_UP or K_DOWN is pressed. This gives us the same result as adding two of the simple horizontal and vertical heading vectors together. If you ever see a mathematical shortcut that lets you do something with less code, feel free to try it out—game developers find that they accumulate many such timesaving gems!

You may have noticed that there are only eight vectors used for this vector movement. If we were to precalculate these vectors and insert them into the code directly, we could reduce the amount of work done when running the script. It’s worth doing as an exercise if you like a challenge, but since calculating the heading vector is only ever done once per frame, speeding it up will make no noticeable difference to the frame rate. Keep your eye out for situations like this; reducing the amount of work the game must do to create a frame is called optimizing and gets more important when there is a lot of action going on.

Rotational Movement with Keys

Moving in eight directions is a little artificial in that you don’t see many things moving like this in real life. Most mobile things can rotate freely, but move in the direction they are pointing, or perhaps backward—but definitely in more than eight compass directions. We can still use the same Up, Down, Left, and Right keys to simulate this, but we have to change what the keys control. What we want to do is have keys for left and right that control rotation and keys for forward and back movement, which would give our sprite the ability to move in any direction. Listing 6-3 uses exactly the same set of keys but uses this free rotation control to move the sprite around the screen.

Listing 6-3. Free Rotation Control (keyrotatemovement.py)

import pygame
from pygame.locals import *
from sys import exit
from gameobjects.vector2 import Vector2
from math import *

background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

background = pygame.image.load(background_image_filename).convert()
sprite = pygame.image.load(sprite_image_filename).convert_alpha()

clock = pygame.time.Clock()

sprite_pos = Vector2(200, 150)
sprite_speed = 300
sprite_rotation = 0
sprite_rotation_speed = 360 # Degrees per second

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()
    pressed_keys = pygame.key.get_pressed()

    rotation_direction = 0.
    movement_direction = 0.

    if pressed_keys[K_LEFT]:
        rotation_direction = +1.0
    if pressed_keys[K_RIGHT]:
        rotation_direction = -1.0
    if pressed_keys[K_UP]:
        movement_direction = +1.0
    if pressed_keys[K_DOWN]:
        movement_direction = -1.0

    screen.blit(background, (0,0))

    rotated_sprite = pygame.transform.rotate(sprite, sprite_rotation)
    w, h = rotated_sprite.get_size()
    sprite_draw_pos = Vector2(sprite_pos.x-w/2, sprite_pos.y-h/2)
    screen.blit(rotated_sprite, (sprite_draw_pos.x,sprite_draw_pos.y))

    time_passed = clock.tick()
    time_passed_seconds = time_passed / 1000.0

    sprite_rotation += rotation_direction * sprite_rotation_speed * time_passed_seconds

    heading_x = sin(sprite_rotation*pi/180.0)
    heading_y = cos(sprite_rotation*pi/180.0)
    heading = Vector2(heading_x, heading_y)
    heading *= movement_direction

    sprite_pos+= heading * sprite_speed * time_passed_seconds

    pygame.display.update()

Listing 6-3 works in a similar way to the previous example, but it calculates the heading differently. Rather than create a heading vector from whatever keys are pressed, we create the heading from the rotation of the sprite (stored in the variable sprite_rotation). It is this value that we modify when the appropriate key is pressed. When the Right key is pressed, we add to the sprite rotation, turning it one way. And when the Left key is pressed, we subtract from the rotation to turn it in the opposite direction.

To calculate the heading vector from the rotation, we must calculate the sine and cosine of the angle, which we can do with the sin and cos functions found in the math module. The sin function calculates the x component, and cos calculates the y component. Both functions take an angle in radians (there are 2 * pi radians in a circle), but because Listing 6-3 uses degrees it has to convert the rotation to radians by multiplying by pi and dividing by 180. After we assemble the heading vector from the two components, it can be used as before to give us time-based movement.

Before Listing 6-3 displays the sprite, the sprite is rotated visually so we can see which way it is facing. This is done with a function from the transform module called rotate, which takes a surface plus an angle and returns a new surface containing the rotated sprite. A problem with this function is that the returned sprite may not have the same dimensions as the original (see Figure 6-2), so we can’t blit it to the same coordinate as the unrotated sprite or it will be drawn in the wrong position on screen. A way to work around this is to draw the sprite in a position that puts the center of the sprite image underneath the sprite’s position on screen. That way, the sprite will be in the same position on the screen regardless of the size of the rotated surface.

9781484209714_Fig06-02

Figure 6-2. Rotating a surface changes its size

Implementing Mouse Control

The mouse has been around for almost as long as the keyboard. Mouse design didn’t change much for many years—the devices became a little more ergonomic (hand-shaped) but the design remained the same.

Classic mice have a rubber-coated ball underneath that rolls over the desk or mousepad. The movement of the ball is picked up by two rollers inside the mouse that are in contact with the ball. Nowadays, almost all mice are laser mice, offering more precision. Gone are the days where friends can remove each other’s mouse ball as a practical joke, but now we can always tape paper over the laser hole for just as much fun.

As time goes on, more and more mice also have various buttons. Almost all have a mouse wheel that both rolls and can act as a third clicker. There are many mice with lots of various buttons all over as well.

Games often make use of these mice innovations. Extra buttons always come in handy for quick access to game controls, and the mouse wheel could have been made for switching weapons in a first-person shooter! And of course the extra accuracy of laser mice always comes in handy when picking off enemies with a sniper rifle.

Rotational Movement with the Mouse

You have seen that drawing a mouse cursor on the screen is quite straightforward: you simply need to get the coordinates of the mouse from a MOUSEMOTION event or directly from the pygame.mouse.get_pos function. Either method is fine if you just want to display a mouse cursor, but mouse movement can also be used to control something other than an absolute position, such as rotating or looking up and down in a 3D game. In this case, we can’t use the mouse position directly because the coordinates would be restricted to the edges of the screen, and we don’t want the player to be restricted in how many times he turns left or right! In these situations, we want to get the relative movement of the mouse, often called the mouse mickeys, which just means how far the mouse has moved since the previous frame. Listing 6-4 adds mouse rotation movement to the sprite demo. In addition to the cursor keys, the sprite will rotate when the mouse is moved left or right.

Image Warning  If you’re planning to copy, paste, and run this code, take note that, to exit, you will not be able to use your mouse. You will exit the game using the Esc key on your keyboard!

Listing 6-4. Rotational Mouse Movement (mouserotatemovement.py)

import pygame
from pygame.locals import *
from sys import exit
from gameobjects.vector2 import Vector2
from math import *

background_image_filename = 'sushiplate.jpg'
sprite_image_filename = 'fugu.png'

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

background = pygame.image.load(background_image_filename).convert()
sprite = pygame.image.load(sprite_image_filename).convert_alpha()

clock = pygame.time.Clock()

pygame.mouse.set_visible(False)
pygame.event.set_grab(True)

sprite_pos = Vector2(200, 150)
sprite_speed = 300.
sprite_rotation = 0.
sprite_rotation_speed = 360. # Degrees per second

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()
        if event.type == KEYDOWN:
            if event.key == K_ESCAPE:
                pygame.quit()
                exit()

    pressed_keys = pygame.key.get_pressed()
    pressed_mouse = pygame.mouse.get_pressed()

    rotation_direction = 0.
    movement_direction = 0.

    rotation_direction = pygame.mouse.get_rel()[0] / 3.

    if pressed_keys[K_LEFT]:
        rotation_direction = +1.
    if pressed_keys[K_RIGHT]:
        rotation_direction = -1.
    if pressed_keys[K_UP] or pressed_mouse[0]:
        movement_direction = +1.
    if pressed_keys[K_DOWN] or pressed_mouse[2]:
        movement_direction = -1.
    screen.blit(background, (0,0))

    rotated_sprite = pygame.transform.rotate(sprite, sprite_rotation)
    w, h = rotated_sprite.get_size()
    sprite_draw_pos = Vector2(sprite_pos.x-w/2, sprite_pos.y-h/2)
    screen.blit(rotated_sprite, (sprite_draw_pos.x, sprite_draw_pos.y))

    time_passed = clock.tick()
    time_passed_seconds = time_passed / 1000.0

    sprite_rotation += rotation_direction * sprite_rotation_speed * time_passed_seconds

    heading_x = sin(sprite_rotation*pi/180.)
    heading_y = cos(sprite_rotation*pi/180.)
    heading = Vector2(heading_x, heading_y)
    heading *= movement_direction

    sprite_pos+= heading * sprite_speed * time_passed_seconds

    pygame.display.update()

To use the mouse to control sprite rotation, Listing 6-4 has to enable virtual infinite area for the mouse, which prevents Pygame from restricting the mouse to the physical screen area. This is done with the following two lines:

pygame.mouse.set_visible(False)
pygame.event.set_grab(True)

The call to set_visible(False) switches off the mouse cursor and the call to set_grab(True) grabs the mouse so that Pygame has complete control over it. A side effect of this is that you cannot use the mouse to close the window, so you must provide an alternative way of closing the script (Listing 6-4 exits if the Esc key is pressed). Using the mouse in this way also makes it difficult to use other applications, which is why it is usually best used in full-screen mode.

The following line in Listing 6-4 gets the mouse mickeys for the x axis (at index [0]). It divides them by 5, because I found that using the value directly rotated the sprite too quickly. You can adjust this value to change the mouse sensitivity (the greater the number, the less sensitive the mouse control will be). Games often let the player adjust the mouse sensitivity in a preferences menu.

rotation_direction = pygame.mouse.get_rel()[0] / 5.

In addition to rotating with the mouse, the buttons are used to move the sprite forward (left mouse button) and backward (right mouse button). Detecting pressed mouse buttons is similar to keys. The function pygame.mouse.get_pressed() returns a tuple of three booleans for the left mouse button, the middle mouse button, and the right mouse button. If any are True, then the corresponding mouse button was held down at the time of the call. An alternative way to use this function is the following, which unpacks the tuple into three values:

lmb, mmb, rmb = pygame_mouse.get_pressed()

Let’s look at pygame.mouse in more detail. It is another simple module, and has only eight functions:

·     pygame.mouse.get_pressed—Returns the mouse buttons pressed as a tuple of three booleans, one for the left, middle, and right mouse buttons.

·     pygame.mouse.get_rel—Returns the relative mouse movement (or mickeys) as a tuple with the x and y relative movement.

·     pygame.mouse.get_pos—Returns the mouse coordinates as a tuple of the x and y values.

·     pygame.mouse.set_pos—Sets the mouse position. Takes the coordinate as a tuple or list for x and y.

·     pygame.mouse.set_visible—Changes the visibility of the standard mouse cursor. If False, the cursor will be invisible.

·     pygame.mouse.get_focused—Returns True if the Pygame window is receiving mouse input. When Pygame runs in a window, it will only receive mouse input when the window is selected and at the front of the display.

·     pygame.mouse.set_cursor—Sets the standard cursor image. This is rarely needed because better results can be achieved by blitting an image to the mouse coordinate.

·     pyGame.mouse.get_cursor—Gets the standard cursor image. See the previous item.

Mouse Gameplay

The humble mouse can allow you to be quite creative in a game. Even a simple cursor can be a great gameplay tool for puzzle and strategy games, where the mouse is often used to pick and drop game elements in the game arena. These elements could be anything from laser-reflecting mirrors to bloodthirsty ogres, depending on the game. The actual mouse pointer doesn’t need to be an arrow. For a god game, it would probably be the omnipotent hand of a deity or perhaps a mysterious alien probe. The classic point ‘n’ click adventure games also made excellent use of the mouse. In these games, the game character would walk over to whatever the player clicked on and have a look at it. If it was a useful object, it could be stored away for use later in the game.

For games with more direct control of the player character, the mouse is used for rotation or movement. A flight sim may use the x and y axes of the mouse to adjust the pitch and yaw of the aircraft so that the player can make fine adjustments to the trajectory. A simple way to do this in Pygame would be to add the mouse onto the angle of the aircraft. I also like to use the mouse for first-person shooters and use the x axis for rotating left and right, and the y axis for looking up and down. This feels quite natural because heads tend to move in that way; I can rotate my head to either side as well as look up and down. I can also tilt my head to either side, but I don’t do it very often!

You can also combine mouse and keyboard controls. I especially like games that use the keyboard for movement and the mouse for aiming. A tank, for example, might be moved about with the cursor keys, but use the mouse to rotate the turret so that you can fire on an enemy without facing in the same direction.

Of course, you are not limited to using the mouse in the same way as other games. Be as creative as you wish—just be sure to test it on a few of your friends first!

Implementing Joystick Control

Joysticks have not been limited by the need to use them elsewhere besides games, and have been completely free to innovate, so modern joysticks have a smooth molded design and make use of every spare digit players have at their disposal. Although the word joystick is often used to mean a game controller of any kind, it more technically describes a flight stick as used in f light simulations.

Nowadays the joypad is most popular with game players and often comes with consoles. They fit comfortably in two hands and have many buttons, in addition to a directional pad and two thumb-operated analog sticks. Think of your Xbox or PlayStation controllers. Many people plug in their Xbox controller to their computer.

Pygame’s joystick module supports all kinds of controllers with the same interface and doesn’t distinguish between the various devices available. This is a good thing because we don’t want to have to write code for every game controller out there!

Joystick Basics

Let’s start by looking at the pygame.joystick module, which contains just five simple functions:

·     pygame.joystick.init—Initializes the joystick module. This is called automatically by pygame.init, so you rarely need to call it yourself.

·     pygame.joystick.quit—Uninitializes the joystick module. Like init, this function is called automatically and so is not often needed.

·     pygame.joystick.get_init—Returns True if the joystick module has been initialized. If it returns False, the next two joystick functions won’t work.

·     pygame.joystick.get_count—Returns the number of joysticks currently plugged into the computer.

·     pygame.joystick.Joystick—Creates a new joystick object that is used to access all information for that stick. The constructor takes the ID of the joystick—the first joystick is ID 0, then ID 1, and so on, up to the number of joysticks available on the system.

The first three functions in the joystick module are used in the initialization process and aren’t often called manually. The other two functions are more important because we will need them to find out how many joysticks are plugged into the computer and to create joystick objects for any of the sticks we want to use. These joystick objects can be used to query information about the joystick as well as get the state of any buttons and the position of any analog sticks. Here’s how we might get the first joystick plugged into the computer:

joystick = None
if pygame.joystick.get_count() > 0:
    joystick = pygame.joystick.Joystick(0)

This code uses a variable called joystick that will reference the first joystick. It is set to None initially because it is possible that there are no joysticks plugged in to the computer. If there is at least one plugged in (pygame.joystick.get_count() > 0), a joystick object is created for the first joystick ID, which is always 0. We have to be careful when working this way because if we try to use joystick when it is None, Python will throw an exception. So you should first test whether there is a joystick by using if joystick is not None before you attempt to use it.

Joystick Buttons

Joysticks have so many buttons that few games can use them all. For a typical joypad there are four buttons on the right side that are most often used as fire buttons, or for performing the most basic of actions in the game, and one or two buttons in the middle that are used for thing like select, start, or pause. There are also generally two or four shoulder buttons, which are elongated buttons on the edge of the joypad underneath where the index and forefinger would naturally be placed. I find these buttons are good for actions that require the button to be held down. A racing game, for example, would probably use the shoulder buttons for accelerate and break. Shoulder buttons can also be good in a first-person shooter to strafe (sidestep) left and right. Some pads also have an extra two buttons hidden away on the underside of the pad, which can be a little difficult to access and so are probably best for infrequently needed actions. Finally, there are usually two buttons that are located underneath the analog sticks so that you have to press the stick down to activate them. These are probably best used to activate something that is being moved with the stick, because it will spring back to the center if the player was to press another button. Not many games make use of these stick buttons, because few game players are even aware of them!

If you do not have a joystick, you may want to at least review the code, but you will not need a joystick to complete the rest of this chapter, or the book.

As with the other input devices, you have two options when accessing the joypad: either through events or by querying the state of the pad directly. When a button is pressed on the pad, Pygame issues a JOYBUTTONDOWN event that contains the joystick ID and the index of the button (buttons are numbered from 0 on). You can get the number of buttons on the joystick from the get_numbuttons member function of joystick objects. When a button is released, Pygame issues a corresponding JOYBUTTONDOWN event with the same information.

Let’s adapt the events script (Listing 3-2) from Chapter 3 to see the joystick events as they are generated. Listing 6-5 is similar to the original code but initializes all joysticks currently plugged in and filters out any non-joystick-related events.

Listing 6-5. Displaying the Joystick Events (events.py)

import pygame
from pygame.locals import *
from sys import exit

pygame.init()

SCREEN_SIZE = (640, 480)
screen = pygame.display.set_mode( SCREEN_SIZE, 0, 32)

font = pygame.font.SysFont("arial", 16);
font_height = font.get_linesize()
event_text = []

joysticks = []
for joystick_no in range(pygame.joystick.get_count()):
    stick = pygame.joystick.Joystick(joystick_no)
    stick.init()
    joysticks.append(stick)

while True:

    event = pygame.event.wait()
    if event.type in (JOYAXISMOTION,
                      JOYBALLMOTION,
                      JOYHATMOTION,
                      JOYBUTTONUP,
                      JOYBUTTONDOWN):
       event_text.append(str(event))

    event_text = event_text[int(-SCREEN_SIZE[1]/font_height):]

    if event.type == QUIT:
        pygame.quit()
        exit()

    screen.fill((255, 255, 255))

    y = SCREEN_SIZE[1]-font_height
    for text in reversed(event_text):
        screen.blit( font.render(text, True, (0, 0, 0)), (0, y) )
        y-=font_height

    pygame.display.update()

When you run Listing 6-5 and press some of the buttons on your joystick, you should see the JOYBUTTONDOWN and corresponding JOYBUTTONUP for each button press (see Figure 6-3). If you wiggle the analog sticks and press the directional pad, you will also see a variety of other events, which we will cover in the next section.

9781484209714_Fig06-03

Figure 6-3. Displaying the joystick events

Detecting the state of joypad buttons differs slightly from keys and mouse buttons. Rather than return a list of boolean values, you use the get_button member function in joystick objects that takes the index of the button and returns its state. Even though you can find the state of a joystick button at any time, it is often a good idea to get a snapshot of the state of the buttons at the start of each frame. That way, you can be sure that the state won’t change as you are drawing the screen. Here’s how we might get a list of booleans for the button states:

joystick_buttons = []
for button_no in range(joystick.get_numbuttons()):
    joystick_buttons.append (joystick.get_button(button_no) )

Joystick Direction Controls

Whereas buttons on a joystick are most often used for activating actions, we still need some way of moving about or aiming in a game. This is where the directional controls come in. There are typically two forms of direction control on a joystick: the directional pad (d-pad) and the analog stick. Which one you use is heavily dependent on the type of game. Often one is obviously better, but they can also be used in combination for some games. This section covers both, and explains how to convert input from the directional controls into in-game motion.

D-pads

The d-pad, a small circular or cross-shaped button on the joypad, is used to indicate a direction by pressing an edge, usually with the thumb. If you were to look underneath the d-pad, you would see that there are four switches arranged in a cross. When you press the pad in any direction, you also push down one or two of these switches, which are interpreted like cursor keys to give eight directions. D-pads may be old technology, but they are often the best choice for certain game actions. I like using d-pads for selecting menus, panning maps, and jumping around in platform games.

Pygame refers to d-pads as “hats” because joysticks have a d-pad-like control directly on top of the stick. For our purposes, though, the hat and d-pad are the same thing.

When you press the d-pad, Pygame sends you a JOYHATMOTION event. This event contains three values: joy, hat, and value. The first, joy, is the index of the joystick the event came from; that is the index of the hat that was pressed; and value indicates which way it was pressed. The hat value is actually a tuple of the changes in the x and y axes—negative number for an axis indicates left or down, and a positive number indicates right or up.

Image Note  There aren’t any up and down events for the d-pad because when it is released, it springs back to the middle and another JOYHATMOTION event is sent.

We can also bypass the events for the d-pad and ask Pygame for the current state of the d-pad. The first step is to find out how many d-pads a joystick has (there could be none), which we can do with the get_numhats member function of joystick objects. Then we can get the state of each d-pad with a call to get_hat, which takes the hat index and returns the axis tuple.

This tuple is very similar to the key_direction vector we went to the trouble of creating in Listing 6-2, and we can very conveniently create a heading vector out of it simply by making a Vector2 object from its values and normalizing the result! Listing 6-6 uses this method to scroll an image around the screen.

Listing 6-6. Using the D-pad to Scroll (hatscroll.py)

import pygame
from pygame.locals import *
from sys import exit
from gameobjects.vector2 import Vector2

picture_file = map.png'

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

picture = pygame.image.load(picture_file).convert()
picture_pos = Vector2(0, 0)
scroll_speed = 1000.

clock = pygame.time.Clock()

joystick = None
if pygame.joystick.get_count() > 0:
    joystick = pygame.joystick.Joystick(0)
    joystick.init()

if joystick is None:
    print("Sorry, you need a joystick for this!")
    pygame.quit()
    exit()

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

    scroll_direction = Vector2(*joystick.get_hat(0))
    scroll_direction.normalize()

    screen.fill((255, 255, 255))
    screen.blit(picture, (-picture_pos.x, picture_pos.y))

    time_passed = clock.tick()
    time_passed_seconds = time_passed / 1000.0

    picture_pos += scroll_direction * scroll_speed * time_passed_seconds

    pygame.display.update()

You can find your own map to use in this code. I recommend going into Google search and typing in “map.” You can find a nice, large, 2000 x 1000, or larger, map to use. The bigger, the better. That should give you a feel for it. You can also search for the map of your favorite game if it has one.

Analog Sticks

All modern joypads have at least one analog stick, which are like spring-mounted buttons that can be moved around with the thumbs. Gamers favor these over the d-pad because they offer finer control and feel more natural for games with a higher level of realism.

Pygame treats analog sticks as two individual axes: one for x (left and right) and one for y (up and down). The reason for treating them separately is that the same functions are used for other devices that may have just a single axis, although analog sticks are by far the most common.

The event for analog stick movement is JOYAXISMOVEMENT, which supplies three pieces of information: joy, axis, and value. The first of these, joy, is the ID of the joystick object; axis is an index for the axis; and value indicates the current position of the axis, which varies between 1 (left or down) and +1 (right or up).

Image Note  The y axis always follows the x axis, so the first stick uses axis index 0 and 1. If there is a second analog stick available, it will use slot 2 and 3.

In addition to the JOYAXISMOVEMENT event, you can get the state of any axis from the joystick object. Use get_numaxis to query the number of axes on the joystick and get_axis to retrieve its current value. Let’s expand on Listing 6-6 by adding the ability to scroll with an analog stick (which makes it easier to accurately pan the image).

Listing 6-7 calculates an additional vector, analog_scroll, from the x and y axes. This vector isn’t normalized because we will be using it to indicate how fast we want to move, in addition to the direction we want to move in. We still need to multiply by a value for speed, because the axes range from –1 to +1, which would result in very slow movement.

Listing 6-7. Scrolling with the Analog Stick (analoghatscroll.py)

import pygame
from pygame.locals import *
from sys import exit
from gameobjects.vector2 import Vector2

picture_file = 'map.png

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

picture = pygame.image.load(picture_file).convert()
picture_pos = Vector2(0, 0)
scroll_speed = 1000.

clock = pygame.time.Clock()

joystick = None
if pygame.joystick.get_count() > 0:
    joystick = pygame.joystick.Joystick(0)
    joystick.init()

if joystick is None:
    print("Sorry, you need a joystick for this!")
    pygame.quit()
    exit()

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

    scroll_direction = Vector2(0, 0)
    if joystick.get_numhats() > 0:
        scroll_direction = Vector2(*joystick.get_hat(0))
        scroll_direction.normalize()

    analog_scroll = Vector2(0, 0)
    if joystick.get_numaxes() >= 2:
        axis_x = joystick.get_axis(0)
        axis_y = joystick.get_axis(1)
        analog_scroll = Vector2(axis_x, -axis_y)

    screen.fill((255, 255, 255))
    screen.blit(picture, (-picture_pos.x, picture_pos.y))

    time_passed = clock.tick()
    time_passed_seconds = time_passed / 1000.0

    picture_pos += scroll_direction * scroll_speed * time_passed_seconds
    picture_pos += analog_scroll * scroll_speed * time_passed_seconds

    pygame.display.update()

Although Listing 6-7 simply pans an image around the screen, it could be the basis for a scrolling map in a strategy or god game.

Dealing with Joystick Dead Zones

Because analog sticks and other axis devices are mechanical things, they suffer from wear and tear, and even brand-new controllers can have imperfections. This can result in the stick wobbling a tiny amount when it is not being manipulated. If you see a constant stream ofJOYAXISMOTION events without touching anything, then your controller suffers from this. Don’t throw it away just yet, though—this problem is easily handled in code. If the values for the axis are very close to zero, then set them to zero. This creates what is called a dead zone in the center of the stick, where no movement will be detected. It should be small enough that the player won’t notice it but still mask any noise from worn-out sticks.

Add the following snippet to the previous listing (just after axis_x and axis_y are assigned) to create a dead zone:

if abs(axis_x) < 0.1:
    axis_x = 0.
if abs(axis_y) < 0.1:
    axis_y = 0.

Joystick Objects

Joystick objects hold all the information that you need from a joystick. There can be one for each joystick or game controller you have plugged in. Let’s take a look at joystick objects in more detail. They contain the following methods:

·     joystick.init—Initializes the joystick. Must be called prior to other functions in the joystick object.

·     joystick.quit—Uninitializes the joystick. After a call to this, Pygame won’t send any more joystick-related events from the device.

·     joystick.get_id—Retrieves the ID of the joystick (the same ID that was given to the Joystick constructor).

·     joystick.get_name—Retrieves the name of the joystick (usually a string supplied by the manufacturer). This string is unique for all the joysticks.

·     joystick.get_numaxes—Retrieves the number of axes on the joystick.

·     joystick.get_axis—Retrieves a value between –1 and +1 for an axis. This function takes the index of the axis you are interested in.

·     joystick.get_numballs—Retrieves the number of trackballs on the joystick. A trackball is similar to a mouse but only gives relative motion.

·     joystick.get_ball—Retrieves a tuple containing the relative motion in the x and y axes of a ball since the previous call to get_ball. Takes the index of the ball you are interested in.

·     joystick.get_button—Retrieves the state of a button, which will either be True (for pressed) or False (for not pressed). This function takes the index of the button you are interested in.

·     joystick.get_numhats—Retrieves the number of d-pads on a joystick.

·     joystick.get_hat—Retrieves the state of hat as a tuple of two values for the x and y axes. This function takes an index of the hat you are interested in.

Seeing Joysticks in Action

Let’s write a script to help you play with the pygame.joystick module. Listing 6-8 draws a crude representation of the current state of your joystick, including the axes, the d-pads, and all the buttons. You can switch between joysticks you have plugged in by pressing the number keys (0 is the first joystick, 1 is the second, and so on).

If you ever have trouble getting a joystick to work in a game, test it with Listing 6-8 (see Figure 6-4). You can easily tell whether a button or control isn’t working properly.

9781484209714_Fig06-04

Figure 6-4. The joystick demo script in action

Listing 6-8. Joystick Demo (joystickdemo.py)

import pygame
from pygame.locals import *
from sys import exit

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)

# Get a list of joystick objects
joysticks = []
for joystick_no in range(pygame.joystick.get_count()):
    stick = pygame.joystick.Joystick(joystick_no)
    stick.init()
    joysticks.append(stick)

if not joysticks:
    print("Sorry! No joystick(s) to test.")
    pygame.quit()
    exit()

active_joystick = 0

pygame.display.set_caption(joysticks[0].get_name())

def draw_axis(surface, x, y, axis_x, axis_y, size):

    line_col = (128, 128, 128)
    num_lines = 40
    step = size / float(num_lines)
    for n in range(num_lines):
        line_col = [(192, 192, 192), (220, 220, 220)][n&1]
        pygame.draw.line(surface, line_col, (x+n*step, y), (x+n*step, y+size))
        pygame.draw.line(surface, line_col, (x, y+n*step), (x+size, y+n*step))

    pygame.draw.line(surface, (0, 0, 0), (x, y+size/2), (x+size, y+size/2))
    pygame.draw.line(surface, (0, 0, 0), (x+size/2, y), (x+size/2, y+size))

    draw_x = int(x + (axis_x * size + size) / 2.)
    draw_y = int(y + (axis_y * size + size) / 2.)
    draw_pos = (draw_x, draw_y)
    center_pos = (x+size/2, y+size/2)
    pygame.draw.line(surface, (0, 0, 0), center_pos, draw_pos, 5)
    pygame.draw.circle(surface, (0, 0, 255), draw_pos, 10)

def draw_dpad(surface, x, y, axis_x, axis_y):

    col = (255, 0, 0)
    if axis_x == -1:
        pygame.draw.circle(surface, col, (x-20, y), 10)
    elif axis_x == +1:
       pygame.draw.circle(surface, col, (x+20, y), 10)

    if axis_y == -1:
        pygame.draw.circle(surface, col, (x, y+20), 10)
    elif axis_y == +1:
        pygame.draw.circle(surface, col, (x, y-20), 10)

while True:

    joystick = joysticks[active_joystick]

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

    if event.type == KEYDOWN:
        if event.key >= K_0 and event.key <= K_1:
            num = event.key - K_0

            if num < len(joysticks):
                active_joystick = num
                name = joysticks[active_joystick].get_name()
                pygame.display.set_caption(name)

    # Get a list of all the axis
    axes = []
    for axis_no in range(joystick.get_numaxes()):
        axes.append( joystick.get_axis(axis_no) )

    axis_size = min(256, 640 / (joystick.get_numaxes()/2))

    pygame.draw.rect(screen, (255, 255,255), (0, 0, 640, 480))

    # Draw all the axes (analog sticks)
    x = 0
    for axis_no in range(0, len(axes), 2):
        axis_x = axes[axis_no]
        if axis_no+1 < len(axes):
            axis_y = axes[axis_no+1]
        else:
            axis_y = 0.
        draw_axis(screen, x, 0, axis_x, axis_y, axis_size)
        x += axis_size

    # Draw all the hats (d-pads)
    x, y = 50, 300
    for hat_no in range(joystick.get_numhats()):
        axis_x, axis_y = joystick.get_hat(hat_no)
        draw_dpad(screen, x, y, axis_x, axis_y)
        x+= 100

    #Draw all the buttons x, y = 0.0, 390.0
    button_width = 640 / joystick.get_numbuttons()
    for button_no in range(joystick.get_numbuttons()):
        if joystick.get_button(button_no):
            pygame.draw.circle(screen, (0, 255, 0), (int(x), int(y)), 20)
        x += button_width

    pygame.display.update()

Summary

There are three modules for controls in Pygame: pygame.keyboard, pygame.mouse, and pygame.joystick. Among them, you can support just about any device the player wants to use in a game. You don’t have to support all three, and you can always give the player a choice if there is no clear winner for a control method.

Sometimes the controls influence the player character instantly, so that it will obey exactly what the player tells it to do. Classic shoot-’em-ups were like this; you pressed left, the ship instantly went left, and vice versa. For a little more realism, though, the controls should affect the player character indirectly by applying forces like thrust and break. Now that you know how to read information from game controllers, the following chapters will teach you how to use this information to manipulate objects in more realistic gaming worlds.

The controls for a game are how the player interfaces with the gaming universe. Because we can’t jack into the game, Matrix style, the controls should feel as natural as possible. When considering control methods for a game, it is a good idea to take a look at how similar games are played. Controls don’t vary a great deal for games of a similar genre. This is not because game designers lack imagination; it’s just that game players have come to expect games to work in similar ways. If you do use more creative control methods, be ready to justify it to the players or offer them a more standard alternative!

In the next chapter we will explore the topic of artificial intelligence and you will learn how to add non-player characters to a game.