34. Moving things

Chapters 32 and 33 covered drawing and input. This chapter connects them: objects that move in response to input, bounce off walls, and collide with each other.

Position as variables

The simplest way to move anything is to store its position in variables and update them each frame. In the draw section, use those variables to position the shape:

x = 100
y = 100

# In the game loop update section:
x += 3   # move 3 pixels right every frame

# In the draw section:
pygame.draw.rect(screen, (255, 100, 0), (x, y, 40, 40))

At 60 fps, adding 3 each frame moves the object 180 pixels per second.

Velocity

Velocity is the change in position per frame. Store it in variables dx (change in x) and dy (change in y):

x  = 100
y  = 100
dx = 3
dy = 2

# Update section each frame:
x += dx
y += dy

To reverse direction, negate the velocity: dx = -dx. This is the "bounce" pattern — used for bouncing balls.

pygame.Rect

pygame.Rect(x, y, width, height) is a rectangle object that pygame provides for convenient positioning and collision detection.

rect = pygame.Rect(100, 200, 60, 60)

Useful attributes:

Attribute Meaning
rect.x Left edge x position
rect.y Top edge y position
rect.width Width in pixels
rect.height Height in pixels
rect.centerx x coordinate of the centre
rect.centery y coordinate of the centre
rect.right x coordinate of the right edge
rect.bottom y coordinate of the bottom edge

Move the rect by assigning to rect.x and rect.y. Use it in draw calls directly — pygame.draw.rect accepts a Rect:

rect = pygame.Rect(100, 200, 60, 60)
rect.x += 3

pygame.draw.rect(screen, (255, 0, 0), rect)

Setting rect.centerx and rect.centery moves the rect so its centre is at that position:

rect.centerx = 400
rect.centery = 300
# rect is now centred in an 800x600 window

Controlling frame rate

clock.tick(60) limits the loop to 60 frames per second. Without it, a fast CPU runs thousands of frames per second and objects fly across the screen far too quickly. With it, movement speed is predictable: adding 4 to x each frame moves an object 4 * 60 = 240 pixels per second, on any machine.

On a very slow machine clock.tick(60) cannot force the loop to run faster than it naturally runs. The actual frame rate may be lower than 60 on slow hardware. For this book's purposes, 60 fps is reliable enough.

Boundary checking

Objects that move off the screen need to be stopped or bounced back. Check the edges manually each frame.

Stop at the wall:

if rect.left < 0:
    rect.left = 0
if rect.right > 800:
    rect.right = 800
if rect.top < 0:
    rect.top = 0
if rect.bottom > 600:
    rect.bottom = 600

rect.left < 0 means the left edge has gone past the left wall. Setting rect.left = 0 pushes it back.

Bounce off the wall:

if rect.left < 0 or rect.right > 800:
    dx = -dx
if rect.top < 0 or rect.bottom > 600:
    dy = -dy

Reversing velocity sends the object back the way it came.

Collision detection with Rect.colliderect

Rect.colliderect(other_rect) returns True if the two rectangles overlap:

player_rect = pygame.Rect(100, 100, 50, 50)
coin_rect   = pygame.Rect(120, 110, 30, 30)

if player_rect.colliderect(coin_rect):
    print("collected!")

Use this to detect when a player touches a coin, an enemy, or any other object.

Full example: arrow-key square that bounces

import pygame

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

rect  = pygame.Rect(370, 270, 60, 60)
speed = 4

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

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

    # Clamp to window edges
    if rect.left < 0:
        rect.left = 0
    if rect.right > 800:
        rect.right = 800
    if rect.top < 0:
        rect.top = 0
    if rect.bottom > 600:
        rect.bottom = 600

    # 3. Draw
    screen.fill((30, 30, 30))
    pygame.draw.rect(screen, (255, 100, 50), rect)

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

pygame.quit()

Open exercises/34/01-bouncing-ball.py. Add a ball (circle) that moves on its own using dx and dy. Each frame, add dx to x and dy to y. When the ball hits the left or right edge of the window, negate dx. When it hits the top or bottom, negate dy. Start with dx = 3, dy = 2.

Homework

Problem 1 — Clamped mover

Open exercises/34/homework/01-clamped-mover.py. A square moves with arrow keys. It must never leave the window — clamp it to all four edges.

Problem 2 — Two-ball bounce

Open exercises/34/homework/02-two-balls.py. Two balls start at different positions with different velocities. Both bounce off all four walls independently. Each ball should be a different colour.

Problem 3 — Follow the mouse

Open exercises/34/homework/03-follow-mouse.py. A square moves toward the current mouse position each frame. It does not jump to the cursor; it moves a fixed number of pixels toward it per frame, stopping when it arrives. Hint: compute the difference in x and y, and move a fraction of that difference.

Challenge — Collect a target

Open exercises/34/homework/04-collect-target.py. A player square moves with arrow keys. A stationary target circle is drawn somewhere on screen. When the player's rect overlaps the target (use colliderect or a distance check for circles), move the target to a new random position and print "collected!" to the terminal.

Stuck or finished? Open the homework solutions page.