[personal profile] tangaroa

Some notes on keypress handling in Python's PyGame library, from about seven years ago:

pygame offers two obvious keyboard input handling methods. One is to use pygame.key.get_pressed() to get an array of all the keys' state and pick and choose from that, and the other is to use pygame's event model.

I'm writing a simple game where the player moves around the screen while being chased by a computer character, and I'm thinking in a future version there might be a swarm of multiple player characters moving in tandem like in Atari Combat's air fighter mode.

At first, I decide to use the event model since it seems like the most straightforward way of handling key events -- a key is pressed, you handle it, the next key is pressed, and so on.

The first attempt had a handleEvent() function inside the player object class, but I ripped it out since it would get in the way if I had multiple objects.

Attempt #2

Create a global keyState array for the four keys we're watching for. Each frame, loop through events and run custom handleEvent() function on each, which does:

 if(e.type == KEYDOWN):
        if(e.key == K_UP):
            keyState[0] = 1
        elif(e.key == K_DOWN):
            keyState[1] = 1
        elif(e.key == K_LEFT):
            keyState[2] = 1
        elif(e.key == K_RIGHT):
            keyState[3] = 1
    elif(e.type == KEYUP):
        if(e.key == K_UP):
            keyState[0] = 0
        elif(e.key == K_DOWN):
            keyState[1] = 0
        elif(e.key == K_LEFT):
            keyState[2] = 0
        elif(e.key == K_RIGHT):
            keyState[3] = 0

Then the player object's move() function checks keyState[] for whether a key is up or down.

This isn't very robust, but it's a start. Unfortunately, it has a more serious problem: the keypad doesn't work since we're only checking the arrow keys.

Some quick testing shows that pygame doesn't care whether numlock is pressed, so we can easily add the keypad constants to the if conditions:

Attempt #3

	e.key == K_UP or e.key == K_KP8
            keyState[0] = 1

Unfortunately, making the keypad work introduces a bug. If both of the movement keys for the same direction are ever pressed down at the same time, releasing one will negate the effect of the other. For instance, assume you pressed the up arrow and then pressed the keypad's '8' up arrow while still holding the regular up arrow. If you then let go of any one of the keys, your character stops moving forward even though you still have the other key pressed. While this side effect might be seen favourably by the player's younger sibling, it's not good for gameplay.

A simple solution is to make keyState a counter instead of a boolean. Instead of saying whether a key is pressed, it will count how many of that key are pressed

            keyState[0] += 1

and change the "= 0" part to "-= 1", and change the player's keyState handling code to match the new logic.

However, this introduces another bug. But wait, isn't this solution logically sound? While the software looks fine, it assumes that the hardware will always play fair. Keyboards don't. If you mash a whole bunch of keys at once, some events do not come through. Theoretically, then, we have the possibility of the player's movement getting locked in motion with no keys pressed or disabled when only one of two keys is released.

While this solution might be good enough for most situations, it is insufficiently perfect for me. We need to be absolutely sure whether the keys are being pressed or not, and for that, we have to go back to the pygame.key.get_pressed() function that I decided not to use in the first place.

Since we're already checking against 'an array of whether keys are pressed', why not just use the array returned by get_pressed()? I originally thought "because we don't need all the keys" but it's not a whole lot of memory and I can't see it being a huge performance hit.

Since I'm too lazy to rewrite the player end of the code, I just create a function to do this:

    kArray = pygame.key.get_pressed()
    keyState[0] = kArray[K_UP] + kArray[K_KP8]
    keyState[1] = kArray[K_DOWN] + kArray[K_KP2]
    keyState[2] = kArray[K_LEFT] + kArray[K_KP4]
    keyState[3] = kArray[K_RIGHT] + kArray[K_KP6]

So the old keyState array is really being used, but it's fed by the kArray values.

I still keep enough of a for loop through pygame.event.get to check for a QUIT event on hitting the game window's close button. If I didn't have this loop, I'd need to add a pygame.event.pump() call or else the keyboard events just wouldn't be handled by the pygame internals that produce the get_pressed() array.

There is still a button mashing problem -- if you press enough buttons at once, get_pressed() doesn't recognize that they're all being pressed. However, once you start releasing a few keys, the others are recognized. This wasn't the case with the custom code earlier.

Page generated Jan. 22nd, 2026 11:48 am
Powered by Dreamwidth Studios