[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 > 0before 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 > 0on 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]
Find the closest enemy within range using distance
pygame needs a real window — copy this into a .py file and run it locally.
Calculate vx/vy to fire toward a target
pygame needs a real window — copy this into a .py file and run it locally.
# challenges [2]