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.

python
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.

python
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.

python
    # 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.

python
    # 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.

python
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 milliseconds
  • math.cos(), math.sin(): velocity components
  • math.radians(): convert degrees to radians
  • abs(): absolute value to force a direction

Complete code

python
"""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