Simulating a Self-Driving Car in Python
- Published on

Ever wondered how self-driving cars navigate complex environments? In this article, we'll explore how to simulate a self-driving car using Python and Pygame. We'll walk through building a simple car model equipped with virtual sensors, capable of navigating a track autonomously. Let's dive in!
- Introduction
- Prerequisites
- Project Overview
- Step-by-Step Implementation
- Full Source Code
- How It Works
- Running the Simulation
- Conclusion
- Next Steps
- Final Thoughts
Introduction
Self-driving cars combine various technologies like computer vision, sensor fusion, and intelligent decision-making to navigate roads safely. While building a real autonomous vehicle is a massive undertaking, simulating one provides valuable insights into the underlying principles. Using Python and Pygame, we can create a simplified environment to experiment with these concepts.
Prerequisites
- Python 3.x installed on your system.
- Pygame library installed. You can install it using:
pip install pygame
- Basic understanding of Python programming.
Project Overview
Our simulation will feature:
- A car represented as a simple rectangle.
- Virtual sensors that detect distances to track boundaries.
- A track with inner and outer boundaries to navigate.
- A decision-making algorithm to control the car based on sensor inputs.
Step-by-Step Implementation
1. Setting Up the Environment
First, we'll import the necessary libraries and initialize Pygame.
import pygame
import math
import sys
pygame.init()
# Screen dimensions
WIDTH, HEIGHT = 800, 600
win = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Self-Driving Car Simulation")
2. Defining the Car Class
The Car
class will represent our vehicle. It will handle the car's position, movement, drawing, and sensor operations.
class Car:
def __init__(self, x, y):
self.x = x
self.y = y
self.angle = 0 # Facing right initially
self.speed = 0
self.max_speed = 2
self.acceleration = 0.05
self.rotation_speed = 2
self.length = 20
self.width = 10
self.sensors = []
self.sensor_range = 100
self.radius = max(self.length, self.width) / 2 # For collision detection
def update(self):
# Update position based on speed and angle
rad = math.radians(self.angle)
dx = self.speed * math.cos(rad)
dy = self.speed * math.sin(rad)
self.x += dx
self.y += dy
# Keep angle between 0 and 360 degrees
self.angle %= 360
def draw(self, win):
# Draw the car as a rotated rectangle
car_surface = pygame.Surface((self.width, self.length), pygame.SRCALPHA)
car_surface.fill((0, 255, 0)) # Green color
rotated_car = pygame.transform.rotate(car_surface, -self.angle)
car_rect = rotated_car.get_rect(center=(self.x, self.y))
win.blit(rotated_car, car_rect)
3. Implementing Sensors
Our car will use virtual sensors to detect walls. We'll simulate sensors using rays cast from the car in different directions.
def cast_sensors(self, walls):
self.sensors = []
sensor_angles = [-90, -60, -30, 0, 30, 60, 90] # More sensors for better detection
for angle_offset in sensor_angles:
angle = self.angle + angle_offset
rad = math.radians(angle)
end_x = self.x + self.sensor_range * math.cos(rad)
end_y = self.y + self.sensor_range * math.sin(rad)
sensor_line = ((self.x, self.y), (end_x, end_y))
# Check for collision with walls
collision_point = None
min_distance = self.sensor_range
for wall in walls:
hit_point = line_rect_collision(sensor_line, wall)
if hit_point:
distance = math.hypot(hit_point[0] - self.x, hit_point[1] - self.y)
if distance < min_distance:
min_distance = distance
collision_point = hit_point
if collision_point:
pygame.draw.line(win, (255, 0, 0), (self.x, self.y), collision_point, 1)
else:
pygame.draw.line(win, (255, 0, 0), (self.x, self.y), (end_x, end_y), 1)
self.sensors.append(min_distance)
4. Collision Detection Functions
We need functions to detect collisions between sensor lines and walls.
def line_rect_collision(line, rect):
x1, y1 = line[0]
x2, y2 = line[1]
# Get rect sides
rect_lines = [
((rect.left, rect.top), (rect.right, rect.top)),
((rect.right, rect.top), (rect.right, rect.bottom)),
((rect.right, rect.bottom), (rect.left, rect.bottom)),
((rect.left, rect.bottom), (rect.left, rect.top))
]
closest_point = None
min_distance = float('inf')
for rect_line in rect_lines:
hit_point = line_line_collision(line, rect_line)
if hit_point:
distance = math.hypot(hit_point[0] - x1, hit_point[1] - y1)
if distance < min_distance:
min_distance = distance
closest_point = hit_point
return closest_point
def line_line_collision(line1, line2):
# Line segments: line1 from (x1, y1) to (x2, y2), line2 from (x3, y3) to (x4, y4)
x1, y1 = line1[0]
x2, y2 = line1[1]
x3, y3 = line2[0]
x4, y4 = line2[1]
denom = (y4 - y3)*(x2 - x1) - (x4 - x3)*(y2 - y1)
if denom == 0:
return None # Lines are parallel
ua = ((x4 - x3)*(y1 - y3) - (y4 - y3)*(x1 - x3)) / denom
ub = -((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / denom
if 0 <= ua <= 1 and 0 <= ub <= 1:
# Intersection point is within both line segments
x = x1 + ua*(x2 - x1)
y = y1 + ua*(y2 - y1)
return (x, y)
else:
return None
5. Creating the Track
We'll define a track with inner and outer boundaries to simulate a real driving environment.
def create_track():
walls = []
# Outer boundaries
track_outer = [
(100, 100, 600, 20), # Top wall
(680, 100, 20, 400), # Right wall
(100, 480, 600, 20), # Bottom wall
(100, 100, 20, 400) # Left wall
]
# Inner boundaries
track_inner = [
(200, 200, 400, 20), # Top wall
(580, 200, 20, 200), # Right wall
(200, 380, 400, 20), # Bottom wall
(200, 200, 20, 200) # Left wall
]
# Create walls for outer track
for rect in track_outer:
walls.append(pygame.Rect(rect))
# Create walls for inner track
for rect in track_inner:
walls.append(pygame.Rect(rect))
return walls
def draw_track(win, walls):
for wall in walls:
pygame.draw.rect(win, (0, 0, 0), wall)
6. Decision-Making Algorithm
Our car needs to make decisions based on sensor inputs. We'll implement a simple algorithm that adjusts the car's angle and speed.
def make_decision(car):
sensors = car.sensors
if sensors:
front_sensor = sensors[3]
left_sensors = sensors[:3]
right_sensors = sensors[4:]
# If an obstacle is detected in front, decide to turn
if front_sensor < 30:
if sum(left_sensors) > sum(right_sensors):
car.angle -= car.rotation_speed # Turn left
else:
car.angle += car.rotation_speed # Turn right
if car.speed > 0:
car.speed -= car.acceleration # Slow down
else:
# Adjust course slightly if side sensors detect walls
if min(left_sensors) < 50:
car.angle += car.rotation_speed / 2 # Adjust right
elif min(right_sensors) < 50:
car.angle -= car.rotation_speed / 2 # Adjust left
if car.speed < car.max_speed:
car.speed += car.acceleration # Accelerate
else:
car.speed = 0 # Stop if no sensor data
7. Collision Handling
To prevent the car from going through walls, we'll implement collision detection.
def check_collision(car, walls):
car_circle = pygame.Rect(
car.x - car.radius, car.y - car.radius, car.radius*2, car.radius*2)
for wall in walls:
if car_circle.colliderect(wall):
return True
return False
8. Running the Simulation
We'll set up the main loop to run the simulation.
def main():
# Starting position adjusted to the track
car = Car(150, 300)
walls = create_track()
clock = pygame.time.Clock()
run = True
while run:
clock.tick(60)
win.fill((200, 200, 200))
# Event handling
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
# Update and draw track
draw_track(win, walls)
# Car operations
car.cast_sensors(walls)
make_decision(car)
car.update()
if check_collision(car, walls):
# If collision detected, stop the car and reverse a bit
car.speed = -car.max_speed / 2
car.update()
car.speed = 0
car.draw(win)
pygame.display.update()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
Full Source Code
Combining all the pieces, here's the complete code:
import pygame
import math
import sys
pygame.init()
# Screen dimensions
WIDTH, HEIGHT = 800, 600
win = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Self-Driving Car Simulation")
class Car:
def __init__(self, x, y):
self.x = x
self.y = y
self.angle = 0 # Facing right initially
self.speed = 0
self.max_speed = 2
self.acceleration = 0.05
self.rotation_speed = 2
self.length = 20
self.width = 10
self.sensors = []
self.sensor_range = 100
self.radius = max(self.length, self.width) / 2 # For collision detection
def update(self):
# Update position based on speed and angle
rad = math.radians(self.angle)
dx = self.speed * math.cos(rad)
dy = self.speed * math.sin(rad)
self.x += dx
self.y += dy
# Keep angle between 0 and 360 degrees
self.angle %= 360
def draw(self, win):
# Draw the car as a rotated rectangle
car_surface = pygame.Surface((self.width, self.length), pygame.SRCALPHA)
car_surface.fill((0, 255, 0)) # Green color
rotated_car = pygame.transform.rotate(car_surface, -self.angle)
car_rect = rotated_car.get_rect(center=(self.x, self.y))
win.blit(rotated_car, car_rect)
def cast_sensors(self, walls):
self.sensors = []
sensor_angles = [-90, -60, -30, 0, 30, 60, 90] # More sensors for better detection
for angle_offset in sensor_angles:
angle = self.angle + angle_offset
rad = math.radians(angle)
end_x = self.x + self.sensor_range * math.cos(rad)
end_y = self.y + self.sensor_range * math.sin(rad)
sensor_line = ((self.x, self.y), (end_x, end_y))
# Check for collision with walls
collision_point = None
min_distance = self.sensor_range
for wall in walls:
hit_point = line_rect_collision(sensor_line, wall)
if hit_point:
distance = math.hypot(hit_point[0] - self.x, hit_point[1] - self.y)
if distance < min_distance:
min_distance = distance
collision_point = hit_point
if collision_point:
pygame.draw.line(win, (255, 0, 0), (self.x, self.y), collision_point, 1)
else:
pygame.draw.line(win, (255, 0, 0), (self.x, self.y), (end_x, end_y), 1)
self.sensors.append(min_distance)
def line_rect_collision(line, rect):
x1, y1 = line[0]
x2, y2 = line[1]
# Get rect sides
rect_lines = [
((rect.left, rect.top), (rect.right, rect.top)),
((rect.right, rect.top), (rect.right, rect.bottom)),
((rect.right, rect.bottom), (rect.left, rect.bottom)),
((rect.left, rect.bottom), (rect.left, rect.top))
]
closest_point = None
min_distance = float('inf')
for rect_line in rect_lines:
hit_point = line_line_collision(line, rect_line)
if hit_point:
distance = math.hypot(hit_point[0] - x1, hit_point[1] - y1)
if distance < min_distance:
min_distance = distance
closest_point = hit_point
return closest_point
def line_line_collision(line1, line2):
# Line segments: line1 from (x1, y1) to (x2, y2), line2 from (x3, y3) to (x4, y4)
x1, y1 = line1[0]
x2, y2 = line1[1]
x3, y3 = line2[0]
x4, y4 = line2[1]
denom = (y4 - y3)*(x2 - x1) - (x4 - x3)*(y2 - y1)
if denom == 0:
return None # Lines are parallel
ua = ((x4 - x3)*(y1 - y3) - (y4 - y3)*(x1 - x3)) / denom
ub = -((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / denom
if 0 <= ua <= 1 and 0 <= ub <= 1:
# Intersection point is within both line segments
x = x1 + ua*(x2 - x1)
y = y1 + ua*(y2 - y1)
return (x, y)
else:
return None
def create_track():
walls = []
# Outer boundaries
track_outer = [
(100, 100, 600, 20), # Top wall
(680, 100, 20, 400), # Right wall
(100, 480, 600, 20), # Bottom wall
(100, 100, 20, 400) # Left wall
]
# Inner boundaries
track_inner = [
(200, 200, 400, 20), # Top wall
(580, 200, 20, 200), # Right wall
(200, 380, 400, 20), # Bottom wall
(200, 200, 20, 200) # Left wall
]
# Create walls for outer track
for rect in track_outer:
walls.append(pygame.Rect(rect))
# Create walls for inner track
for rect in track_inner:
walls.append(pygame.Rect(rect))
return walls
def draw_track(win, walls):
for wall in walls:
pygame.draw.rect(win, (0, 0, 0), wall)
def make_decision(car):
sensors = car.sensors
if sensors:
front_sensor = sensors[3]
left_sensors = sensors[:3]
right_sensors = sensors[4:]
# If an obstacle is detected in front, decide to turn
if front_sensor < 30:
if sum(left_sensors) > sum(right_sensors):
car.angle -= car.rotation_speed # Turn left
else:
car.angle += car.rotation_speed # Turn right
if car.speed > 0:
car.speed -= car.acceleration # Slow down
else:
# Adjust course slightly if side sensors detect walls
if min(left_sensors) < 50:
car.angle += car.rotation_speed / 2 # Adjust right
elif min(right_sensors) < 50:
car.angle -= car.rotation_speed / 2 # Adjust left
if car.speed < car.max_speed:
car.speed += car.acceleration # Accelerate
else:
car.speed = 0 # Stop if no sensor data
def check_collision(car, walls):
car_circle = pygame.Rect(
car.x - car.radius, car.y - car.radius, car.radius*2, car.radius*2)
for wall in walls:
if car_circle.colliderect(wall):
return True
return False
def main():
# Starting position adjusted to the track
car = Car(150, 300)
walls = create_track()
clock = pygame.time.Clock()
run = True
while run:
clock.tick(60)
win.fill((200, 200, 200))
# Event handling
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
# Update and draw track
draw_track(win, walls)
# Car operations
car.cast_sensors(walls)
make_decision(car)
car.update()
if check_collision(car, walls):
# If collision detected, stop the car and reverse a bit
car.speed = -car.max_speed / 2
car.update()
car.speed = 0
car.draw(win)
pygame.display.update()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
How It Works
- Car Initialization: The car starts at position
(150, 300)
facing right. - Sensors: The car has seven sensors cast at angles relative to its current heading.
- Track: The track consists of inner and outer walls, creating a loop for the car to navigate.
- Decision-Making: The car adjusts its speed and angle based on sensor inputs to avoid walls and stay on track.
- Collision Handling: If the car collides with a wall, it reverses slightly and stops to prevent passing through walls.
Running the Simulation
Save the Code: Copy the complete code into a file named
self_driving_car_simulation.py
.Run the Script:
python self_driving_car_simulation.py
Observe: A window will open displaying the track and the car. The car will navigate the track autonomously.

Conclusion
This simulation demonstrates the basics of how a self-driving car perceives its environment and makes decisions based on sensor data. While simplified, it provides a foundation for understanding more complex concepts in autonomous vehicle navigation.
Next Steps
- Enhance the Decision Algorithm: Implement advanced algorithms like PID controllers or machine learning models for smoother navigation.
- Improve the Track: Create more complex tracks with curves and intersections.
- Add Obstacles: Introduce dynamic obstacles to simulate real-world driving conditions.
- User Control: Allow manual control of the car to compare with autonomous navigation.
Final Thoughts
Simulating a self-driving car in Python with Pygame is both educational and entertaining. It bridges the gap between theoretical concepts and practical implementation, offering a hands-on approach to learning about autonomous systems. Keep experimenting, and who knows—you might develop the next breakthrough in self-driving technology!
Note: This simulation is intended for educational purposes and simplifies many aspects of real-world autonomous driving systems.