How can I allow keyboard inputs to override each other in pygame?

Question:

I’m fairly new to Python (and programming in general), and brand-new to Pygame.

My desired outcome is: if I’m holding the A key, the character moves left. If I continue holding the A key and additionally hold down the W key, the character moves up. If I release the W key but not the A key, the character continues left.

This is the main loop:

direction = 0 #0 left, 1 right, 2 up, 3 down

move = False

running = True
while running:
    #cap framerate
    clock.tick(FPS)

    #locational updates
    dx = 0
    dy = 0
    if move:
        if direction == 0:
            dx = -SPEED
        if direction == 1:
            dx = SPEED
        if direction == 2:
            dy = -SPEED
        if direction == 3:
            dy = SPEED

    #draw background
    screen.fill(GREY)

    player.move(dx, dy)
    player.update()
    player.draw(screen)

    #handle keypresses
    keys = pygame.key.get_pressed()
    if keys[pygame.K_a] or keys[pygame.K_LEFT]:
        direction = 0
        move = True
    elif keys[pygame.K_d] or keys[pygame.K_RIGHT]:
        direction = 1
        move = True
    elif keys[pygame.K_w] or keys[pygame.K_UP]:
        direction = 2
        move = True
    elif keys[pygame.K_s] or keys[pygame.K_DOWN]:
        direction = 3
        move = True
    else:
        move = False

    #event handler
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False

    #show all the things
    pygame.display.update()

The actual outcome is: the S key can be overridden as desired, since it’s at the bottom of the if statements. The W key can be overridden by any key except S, D can only be overridden by A, and A cannot be overridden at all.

I tried using KEYDOWN and KEYUP, but that was even further from what I wanted. I also tried changing the elifs to ifs, but that just reversed the hierarchy, which makes sense. I’ve also searched for the answer, but I haven’t found anything which specifically relates to my problem.

I understand that the root of the problem is that Python reads the code line by line, but with my limited knowledge I don’t know how to get around that. Also, I’m specifically trying to avoid diagonal movement. Thanks for any help you guys can give.

Asked By: ismynamegeoff

||

Answers:

You have to use the KEYDOW and KEYUP event. The last key pressed defines the direction of the movement. You need to implement a queue of hit keys.

Create a dictionary that maps keys to direction vectors and an empty direction queue:

direction_map = {
    pygame.K_a: (-1, 0), pygame.K_LEFT: (-1, 0), pygame.K_d: (1, 0), pygame.K_RIGHT: (1, 0),
    pygame.K_w: (0, -1), pygame.K_UP: (0, -1), pygame.K_s: (0, 1), pygame.K_DOWN: (0, 1) 
}
direction_queue = []

Add or move a direction to the end of the queue in the ‘KEYDOWN’ event:

while run:
    for event in pygame.event.get():
        # [...]

        if event.type == pygame.KEYDOWN:
            new_direction = direction_map.get(event.key)
            if new_direction in direction_queue:
                direction_queue.remove(new_direction)
            if new_direction:
                direction_queue.append(new_direction)

Removes a direction vector from the queue in the ‘KEYUP’ event:

while run:
    for event in pygame.event.get():
        # [...]

        if event.type == pygame.KEYUP:
            release_direction = direction_map.get(event.key)
            if release_direction in direction_queue:
                direction_queue.remove(release_direction)

Set the movement depending on the last item in the queue:

while run:
    # [...]

    dx, dy = direction_queue[-1] if direction_queue else (0, 0)

Minimal example:

import pygame

pygame.init()
window = pygame.display.set_mode((400, 400))
clock = pygame.time.Clock()

rect = pygame.Rect(190, 190, 20, 20)
speed = 1

direction_map = {
    pygame.K_a: (-1, 0), pygame.K_LEFT: (-1, 0), pygame.K_d: (1, 0), pygame.K_RIGHT: (1, 0),
    pygame.K_w: (0, -1), pygame.K_UP: (0, -1), pygame.K_s: (0, 1), pygame.K_DOWN: (0, 1) 
}
direction_queue = []

