33. Events and input

A game that ignores the keyboard and mouse is just a screensaver. This chapter covers how pygame collects input and how to act on it.

The event queue

Every time something happens — a key press, a mouse click, the user closing the window — pygame stores it in an internal event queue. At the start of each frame, pygame.event.get() drains that queue and returns a list of Event objects. You loop through the list and check each event's .type attribute:

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

If you never call pygame.event.get(), the queue fills up and the window stops responding to the X button.

Keyboard events: KEYDOWN and KEYUP

pygame.KEYDOWN fires once when a key is pressed down. pygame.KEYUP fires once when a key is released.

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        running = False
    if event.type == pygame.KEYDOWN:
        print(f"key pressed: {event.key}")
    if event.type == pygame.KEYUP:
        print(f"key released: {event.key}")

event.key is an integer constant. pygame provides named constants for every key:

Constant Key
pygame.K_LEFT Left arrow
pygame.K_RIGHT Right arrow
pygame.K_UP Up arrow
pygame.K_DOWN Down arrow
pygame.K_SPACE Spacebar
pygame.K_RETURN Enter
pygame.K_ESCAPE Escape
pygame.K_a A key
pygame.K_z Z key
pygame.K_0 Number 0
pygame.K_9 Number 9

Letter constants are lowercase: pygame.K_a through pygame.K_z.

KEYDOWN fires once per press. It does not repeat while the key is held. This makes it good for one-shot actions like jumping or toggling a menu:

if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_SPACE:
        jump()         # called once, not 60 times per second
    if event.key == pygame.K_ESCAPE:
        running = False

Open exercises/33/01-print-key.py. In the event loop, add a KEYDOWN handler that prints f"pressed: {pygame.key.name(event.key)}". pygame.key.name(key) converts a key constant to a human-readable string like "left" or "space". Run the program and press several keys.

Continuous input: pygame.key.get_pressed()

KEYDOWN is awkward for movement because it fires only once. If you want a character to keep moving while the player holds a key, use pygame.key.get_pressed():

keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
    x -= 5
if keys[pygame.K_RIGHT]:
    x += 5

pygame.key.get_pressed() returns a sequence where each position is True if that key is currently held and False otherwise. Call it once per frame in the update state section of the loop (not inside the event loop). Because it is checked every frame, movement is smooth.

KEYDOWN vs get_pressed — when to use each.

  • KEYDOWN: one-shot actions that should happen exactly once per press (jump, fire, toggle).
  • get_pressed: continuous actions that should repeat every frame the key is held (walk left, accelerate, scroll).

Mouse events

pygame.MOUSEBUTTONDOWN fires when a mouse button is pressed.

if event.type == pygame.MOUSEBUTTONDOWN:
    print(f"click at {event.pos}, button {event.button}")
  • event.pos(x, y) tuple of the cursor position at the time of the click.
  • event.button — which button: 1 = left, 2 = middle scroll wheel, 3 = right.

pygame.MOUSEBUTTONUP fires when a button is released (same attributes).

To get the current mouse position at any time (not just on click):

mx, my = pygame.mouse.get_pos()

This is useful for drawing something that follows the cursor:

# In the draw section:
mx, my = pygame.mouse.get_pos()
pygame.draw.circle(screen, (255, 255, 0), (mx, my), 10)

Putting input together

Here is a program that moves a square with the arrow keys, changes its colour on spacebar, and prints the mouse position on left-click:

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Input demo")
clock = pygame.time.Clock()

x, y = 370, 270
size = 60
color = (255, 50, 50)

running = True
while running:
    # 1. Events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                color = (50, 50, 255)   # turn blue on spacebar
        if event.type == pygame.KEYUP:
            if event.key == pygame.K_SPACE:
                color = (255, 50, 50)   # back to red when released
        if event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                print(f"clicked at {event.pos}")

    # 2. Update
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        x -= 4
    if keys[pygame.K_RIGHT]:
        x += 4
    if keys[pygame.K_UP]:
        y -= 4
    if keys[pygame.K_DOWN]:
        y += 4

    # 3. Draw
    screen.fill((30, 30, 30))
    pygame.draw.rect(screen, color, (x, y, size, size))

    # 4. Flip
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Homework

Problem 1 — Key reporter

Open exercises/33/homework/01-key-reporter.py. Show which key is currently held on screen. Use pygame.key.get_pressed() to detect held keys. When no key is held, show "no key". Display the text as a printed message to the terminal each frame (text rendering comes in chapter 35).

Problem 2 — Click counter

Open exercises/33/homework/02-click-counter.py. Count how many times the user left-clicks in the window. Print the count to the terminal each time it increases.

Problem 3 — Arrow mover

Open exercises/33/homework/03-arrow-mover.py. A circle starts at the centre. Arrow keys move it continuously (get_pressed). Pressing Escape quits the program. Print "moved" to the terminal each frame the circle is moving.

Challenge — Mouse follower

Open exercises/33/homework/04-mouse-follower.py. Draw a small circle at the current mouse position every frame so it follows the cursor. When the left mouse button is held (check pygame.MOUSEBUTTONDOWN and MOUSEBUTTONUP to track a held state), change the circle's colour.

Stuck or finished? Open the homework solutions page.