Basic Collision Detection
Making Things Interact!
Collision detection is what makes games feel real! It's how we know when the player touches a coin, when a bullet hits an enemy, or when you crash into a wall. Without collision detection, objects would pass through each other like ghosts! 👻
Types of Collision Detection
🎯 The Security Check Analogy
Think of collision detection like airport security checks:
- Bounding Box: Like a metal detector gate - quick but not precise
- Circle Collision: Like a security perimeter - good for round objects
- Pixel Perfect: Like a full body scan - very precise but slower
- Spatial Partitioning: Like having multiple security lanes - speeds up checking many objects
Rectangle Collision (AABB)
AABB stands for "Axis-Aligned Bounding Box" - rectangles that don't rotate. This is the most common collision detection in 2D games!
Basic Rectangle Collision
# Check if two rectangles overlap
def rectangles_collide(rect1, rect2):
# rect format: (x, y, width, height)
return (rect1[0] < rect2[0] + rect2[2] and
rect1[0] + rect1[2] > rect2[0] and
rect1[1] < rect2[1] + rect2[3] and
rect1[1] + rect1[3] > rect2[1])
# Using Pygame's built-in Rect
import pygame
rect1 = pygame.Rect(100, 100, 50, 50)
rect2 = pygame.Rect(120, 120, 50, 50)
if rect1.colliderect(rect2):
print("Collision detected!")
# Check point in rectangle
point = (125, 125)
if rect1.collidepoint(point):
print("Point is inside rectangle!")
Interactive Collision Demo
Move your mouse to control the blue box. Watch it turn red on collision!
Collision Status: No Collision
Circle Collision Detection
Perfect for round objects like balls, coins, or explosions!
import math
def circles_collide(circle1, circle2):
# circle format: (x, y, radius)
dx = circle1[0] - circle2[0]
dy = circle1[1] - circle2[1]
distance = math.sqrt(dx * dx + dy * dy)
return distance < circle1[2] + circle2[2]
# Optimized version (no square root)
def circles_collide_fast(circle1, circle2):
dx = circle1[0] - circle2[0]
dy = circle1[1] - circle2[1]
distance_squared = dx * dx + dy * dy
radius_sum = circle1[2] + circle2[2]
return distance_squared < radius_sum * radius_sum
Pygame Sprite Collision
import pygame
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill((0, 100, 255))
self.rect = self.image.get_rect()
self.rect.center = (x, y)
class Enemy(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((30, 30))
self.image.fill((255, 0, 0))
self.rect = self.image.get_rect()
self.rect.center = (x, y)
# Create sprite groups
all_sprites = pygame.sprite.Group()
enemies = pygame.sprite.Group()
player = Player(400, 300)
all_sprites.add(player)
for i in range(5):
enemy = Enemy(100 + i * 100, 200)
all_sprites.add(enemy)
enemies.add(enemy)
# Check collision between player and enemies
hits = pygame.sprite.spritecollide(player, enemies, False)
for hit in hits:
print(f"Player hit enemy at {hit.rect.center}")
# Check collision between groups
collisions = pygame.sprite.groupcollide(enemies, bullets, True, True)
Collision Response
What Happens After Collision?
Detecting collision is only half the battle. You need to respond appropriately:
- Bounce: Reverse velocity (balls, bullets)
- Stop: Prevent movement (walls, obstacles)
- Destroy: Remove object (enemies, pickups)
- Trigger: Start event (doors, switches)
- Damage: Reduce health (combat)
Separating Overlapping Objects
def separate_rectangles(rect1, rect2):
"""Push rectangles apart when they overlap"""
# Calculate overlap on each axis
overlap_left = rect2.right - rect1.left
overlap_right = rect1.right - rect2.left
overlap_top = rect2.bottom - rect1.top
overlap_bottom = rect1.bottom - rect2.top
# Find minimum overlap (smallest push needed)
min_overlap_x = min(overlap_left, overlap_right)
min_overlap_y = min(overlap_top, overlap_bottom)
# Push apart on axis with smallest overlap
if min_overlap_x < min_overlap_y:
if overlap_left < overlap_right:
rect1.left = rect2.right
else:
rect1.right = rect2.left
else:
if overlap_top < overlap_bottom:
rect1.top = rect2.bottom
else:
rect1.bottom = rect2.top
Platform Collision Example
class Player:
def __init__(self, x, y):
self.rect = pygame.Rect(x, y, 32, 32)
self.vel_y = 0
self.on_ground = False
self.gravity = 0.5
self.jump_speed = -12
def update(self, platforms):
# Apply gravity
self.vel_y += self.gravity
# Move vertically
self.rect.y += self.vel_y
# Check platform collisions
self.on_ground = False
for platform in platforms:
if self.rect.colliderect(platform):
if self.vel_y > 0: # Falling down
self.rect.bottom = platform.top
self.vel_y = 0
self.on_ground = True
elif self.vel_y < 0: # Jumping up
self.rect.top = platform.bottom
self.vel_y = 0
def jump(self):
if self.on_ground:
self.vel_y = self.jump_speed
Optimization Techniques
⚡ Speed Up Your Collision Detection
- Broad Phase: Quick check to eliminate impossible collisions
- Spatial Partitioning: Divide space into regions, only check nearby objects
- Collision Layers: Only check objects that can actually collide
- Caching: Store collision results if objects haven't moved
- Early Exit: Stop checking once you find what you need
Broad Phase Optimization
def broad_phase_check(obj1, obj2, margin=50):
"""Quick distance check before detailed collision"""
# If objects are far apart, skip detailed check
if abs(obj1.rect.centerx - obj2.rect.centerx) > margin:
return False
if abs(obj1.rect.centery - obj2.rect.centery) > margin:
return False
return True
# Only do detailed collision if broad phase passes
for enemy in enemies:
if broad_phase_check(player, enemy):
if player.rect.colliderect(enemy.rect):
# Handle collision
pass
Spatial Grid
class SpatialGrid:
def __init__(self, width, height, cell_size):
self.cell_size = cell_size
self.cols = width // cell_size + 1
self.rows = height // cell_size + 1
self.grid = {}
def clear(self):
self.grid.clear()
def add(self, obj):
# Find which cells the object occupies
start_col = obj.rect.left // self.cell_size
end_col = obj.rect.right // self.cell_size
start_row = obj.rect.top // self.cell_size
end_row = obj.rect.bottom // self.cell_size
# Add to all occupied cells
for col in range(start_col, end_col + 1):
for row in range(start_row, end_row + 1):
key = (col, row)
if key not in self.grid:
self.grid[key] = []
self.grid[key].append(obj)
def get_nearby(self, obj):
# Get all objects in same cells as obj
nearby = set()
start_col = obj.rect.left // self.cell_size
end_col = obj.rect.right // self.cell_size
start_row = obj.rect.top // self.cell_size
end_row = obj.rect.bottom // self.cell_size
for col in range(start_col, end_col + 1):
for row in range(start_row, end_row + 1):
key = (col, row)
if key in self.grid:
nearby.update(self.grid[key])
nearby.discard(obj) # Don't include self
return nearby
Complete Collision System Example
import pygame
import random
class GameObject(pygame.sprite.Sprite):
def __init__(self, x, y, width, height, color):
super().__init__()
self.image = pygame.Surface((width, height))
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.vel_x = 0
self.vel_y = 0
class Player(GameObject):
def __init__(self, x, y):
super().__init__(x, y, 30, 30, (0, 100, 255))
self.speed = 5
self.health = 100
def update(self, walls):
# Store old position
old_x = self.rect.x
old_y = self.rect.y
# Move
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT]:
self.rect.x += self.speed
if keys[pygame.K_UP]:
self.rect.y -= self.speed
if keys[pygame.K_DOWN]:
self.rect.y += self.speed
# Check wall collisions and revert if needed
for wall in walls:
if self.rect.colliderect(wall.rect):
self.rect.x = old_x
self.rect.y = old_y
break
class Enemy(GameObject):
def __init__(self, x, y):
super().__init__(x, y, 25, 25, (255, 0, 0))
self.vel_x = random.choice([-2, 2])
self.vel_y = random.choice([-2, 2])
def update(self, walls):
self.rect.x += self.vel_x
self.rect.y += self.vel_y
# Bounce off walls
for wall in walls:
if self.rect.colliderect(wall.rect):
# Simple bounce
self.vel_x = -self.vel_x
self.vel_y = -self.vel_y
break
# Bounce off screen edges
if self.rect.left <= 0 or self.rect.right >= 800:
self.vel_x = -self.vel_x
if self.rect.top <= 0 or self.rect.bottom >= 600:
self.vel_y = -self.vel_y
class Wall(GameObject):
def __init__(self, x, y, width, height):
super().__init__(x, y, width, height, (128, 128, 128))
class Coin(GameObject):
def __init__(self, x, y):
super().__init__(x, y, 20, 20, (255, 215, 0))
self.collected = False
# Game setup
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
# Create groups
all_sprites = pygame.sprite.Group()
walls = pygame.sprite.Group()
enemies = pygame.sprite.Group()
coins = pygame.sprite.Group()
# Create objects
player = Player(400, 300)
all_sprites.add(player)
# Create walls
wall_positions = [
(200, 200, 100, 20),
(500, 300, 20, 100),
(300, 400, 150, 20)
]
for x, y, w, h in wall_positions:
wall = Wall(x, y, w, h)
all_sprites.add(wall)
walls.add(wall)
# Create enemies
for _ in range(5):
enemy = Enemy(random.randint(50, 750), random.randint(50, 550))
all_sprites.add(enemy)
enemies.add(enemy)
# Create coins
for _ in range(10):
coin = Coin(random.randint(50, 750), random.randint(50, 550))
all_sprites.add(coin)
coins.add(coin)
score = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Update
player.update(walls)
enemies.update(walls)
# Check player-enemy collisions
enemy_hits = pygame.sprite.spritecollide(player, enemies, False)
if enemy_hits:
player.health -= 1
# Check player-coin collisions
coin_hits = pygame.sprite.spritecollide(player, coins, True)
score += len(coin_hits)
# Draw
screen.fill((20, 20, 20))
all_sprites.draw(screen)
# Display info
font = pygame.font.Font(None, 36)
score_text = font.render(f"Score: {score}", True, (255, 255, 255))
health_text = font.render(f"Health: {player.health}", True, (255, 255, 255))
screen.blit(score_text, (10, 10))
screen.blit(health_text, (10, 50))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Common Collision Problems
⚠️ Watch Out For These Issues!
- Tunneling: Fast objects passing through thin walls - use continuous collision detection
- Getting Stuck: Objects trapped inside each other - separate them properly
- Jittering: Objects vibrating at collision boundaries - use proper response
- False Positives: Detecting collision when visually separate - adjust hitboxes
- Performance: Checking too many collisions - use optimization techniques
Practice Exercises
🎯 Challenge Yourself!
- Breakout Clone: Ball bouncing off paddle and bricks
- Maze Game: Player navigating walls without passing through
- Bullet Hell: Dodge many projectiles with precise hitboxes
- Pool/Billiards: Realistic ball-to-ball collisions
- Fighting Game: Different hitboxes for attacks and hurt boxes
Collision Detection Best Practices
💡 Pro Tips
- Start Simple: Use rectangles first, optimize later if needed
- Visual Debugging: Draw collision boxes to see what's happening
- Smaller Hitboxes: Make hitboxes slightly smaller than sprites for better game feel
- Layer System: Use collision layers (player, enemy, environment)
- Predictive Collision: Check where objects will be, not just where they are
- Response Priority: Handle important collisions first (player death vs coin pickup)
Key Takeaways
- 📦 Rectangle collision is fast and works for most games
- ⭕ Circle collision is great for round objects
- 🎯 Pygame provides built-in collision detection methods
- ⚡ Optimize only when you have performance problems
- 🔄 Collision response is as important as detection
- 🎮 Good collision makes games feel solid and responsive
🏋️♂️ Practice Exercise: Coin Collector
🏋️♂️ Exercise 1: AABB Collision in 30 Lines
Objective: Build a top-down 'coin collector' demo where a player square moves with the arrow keys and collects scattered coins by touching them. Use pygame.Rect objects for both the player and the coins, and lean on Pygame's built-in rect.colliderect() for the actual collision test — the canonical AABB pattern that the lesson calls out as 'fast, simple, and works for most 2D games.'
Instructions:
- Initialize Pygame and create an 800×600 window. Build the player as
player = pygame.Rect(380, 280, 40, 40)— a Rect object, not just(x, y)coordinates — so you getcolliderectandclamp_ipfor free. - Build a list of five coin Rects at fixed scattered positions, each 20×20 — e.g.
pygame.Rect(120, 90, 20, 20), plus four others around the field. - In the game loop, handle
pygame.QUIT, then usepygame.key.get_pressed()to move the player (this is the state-based input pattern from the previous lesson — updateplayer.x/player.yby a smallSPEEDfor each arrow key held). - Clamp the player to the screen with
player.clamp_ip(screen.get_rect())so it can't leave the playfield. - Test collisions with a single list-comprehension:
coins = [c for c in coins if not player.colliderect(c)]. Any coin that overlaps the player is filtered out of the list — it stops being drawn and stops being checked next frame. - Render: clear the screen, draw each remaining coin as a gold rect, draw the player on top as a blue rect, then
pygame.display.flip()andclock.tick(60). - Run it. Walk over each coin in turn — they should disappear the moment your player rect overlaps theirs.
💡 Hint
Two pieces of pygame.Rect API do the heavy lifting. a.colliderect(b) returns True if Rect a and Rect b overlap on both axes — it's pure axis-aligned bounding-box math (fast, branch-free, no square roots). rect.clamp_ip(other_rect) moves rect in-place so it stays fully inside other_rect — perfect for keeping the player on-screen. You can also tweak the player's hitbox vs visual by drawing a 40×40 blue rect but using a slightly inset player.inflate(-8, -8) Rect for the collision test — the lesson's Pro Tip 'make hitboxes slightly smaller than sprites for better game feel' in two lines of code.
✅ Example Solution
import sys
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Coin Collector")
clock = pygame.time.Clock()
# Player as a pygame.Rect — gets colliderect / clamp_ip for free
player = pygame.Rect(380, 280, 40, 40)
SPEED = 4
# Five coins scattered around the field
coins = [
pygame.Rect(120, 90, 20, 20),
pygame.Rect(640, 140, 20, 20),
pygame.Rect(220, 460, 20, 20),
pygame.Rect(560, 480, 20, 20),
pygame.Rect(700, 320, 20, 20),
]
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Movement (state polling — recap from the input lesson)
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
# Stay inside the playfield
player.clamp_ip(screen.get_rect())
# Collision: drop any coin whose rect overlaps the player's
coins = [c for c in coins if not player.colliderect(c)]
# Render
screen.fill((20, 30, 50))
for c in coins:
pygame.draw.rect(screen, (255, 215, 0), c)
pygame.draw.rect(screen, (80, 200, 240), player)
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()
🎯 Quick Quiz
Question 1: The lesson teaches several collision detection methods (AABB rectangles, circles, pixel-perfect, polygons). Why does it recommend reaching for AABB rectangles first — and only swapping in something more precise when you actually need to?
Question 2: Given two pygame.Rect objects a and b, what does a.colliderect(b) return?
Question 3: The lesson's Pro Tips list includes 'make hitboxes slightly smaller than sprites for better game feel.' What does that actually buy you?
What's Next?
Now that you can detect when objects collide, next we'll add sound and music to make your games come alive with audio feedback! You'll learn how to play sound effects for collisions, background music, and create an immersive audio experience.