run = True
while run:
    clock.tick(100)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False 
        if event.type == pygame.KEYDOWN:
            new_direction = direction_map.get(event.key)
            if new_direction in direction_queue:
                direction_queue.remove(new_direction)
            if new_direction:
                direction_queue.append(new_direction)
        if event.type == pygame.KEYUP:
            release_direction = direction_map.get(event.key)
            if release_direction in direction_queue:
                direction_queue.remove(release_direction)
                
    dx, dy = direction_queue[-1] if direction_queue else (0, 0)
    rect.x = (rect.x + dx * speed) % 400
    rect.y = (rect.y + dy * speed) % 400

    window.fill(0)
    pygame.draw.rect(window, "red", rect)
    pygame.display.flip()
    
pygame.quit()
exit()
Answered By: Rabbid76

So with help from @Rabbid76, I found this solution, in case anyone else has this problem (though there’s probably a better way to do it):

First, instead of using the direction and move variables, I used these two:

#save the last pressed key
last_key = None
#create a list of key presses
key_list = []

Second, this section now uses the last_key variable to determine direction:

running = True
while running:
    #[...]

    #locational updates
    dx, dy = 0, 0
    #check which key should be controlling movement
    if last_key in [pygame.K_a, pygame.K_LEFT]:
        dx = -SPEED
    elif last_key in [pygame.K_d, pygame.K_RIGHT]:
        dx = SPEED
    elif last_key in [pygame.K_w, pygame.K_UP]:
        dy = -SPEED
    elif last_key in [pygame.K_s, pygame.K_DOWN]:
        dy = SPEED
    else:
        dx, dy = 0, 0

Finally, my event handler now adds KEYDOWN events to the list stored in the key_list variable, then uses that list during KEYUP events to see if it should revert to a previous direction:

running = True
while running:
    #[...]

    for event in pygame.event.get():
        #handle keypresses
        if event.type == pygame.KEYDOWN:
            if event.key in [pygame.K_a, pygame.K_LEFT]:
                last_key = event.key
                #avoid redundancy in list of key presses
                if last_key in key_list:
                    key_list.remove(last_key)
                #add the key to the list
                key_list.append(last_key)
            if event.key in [pygame.K_d, pygame.K_RIGHT]:
                last_key = event.key
                if last_key in key_list:
                    key_list.remove(last_key)
                key_list.append(last_key)
            if event.key in [pygame.K_w, pygame.K_UP]:
                last_key = event.key
                if last_key in key_list:
                    key_list.remove(last_key)
                key_list.append(last_key)
            if event.key in [pygame.K_s, pygame.K_DOWN]:
                last_key = event.key
                if last_key in key_list:
                    key_list.remove(last_key)
                key_list.append(last_key)

        if event.type == pygame.KEYUP:
            if event.key == last_key: #this is important or else the character stops moving if you, say, hold right, then left, then release right
                try:
                    #check the second-to-last key to see if it is still pressed
                    if pygame.key.get_pressed()[key_list[len(key_list) - 2]]:
                        #if so, that is now the key to use
                        last_key = key_list[len(key_list) - 2]
                    #check if third-to-last key is still pressed
                    elif pygame.key.get_pressed()[key_list[len(key_list) - 3]]:
                        last_key = key_list[len(key_list) - 3]
                    #check if fourth-to-last key is still pressed
                    elif pygame.key.get_pressed()[key_list[len(key_list) - 4]]:
                        last_key = key_list[len(key_list) - 4]
                    #this breaks if you mix arrows and wasd, but you could add more of the above checks to fix that if it's an issue
                    else:
                        last_key = None
                except:
                    last_key = None


        if event.type == QUIT:
            running = False
Answered By: ismynamegeoff
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.