Hey devs! Today, we’re diving into game development to create a racing game where the player controls a car and competes against other cars to win. This project is a fun way to learn more about Python and Pygame.
You can find the source code in the GitHub repository.
Setting Up the Project
As usual, our first step is to set up the project. Make sure you have Python installed on your operating system; I will be using version 3.11.4.
To check whether Python is installed, you can run the following command in your terminal:
$ python --version
Some newer versions of Python may not be supported by Pygame, so be careful. If you already have Python installed, you can go ahead and install Pygame. Otherwise, you can download Python from the official Python website by following this link.
What is Pygame? It is a collection of Python modules that allows you to create games using the Python programming language. These modules are built on top of the SDL development library, which provides access to audio, mouse input, graphics, and more, enabling you to build your own games.
To install Pygame, run the following command in your terminal:
$ pip install pygame
Now, let’s create a new main.py file inside the directory where you want to build your game. In my case, this will be the racingGame folder.
Inside main.py, we will import Pygame and print its version to make sure everything is working correctly.
import pygame
print(pygame.version) ## pygame 2.6.1 (SDL 2.28.4, Python 3.11.4)
To run the code, use this command in your terminal:
$ python main.py
Initialize the Game
Our next step is to initialize the game engine and display the game window. Inside main.py, add the following code:
import pygame
# --- Setup ---
pygame.init()
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("2D Racing Game")
clock = pygame.time.Clock()
FPS = 60
background = pygame.Color(0, 0, 0)
running = True
while running:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Draw
screen.fill(background)
# Draw a rectangle (example)
pygame.draw.rect(
screen,
(180, 180, 180), # soft grey
(300, 250, 50, 100), # x, y, width, height
2 # outline thickness
)
pygame.display.flip()
Here, we initialize a new game with pygame.init(). We also set up the game window to be 800 × 600 pixels with a black background. Then, we create a game loop that will run while the running variable is True. If the user clicks the quit button, the game will stop.
During the loop, we fill the background and draw a gray rectangle, specifying its color and position using x and y coordinates.
The final step, pygame.display.flip(), is very important. Without it, you won’t see any drawings. Pygame renders everything line by line on an “invisible” board, and flip() displays the finished drawing on the screen.
Creating a Player's Car
Now, we want to replace our rectangle with an actual car for the main player. We’ll create a new class to represent the car.
First, create a new folder named Models.
This folder will contain all the classes and objects that our game needs. Inside the folder, create a new file named Car.py. The code for this class will look like this:
import pygame
class Car:
def __init__(self, x, y, image_path="assets/images/player-car.png"):
self.x = x
self.y = y
self.speed = 0
self.max_speed = 5
self.acceleration = 0.2
self.friction = 0.05
self.angle = 0
self.active = True
# Load and scale car image
self.original_image = pygame.image.load(image_path).convert_alpha()
self.original_image = pygame.transform.scale(self.original_image, (60, 120))
self.image = self.original_image
# Rect for collisions and drawing
self.rect = self.image.get_rect(center=(self.x, self.y))
def draw(self, surface):
if self.active:
surface.blit(self.image, self.rect.topleft)
Our Car.py file defines a new class named Car and initializes it with x and y positions, as well as an image_path. We also define several properties that will be used to control how the car moves.
Additionally, there is a function responsible for drawing the car image in the game, which we will call from main.py.
Currently, the image path in the initializer does not exist, so let’s create a new folder in the project root:
$ mkdir -p assets/images
Place the car image into the folder you just created. If the file name is different from player-car.png, be sure to update it in the Car class.
Enable Movement When Keys Are Pressed
Our car doesn’t move yet, so we need to define a function that will be triggered when the user presses the arrow keys, inside Car.py.
Let’s create a new function using def keyword:
## prev code is the same
def update(self, keys):
# Horizontal movement
if keys[pygame.K_LEFT]:
self.x -= 5
if keys[pygame.K_RIGHT]:
self.x += 5
# Optional vertical movement (small)
if keys[pygame.K_UP]:
self.y -= 5
if keys[pygame.K_DOWN]:
self.y += 5
if self.y < 70:
self.y += 650
self.rect.center = (self.x, self.y)
This function checks which key is pressed and updates the car’s movement accordingly. If the Left key is pressed, the car moves left across the screen, and if the Right key is pressed, it moves right.
The car is constrained within the screen boundaries, so it cannot go off the road. We also added the ability to accelerate when the Up key is pressed and to decrease the speed accordingly.
Add the Car to main.py
Now we can import our Car.py into the game and draw it inside the game loop.
import pygame
import sys
from models.car import Car # importing the Car file
## Prev code is the same
running = True
# new line where initializing the player
player = Car(350, 500, "assets/images/player-car.png")
while running:
clock.tick(FPS)
# We ask pygame which keys are pressed right now
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
player.update(keys)
screen.fill(background)
# We creating a new car and draw it
player.draw(screen)
pygame.display.flip()
We replaced the gray rectangle with the new Car class and called its draw() and update() methods, so our car can now move around.
Adding an Opponent
Next, we’ll create the cars for the player’s opponents, which the player will be racing against. We’ll create a new model class named CompetitorCar.py.
import pygame
from models.car import Car
import random
class CompetitorCar(Car):
def __init__(self, x, y, playerCar, image_path="assets/images/opponent_car.png"):
super().__init__(x, y, image_path)
self.base_speed = random.randint(2, 4)
self.speed = self.base_speed
self.playerCar = playerCar # we also pass our playerCar here
self.active = True
def update_opponent(self, player_speed, road_speed):
# Move relative to player
relative_speed = player_speed - self.speed
self.y -= self.speed
self.rect.y = self.y
if self.y < 70:
self.y += 650
elif self.playerCar.y < 70 and self.y > 70:
self.y == 400
self.rect.center = (self.x, self.y)
In our CompetitorCar class, we inherit properties from the base Car class. We only add a few additional features, such as its own speed and movement behavior. We also added a separate image to the assets/images folder and ensure that the car cannot move beyond the screen boundaries.
Now, let's import our CompetitorCar into main.py and use it.
import pygame
import sys
from models.car import Car
from models.competitorCar import CompetitorCar # add import
# prev Code is the same
running = True
player = Car(350, 500, "assets/images/player-car.png")
# initialize the opponent
opponent = CompetitorCar(500, 500, player, "assets/images/opponent-car.png")
while running:
clock.tick(FPS)
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Draw bg and cars
screen.fill(background)
player.draw(screen)
opponent.draw(screen)
# Update cars
player.update(keys)
opponent.update_opponent()
pygame.display.flip()
Replace Background with an Actual Image
Currently, our background is just a black screen. I have prepared an image to replace it. We will update our main.py file and add background image and enable scrolling to create the illusion of movement.
import pygame
import sys
from models.car import Car
from models.competitorCar import CompetitorCar
import random
# --- Setup ---
pygame.init()
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("2D Racing Game")
clock = pygame.time.Clock()
FPS = 60
# --- Background ---
background = pygame.image.load("assets/images/track.png").convert()
bg_height = background.get_height()
scroll_y = 0
road_speed = 6
while running:
clock.tick(FPS)
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# --- Scroll background faster for forward motion ---
scroll_speed = (road_speed + player.speed) * 2 # double speed for effect
scroll_y += scroll_speed
if scroll_y >= bg_height:
scroll_y = 0
# --- Draw background ---
screen.blit(background, (0, scroll_y))
screen.blit(background, (0, scroll_y - bg_height))
# --- Update cars ---
player.update(keys)
opponent.update_opponent(player.speed, road_speed)
opponent.draw(screen)
player.draw(screen)
pygame.display.flip()
Here, we defined a new background image and obtained its height so we can use it for the scrolling behavior. We also defined the road_speed variable and scroll_y
In our game loop, we add the road speed and player speed, multiply the sum by 2, and then add this value to scroll_y.
If our scroll_y exceeds the background height, we reset it to 0. Finally, we draw our background.
Adding Collision and Game Over Screen
Adding the Explosion Effect
The game should end if the cars collide, so we will update the Car.cs file to add a variable that stores the explosion state.
# Car.py
import pygame
class Car:
def __init__(self, x, y, image_path="assets/images/player-car.png"):
#Everything is the same, add this property
self.exploding = False # add this property
#Everything is the same
# Update the function
def update(self, keys):
if self.exploding:
# Stop movement when exploded
return
# Horizontal movement
if keys[pygame.K_LEFT]:
self.x -= 5
if keys[pygame.K_RIGHT]:
self.x += 5
# Optional vertical movement (small)
if keys[pygame.K_UP]:
self.y -= 5
if keys[pygame.K_DOWN]:
self.y += 5
if self.y < 70:
self.y += 650
self.rect.center = (self.x, self.y)
# Add a new method for explosion
def explode(self, explosionImage):
self.exploding = True
self.image = pygame.image.load(explosionImage).convert_alpha()
self.image = pygame.transform.scale(self.image, (100, 100))
self.rect = self.image.get_rect(center=self.rect.center)
We added a new def explode function, which will be called from our main.py loop to end the game. Before doing that, let's create a game over screen and implement the corresponding logic.
Adding Game Over Logic
The game over screen should appear if the player loses or a collision occurs. If the player crosses the finish line first, a You Won screen will be displayed instead. Make sure to add the images won.png, game-over.jpg, and finish-line.png to your assets/images folder. We will also implement the collision logic.
# main.py
# --- Setup ---
# Stays the same
# --- Background ---
# Stays the same
# Adding new Game State variables
# --- Game State ---
game_over = False
game_over_start = 0
game_over_delay = 3000 # 3 seconds after explosion
finish_start_time = pygame.time.get_ticks()
finish_delay = 8000 # Finish line appears after 8 seconds
finish_reached = False
# --- Game Over & Win Screens ---
game_over_bg = pygame.image.load("assets/images/game-over.jpg").convert()
game_over_bg = pygame.transform.scale(game_over_bg, (WIDTH, HEIGHT))
win_bg = pygame.image.load("assets/images/won.png").convert()
win_bg = pygame.transform.scale(win_bg, (WIDTH, HEIGHT))
# --- Finish Line ---
finish_line = pygame.image.load("assets/images/finish-line.png").convert_alpha()
finish_line = pygame.transform.scale(finish_line, (WIDTH, 0))
finish_y = 100 # Y position of finish line
finish_visible = False
# --- Player & Opponent ---
player = Car(350, 500, "assets/images/player-car.png")
opponent = CompetitorCar(500, 500, player, "assets/images/opponent-car.png")
# --- Game Loop ---
while running:
clock.tick(FPS)
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
current_time = pygame.time.get_ticks()
# --- Show finish line after 8 seconds ---
if current_time - finish_start_time >= finish_delay:
finish_visible = True
# --- Scroll background faster for forward motion ---
scroll_speed = (road_speed + player.speed) * 2
scroll_y += scroll_speed
if scroll_y >= bg_height:
scroll_y = 0
# --- Draw background ---
screen.blit(background, (0, scroll_y))
screen.blit(background, (0, scroll_y - bg_height))
if not game_over and not finish_reached:
# --- Update cars ---
player.update(keys)
opponent.update_opponent(player.speed, road_speed)
opponent.draw(screen)
player.draw(screen)
# --- Draw finish line if visible ---
if finish_visible:
screen.blit(finish_line, (0, finish_y))
finish_rect = finish_line.get_rect(topleft=(0, finish_y))
if player.rect.colliderect(finish_rect):
finish_reached = True
# --- Collision detection ---
if player.rect.colliderect(opponent.rect):
player.explode("assets/images/boom.png")
opponent.explode("assets/images/boom.png")
road_speed = 0
game_over = True
game_over_start = current_time
elif game_over:
if current_time - game_over_start >= game_over_delay:
screen.blit(game_over_bg, (0, 0))
else:
# Keep last frame with explosion
player.draw(screen)
opponent.draw(screen)
if finish_visible:
screen.blit(finish_line, (0, finish_y))
elif finish_reached:
screen.blit(win_bg, (0, 0))
pygame.display.flip()
We added a new Game State variable, which indicates when the game is over. It will be set to True if our car or the opponent car collides.
Additionally, we added a finish line image and logic. The finish line will be displayed after 8 seconds, and the finish_reached variable controls whether the player has crossed the finish line.
Handle Race Logic
Currently, our cars move without proper race logic. They may disappear from the screen, and their behavior does not make much sense. To fix this, we will add a new property called place to both the Car and CompetitorCar classes. Both cars will start with a value of 0, and depending on which car reaches the end of the screen first, their place will be set to 1 or 2.
# Car.py
#--prev code is the same--
self.exploding = False
self.place = 0 # add this line
self.active = True
#--rest of the code--
#competitorCar.py
#-- prev code --
self.playerCar = playerCar
self.place = 0 # add this line
self.active = True
#-- the rest of the code--
Now let's update our main.py
# --- Player & Opponent ---
player = Car(350, 500, "assets/images/player-car.png")
opponent = CompetitorCar(500, 500, player, "assets/images/opponent-car.png")
inactivityTime = None # add new line here
# prev code is the same
if not game_over and not finish_reached:
# --- Update cars ---
player.update(keys)
opponent.update_opponent(player.speed, road_speed)
opponent.draw(screen)
player.draw(screen)
# --- Detect initial race positions ---
if player.y <= 80 and player.place == 0:
player.place = 1
opponent.place = 2
elif opponent.y <= 80 and opponent.place == 0:
opponent.place = 1
player.place = 2
# --- Hide opponent if player is first and above threshold ---
if player.place == 1 and player.y < 80 and opponent.active:
opponent.active = False
inactivityTime = current_time
# --- Reactivate opponent after random delay ---
if inactivityTime is not None and not opponent.active:
randomDelay = random.randint(1000, 3000) # milliseconds
if current_time - inactivityTime >= randomDelay:
opponent.y = 700 # respawn at bottom
opponent.active = True
opponent.speed = random.randint(3, 8)
inactivityTime = None # reset timer
# --- Extra logic if opponent is in first place ---
if opponent.place == 1 and opponent.y <= 80:
opponent.active = False # hide it if at top
# --- Optional respawn if player falls behind ---
if player.place == 2 and player.y > 600:
opponent.y = 300
opponent.active = True
# --- Draw finish line if visible ---
if finish_visible:
screen.blit(finish_line, (0, finish_y))
finish_rect = finish_line.get_rect(topleft=(0, finish_y))
if player.rect.colliderect(finish_rect):
finish_reached = True
# --- Collision detection ---
if player.rect.colliderect(opponent.rect):
player.explode("assets/images/boom.png")
opponent.explode("assets/images/boom.png")
road_speed = 0
game_over = True
game_over_start = current_time
elif game_over:
if current_time - game_over_start >= game_over_delay:
screen.blit(game_over_bg, (0, 0))
else:
# Keep last frame with explosion
player.draw(screen)
opponent.draw(screen)
if finish_visible:
screen.blit(finish_line, (0, finish_y))
elif finish_reached:
screen.blit(win_bg, (0, 0))
pygame.display.flip()
Initially, our cars have a value of 0 for the self.place variable. In our game loop, we include a conditional statement that checks whether the player has reached the end of the screen and their place is 0 meaning this is the first car to finish. If so, we assign the places.
Then, we hide the opponent and respawn it 3 seconds later somewhere behind the player with a random speed. If the opponent finishes first, we simply make it inactive, and once the player appears again, we display the competitor car.
Final Step: Add Game Start Countdown
The last thing we want to add is a game countdown. When we launch the game, a black screen will appear and count down from 3 to 1. After that, the black screen will switch to our background, and the cars will begin moving.
# main.py
# prev code is the same
running = True
# --- Countdown to start the game ---
countdown_start = pygame.time.get_ticks()
countdown_duration = 3000 # 3 seconds
race_started = False
font = pygame.font.SysFont(None, 120)
while running:
clock.tick(FPS)
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
current_time = pygame.time.get_ticks()
# =========================
# COUNTDOWN BEFORE START
# =========================
if not race_started:
elapsed = current_time - countdown_start
screen.fill((0, 0, 0))
if elapsed < 1000:
text = "3"
elif elapsed < 2000:
text = "2"
elif elapsed < 3000:
text = "1"
elif elapsed < 4000:
text = "GO!"
else:
race_started = True
finish_start_time = pygame.time.get_ticks()
continue
countdown_text = font.render(text, True, (255, 255, 255))
text_rect = countdown_text.get_rect(center=(WIDTH // 2, HEIGHT // 2))
screen.blit(countdown_text, text_rect)
pygame.display.flip()
continue
Here, we added a few variables to track the start time, the countdown duration, and whether the race has started. In the game loop, we added an if statement that checks if the race has not started yet.
If it hasn’t, we display a black screen and, based on the elapsed time, show the numbers from 3 to 1, followed by “Go.” We render and display the text on the screen, and after the countdown finishes, the actual game begins.
To run our game, we simply execute main.py. After the countdown screen, we should see our cars. In my case, it looks like this:
Thank you so much for staying with me and reading this article. I also have a YouTube channel where I post video tutorials, as well as a Telegram channel where I’m building a community to learn programming together.
I hope to see you soon and for now, Happy coding!🚀
Comments