34. Moving things — Homework solutions
Problem 1 — Clamped mover
Problem. A square that moves with arrow keys and stays inside the window.
How to think about it. Apply the
get_pressed movement, then clamp all four edges using
rect.left, rect.right, rect.top,
rect.bottom.
Worked solution.
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Clamped Mover")
clock = pygame.time.Clock()
rect = pygame.Rect(370, 270, 60, 60)
speed = 5
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
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
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
screen.fill((20, 20, 40))
pygame.draw.rect(screen, (100, 200, 255), rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()Common mistakes.
- Clamping
rect.x < 0but not accounting for width. If you checkx < 0and clamp tox = 0, the left edge is correct. But for the right edge you must checkrect.right > 800(which isrect.x + rect.width > 800). Usingrect.rightandrect.leftis simpler than doing the arithmetic manually.
Problem 2 — Two-ball bounce
Problem. Two balls bouncing off all four walls independently.
How to think about it. Use two sets of position and velocity variables. The bounce logic is the same for both; just write it twice (or make a function).
Worked solution.
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Two Balls")
clock = pygame.time.Clock()
ax, ay, adx, ady = 200, 150, 4, 3
bx, by, bdx, bdy = 600, 400, -3, 5
radius = 20
W, H = 800, 600
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
ax += adx
ay += ady
if ax - radius < 0 or ax + radius > W:
adx = -adx
if ay - radius < 0 or ay + radius > H:
ady = -ady
bx += bdx
by += bdy
if bx - radius < 0 or bx + radius > W:
bdx = -bdx
if by - radius < 0 or by + radius > H:
bdy = -bdy
screen.fill((10, 10, 10))
pygame.draw.circle(screen, (255, 80, 80), (ax, ay), radius)
pygame.draw.circle(screen, (80, 180, 255), (bx, by), radius)
pygame.display.flip()
clock.tick(60)
pygame.quit()The boundary check uses ax - radius < 0 because the
position is the centre of the circle, and the circle extends
radius pixels in each direction.
Common mistakes.
- Checking
ax < 0instead ofax - radius < 0. The centre can be at(5, ...)while the left edge of the ball is already off screen.
Problem 3 — Follow the mouse
Problem. A square moves toward the mouse position each frame, a fixed step at a time.
How to think about it. Compute
dx = mx - rect.centerx and
dy = my - rect.centery. These are the distances to travel.
Each frame, move a fraction of that distance (a step smaller than the
full gap) or stop when close enough.
Worked solution.
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Follow Mouse")
clock = pygame.time.Clock()
rect = pygame.Rect(370, 270, 50, 50)
speed = 5
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
mx, my = pygame.mouse.get_pos()
dx = mx - rect.centerx
dy = my - rect.centery
if abs(dx) > speed:
rect.x += speed if dx > 0 else -speed
else:
rect.centerx = mx
if abs(dy) > speed:
rect.y += speed if dy > 0 else -speed
else:
rect.centery = my
screen.fill((20, 20, 20))
pygame.draw.rect(screen, (255, 200, 50), rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()speed if dx > 0 else -speed moves one step in the
direction of the target. The else branch snaps to the
target when the remaining gap is smaller than one step, preventing
jittering.
Challenge — Collect a target
Problem. Player moves with arrow keys. Touching the target moves it to a random position.
How to think about it. Use
pygame.Rect.colliderect for the collision.
import random to pick the new position. The target's rect
must stay fully inside the window — subtract target.width
and target.height from the upper bound.
Worked solution.
import pygame
import random
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Collect Target")
clock = pygame.time.Clock()
W, H = 800, 600
player = pygame.Rect(370, 270, 50, 50)
target = pygame.Rect(random.randint(0, W - 40), random.randint(0, H - 40), 40, 40)
speed = 5
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.x -= speed
if keys[pygame.K_RIGHT]:
player.x += speed
if keys[pygame.K_UP]:
player.y -= speed
if keys[pygame.K_DOWN]:
player.y += speed
# clamp
player.clamp_ip(pygame.Rect(0, 0, W, H))
if player.colliderect(target):
print("collected!")
target.x = random.randint(0, W - target.width)
target.y = random.randint(0, H - target.height)
screen.fill((20, 20, 30))
pygame.draw.circle(screen, (255, 220, 0),
(target.centerx, target.centery), target.width // 2)
pygame.draw.rect(screen, (100, 200, 255), player)
pygame.display.flip()
clock.tick(60)
pygame.quit()pygame.Rect.clamp_ip(boundary_rect) moves the rect so it
fits inside the boundary — a built-in alternative to four separate edge
checks.
Common mistakes.
- Placing the target randomly at any x up to
Wwithout subtractingtarget.width, allowing part of the target to spawn off-screen.
Done?
Chapter 35 adds text rendering and score tracking to complete a basic game loop.