47  The Art of Collision Detection: Making Your Game Interactive

Today, we take a crucial step toward making our games truly interactive by mastering collision detection. Just as in a medieval tournament where knights must determine whether lances struck shields, our game needs to know when objects touch each other.

47.1 What is Collision Detection and Why Does It Matter?

Collision detection is the process of determining when two or more objects in your game occupy the same space. This fundamental concept enables virtually all game interactions:

  • Players collecting coins or power-ups
  • Enemies damaging the player character
  • Projectiles hitting targets
  • Characters landing on platforms
  • Preventing movement through walls and obstacles

Without collision detection, you’d have beautiful sprites moving through an unresponsive world where nothing interacts. With it, your virtual world comes alive with cause and effect.

47.2 Types of Collision Detection

In 2D games, there are several common approaches to collision detection, each with different levels of complexity and precision:

  1. Rectangle (AABB) Collision: Checking if rectangles overlap (simplest and most common)
  2. Circle Collision: Checking if circles overlap (good for round objects)
  3. Pixel-Perfect Collision: Checking at the pixel level (most precise but computationally expensive)
  4. Line/Ray Casting: Using rays to detect collisions at a distance (useful for vision and projectiles)

Today, we’ll focus on the two most practical methods for Pyxel games: rectangle and circle collision detection.

47.3 Rectangle Collision: The Workhorse of Game Development

Rectangle collision detection (also called Axis-Aligned Bounding Box or AABB collision) is simple, efficient, and works well for most game objects. The idea is to treat each sprite as a rectangle and check if these rectangles overlap.

For two rectangles to overlap, all of these conditions must be true:

  • The right edge of rectangle A is to the right of the left edge of rectangle B
  • The left edge of rectangle A is to the left of the right edge of rectangle B
  • The bottom edge of rectangle A is below the top edge of rectangle B
  • The top edge of rectangle A is above the bottom edge of rectangle B

Let’s implement this in code:

def check_rectangle_collision(x1, y1, w1, h1, x2, y2, w2, h2):
    """
    Check if two rectangles overlap.
    
    (x1, y1): Top-left corner of the first rectangle
    w1, h1: Width and height of the first rectangle
    (x2, y2): Top-left corner of the second rectangle
    w2, h2: Width and height of the second rectangle
    """
    # Check if rectangles overlap on x-axis
    if x1 + w1 <= x2 or x1 >= x2 + w2:
        return False
        
    # Check if rectangles overlap on y-axis
    if y1 + h1 <= y2 or y1 >= y2 + h2:
        return False
        
    # If we get here, the rectangles must overlap
    return True

Let’s see this in action with a simple example where we check if a player character collides with a coin:

import pyxel

class CollisionDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Rectangle Collision Demo")
        
        # Player properties
        self.player_x = 40
        self.player_y = 60
        self.player_width = 16
        self.player_height = 16
        self.player_speed = 2
        
        # Coin properties
        self.coin_x = 100
        self.coin_y = 60
        self.coin_width = 8
        self.coin_height = 8
        self.coin_active = True
        
        # Score
        self.score = 0
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Move player with arrow keys
        if pyxel.btn(pyxel.KEY_LEFT):
            self.player_x = max(self.player_x - self.player_speed, 0)
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.player_x = min(self.player_x + self.player_speed, 160 - self.player_width)
        if pyxel.btn(pyxel.KEY_UP):
            self.player_y = max(self.player_y - self.player_speed, 0)
        if pyxel.btn(pyxel.KEY_DOWN):
            self.player_y = min(self.player_y + self.player_speed, 120 - self.player_height)
        
        # Check collision between player and coin
        if self.coin_active and self.check_collision(
            self.player_x, self.player_y, self.player_width, self.player_height,
            self.coin_x, self.coin_y, self.coin_width, self.coin_height
        ):
            # Collision detected!
            self.coin_active = False
            self.score += 10

    def check_collision(self, x1, y1, w1, h1, x2, y2, w2, h2):
        # Check for collision between two rectangles
        if x1 + w1 <= x2 or x1 >= x2 + w2:
        # if right edge of A left of left edge of B OR left edge is right of right edge of B
            return False
        if y1 + h1 <= y2 or y1 >= y2 + h2:
        # if bottom edge of A is above top edge of B OR top edge of A is below bottom edge of B
            return False
        return True

    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue

        # Draw player (green rectangle)
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_width, self.player_height, 11)

        # Draw coin (yellow circle) if active
        if self.coin_active:
            pyxel.circ(self.coin_x + 4, self.coin_y + 4, 4, 10)

        # Draw score
        pyxel.text(5, 5, f"SCORE: {self.score}", 7)

        # Draw instructions
        pyxel.text(5, 15, "Use arrow keys to move", 7)
        pyxel.text(5, 25, "Collect the coin by touching it", 7)

