User Input
Keyboard & Mouse in Pygame
Every game lives or dies by how it feels to control. In this module you'll learn Pygame's two input models — discrete events (KEYDOWN, KEYUP, MOUSEBUTTONDOWN) and continuous state polling (get_pressed()) — and how to combine them for responsive movement, mouse dragging, and scroll-based zooming.
Keyboard Events: KEYDOWN and KEYUP
Pygame reports keyboard activity as events that flow through the event queue you already loop over with pygame.event.get(). Two event types matter most for keys: pygame.KEYDOWN fires exactly once the instant a key is pressed, and pygame.KEYUP fires exactly once the instant it's released. These are perfect for "one-shot" actions — jumping, firing a single bullet, opening a menu, pausing the game — because they don't repeat every frame while the key stays held.
Each keyboard event carries a .key attribute you compare against Pygame's key constants, all prefixed K_ — K_SPACE, K_ESCAPE, K_a, K_LEFT, and so on. Here's the pattern for detecting a single key press and its corresponding release:
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
print("Jump! (fired once per press)")
if event.key == pygame.K_ESCAPE:
running = False
if event.type == pygame.KEYUP:
if event.key == pygame.K_SPACE:
print("Space released")
screen.fill((15, 20, 35))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Multiple Keys at Once: get_pressed()
Real movement needs to feel continuous, and players often hold two keys simultaneously (diagonal movement, sprint + move). pygame.key.get_pressed() returns a list-like object of booleans for every key on the keyboard, indexed by the same K_* constants, reflecting their state at this exact frame. Call it once per frame — outside the event loop — and check as many keys as you like without missing simultaneous presses.
This is the standard pattern for 2D top-down or platformer movement: read the pressed-state dictionary every frame, accumulate a movement vector from however many direction keys are held, then apply it to the player's position scaled by speed and delta time.
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
player = pygame.Rect(400, 300, 40, 40)
speed = 300 # pixels per second
running = True
while running:
dt = clock.tick(60) / 1000 # delta time in seconds
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
keys = pygame.key.get_pressed()
dx, dy = 0, 0
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
dx -= 1
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
dx += 1
if keys[pygame.K_UP] or keys[pygame.K_w]:
dy -= 1
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
dy += 1
# Normalize diagonal movement so it isn't faster than straight movement
if dx != 0 and dy != 0:
dx *= 0.7071
dy *= 0.7071
player.x += dx * speed * dt
player.y += dy * speed * dt
screen.fill((15, 20, 35))
pygame.draw.rect(screen, (52, 211, 153), player)
pygame.display.flip()
pygame.quit()
Mouse Clicks and Mouse Position
Mouse input mirrors the keyboard's event/poll split. pygame.MOUSEBUTTONDOWN and pygame.MOUSEBUTTONUP events fire once per click, carrying a .button attribute (1 = left, 2 = middle, 3 = right) and a .pos tuple with the exact pixel coordinates of the click. For the mouse's live position at any moment — useful for aiming reticles, tooltips, or hover highlighting — call pygame.mouse.get_pos(), which returns an (x, y) tuple every frame without needing an event at all.
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # left click
print(f"Left-clicked at {event.pos}")
elif event.button == 3: # right click
print(f"Right-clicked at {event.pos}")
if event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
print("Left button released")
# Live position, read every frame (no event needed)
mouse_x, mouse_y = pygame.mouse.get_pos()
my_rect.collidepoint(event.pos) inside the MOUSEBUTTONDOWN branch — this is the standard way to build clickable UI in raw Pygame.Mouse Dragging
Dragging is a three-part state machine: detect the press (start dragging), track movement while the button stays held (via MOUSEMOTION events or get_pos() each frame), and detect the release (stop dragging). You need a boolean flag to remember "am I currently dragging" between frames, plus an offset so the dragged object doesn't snap its center to the cursor the instant you grab it.
box = pygame.Rect(300, 250, 100, 100)
dragging = False
drag_offset = (0, 0)
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if box.collidepoint(event.pos):
dragging = True
drag_offset = (box.x - event.pos[0], box.y - event.pos[1])
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
dragging = False
if event.type == pygame.MOUSEMOTION and dragging:
box.x = event.pos[0] + drag_offset[0]
box.y = event.pos[1] + drag_offset[1]
Notice that MOUSEMOTION events fire continuously while the mouse moves (not just once), which is exactly what dragging needs — the box's position updates on every pixel of motion, but only while dragging is True.
Scroll Wheel and Custom Controls
Modern Pygame (2.0+) reports the scroll wheel as a dedicated pygame.MOUSEWHEEL event with a .y attribute: positive when scrolling up, negative when scrolling down. It's ideal for zooming a camera, cycling inventory slots, or adjusting volume sliders without needing extra buttons.
Beyond built-in defaults, well-designed games expose custom, remappable controls — instead of hardcoding K_w everywhere, store the current bindings in a dictionary you can update from a settings menu. This one extra layer of indirection is what makes a control scheme reconfigurable.
zoom = 1.0
# Remappable bindings — change these from a settings screen
controls = {
"jump": pygame.K_SPACE,
"left": pygame.K_a,
"right": pygame.K_d,
}
for event in pygame.event.get():
if event.type == pygame.MOUSEWHEEL:
zoom += event.y * 0.1
zoom = max(0.5, min(zoom, 3.0)) # clamp zoom range
if event.type == pygame.KEYDOWN:
if event.key == controls["jump"]:
print("Jump triggered via remapped key")
| Input Type | Detection Method | Best For |
|---|---|---|
| Key press/release | KEYDOWN / KEYUP | Jump, pause, menu select |
| Held keys | key.get_pressed() | Walking, running, aiming |
| Mouse click | MOUSEBUTTONDOWN/UP | UI buttons, shooting, selecting |
| Mouse position | mouse.get_pos() | Aiming, hover, tooltips |
| Dragging | MOUSEMOTION + flag | Draggable UI, level editors |
| Scroll wheel | MOUSEWHEEL | Zoom, inventory scroll, volume |
Practice: Combine Everything
The strongest way to lock in these concepts is to build one small scene that uses every technique above at once. Try building a scene where WASD moves a square smoothly (using get_pressed()), space bar triggers a one-shot color flash (using KEYDOWN), left-click-and-drag repositions a second box, and the scroll wheel scales a third shape up and down. Combining event-based and poll-based input in the same loop is exactly what real games do constantly.
Where Input Actually Goes: The Event Flow
Whether a player presses a key or clicks the mouse, Pygame funnels it through the exact same pipeline: the operating system reports the action, Pygame packages it as an event and appends it to one shared event queue, and your game loop drains that queue with pygame.event.get() once per frame. Keyboard and mouse input never take separate paths — they both land in the same queue, in the order they happened.