Skip to main content

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:

graph TD A["Collision Detection Methods"] --> B["Rectangle/AABB"] A --> C["Circle"] A --> D["Pixel Perfect"] A --> E["Polygon"] B --> F["Fast, Simple, Most Common"] C --> G["Good for Round Objects"] D --> H["Precise but Slow"] E --> I["Complex Shapes"]

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:

graph LR A["Collision Detected"] --> B{"Response Type"} B --> C["Physical Response"] B --> D["Game Logic Response"] C --> E["Push Apart"] C --> F["Bounce"] C --> G["Stop Movement"] D --> H["Deal Damage"] D --> I["Collect Item"] D --> J["Trigger Event"]

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 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!

Practice Exercises

🎯 Challenge Yourself!

  1. Breakout Clone: Ball bouncing off paddle and bricks
  2. Maze Game: Player navigating walls without passing through
  3. Bullet Hell: Dodge many projectiles with precise hitboxes
  4. Pool/Billiards: Realistic ball-to-ball collisions
  5. Fighting Game: Different hitboxes for attacks and hurt boxes

Collision Detection Best Practices

💡 Pro Tips

Key Takeaways

🏋️‍♂️ 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:

  1. 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 get colliderect and clamp_ip for free.
  2. 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.
  3. In the game loop, handle pygame.QUIT, then use pygame.key.get_pressed() to move the player (this is the state-based input pattern from the previous lesson — update player.x / player.y by a small SPEED for each arrow key held).
  4. Clamp the player to the screen with player.clamp_ip(screen.get_rect()) so it can't leave the playfield.
  5. 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.
  6. 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() and clock.tick(60).
  7. 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.