CollisionDemo()

In this example:

  1. We have a player rectangle (green) and a coin (yellow circle)
  2. We check for collision between them using rectangle collision detection
  3. When a collision occurs, we mark the coin as inactive and increase the score

Although the coin is drawn as a circle, we’re treating it as a rectangle for collision purposes. This simplified approach works well for many games.

47.4 Visualizing Collision Rectangles

When developing collision detection, it’s often helpful to visualize the collision rectangles. Let’s modify our example to show these rectangles:

import pyxel

class CollisionVisualizationDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Collision Visualization")

        # Player properties
        self.player_x = 40
        self.player_y = 60
        self.player_width = 16
        self.player_height = 16
        self.player_speed = 2

        # Coin properties
        self.coin_x = 100
        self.coin_y = 60
        self.coin_width = 8
        self.coin_height = 8
        self.coin_active = True

        # Visualization option
        self.show_collision_boxes = True
        
        # Score
        self.score = 0
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Toggle collision box visualization with T key
        if pyxel.btnp(pyxel.KEY_T):
            self.show_collision_boxes = not self.show_collision_boxes
        
        # Move player with arrow keys
        if pyxel.btn(pyxel.KEY_LEFT):
            self.player_x = max(self.player_x - self.player_speed, 0)
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.player_x = min(self.player_x + self.player_speed, 160 - self.player_width)
        if pyxel.btn(pyxel.KEY_UP):
            self.player_y = max(self.player_y - self.player_speed, 0)
        if pyxel.btn(pyxel.KEY_DOWN):
            self.player_y = min(self.player_y + self.player_speed, 120 - self.player_height)
        
        # Check collision between player and coin
        if self.coin_active and self.check_collision(
            self.player_x, self.player_y, self.player_width, self.player_height,
            self.coin_x, self.coin_y, self.coin_width, self.coin_height
        ):
            # Collision detected!
            self.coin_active = False
            self.score += 10
        
        # Reset coin if collected
        if not self.coin_active and pyxel.btnp(pyxel.KEY_R):
            self.coin_active = True
    
    def check_collision(self, x1, y1, w1, h1, x2, y2, w2, h2):
        # Check for collision between two rectangles
        if x1 + w1 <= x2 or x1 >= x2 + w2:
            return False
        if y1 + h1 <= y2 or y1 >= y2 + h2:
            return False
        return True
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw player
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_width, self.player_height, 11)
        
        # Draw coin if active
        if self.coin_active:
            pyxel.circ(self.coin_x + 4, self.coin_y + 4, 4, 10)
        
        # Draw collision boxes if enabled
        if self.show_collision_boxes:
            # Player collision box
            pyxel.rectb(self.player_x, self.player_y, 
                      self.player_width, self.player_height, 7)
            
            # Coin collision box (if active)
            if self.coin_active:
                pyxel.rectb(self.coin_x, self.coin_y, 
                          self.coin_width, self.coin_height, 7)
        
        # Draw score and instructions
        pyxel.text(5, 5, f"SCORE: {self.score}", 7)
        pyxel.text(5, 15, "Use arrow keys to move", 7)
        pyxel.text(5, 25, "T: Toggle collision boxes", 7)
        pyxel.text(5, 35, "R: Reset coin if collected", 7)
        
        # Draw collision status
        is_colliding = self.coin_active and self.check_collision(
            self.player_x, self.player_y, self.player_width, self.player_height,
            self.coin_x, self.coin_y, self.coin_width, self.coin_height
        )
        status = "COLLISION DETECTED!" if is_colliding else "No collision"
        pyxel.text(5, 110, status, 8 if is_colliding else 7)

CollisionVisualizationDemo()

This enhanced version:

  1. Shows collision boxes with white outlines
  2. Allows toggling the visibility of these boxes with the T key
  3. Displays the current collision status
  4. Lets you reset the coin with the R key after collecting it

Visualization like this is invaluable for debugging your collision detection system.

47.5 Circle Collision: Perfect for Round Objects

While rectangle collision works well for most sprites, some objects are naturally round (balls, planets, bubbles). For these, circle collision often provides more accurate results.

Circle collision is based on a simple principle: two circles collide if the distance between their centers is less than the sum of their radii.

Here’s how to implement it:

def check_circle_collision(x1, y1, r1, x2, y2, r2):
    """
    Check if two circles overlap.
    
    (x1, y1): Center of first circle
    r1: Radius of first circle
    (x2, y2): Center of second circle
    r2: Radius of second circle
    """
    # Calculate the distance between circle centers
    distance_squared = (x2 - x1)**2 + (y2 - y1)**2
    
    # Check if this distance is less than the sum of radii
    return distance_squared < (r1 + r2)**2

