What you will learn
This chapter is where your game comes alive: the ball will finally move! You will discover how to animate an object on screen, how to make it bounce off walls, and why it is essential to use delta time for smooth, frame-rate-independent movement.
The concept
To move an object, we change its position every frame by adding a velocity (speed with a direction). Velocity is split into two components:
vx: horizontal speed (positive = rightward)vy: vertical speed (positive = downward, because the Y axis points down in Pygame)
Every frame we do: ball_x += vx and ball_y += vy. But there is a problem: if the game runs at 60 FPS on a fast PC and at 30 FPS on a slow PC, the ball will move twice as fast on the fast PC!
The solution is delta time (dt): the time elapsed since the last frame, in seconds. We multiply velocity by dt: ball_x += vx * dt. This way, speed is expressed in pixels per second and stays constant regardless of the frame rate.
clock.tick(FPS) returns the number of milliseconds since the last call. We divide by 1000 to get seconds: dt = clock.tick(FPS) / 1000.0.
Bouncing
To bounce the ball off a wall, we simply reverse the relevant velocity component:
- Left or right wall: reverse
vx - Top wall: reverse
vy - Bottom: the ball is lost (we reset it to the center)
We use abs() to guarantee the correct direction after the bounce: ball_vx = abs(ball_vx) to go rightward after bouncing off the left wall.
Step by step
1. Velocity and delta time
We define the base speed in pixels per second and compute the initial components with trigonometry. An angle of -60 degrees launches the ball upward to the right.
import math
BALL_BASE_SPEED = 300 # pixels per second
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
BALL_SPEED_INCREMENT = 10 # speed increase per bounce
math.radians() converts degrees to radians, because math.cos() and math.sin() expect radians. An angle of -60 degrees gives an upward-right trajectory.
2. Getting the delta time
At the start of each loop iteration, we retrieve the time elapsed since the last frame.
while running:
dt = clock.tick(FPS) / 1000.0 # seconds since last frame
# ...
3. Moving the ball
We multiply velocity by dt to get frame-rate-independent movement.
# Move ball
ball_x += ball_vx * dt
ball_y += ball_vy * dt
4. Wall bounces
We check each screen edge. When the ball hits a wall, we reposition it right at the boundary and reverse the appropriate velocity component. We also slightly increase the speed on each bounce.
# Bounce off left wall
if ball_x - BALL_RADIUS <= 0:
ball_x = BALL_RADIUS
ball_vx = abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
# Bounce off right wall
if ball_x + BALL_RADIUS >= SCREEN_WIDTH:
ball_x = SCREEN_WIDTH - BALL_RADIUS
ball_vx = -abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
# Bounce off top wall
if ball_y - BALL_RADIUS <= 0:
ball_y = BALL_RADIUS
ball_vy = abs(ball_vy)
ball_speed += BALL_SPEED_INCREMENT
# Ball falls off bottom: reset
if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
reset_ball()
5. Reset function
When the ball falls off the bottom of the screen, we place it back at the center with its initial speed.
def reset_ball():
global ball_x, ball_y, ball_vx, ball_vy, ball_speed
ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
Key takeaways
Tip: use float values for ball positions. Integers would cause rounding errors that make movement jerky, especially at low speeds.
Common pitfall: if you forget to reposition the ball after a bounce (ball_x = BALL_RADIUS), it could get stuck inside the wall and bounce endlessly.
Functions used:
clock.tick(FPS): returns elapsed millisecondsmath.cos(),math.sin(): velocity componentsmath.radians(): convert degrees to radiansabs(): absolute value to force a direction
Complete code
"""Brick Breaker - Chapter 6: Moving Things"""
import pygame
import sys
import math
pygame.init()
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
BG_COLOR = (20, 20, 40)
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_SPEED = 8
paddle_x = SCREEN_WIDTH // 2 - PADDLE_WIDTH // 2
paddle_y = SCREEN_HEIGHT - 40
BALL_RADIUS = 8
ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
BALL_BASE_SPEED = 300 # pixels per second
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
BALL_SPEED_INCREMENT = 10 # speed increase per bounce
BRICK_ROWS = 6
BRICK_COLS = 10
BRICK_WIDTH = 70
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_X = 30
BRICK_OFFSET_Y = 50
def rainbow_color(row, total_rows):
hue = int(360 * row / total_rows)
color = pygame.Color(0)
color.hsva = (hue, 100, 100, 100)
return (color.r, color.g, color.b)
def create_paddle_image(width, height):
surf = pygame.Surface((width, height), pygame.SRCALPHA)
for y in range(height):
brightness = 200 + int(55 * (1 - y / height))
pygame.draw.line(surf, (brightness, brightness, brightness),
(0, y), (width, y))
pygame.draw.rect(surf, (0, 0, 0, 0), (0, 0, 3, 3))
pygame.draw.rect(surf, (0, 0, 0, 0), (width - 3, 0, 3, 3))
return surf.convert_alpha()
def create_ball_image(radius):
size = radius * 4
surf = pygame.Surface((size, size), pygame.SRCALPHA)
for r in range(radius * 2, radius, -1):
alpha = int(100 * (1 - (r - radius) / radius))
pygame.draw.circle(surf, (100, 100, 255, alpha),
(size // 2, size // 2), r)
pygame.draw.circle(surf, (255, 255, 255),
(size // 2, size // 2), radius)
pygame.draw.circle(surf, (255, 255, 255, 200),
(size // 2 - 2, size // 2 - 2), radius // 3)
return surf.convert_alpha()
def create_brick_image(width, height, color):
surf = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.rect(surf, color, (0, 0, width, height))
highlight = tuple(min(255, c + 50) for c in color)
pygame.draw.line(surf, highlight, (1, 1), (width - 2, 1))
pygame.draw.line(surf, highlight, (1, 1), (1, height - 2))
shadow = tuple(max(0, c - 50) for c in color)
pygame.draw.line(surf, shadow, (1, height - 1), (width - 1, height - 1))
pygame.draw.line(surf, shadow, (width - 1, 1), (width - 1, height - 1))
return surf.convert_alpha()
def reset_ball():
global ball_x, ball_y, ball_vx, ball_vy, ball_speed
ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 6")
clock = pygame.time.Clock()
font = pygame.font.SysFont("monospace", 14)
paddle_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)
brick_images = []
for row in range(BRICK_ROWS):
color = rainbow_color(row, BRICK_ROWS)
brick_images.append(create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color))
use_mouse = False
running = True
while running:
dt = clock.tick(FPS) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
if event.key == pygame.K_m:
use_mouse = not use_mouse
if use_mouse:
mx, _ = pygame.mouse.get_pos()
paddle_x = mx - PADDLE_WIDTH // 2
else:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
paddle_x -= PADDLE_SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
paddle_x += PADDLE_SPEED
paddle_x = max(0, min(paddle_x, SCREEN_WIDTH - PADDLE_WIDTH))
# Move ball
ball_x += ball_vx * dt
ball_y += ball_vy * dt
# Bounce off left wall
if ball_x - BALL_RADIUS <= 0:
ball_x = BALL_RADIUS
ball_vx = abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
# Bounce off right wall
if ball_x + BALL_RADIUS >= SCREEN_WIDTH:
ball_x = SCREEN_WIDTH - BALL_RADIUS
ball_vx = -abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
# Bounce off top wall
if ball_y - BALL_RADIUS <= 0:
ball_y = BALL_RADIUS
ball_vy = abs(ball_vy)
ball_speed += BALL_SPEED_INCREMENT
# Ball falls off bottom: reset
if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
reset_ball()
# Draw
screen.fill(BG_COLOR)
for row in range(BRICK_ROWS):
for col in range(BRICK_COLS):
x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
screen.blit(brick_images[row], (x, y))
screen.blit(paddle_img, (paddle_x, paddle_y))
ball_offset = BALL_RADIUS * 2
screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))
speed_text = font.render(f"Speed: {int(ball_speed)}", True,
(150, 150, 150))
screen.blit(speed_text, (SCREEN_WIDTH - 150, 10))
info = font.render("[M] Mouse/Keyboard [Arrows] Move [ESC] Quit",
True, (150, 150, 150))
screen.blit(info, (10, SCREEN_HEIGHT - 25))
pygame.display.flip()
pygame.quit()
sys.exit()
Download
Download the source code for this chapter: brick-breaker-ch06.zip
