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:
- Rectangle (AABB) Collision: Checking if rectangles overlap (simplest and most common)
- Circle Collision: Checking if circles overlap (good for round objects)
- Pixel-Perfect Collision: Checking at the pixel level (most precise but computationally expensive)
- 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):
160, 120, title="Rectangle Collision Demo")
pyxel.init(
# 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
self.update, self.draw)
pyxel.run(
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):
1) # Clear screen with dark blue
pyxel.cls(
# Draw player (green rectangle)
self.player_x, self.player_y,
pyxel.rect(self.player_width, self.player_height, 11)
# Draw coin (yellow circle) if active
if self.coin_active:
self.coin_x + 4, self.coin_y + 4, 4, 10)
pyxel.circ(
# Draw score
5, 5, f"SCORE: {self.score}", 7)
pyxel.text(
# Draw instructions
5, 15, "Use arrow keys to move", 7)
pyxel.text(5, 25, "Collect the coin by touching it", 7)
pyxel.text(
CollisionDemo()
In this example:
- We have a player rectangle (green) and a coin (yellow circle)
- We check for collision between them using rectangle collision detection
- 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):
160, 120, title="Collision Visualization")
pyxel.init(
# 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
self.update, self.draw)
pyxel.run(
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):
1) # Clear screen with dark blue
pyxel.cls(
# Draw player
self.player_x, self.player_y,
pyxel.rect(self.player_width, self.player_height, 11)
# Draw coin if active
if self.coin_active:
self.coin_x + 4, self.coin_y + 4, 4, 10)
pyxel.circ(
# Draw collision boxes if enabled
if self.show_collision_boxes:
# Player collision box
self.player_x, self.player_y,
pyxel.rectb(self.player_width, self.player_height, 7)
# Coin collision box (if active)
if self.coin_active:
self.coin_x, self.coin_y,
pyxel.rectb(self.coin_width, self.coin_height, 7)
# Draw score and instructions
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)
pyxel.text(
# Draw collision status
= self.coin_active and self.check_collision(
is_colliding 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!" if is_colliding else "No collision"
status 5, 110, status, 8 if is_colliding else 7)
pyxel.text(
CollisionVisualizationDemo()
This enhanced version:
- Shows collision boxes with white outlines
- Allows toggling the visibility of these boxes with the T key
- Displays the current collision status
- 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
= (x2 - x1)**2 + (y2 - y1)**2
distance_squared
# 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):
160, 120, title="Circle Collision Demo")
pyxel.init(
# 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
self.update, self.draw)
pyxel.run(
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):
= (x2 - x1)**2 + (y2 - y1)**2
distance_squared return distance_squared < (r1 + r2)**2
def draw(self):
1) # Clear screen with dark blue
pyxel.cls(
# Draw player (blue circle)
= 12
player_color self.player_x, self.player_y, self.player_radius, player_color)
pyxel.circ(
# Draw enemy (red or pink circle)
= 8 if self.collision else 14
enemy_color self.enemy_x, self.enemy_y, self.enemy_radius, enemy_color)
pyxel.circ(
# Draw collision status
= "COLLISION DETECTED!" if self.collision else "No collision"
status 5, 5, status, 8 if self.collision else 7)
pyxel.text(
# Draw instructions
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)
pyxel.text(
CircleCollisionDemo()
In this demo:
- Both the player and enemy are represented as circles
- We check for collisions using the circle collision formula
- The enemy changes color when a collision is detected
- 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
= max(rect_x, min(circle_x, rect_x + rect_w))
closest_x = max(rect_y, min(circle_y, rect_y + rect_h))
closest_y
# Calculate the distance between the circle's center and the closest point
= (circle_x - closest_x)**2 + (circle_y - closest_y)**2
distance_squared
# 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):
160, 120, title="Circle-Rectangle Collision")
pyxel.init(
# 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
self.update, self.draw)
pyxel.run(
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
= max(rect_x, min(circle_x, rect_x + rect_w))
closest_x = max(rect_y, min(circle_y, rect_y + rect_h))
closest_y
# Calculate distance squared
= (circle_x - closest_x)**2 + (circle_y - closest_y)**2
distance_squared
# Check if distance is less than radius squared
return distance_squared < radius**2
def draw(self):
1) # Clear screen with dark blue
pyxel.cls(
# Draw ball (yellow circle)
self.ball_x, self.ball_y, self.ball_radius, 10)
pyxel.circ(
# Draw paddle (white rectangle)
= 7
paddle_color self.paddle_x, self.paddle_y,
pyxel.rect(self.paddle_width, self.paddle_height, paddle_color)
# Draw score
5, 5, f"SCORE: {self.score}", 7)
pyxel.text(
# Draw instructions
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)
pyxel.text(
MixedCollisionDemo()
This simple Breakout-style game demonstrates:
- Circle-rectangle collision between a ball and paddle
- Bouncing physics based on collision detection
- Score tracking based on successful bounces
- 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):
160, 120, title="Coin Collector Game")
pyxel.init(
# 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
self.update, self.draw)
pyxel.run(
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
= self.player_x
prev_x = self.player_y
prev_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,
0], obstacle[1], obstacle[2], obstacle[3]
obstacle[
):# 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(
0], coin[1], coin[2],
coin[self.player_x, self.player_y, self.player_width, self.player_height
):# Collected a coin!
3] = False
coin[self.score += 10
# Check if all coins are collected
= all(not coin[3] for coin in self.coins)
all_collected 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
= max(rect_x, min(circle_x, rect_x + rect_w))
closest_x = max(rect_y, min(circle_y, rect_y + rect_h))
closest_y
# Calculate distance squared
= (circle_x - closest_x)**2 + (circle_y - closest_y)**2
distance_squared
# Check if distance is less than radius squared
return distance_squared < radius**2
def draw(self):
1) # Clear screen with dark blue
pyxel.cls(
# Draw player (green rectangle)
self.player_x, self.player_y,
pyxel.rect(self.player_width, self.player_height, 11)
# Draw coins (yellow circles)
for coin in self.coins:
if coin[3]: # If active
0], coin[1], coin[2], 10)
pyxel.circ(coin[
# Draw obstacles (red rectangles)
for obstacle in self.obstacles:
0], obstacle[1], obstacle[2], obstacle[3], 8)
pyxel.rect(obstacle[
# Draw score
5, 5, f"SCORE: {self.score}", 7)
pyxel.text(
# Draw instructions
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)
pyxel.text(
# Draw game over screen
if self.game_over:
50, 50, 60, 30, 7)
pyxel.rectb(51, 51, 58, 28, 5)
pyxel.rect(65, 60, "YOU WIN!", 10)
pyxel.text(60, 70, "Press R to restart", 7)
pyxel.text(
CollisionGame()
This more complete game example demonstrates:
- Rectangle collision for obstacles (solid objects the player can’t pass through)
- Circle-rectangle collision for coins (items the player can collect)
- Collision resolution by reverting to the previous position when hitting obstacles
- 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):
160, 120, title="Tile Collision Demo")
pyxel.init(
# 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]
[
]
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Store previous position for collision resolution
= self.player_x
prev_x = self.player_y
prev_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
= max(0, x // self.tile_size)
left_tile = min(19, (x + width - 1) // self.tile_size)
right_tile = max(0, y // self.tile_size)
top_tile = min(14, (y + height - 1) // self.tile_size)
bottom_tile
# 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):
1) # Clear screen with dark blue
pyxel.cls(
# 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)
* self.tile_size, y * self.tile_size,
pyxel.rect(x self.tile_size, self.tile_size, 4)
# Draw the player (green rectangle)
self.player_x, self.player_y,
pyxel.rect(self.player_width, self.player_height, 11)
# Draw instructions
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)
pyxel.text(
TileCollisionDemo()
This tile-based collision example demonstrates:
- A 2D grid representing a simple maze
- Converting pixel coordinates to tile coordinates for collision checking
- Checking if the player’s rectangle overlaps with any solid tiles
- 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:
- Blocking Movement: Prevent the player from moving through solid objects (as we’ve done in several examples)
- Collecting Items: Remove collectible items when the player touches them
- Taking Damage: Reduce player health when colliding with enemies or hazards
- Bouncing: Change direction based on collision (like in our ball example)
- Pushing: Allow objects to push each other
- 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):
160, 120, title="Sliding Collision Demo")
pyxel.init(
# 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
[
]
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Calculate movement in X and Y directions
= 0
dx = 0
dy
if pyxel.btn(pyxel.KEY_LEFT):
= -self.player_speed
dx if pyxel.btn(pyxel.KEY_RIGHT):
= self.player_speed
dx if pyxel.btn(pyxel.KEY_UP):
= -self.player_speed
dy if pyxel.btn(pyxel.KEY_DOWN):
= self.player_speed
dy
# 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,
0], wall[1], wall[2], wall[3]):
wall[# 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,
0], wall[1], wall[2], wall[3]):
wall[# 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):
1) # Clear screen with dark blue
pyxel.cls(
# Draw the walls (brown rectangles)
for wall in self.walls:
0], wall[1], wall[2], wall[3], 4)
pyxel.rect(wall[
# Draw the player (green rectangle)
self.player_x, self.player_y,
pyxel.rect(self.player_width, self.player_height, 11)
# Draw instructions
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)
pyxel.text(
SlidingCollisionDemo()
The key insight here is to separate horizontal and vertical movement:
- We move horizontally first and check for collisions
- Then we move vertically and check for collisions again
- 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):
160, 120, title="Box Pushing Demo")
pyxel.init(
# 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
[
]
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Store previous position
= self.player_x
prev_x = self.player_y
prev_y
# Calculate movement
= 0
dx = 0
dy
if pyxel.btn(pyxel.KEY_LEFT):
= -self.player_speed
dx if pyxel.btn(pyxel.KEY_RIGHT):
= self.player_speed
dx if pyxel.btn(pyxel.KEY_UP):
= -self.player_speed
dy if pyxel.btn(pyxel.KEY_DOWN):
= self.player_speed
dy
# Update player position
self.player_x += dx
self.player_y += dy
# Check for collisions with walls (cannot be pushed)
= False
wall_collision for wall in self.walls:
if self.check_collision(self.player_x, self.player_y,
self.player_width, self.player_height,
0], wall[1], wall[2], wall[3]):
wall[= True
wall_collision 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,
0], box[1], box[2], box[3]):
box[# Try to push the box
= box[0] + dx
box_new_x = box[1] + dy
box_new_y
# Check if the box would hit a wall
= False
box_wall_collision for wall in self.walls:
if self.check_collision(box_new_x, box_new_y,
2], box[3],
box[0], wall[1], wall[2], wall[3]):
wall[= True
box_wall_collision break
# Check if the box would hit another box
= False
box_box_collision 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,
2], box[3],
box[0], other_box[1],
other_box[2], other_box[3]):
other_box[= True
box_box_collision 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):
1) # Clear screen with dark blue
pyxel.cls(
# Draw the walls (dark gray rectangles)
for wall in self.walls:
0], wall[1], wall[2], wall[3], 5)
pyxel.rect(wall[
# Draw the boxes (brown rectangles)
for box in self.boxes:
0], box[1], box[2], box[3], 4)
pyxel.rect(box[
# Draw the player (green rectangle)
self.player_x, self.player_y,
pyxel.rect(self.player_width, self.player_height, 11)
# Draw instructions
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)
pyxel.text(
BoxPushingDemo()
This box-pushing example demonstrates:
- Collision detection between the player and pushable boxes
- Attempting to move boxes in the direction the player is moving
- Checking if boxes can be pushed (no walls or other boxes in the way)
- 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:
Create a simple maze game where the player must navigate through walls to reach a goal
Implement both rectangle and circle collision in the same game (e.g., rectangle player, circular collectibles, rectangle obstacles)
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):
160, 120, title="My Collision Game")
pyxel.init(
# 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
self.update, self.draw)
pyxel.run(
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):
1) # Clear screen with dark blue
pyxel.cls(
# Draw game elements
# Your code here
# Draw player
self.player_x, self.player_y,
pyxel.rect(self.player_width, self.player_height, 11)
# Draw goal
self.goal_x, self.goal_y, self.goal_radius, 10)
pyxel.circ(
# Draw instructions
5, 5, "Use arrow keys to move", 7)
pyxel.text(5, 15, "Reach the yellow goal", 7)
pyxel.text(
# 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:
Off-by-One Errors: Be consistent about whether you include or exclude boundary pixels in your collision calculations.
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.
Corner Cases: Test collisions at corners and edges specifically, as these often have unique behaviors.
Collision Resolution Order: When resolving multiple collisions, the order matters. Resolve the most important collisions first.
Memory vs. Performance Trade-offs: More precise collision detection usually requires more computation. Balance precision with performance needs.
Rounding Errors: Floating-point calculations can lead to small errors that accumulate over time. Be careful with equality comparisons.
Collision Feedback Loops: Objects can get stuck in a cycle of colliding, moving back, colliding again, etc. Implement proper collision resolution to avoid this.
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:
Collision Detection for Dummies - A comprehensive guide to 2D collision detection techniques.
2D Collision Detection - An excellent resource with interactive examples for various collision detection methods.
Red Blob Games: Spatial Partitioning - A deeper dive into optimizing collision detection with spatial partitioning.
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!