We use the squared distance to avoid the computationally expensive square root operation. Let’s see circle collision in action:

import pyxel

class CircleCollisionDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Circle Collision Demo")
        
        # Player properties (circle)
        self.player_x = 40  # Center x
        self.player_y = 60  # Center y
        self.player_radius = 8
        self.player_speed = 2
        
        # Enemy properties (circle)
        self.enemy_x = 100  # Center x
        self.enemy_y = 60   # Center y
        self.enemy_radius = 8
        self.enemy_speed_x = 1
        self.enemy_speed_y = 0.5
        
        # Game state
        self.collision = False
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Move player with arrow keys
        if pyxel.btn(pyxel.KEY_LEFT):
            self.player_x = max(self.player_x - self.player_speed, self.player_radius)
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.player_x = min(self.player_x + self.player_speed, 160 - self.player_radius)
        if pyxel.btn(pyxel.KEY_UP):
            self.player_y = max(self.player_y - self.player_speed, self.player_radius)
        if pyxel.btn(pyxel.KEY_DOWN):
            self.player_y = min(self.player_y + self.player_speed, 120 - self.player_radius)
        
        # Move enemy
        self.enemy_x += self.enemy_speed_x
        self.enemy_y += self.enemy_speed_y
        
        # Bounce enemy off walls
        if self.enemy_x - self.enemy_radius <= 0 or self.enemy_x + self.enemy_radius >= 160:
            self.enemy_speed_x *= -1
        if self.enemy_y - self.enemy_radius <= 0 or self.enemy_y + self.enemy_radius >= 120:
            self.enemy_speed_y *= -1
        
        # Check for collision
        self.collision = self.check_circle_collision(
            self.player_x, self.player_y, self.player_radius,
            self.enemy_x, self.enemy_y, self.enemy_radius
        )
    
    def check_circle_collision(self, x1, y1, r1, x2, y2, r2):
        distance_squared = (x2 - x1)**2 + (y2 - y1)**2
        return distance_squared < (r1 + r2)**2
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw player (blue circle)
        player_color = 12
        pyxel.circ(self.player_x, self.player_y, self.player_radius, player_color)
        
        # Draw enemy (red or pink circle)
        enemy_color = 8 if self.collision else 14
        pyxel.circ(self.enemy_x, self.enemy_y, self.enemy_radius, enemy_color)
        
        # Draw collision status
        status = "COLLISION DETECTED!" if self.collision else "No collision"
        pyxel.text(5, 5, status, 8 if self.collision else 7)
        
        # Draw instructions
        pyxel.text(5, 15, "Use arrow keys to move blue circle", 7)
        pyxel.text(5, 25, "Collision turns red ball into pink", 7)
        pyxel.text(5, 35, "Press Q to quit", 7)

CircleCollisionDemo()

In this demo:

  1. Both the player and enemy are represented as circles
  2. We check for collisions using the circle collision formula
  3. The enemy changes color when a collision is detected
  4. The enemy bounces off the walls of the screen

Circle collision provides more natural-looking interactions for round objects, as there are no “corner” artifacts that can occur with rectangle collision.

47.6 Mixed Collision Types: Circle-Rectangle Collision

Sometimes you need to detect collisions between different shapes. A common scenario is checking if a circle (like a ball) collides with a rectangle (like a paddle or wall).

Here’s how to implement circle-rectangle collision:

def check_circle_rect_collision(circle_x, circle_y, radius, rect_x, rect_y, rect_w, rect_h):
    """
    Check if a circle and rectangle overlap.
    
    (circle_x, circle_y): Center of the circle
    radius: Radius of the circle
    (rect_x, rect_y): Top-left corner of the rectangle
    rect_w, rect_h: Width and height of the rectangle
    """
    # Find the closest point on the rectangle to the circle
    closest_x = max(rect_x, min(circle_x, rect_x + rect_w))
    closest_y = max(rect_y, min(circle_y, rect_y + rect_h))
    
    # Calculate the distance between the circle's center and the closest point
    distance_squared = (circle_x - closest_x)**2 + (circle_y - closest_y)**2
    
    # If the distance is less than the radius, there is a collision
    return distance_squared < radius**2

Let’s create a demo of circle-rectangle collision:

import pyxel

class MixedCollisionDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Circle-Rectangle Collision")
        
        # Ball properties (circle)
        self.ball_x = 80
        self.ball_y = 30
        self.ball_radius = 8
        self.ball_speed_x = 1.5
        self.ball_speed_y = 1
        
        # Paddle properties (rectangle)
        self.paddle_width = 32
        self.paddle_height = 8
        self.paddle_x = 80 - self.paddle_width // 2
        self.paddle_y = 100
        self.paddle_speed = 3
        
        # Game state
        self.collision = False
        self.score = 0
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Move paddle with left and right arrow keys
        if pyxel.btn(pyxel.KEY_LEFT):
            self.paddle_x = max(self.paddle_x - self.paddle_speed, 0)
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.paddle_x = min(self.paddle_x + self.paddle_speed, 160 - self.paddle_width)
        
        # Move ball
        self.ball_x += self.ball_speed_x
        self.ball_y += self.ball_speed_y
        
        # Bounce ball off walls
        if self.ball_x - self.ball_radius <= 0 or self.ball_x + self.ball_radius >= 160:
            self.ball_speed_x *= -1
        if self.ball_y - self.ball_radius <= 0:
            self.ball_speed_y *= -1
        
        # Check for collision between ball and paddle
        self.collision = self.check_circle_rect_collision(
            self.ball_x, self.ball_y, self.ball_radius,
            self.paddle_x, self.paddle_y, self.paddle_width, self.paddle_height
        )
        
        # Bounce ball off paddle
        if self.collision and self.ball_speed_y > 0:
            self.ball_speed_y *= -1
            self.score += 1
        
        # Reset ball if it goes below the bottom edge
        if self.ball_y - self.ball_radius > 120:
            self.ball_x = 80
            self.ball_y = 30
            self.ball_speed_x = 1.5
            self.ball_speed_y = 1
            self.score = max(0, self.score - 1)  # Lose a point
    
    def check_circle_rect_collision(self, circle_x, circle_y, radius, rect_x, rect_y, rect_w, rect_h):
        # Find closest point on rectangle to circle
        closest_x = max(rect_x, min(circle_x, rect_x + rect_w))
        closest_y = max(rect_y, min(circle_y, rect_y + rect_h))
        
        # Calculate distance squared
        distance_squared = (circle_x - closest_x)**2 + (circle_y - closest_y)**2
        
        # Check if distance is less than radius squared
        return distance_squared < radius**2
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw ball (yellow circle)
        pyxel.circ(self.ball_x, self.ball_y, self.ball_radius, 10)
        
        # Draw paddle (white rectangle)
        paddle_color = 7
        pyxel.rect(self.paddle_x, self.paddle_y, 
                 self.paddle_width, self.paddle_height, paddle_color)
        
        # Draw score
        pyxel.text(5, 5, f"SCORE: {self.score}", 7)
        
        # Draw instructions
        pyxel.text(5, 15, "Use left/right arrow keys to move paddle", 7)
        pyxel.text(5, 25, "Bounce the ball to score points", 7)
        pyxel.text(5, 35, "Press Q to quit", 7)

MixedCollisionDemo()

This simple Breakout-style game demonstrates:

  1. Circle-rectangle collision between a ball and paddle
  2. Bouncing physics based on collision detection
  3. Score tracking based on successful bounces
  4. Ball reset when it falls off the bottom of the screen

47.7 Using Collision Detection in a Game: Coins and Obstacles

Now that we understand the basic collision techniques, let’s create a more complete game example with multiple collision types:

import pyxel

