pyodide: loading…

[practice]Game Development with Pygame

Collision Detection

# theory

from the videos

Ball and Platform Collision (Brick Breakaway Video 7)

  • colliderect() for rect-to-rect detection
  • Checking vy > 0 before bouncing (prevent stuck ball)
  • The offset trick for angled bounces

Brick Collisions (Brick Breakaway Video 9)

  • pygame.sprite.spritecollide() for one-vs-group
  • Removing bricks when hit
  • Figuring out which direction to bounce

Enemy Methods (Tower Defense Video 8)

  • Waypoint path following for enemies
  • Moving toward a target point
  • Distance checking to know when you've arrived

Projectile Class (Tower Defense Videos 10-11)

  • Velocity toward target using angle calculation
  • math.atan2(dy, dx) for angle between two points
  • Tracking a moving target

Collisions and Bug Fixes (Sprite Game Video 13)

  • Player vs NPC collision
  • Common collision bugs and how to fix them

collision detection

Without it, things pass through each other. Bullets don't hit enemies. The ball goes through the paddle. The player walks through walls. Collision detection is what makes games feel real.

Rect collision (fastest)

The simplest method; check if two rectangles overlap:

# Two rect objects
if rect1.colliderect(rect2):
    print("collision!")

# Check a rect against a point (like mouse position)
if rect.collidepoint(mouse_x, mouse_y):
    print("mouse is inside rect")

Rect collision is fast and good enough for most cases.

sprite group collision

For checking one sprite against a whole group:

# Did the player touch any enemy?
hit_list = pygame.sprite.spritecollide(player, enemies, False)
# False = don't kill the enemies on collision
# Returns list of enemies the player collided with

if hit_list:
    player.take_damage(10)

# Did any bullet hit any enemy? Kill both.
hits = pygame.sprite.groupcollide(bullets, enemies, True, True)
# True, True = kill both the bullet and enemy on hit

circle / distance-based collision

More accurate for round objects or when you need range detection (like tower targeting):

import math

def circles_collide(x1, y1, r1, x2, y2, r2):
    dist = math.sqrt((x2-x1)**2 + (y2-y1)**2)
    return dist < r1 + r2

# Or use math.dist (cleaner):
dist = math.dist((x1, y1), (x2, y2))
if dist < r1 + r2:
    # collision!

For tower range checking:

# Is enemy within tower's attack range?
dist = math.dist(tower.rect.center, enemy.rect.center)
if dist <= TOWER_RANGE:
    tower.target = enemy

waypoint following

Enemies follow a list of waypoints. The key is tracking which waypoint they're heading toward:

class Enemy:
    def __init__(self, waypoints):
        self.waypoints = waypoints
        self.waypoint_index = 0
        self.pos = pygame.math.Vector2(waypoints[0])
        self.speed = 100  # pixels per second

    def update(self, dt):
        if self.waypoint_index >= len(self.waypoints):
            return  # reached the end

        target = pygame.math.Vector2(self.waypoints[self.waypoint_index])
        direction = target - self.pos
        dist = direction.length()

        if dist < self.speed * dt:
            # Reached this waypoint, go to next
            self.pos = target
            self.waypoint_index += 1
        else:
            # Move toward waypoint
            direction = direction.normalize()
            self.pos += direction * self.speed * dt

angle calculation with math.atan2

Essential for projectiles that need to aim at a target:

import math

# Get angle from point A to point B
dx = target_x - start_x
dy = target_y - start_y
angle = math.atan2(dy, dx)  # radians

# Convert to velocity
speed = 400  # pixels per second
vx = math.cos(angle) * speed
vy = math.sin(angle) * speed

Why atan2 instead of atan? atan2(y, x) handles all four quadrants correctly. Regular atan doesn't know which quadrant you're in.

projectile that tracks a moving target

class Projectile:
    def __init__(self, start_pos, target_sprite):
        self.pos = pygame.math.Vector2(start_pos)
        self.target = target_sprite  # reference to enemy
        self.speed = 500
        self.damage = 25
        self.alive = True

    def update(self, dt):
        if not self.target or not self.target.alive():
            self.alive = False
            return

        # Aim at target's current position (tracks movement!)
        target_pos = pygame.math.Vector2(self.target.rect.center)
        direction = target_pos - self.pos
        dist = direction.length()

        if dist < self.speed * dt:
            # Hit!
            self.target.take_damage(self.damage)
            self.alive = False
        else:
            direction = direction.normalize()
            self.pos += direction * self.speed * dt

ball + paddle collision (the right way)

Gerber tip: Always check vy > 0 before bouncing off the paddle:

if ball_rect.colliderect(paddle_rect) and ball_vy > 0:
    ball_vy *= -1

Why? If you don't check, the ball can get stuck inside the paddle and bounce back and forth rapidly. vy > 0 means the ball is moving downward; only then should we bounce it up.

side-aware bounce (which side did i hit?)

For bouncing off bricks, you need to know if you hit the top/bottom or left/right:

def bounce_off_rect(ball_rect, ball_vel, target_rect):
    # Calculate overlap on each side
    overlap_left   = ball_rect.right - target_rect.left
    overlap_right  = target_rect.right - ball_rect.left
    overlap_top    = ball_rect.bottom - target_rect.top
    overlap_bottom = target_rect.bottom - ball_rect.top

    min_horizontal = min(overlap_left, overlap_right)
    min_vertical   = min(overlap_top, overlap_bottom)

    # Bounce off the axis with smaller overlap
    if min_horizontal < min_vertical:
        ball_vel[0] *= -1  # horizontal bounce
    else:
        ball_vel[1] *= -1  # vertical bounce

    return ball_vel

Why smallest overlap? The smallest overlap indicates which side the ball entered from. If horizontal overlap is smaller, it came from the left/right. If vertical is smaller, it came from top/bottom.


tips

  • Gerber tip: Check vy > 0 on paddle collision; prevents ball getting stuck
  • Tip: math.atan2(dy, dx) gives angle from point A to point B; essential for Tower Defense
  • Tip: Use math.dist() instead of manual sqrt; cleaner and just as fast
  • Tip: Keep a reference to the target, not just its position, for tracking projectiles
  • Tip: Process only one brick collision per frame to prevent weird double-bounces

common mistakes

  • Not checking vy > 0 on paddle: ball gets stuck and vibrates
  • Using position-at-fire instead of tracking: projectiles miss moving targets
  • Bouncing off multiple bricks in one frame: causes erratic behavior
  • Forgetting to check if target is still alive: crashes or weird behavior
  • Using atan instead of atan2: wrong angles in some quadrants

# examples [2]

# example 01 · tower targeting with range check

Find the closest enemy within range using distance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# example 02 · projectile velocity from angle

Calculate vx/vy to fire toward a target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# challenges [2]

# challenge 01/02todo
Why do you check 'vy > 0' before bouncing the ball off the paddle in Brick Breakaway?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
🐍
Loading PythonSetting up pandas & numpy...
# challenge 02/02todo
What does math.atan2(dy, dx) return, and why use it instead of math.atan(dy/dx)?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
🐍
Loading PythonSetting up pandas & numpy...