Pygame Movement, Collisions, and Scores

Two boxes, and whether they touch

Advanced 14 min

In this lesson

A window with a moving square is a demo. Adding input, collision and a score makes it a game.

All three are simpler than they look. Movement is changing a number; collision is asking whether two boxes overlap; a score is a variable. The work is in wiring them together cleanly.

Explain it like I’m 5

Movement is just changing a location number. Collision is checking whether two boxes touch. A score is a number that goes up when they do.

Keyboard input that feels right

There are two ways to read the keyboard, and picking the wrong one is why some games feel stuttery.

Events (KEYDOWN) fire once, at the moment a key goes down. Right for things that happen once: jump, shoot, pause.

State (pygame.key.get_pressed()) reports which keys are held down right now. Right for continuous movement, because it is true on every frame the key is held.

Use KEYDOWN for walking and the player moves once, then pauses, then repeats at the operating system's key-repeat rate, exactly the stutter you get when holding a key in a text editor.

Example · collect_coins.py
import pygame

WIDTH, HEIGHT = 400, 300
SIZE, SPEED = 40, 5

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()

player = pygame.Rect(50, 130, SIZE, SIZE)
coin = pygame.Rect(300, 130, 20, 20)
score = 0
running = True

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

    # Held keys: smooth, continuous movement.
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        player.x -= SPEED
    if keys[pygame.K_RIGHT]:
        player.x += SPEED

    # Keep the player on screen.
    player.x = max(0, min(player.x, WIDTH - SIZE))

    # Collision, and a score.
    if player.colliderect(coin):
        score += 1
        coin.x = (coin.x + 137) % (WIDTH - 20)

    screen.fill((20, 24, 40))
    pygame.draw.rect(screen, (43, 124, 211), player)
    pygame.draw.rect(screen, (240, 190, 60), coin)
    pygame.display.flip()
    clock.tick(60)

print("final score:", score)
pygame.quit()
Output
# Arrow keys slide the blue square left and right.
# Touching the yellow coin scores a point and moves the coin.
# The player cannot leave the window.
A playable game in forty lines. Run locally with: python collect_coins.py

What colliderect actually does

colliderect() is four comparisons, and knowing them is worth more than the method itself: the same logic turns up in every game framework, and in plenty of code that has nothing to do with games.

Two rectangles overlap when they overlap on both axes. Written out: A's left edge is left of B's right edge, and A's right edge is right of B's left edge, and the same pair vertically.

The easier way to think about it is backwards: they miss if there is a gap on either axis. If one box is entirely to the left, or entirely above, they cannot be touching, whatever the other axis says.

Example
def overlaps(a, b):
    """Do two boxes touch? Each box is (x, y, width, height)."""
    ax, ay, aw, ah = a
    bx, by, bw, bh = b
    return (ax < bx + bw and ax + aw > bx and
            ay < by + bh and ay + ah > by)

player = (10, 10, 20, 20)
coin = (25, 25, 10, 10)
wall = (100, 100, 30, 30)

print("player hits coin:", overlaps(player, coin))
print("player hits wall:", overlaps(player, wall))

score = 0
for item in [coin, wall, (15, 15, 5, 5)]:
    if overlaps(player, item):
        score += 10
print("score:", score)
Output
player hits coin: True
player hits wall: False
score: 20
colliderect, written out in full. Four comparisons.

Use the collision test to collect coins. Keep only the coins that overlap the player, score ten points each, and then check the edge case: does a coin whose left edge sits exactly on the player's right edge count as touching?

def overlaps(a, b):
    ax, ay, aw, ah = a
    bx, by, bw, bh = b
    return (ax < bx + bw and ax + aw > bx and
            ay < by + bh and ay + ah > by)

player = (0, 0, 10, 10)
coins = [(5, 5, 4, 4), (50, 50, 4, 4), (9, 9, 4, 4)]

# TODO: keep only the coins that overlap the player
collected = []

print("coins collected:", len(collected))
print("score:", len(collected) * 10)
print("touching at exactly x=10:", overlaps(player, (10, 0, 4, 4)))

Scores, wins, and losses

A score is a variable that goes up. What makes a game is what the score leads to, and that means the program needs to know which phase it is in.

The usual mistake is a tangle of booleans: game_over, won, paused, all separately true or false, which allows nonsense like won and lost at once. A single state variable holding one of "playing", "won", "lost" cannot contradict itself, and the loop simply draws whatever the current phase calls for.

Restarting is then the previous lesson's lesson again: build a fresh state rather than resetting fields one at a time. Forgetting to zero the score on restart is the classic bug, and creating a new state makes it impossible.

Common mistake: Using KEYDOWN events for continuous movement

Why it happens:

Events are how the quit button was handled, so they look like the way to read keys.

How to fix it:

KEYDOWN fires once per press. Use pygame.key.get_pressed() for held keys and smooth movement.

Common mistake: Letting the player leave the window

Why it happens:

Nothing stops a coordinate growing forever.

How to fix it:

Clamp after moving: max(0, min(x, WIDTH - size)). Subtracting the object's own size keeps it fully on screen.

Common mistake: Tracking game phase with several booleans

Why it happens:

Each flag is added when its feature is, one at a time.

How to fix it:

Use one variable holding "playing", "won" or "lost". Separate flags allow impossible combinations.

Common mistake: Forgetting to reset the score when restarting

Why it happens:

The score lives outside the round and nothing clears it.

How to fix it:

Build a fresh state for a new game instead of resetting fields by hand.

Which is right for smooth, continuous movement?

What does colliderect() check?

How should you keep a player inside the window?

Mini exercise (medium)

Write the scoring pass a game runs every frame. Given a player box and a list of coin boxes, return the coins that were not collected along with the points earned, so the caller can replace the coin list and add to the score. Then report whether the level is complete.

Now you. Edit the starter code below, then Run it, everything happens in the browser.

def overlaps(a, b):
    ax, ay, aw, ah = a
    bx, by, bw, bh = b
    return (ax < bx + bw and ax + aw > bx and
            ay < by + bh and ay + ah > by)

def collect(player, coins):
    """Return (coins still on screen, points earned)."""
    # TODO: sort the coins into collected and remaining
    # TODO: 10 points per collected coin
    return coins, 0

player = (0, 0, 10, 10)
coins = [(5, 5, 4, 4), (50, 50, 4, 4), (9, 9, 4, 4), (80, 80, 4, 4)]

remaining, points = collect(player, coins)

print("remaining:", len(remaining))
print("points:", points)
print("level complete:", len(remaining) == 0)

What to learn next

You made it playable: held-key input for smooth movement, clamping to keep the player on screen, pygame.Rect and the four comparisons that colliderect() really performs, scoring, and one state variable rather than a pile of contradictory booleans.

Same loop, different clothes. Tkinter Basics builds desktop windows with buttons and text boxes, and runs the event loop for you.