class CollisionGame:
    def __init__(self):
        pyxel.init(160, 120, title="Coin Collector Game")
        
        # Player properties (rectangle)
        self.player_x = 80
        self.player_y = 60
        self.player_width = 8
        self.player_height = 8
        self.player_speed = 2
        
        # Coins (circles)
        self.coins = [
            [20, 20, 4, True],   # x, y, radius, active
            [40, 30, 4, True],
            [60, 40, 4, True],
            [80, 50, 4, True],
            [100, 60, 4, True],
            [120, 70, 4, True],
            [140, 80, 4, True]
        ]
        
        # Obstacles (rectangles)
        self.obstacles = [
            [30, 50, 20, 8],     # x, y, width, height
            [70, 30, 20, 8],
            [110, 90, 20, 8],
            [50, 80, 8, 20],
            [90, 20, 8, 20],
            [130, 50, 8, 20]
        ]
        
        # Game state
        self.score = 0
        self.game_over = False
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Reset game with R key
        if self.game_over and pyxel.btnp(pyxel.KEY_R):
            self.__init__()
            return
        
        if not self.game_over:
            # Store the previous position for collision resolution
            prev_x = self.player_x
            prev_y = self.player_y
            
            # Move player with arrow keys
            if pyxel.btn(pyxel.KEY_LEFT):
                self.player_x = max(self.player_x - self.player_speed, 0)
            if pyxel.btn(pyxel.KEY_RIGHT):
                self.player_x = min(self.player_x + self.player_speed, 160 - self.player_width)
            if pyxel.btn(pyxel.KEY_UP):
                self.player_y = max(self.player_y - self.player_speed, 0)
            if pyxel.btn(pyxel.KEY_DOWN):
                self.player_y = min(self.player_y + self.player_speed, 120 - self.player_height)
            
            # Check for collisions with obstacles
            for obstacle in self.obstacles:
                if self.check_rect_collision(
                    self.player_x, self.player_y, self.player_width, self.player_height,
                    obstacle[0], obstacle[1], obstacle[2], obstacle[3]
                ):
                    # Collision with obstacle! Revert to previous position
                    self.player_x = prev_x
                    self.player_y = prev_y
                    break
            
            # Check for collisions with coins
            for coin in self.coins:
                if coin[3] and self.check_circle_rect_collision(
                    coin[0], coin[1], coin[2],
                    self.player_x, self.player_y, self.player_width, self.player_height
                ):
                    # Collected a coin!
                    coin[3] = False
                    self.score += 10
            
            # Check if all coins are collected
            all_collected = all(not coin[3] for coin in self.coins)
            if all_collected:
                self.game_over = True
    
    def check_rect_collision(self, x1, y1, w1, h1, x2, y2, w2, h2):
        # Check for collision between two rectangles
        if x1 + w1 <= x2 or x1 >= x2 + w2:
            return False
        if y1 + h1 <= y2 or y1 >= y2 + h2:
            return False
        return True
    
    def check_circle_rect_collision(self, circle_x, circle_y, radius, rect_x, rect_y, rect_w, rect_h):
        # Find closest point on rectangle to circle
        closest_x = max(rect_x, min(circle_x, rect_x + rect_w))
        closest_y = max(rect_y, min(circle_y, rect_y + rect_h))
        
        # Calculate distance squared
        distance_squared = (circle_x - closest_x)**2 + (circle_y - closest_y)**2
        
        # Check if distance is less than radius squared
        return distance_squared < radius**2
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw player (green rectangle)
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_width, self.player_height, 11)
        
        # Draw coins (yellow circles)
        for coin in self.coins:
            if coin[3]:  # If active
                pyxel.circ(coin[0], coin[1], coin[2], 10)
        
        # Draw obstacles (red rectangles)
        for obstacle in self.obstacles:
            pyxel.rect(obstacle[0], obstacle[1], obstacle[2], obstacle[3], 8)
        
        # Draw score
        pyxel.text(5, 5, f"SCORE: {self.score}", 7)
        
        # Draw instructions
        pyxel.text(5, 15, "Use arrow keys to move", 7)
        pyxel.text(5, 25, "Collect all coins to win", 7)
        pyxel.text(5, 35, "Avoid red obstacles", 7)
        
        # Draw game over screen
        if self.game_over:
            pyxel.rectb(50, 50, 60, 30, 7)
            pyxel.rect(51, 51, 58, 28, 5)
            pyxel.text(65, 60, "YOU WIN!", 10)
            pyxel.text(60, 70, "Press R to restart", 7)

CollisionGame()

This more complete game example demonstrates:

  1. Rectangle collision for obstacles (solid objects the player can’t pass through)
  2. Circle-rectangle collision for coins (items the player can collect)
  3. Collision resolution by reverting to the previous position when hitting obstacles
  4. Game state management and win condition based on collecting all coins

47.8 Implementing Tile-Based Collision

Many 2D games use tile-based maps (like we’ve seen in previous lessons with Pyxel’s tilemap system). For these games, we need a slightly different approach to collision detection:

import pyxel

class TileCollisionDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Tile Collision Demo")
        
        # Player properties
        self.player_x = 16
        self.player_y = 16
        self.player_width = 8
        self.player_height = 8
        self.player_speed = 2
        
        # Tile map representation (0 = empty, 1 = wall)
        self.tile_size = 8
        self.tile_map = [
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1],
            [1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
            [1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
        ]
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Store previous position for collision resolution
        prev_x = self.player_x
        prev_y = self.player_y
        
        # Move player with arrow keys
        if pyxel.btn(pyxel.KEY_LEFT):
            self.player_x -= self.player_speed
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.player_x += self.player_speed
        if pyxel.btn(pyxel.KEY_UP):
            self.player_y -= self.player_speed
        if pyxel.btn(pyxel.KEY_DOWN):
            self.player_y += self.player_speed
        
        # Check for collision with tiles
        if self.check_tile_collision(self.player_x, self.player_y, 
                                     self.player_width, self.player_height):
            # Collision detected! Revert to previous position
            self.player_x = prev_x
            self.player_y = prev_y
    
    def check_tile_collision(self, x, y, width, height):
        """Check if a rectangle collides with any solid tiles in the map."""
        # Convert pixel coordinates to tile coordinates
        left_tile = max(0, x // self.tile_size)
        right_tile = min(19, (x + width - 1) // self.tile_size)
        top_tile = max(0, y // self.tile_size)
        bottom_tile = min(14, (y + height - 1) // self.tile_size)
        
        # Check each tile the rectangle might be touching
        for tile_y in range(top_tile, bottom_tile + 1):
            for tile_x in range(left_tile, right_tile + 1):
                # If this tile is solid (1 in our map), there's a collision
                if self.tile_map[tile_y][tile_x] == 1:
                    return True
        
        # No collision with any solid tiles
        return False
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw the tile map
        for y in range(15):
            for x in range(20):
                if self.tile_map[y][x] == 1:
                    # Draw a wall tile (brown rectangle)
                    pyxel.rect(x * self.tile_size, y * self.tile_size, 
                             self.tile_size, self.tile_size, 4)
        
        # Draw the player (green rectangle)
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_width, self.player_height, 11)
        
        # Draw instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Avoid brown walls", 7)
        pyxel.text(5, 25, "Press Q to quit", 7)

TileCollisionDemo()

This tile-based collision example demonstrates:

  1. A 2D grid representing a simple maze
  2. Converting pixel coordinates to tile coordinates for collision checking
  3. Checking if the player’s rectangle overlaps with any solid tiles
  4. Collision resolution by reverting to the previous position

This approach is much more efficient for large tile-based maps than checking collision with each individual tile rectangle, as we only need to check the few tiles that the player might be touching.

47.9 Collision Response: What Happens After a Collision?

Detecting collisions is only half the battle. The other half is determining how your game should respond when collisions occur. Here are some common collision responses:

  1. Blocking Movement: Prevent the player from moving through solid objects (as we’ve done in several examples)
  2. Collecting Items: Remove collectible items when the player touches them
  3. Taking Damage: Reduce player health when colliding with enemies or hazards
  4. Bouncing: Change direction based on collision (like in our ball example)
  5. Pushing: Allow objects to push each other
  6. Triggering Events: Start events or animations when certain objects collide

Let’s explore a couple of these responses in more detail:

47.9.1 Sliding Along Walls

In many games, when the player hits a wall, they can still slide along it. This feels more natural than completely stopping. Here’s how to implement this:

import pyxel

class SlidingCollisionDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Sliding Collision Demo")
        
        # Player properties
        self.player_x = 80
        self.player_y = 60
        self.player_width = 8
        self.player_height = 8
        self.player_speed = 2
        
        # Obstacles (walls)
        self.walls = [
            [40, 30, 80, 8],    # Horizontal wall
            [40, 30, 8, 60],    # Vertical wall
            [40, 90, 80, 8],    # Horizontal wall
            [120, 30, 8, 60]    # Vertical wall
        ]
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Calculate movement in X and Y directions
        dx = 0
        dy = 0
        
        if pyxel.btn(pyxel.KEY_LEFT):
            dx = -self.player_speed
        if pyxel.btn(pyxel.KEY_RIGHT):
            dx = self.player_speed
        if pyxel.btn(pyxel.KEY_UP):
            dy = -self.player_speed
        if pyxel.btn(pyxel.KEY_DOWN):
            dy = self.player_speed
        
        # Try moving horizontally
        self.player_x += dx
        
        # Check for collisions after horizontal movement
        for wall in self.walls:
            if self.check_collision(self.player_x, self.player_y, 
                                  self.player_width, self.player_height,
                                  wall[0], wall[1], wall[2], wall[3]):
                # Collision detected! Undo horizontal movement
                self.player_x -= dx
                break
        
        # Try moving vertically
        self.player_y += dy
        
        # Check for collisions after vertical movement
        for wall in self.walls:
            if self.check_collision(self.player_x, self.player_y, 
                                  self.player_width, self.player_height,
                                  wall[0], wall[1], wall[2], wall[3]):
                # Collision detected! Undo vertical movement
                self.player_y -= dy
                break
        
        # Keep player within screen bounds
        self.player_x = max(0, min(self.player_x, 160 - self.player_width))
        self.player_y = max(0, min(self.player_y, 120 - self.player_height))
    
    def check_collision(self, x1, y1, w1, h1, x2, y2, w2, h2):
        # Check for collision between two rectangles
        if x1 + w1 <= x2 or x1 >= x2 + w2:
            return False
        if y1 + h1 <= y2 or y1 >= y2 + h2:
            return False
        return True
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw the walls (brown rectangles)
        for wall in self.walls:
            pyxel.rect(wall[0], wall[1], wall[2], wall[3], 4)
        
        # Draw the player (green rectangle)
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_width, self.player_height, 11)
        
        # Draw instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Notice how you can slide along walls", 7)
        pyxel.text(5, 25, "Press Q to quit", 7)

SlidingCollisionDemo()

The key insight here is to separate horizontal and vertical movement:

  1. We move horizontally first and check for collisions
  2. Then we move vertically and check for collisions again
  3. This allows the player to slide along walls when only one direction is blocked

47.9.2 Pushing Objects

Another common collision response is pushing objects. Let’s implement a simple box-pushing mechanic:

import pyxel

class BoxPushingDemo:
    def __init__(self):
        pyxel.init(160, 120, title="Box Pushing Demo")
        
        # Player properties
        self.player_x = 80
        self.player_y = 60
        self.player_width = 8
        self.player_height = 8
        self.player_speed = 2
        
        # Boxes that can be pushed
        self.boxes = [
            [40, 40, 8, 8],    # x, y, width, height
            [100, 40, 8, 8],
            [40, 80, 8, 8],
            [100, 80, 8, 8]
        ]
        
        # Walls that cannot be moved
        self.walls = [
            [20, 20, 120, 4],    # Top wall
            [20, 20, 4, 80],     # Left wall
            [20, 100, 120, 4],   # Bottom wall
            [140, 20, 4, 84]     # Right wall
        ]
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Store previous position
        prev_x = self.player_x
        prev_y = self.player_y
        
        # Calculate movement
        dx = 0
        dy = 0
        
        if pyxel.btn(pyxel.KEY_LEFT):
            dx = -self.player_speed
        if pyxel.btn(pyxel.KEY_RIGHT):
            dx = self.player_speed
        if pyxel.btn(pyxel.KEY_UP):
            dy = -self.player_speed
        if pyxel.btn(pyxel.KEY_DOWN):
            dy = self.player_speed
        
        # Update player position
        self.player_x += dx
        self.player_y += dy
        
        # Check for collisions with walls (cannot be pushed)
        wall_collision = False
        for wall in self.walls:
            if self.check_collision(self.player_x, self.player_y, 
                                  self.player_width, self.player_height,
                                  wall[0], wall[1], wall[2], wall[3]):
                wall_collision = True
                break
        
        if wall_collision:
            # Revert to previous position if hitting a wall
            self.player_x = prev_x
            self.player_y = prev_y
        else:
            # Check for collisions with boxes (can be pushed)
            for i, box in enumerate(self.boxes):
                if self.check_collision(self.player_x, self.player_y, 
                                      self.player_width, self.player_height,
                                      box[0], box[1], box[2], box[3]):
                    # Try to push the box
                    box_new_x = box[0] + dx
                    box_new_y = box[1] + dy
                    
                    # Check if the box would hit a wall
                    box_wall_collision = False
                    for wall in self.walls:
                        if self.check_collision(box_new_x, box_new_y, 
                                             box[2], box[3],
                                             wall[0], wall[1], wall[2], wall[3]):
                            box_wall_collision = True
                            break
                    
                    # Check if the box would hit another box
                    box_box_collision = False
                    for j, other_box in enumerate(self.boxes):
                        if i != j:  # Don't check collision with itself
                            if self.check_collision(box_new_x, box_new_y, 
                                                 box[2], box[3],
                                                 other_box[0], other_box[1], 
                                                 other_box[2], other_box[3]):
                                box_box_collision = True
                                break
                    
                    if box_wall_collision or box_box_collision:
                        # Box can't be pushed, revert player position
                        self.player_x = prev_x
                        self.player_y = prev_y
                    else:
                        # Push the box
                        self.boxes[i][0] = box_new_x
                        self.boxes[i][1] = box_new_y
                    
                    break  # Only push one box at a time
    
    def check_collision(self, x1, y1, w1, h1, x2, y2, w2, h2):
        # Check for collision between two rectangles
        if x1 + w1 <= x2 or x1 >= x2 + w2:
            return False
        if y1 + h1 <= y2 or y1 >= y2 + h2:
            return False
        return True
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw the walls (dark gray rectangles)
        for wall in self.walls:
            pyxel.rect(wall[0], wall[1], wall[2], wall[3], 5)
        
        # Draw the boxes (brown rectangles)
        for box in self.boxes:
            pyxel.rect(box[0], box[1], box[2], box[3], 4)
        
        # Draw the player (green rectangle)
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_width, self.player_height, 11)
        
        # Draw instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Push the brown boxes", 7)
        pyxel.text(5, 25, "Press Q to quit", 7)

BoxPushingDemo()

This box-pushing example demonstrates:

  1. Collision detection between the player and pushable boxes
  2. Attempting to move boxes in the direction the player is moving
  3. Checking if boxes can be pushed (no walls or other boxes in the way)
  4. Different collision responses for different types of objects

47.10 Performance Considerations

As your games grow more complex with more objects to check for collisions, performance can become a concern. Here are some strategies to optimize collision detection:

47.10.1 1. Spatial Partitioning

Instead of checking every object against every other object (which is O(n²)), divide your world into regions and only check objects within the same or adjacent regions.

47.10.2 2. Broad Phase and Narrow Phase

Use a two-phase approach:

  • Broad Phase: Quickly eliminate pairs of objects that are far apart (using techniques like spatial partitioning)
  • Narrow Phase: Perform detailed collision detection only on pairs that could potentially collide

47.10.3 3. Collision Culling

Don’t perform collision checks on:

  • Objects that are too far away
  • Objects that don’t need collision (decorative elements)
  • Objects that are inactive or destroyed

47.10.4 4. Custom Collision Shapes

For complex objects, use simpler collision shapes than the actual visual sprites. For example, represent a complex character with a few simple rectangles or circles for collision purposes.

47.11 Practice Time: Your Collision Detection Quest

Now it’s your turn to practice collision detection. Complete these challenges:

  1. Create a simple maze game where the player must navigate through walls to reach a goal

  2. Implement both rectangle and circle collision in the same game (e.g., rectangle player, circular collectibles, rectangle obstacles)

  3. Add at least one special collision response (like pushing, bouncing, or triggering an event)

Here’s a starting point for your quest:

import pyxel

class MyCollisionGame:
    def __init__(self):
        pyxel.init(160, 120, title="My Collision Game")
        
        # Player properties
        self.player_x = 16
        self.player_y = 16
        self.player_width = 8
        self.player_height = 8
        self.player_speed = 2
        
        # Goal position
        self.goal_x = 136
        self.goal_y = 96
        self.goal_radius = 6
        
        # Initialize walls, collectibles, etc.
        # Your code here
        
        # Game state
        self.game_won = False
        
        pyxel.run(self.update, self.draw)
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
        
        # Update game state
        # Your code here
    
    def check_rect_collision(self, x1, y1, w1, h1, x2, y2, w2, h2):
        # Implement rectangle collision detection
        # Your code here
        pass
    
    def check_circle_collision(self, x1, y1, r1, x2, y2, r2):
        # Implement circle collision detection
        # Your code here
        pass
    
    def check_circle_rect_collision(self, circle_x, circle_y, radius, rect_x, rect_y, rect_w, rect_h):
        # Implement circle-rectangle collision detection
        # Your code here
        pass
    
    def draw(self):
        pyxel.cls(1)  # Clear screen with dark blue
        
        # Draw game elements
        # Your code here
        
        # Draw player
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_width, self.player_height, 11)
        
        # Draw goal
        pyxel.circ(self.goal_x, self.goal_y, self.goal_radius, 10)
        
        # Draw instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Reach the yellow goal", 7)

# Create and start your game
# MyCollisionGame()

47.12 Common Bugs to Watch Out For

As you implement collision detection in your games, be wary of these common issues:

  1. Off-by-One Errors: Be consistent about whether you include or exclude boundary pixels in your collision calculations.

  2. Tunneling: Fast-moving objects can “tunnel” through thin walls if they move far enough in one frame. Solution: use continuous collision detection or smaller movement steps.

  3. Corner Cases: Test collisions at corners and edges specifically, as these often have unique behaviors.

  4. Collision Resolution Order: When resolving multiple collisions, the order matters. Resolve the most important collisions first.

  5. Memory vs. Performance Trade-offs: More precise collision detection usually requires more computation. Balance precision with performance needs.

  6. Rounding Errors: Floating-point calculations can lead to small errors that accumulate over time. Be careful with equality comparisons.

  7. Collision Feedback Loops: Objects can get stuck in a cycle of colliding, moving back, colliding again, etc. Implement proper collision resolution to avoid this.

  8. Z-Order Issues: In 2D games, objects at different visual layers might not need collision detection between them.

47.13 Conclusion and Resources for Further Exploration

You’ve now learned the fundamental techniques for detecting and responding to collisions in your Pyxel games. These skills form the foundation of virtually all game interactions, from collecting coins to battling enemies.

To further enhance your collision detection skills, check out these resources:

  1. Collision Detection for Dummies - A comprehensive guide to 2D collision detection techniques.

  2. 2D Collision Detection - An excellent resource with interactive examples for various collision detection methods.

  3. Red Blob Games: Spatial Partitioning - A deeper dive into optimizing collision detection with spatial partitioning.

  4. Box2D - If you eventually want to implement more realistic physics, Box2D is a popular physics engine (though it’s not directly compatible with Pyxel).

In our next lessons, we’ll build on this foundation to create more complex game mechanics. Keep experimenting with collision detection – it’s the invisible force that brings your game worlds to life by defining how objects interact with each other!