Module 5 of 20 Phase: Player Interaction

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.

📘
10 Lessons
In this module
⏱️
~90 min
Estimated time
🎯
Beginner–Int.
Difficulty
🎮
pygame.event
Primary API
Section 1

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:

keyboard_events.py
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()
💡
Key insight: KEYDOWN/KEYUP are one-time notifications, not continuous signals. If you need to know "is this key currently being held down right now" — for smooth walking, for example — event-based detection is the wrong tool. That's what Section 2 solves.
Section 2

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.

continuous_movement.py
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()
Events
Fire once per press/release. Best for menu clicks, jumping, pausing, single actions that shouldn't repeat every frame.
🔁
get_pressed()
Polls current state every frame. Best for movement, holding to run, or any input that needs to feel continuous and simultaneous.
Section 3

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.

mouse_click.py
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()
🎯
Click-inside-a-rect check: To know if a click landed on a button or sprite, test my_rect.collidepoint(event.pos) inside the MOUSEBUTTONDOWN branch — this is the standard way to build clickable UI in raw Pygame.
Section 4

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.

mouse_drag.py
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.

Section 5

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.

scroll_and_remap.py
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 TypeDetection MethodBest For
Key press/releaseKEYDOWN / KEYUPJump, pause, menu select
Held keyskey.get_pressed()Walking, running, aiming
Mouse clickMOUSEBUTTONDOWN/UPUI buttons, shooting, selecting
Mouse positionmouse.get_pos()Aiming, hover, tooltips
DraggingMOUSEMOTION + flagDraggable UI, level editors
Scroll wheelMOUSEWHEELZoom, inventory scroll, volume
Section 6

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.

🕹️
WASD Movement
get_pressed() every frame, normalized diagonal speed, delta-time scaled.
💥
Space Flash
KEYDOWN one-shot event toggling a color for a few frames.
🖱️
Drag a Box
MOUSEBUTTONDOWN + MOUSEMOTION + a dragging flag.
Checkpoint: If your practice scene handles all four input types without any of them interfering with each other, you're ready for Module 6, where this same input logic will start controlling actual sprite objects instead of plain rectangles.
Visual Reference

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.

Keyboard KEYDOWN / KEYUP Mouse clicks & motion Event Queue pygame.event.get() Game Loop handled once per frame
Keyboard and mouse activity both feed into the same event queue; the game loop drains it once every frame, in the order events occurred.

🧠 Quick Knowledge Check

1. Which is the correct tool for detecting a single "jump" action that shouldn't repeat while the key is held?
2. Why should smooth WASD movement use get_pressed() instead of KEYDOWN events?
3. What does event.button equal to 1 mean in a MOUSEBUTTONDOWN event?
4. In the mouse-dragging pattern, why do we store a drag_offset instead of just setting the box's position directly to the mouse position?
5. Which Pygame event reports scroll wheel movement, with a .y attribute positive for scrolling up?
Finished Module 5?

Mark it complete to track your progress through the Pygame Mastery course.

🗒 Cheat Sheet 📝 Worksheet