Course Module • Jillur Quddus
9. Classes and Objects Part 2
Introduction to Python
Introduction to Python
Jillur Quddus • Founder & Chief Data Scientist • 1st September 2020
Back to Course Overview
Introduction
In this module we will consolidate and apply our knowledge of object oriented programming principles together with what we have learnt from the other modules throughout this course thus far to create a car racing game written in Python!
The source code for this module may be found in the public GitHub repository for this course, and specifically in the Python module
examples/formulapy/formulapy.py
.
1. The Goal
The aim of this module is to create a car racing game in Python using object oriented programming principles together with what we have learn from the other module throughout this course thus far, and using the Pygame engine for writing video games and multimedia programs in Python. We will not explore the PyGame engine in detail, bur rather rely on its API to create our game. To learn more about the PyGame API, please visit the official Pygame documentation.
2. PyGame
Pygame is an open source engine and framework for writing video games and multimedia programs in Python. Though low-level details of the library is beyond the scope of this module, the following subsections detail the high-level core components of its API that we will utilise for our car racing game.
2.1. Installation
At the time of writing (September 2020), Pygame does not officially support Python 3.8. It is possible to install a pre-release of Pygame for Python 3.8 (the latest pre-release version being 2.0.0.dev10 at the time of writing), as follows:
# Install a pre-release of Pygame for environments running Python 3.8
pip install pygame==2.0.0.dev10
However it is recommended that instead you use a Python virtual environment running Python 3.7.7 to install and use the latest official release of Pygame (the latest official release being v1.9.6 at the time of writing). The following command-line statements demonstrate how, using Aanconda, we can create a new conda environment running Python 3.7.7 and thereafter activate it and install Pygame:
# Create a new conda environment called my_pygame_env running Python 3.7.7
conda create -n my_pygame_env anaconda python=3.7
# Activate this new conda environment
conda activate my_pygame_env
# Install Pygame
pip install pygame
To learn more about creating and managing Conda environments, please visit the Managing Environments conda documentation resource.
You should now be able to import
Pygame into your Python modules, as follows:
# Import Pygame
import pygame
To learn more about installing Pygame on various environments, please visit the official Pygame documentation on Pygame Installation.
2.2. Initialisation
After we import
Pygame into our Python modules, we need to initialise it (which has the effect of initialising relevant Pygame modules). The following Python statement initialises Pygame:
# Initialise Pygame
pygame.init()
To learn more about initialising Pygame, please visit the official Pygame documentation on Import and Initialize.
2.3. Display Module
The Pygame pygame.display
module controls the window and screen that contains our game. The following Python statements demonstrate how we can instantiate a new window of a given size (in pixels) and with a given titlebar caption:
# Set the display titlebar caption
screen_display_caption = 'FormulaPy'
pygame.display.set_caption(screen_display_caption)
# Instantiate and return a new display object with a given screen size
screen_size = (600, 800)
display = pygame.display.set_mode(screen_size)
To learn more about the Pygame display module, please visit the official Pygame documentation on pygame.display.
2.4. Image Handling and Shapes
The Pygame pygame.image
module provides a collection of functions designed to handle and transform images. The following Python statements demonstrate how we can load and re-size a given image file from the file system:
# Load an image file
image_filename = 'my-image.png'
image = pygame.image.load(image_filename).convert()
# Transform the image so that any pixels that have the same colour as a given colorkey will be transparent
image.set_colorkey(BLACK)
# Resize the image to a given tuple of dimensions in pixels
image_size = (100, 200)
image = pygame.transform.scale(image, image_size)
Furthermore, the Pygame pygame.draw
module provides a collection of functions designed to create, handle and transform shapes. The following Python statements demonstrate how we can create and render a rectangle given its initial (x, y) co-ordinates, fill colour, width and height (in pixels):
# Create a rectangle of given initial co-ordinates (x, y), width and height
pygame.draw.rect(screen, WHITE, [x, y, width, height])
To learn more about the Pygame image and draw modules, please visit the official Pygame documentation on pygame.image and pygame.draw respectively.
2.5. Rendering and Display Updates
So far we have demonstrated how we can use the Pygame API to load images and create shapes. We then need the ability to render these objects on the display. To do this, Pygame provides the pygame.Surface
class and specifically its Surface.blit
method to render objects onto the display at given (x, y) co-ordinates. The following Python statements demonstrate how we can render an image object onto the display at given (x, y) co-ordinates:
# Render an image object onto the display at given (x, y) co-ordinates
display.blit(image, [x, y])
We then need to explicitly refresh the display so that the object is viewable in the window by the user. To do this, we can use the Pygame pygame.display
module again, and specifically either its display.flip()
(update the contents of the entire display) or display.update()
(update specific portions of the display, and if no argument is passed then it will update the entire display) functions, as follows:
# Update the contents of the entire display
pygame.display.flip()
To learn more about the Pygame flip and update display functions, please visit the official Pygame documentation on pygame.display.flip() and pygame.display.update() respectively.
2.6. Clock and Framerate
The Pygame pygame.time
module provides a collection of classes and functions designed to monitor time. In particular its pygame.time.Clock
class enables us to help track time in our game and consequently set a framerate for our game. The following Python statements demonstate how we can instantiate a Clock
object and thereafter use the Clock.tick()
method to update the clock with a given framerate in order to compute how many milliseconds have passed since the previous clock update. In other words, given a framerate, this method will invoke a delay to keep the game running slower than the given ticks per second, as follows:
# Instantiate a new clock object
clock = pygame.time.Clock()
# Run the game at no more than 60 frames per second
clock.tick(60)
To learn more about the Pygame time module, please visit the official Pygame documentation on pygame.time.
2.7. Event Listeners
The Pygame pygame.event
module provides a collection of classes and functions designed to interact with events and queues. In particular its pygame.event.get()
function and pygame.event.type
attribute allows us to create event listeners in our game to listen for keyboard key presses and mouse clicks, amongst other event types. The following Python statements demonstrate how we can listen for close window and keyboard key down events respectively:
# Event detection loop
for event in pygame.event.get():
# Close Window Event
if event.type == pygame.QUIT:
pygame.quit()
quit()
# Keyboard Key Down Event
if event.type == pygame.KEYDOWN:
# Left Arrow Key
if event.key == pygame.K_LEFT:
....
# Right Arrow Key
if event.key == pygame.K_RIGHT:
....
To learn more about the Pygame event module, please visit the official Pygame documentation on pygame.event.
2.8. Indefinite Gaming Loop
Finally, and in order to continuously listen for relevant events and take subsequent display rendering actions (i.e. the events and subsequent responses that form the basic definition of how a video game works), we define the logic and behaviour of our Pygame within a while
loop. In the case of our FormulaPy car racing game, the while
loop will continue indefinitely until either the user closes the window (pygame.QUIT
event), or a collision event is detected, as follows:
# Game loop
while not request_window_close:
# Whilst there is no collision event
if not collision_event_detected:
...events and subsequent responses and actions defined here...
3. FormulaPy Car Racing Game
Now that we have a high-level understanding of the relevant core components of the Pygame API, we are ready to start developing our FormulaPy car racing game! The source code for our game can be found in the GitHub repository for this course, and specifically in the Python module examples/formulapy/formulapy.py
.
3.1. Game Rules
The rules of our game are simple - everytime you overtake a computer-driven car, your score increases. The game finishes when you either collide with a computer-driven car, or collide with the edges of the screen. As the game progresses, the speed at which the computer-driven cars come towards you increases, thereby making the game more difficult the longer the game continues.
3.2. Running the Game
In order to run our FormulaPy car racing game, clone the Git repository for this course. Then navigate to examples/formulapy
and run the following command:
python formulapy.py
The following splash screen should appear, followed by the game itself.
3.3. Two Dimensional Space
Pygame, like many other simple game engines, models the environment as a two-dimensional space where positions are defined and passed as (x, y) co-ordinates along the x-plane and y-plane respectively. The co-ordinates (x = 0, y = 0) refer to the top-left corner of this two-dimensional space, where moving to the right represents moving in the positive x-plane direction, and moving down represents moving in the positive y-plane direction, as illustrated in the following diagram:
For the purposes of our FormulaPy game, we define a function called initialize_screen()
that instantiates a display that has dimensions 600 pixels (width) x 800 pixels (height), meaning that the visible subset of our two-dimensional space is bounded between the co-ordinates (0, 0) and (600, 800). Furthermore we provide a caption for our display, and render a splash screen image for a defined period of time (in seconds), as follows:
# Game Configuration
SCREEN_SIZE = (600, 800)
SCREEN_DISPLAY_CAPTION = 'FormulaPy'
SPLASH_SCREEN_TIME = 5
SPLASH_SCREEN_IMAGE_FILENAME = 'splash-screen.jpg'
def initialize_screen():
"""Return the display with an initial splash screen."""
# Set the display caption
pygame.display.set_caption(SCREEN_DISPLAY_CAPTION)
# Instantiate a new display with the given screen size
display = pygame.display.set_mode(SCREEN_SIZE)
# Load and transform the splash screen image
splash_screen_image = pygame.image.load(
SPLASH_SCREEN_IMAGE_FILENAME).convert()
splash_screen_image.set_colorkey(BLACK)
splash_screen_image = pygame.transform.scale(
splash_screen_image, SCREEN_SIZE)
# Display the splash screen for a defined period of time
display.blit(splash_screen_image, [0, 0])
pygame.display.flip()
time.sleep(SPLASH_SCREEN_TIME)
return display
3.4. Car Class
Next we create a Car
class from which both the human-controlled car object and computer-controlled car objects will be instantiated. In order to instantiate a car object derived from our Car
class, we must pass the following arguments to its constructor at the time of creation:
pos_x
- the position of the car on the x-axis.pos_y
- the position of the car on the y-axis.delta_x
- the amount by which to move the car relative to the x-axis.delta_y
- the amount by which to move the car relative to the y-axis.human
- whether the car is human-controlled (True
) or not (False
).
Thereafter we define the following four methods in our Car
class:
load_transform_image()
- load an image from the filesystem which will be used to render the car object onto the display. A different image is loaded dependent on whether the car object is human-controlled or not.render_image()
- render the image loaded from the filesystem onto the display at the current co-ordinates of the current object as assigned to the attributespos_x
andpos_y
respectively.turn_left_right()
- increase (or decrease) the value assigned topos_x
by the current value assigned todelta_x
. Ifdelta_x
is positive, then this has the effect of moving the object to the right along the x-axis. Ifdelta_x
is negative, then this has the effect of moving the object to the left along the x-axis.move_up_down()
- increase (or decrease) the value assigned topos_y
by the current value assigned todelta_y
. Ifdelta_y
is positive, then this has the effect of moving the object down along the y-axis. Ifdelta_y
is negative, then this has the effect of moving the object up along the y-axis.
Our final Car
class therefore is as follows:
# Game Configuration
HUMAN_PLAYER_IMAGE_FILENAME = 'human-player-racecar.png'
COMPUTER_PLAYER_IMAGE_FILENAME = 'computer-player-racecar.png'
DELTA_X_LEFT_CONSTANT = -5
DELTA_X_RIGHT_CONSTANT = 5
class Car:
def __init__(self, pos_x=0, pos_y=0, delta_x=DELTA_X_RIGHT_CONSTANT,
delta_y=0, human=False):
"""Initialise a car object.
Args:
pos_x (int): The position of the car on the x-plane.
pos_y (int): The position of the car on the y-plane.
delta_x (int): The amount by which to move the car on the x-plane.
delta_y (int): The amount by which to move the car on the y-plane.
human (bool): Whether the car is a human player or not.
"""
self.pos_x = pos_x
self.pos_y = pos_y
self.delta_x = delta_x
self.delta_y = delta_y
self.human = human
self.image = None
def load_transform_image(self):
"""Load the car image from the filesystem."""
self.image = pygame.image.load(
HUMAN_PLAYER_IMAGE_FILENAME).convert() if self.human else \
pygame.image.load(COMPUTER_PLAYER_IMAGE_FILENAME).convert()
self.image.set_colorkey(BLACK)
self.image = pygame.transform.scale(
self.image, HUMAN_PLAYER_IMAGE_SIZE) if self.human else \
pygame.transform.scale(self.image, COMPUTER_PLAYER_IMAGE_SIZE)
def render_image(self):
"""Render the car image on the display."""
screen.blit(self.image, [self.pos_x, self.pos_y])
def turn_left_right(self):
"""Move the car on the x-plane by delta x."""
self.pos_x += self.delta_x
def move_up_down(self):
"""Move the car on the y-plane by delta y."""
self.pos_y += self.delta_y
3.5. Human Player Instance
Next we define a function called create_human_player()
which creates an instance of our Car
class that represents the human-controlled car object. We define the initial position of the human-controlled car object as in the middle relative to the x-axis and the width of the human-controlled car image, and at the bottom relative to the y-axis minus the height of the human-controlled car image and minus a further 50px buffer. As this car object is human-controlled, delta_x
is set to 0 as it is up to the human player to update delta_x
based on key-down press events of the arrow keys on the keyboard, with a press of the right arrow assigning a positive value to delta_x
, and a press of the left arrow assigning a negative value to delta_x
. Finally delta_y
is set to 0 as the human controlled car will only be able to move relative to the x-axis.
# Game Configuration
SCREEN_SIZE = (600, 800)
HUMAN_PLAYER_IMAGE_SIZE = (72, 168)
def create_human_player():
"""Create a car object that is controlled by the human player."""
# Create the human player car object
human_player_car = Car(
pos_x=int(round((SCREEN_SIZE[0]/2) - (HUMAN_PLAYER_IMAGE_SIZE[0]/2))),
pos_y=SCREEN_SIZE[1] - HUMAN_PLAYER_IMAGE_SIZE[1] - 50,
delta_x=0,
delta_y=0,
human=True)
# Transform and scale the size of the player car image to fit the screen
human_player_car.load_transform_image()
return human_player_car
3.6. Computer Player Instances
Next we define a function called create_computer_players()
which creates multiple instances of our Car
class that each represents a computer-controlled car object, where the number of instances is defined by a given constant value (in our case there will be 3 computer-controlled car objects). The position of each computer-controlled car object relative to the x-axis is defined randomly within the range x=0 and the width of the display minus the width of the computer-controlled car image. The position of each computer-controlled car object relative to the y-axis is also defined randomly within the range y=-125 and y=-25, which means that their start position is not in the visible subset of our two-dimensional space as defined by the dimensions of the display.
Since these are computer-controlled cars, they will not be able to move along the x-axis and their movement will be restricted to moving in the positive direction along the y-axis. Therefore, the value assigned to delta_x
is 0, and the value assigned to delta_y
is a random number between 2 and 3, meaning that they will move in the positive direction along the y-axis at a slow speed to begin with. Finally, each computer-controlled car object that is created is added to a Python list of computer-controlled car objects, as follows:
# Game Configuration
SCREEN_SIZE = (600, 800)
COMPUTER_PLAYER_IMAGE_SIZE = (72, 168)
COMPUTER_PLAYER_COUNT = 3
# Initialise a list to store the computer players
computer_players = []
def create_computer_players():
"""Create a list of car objects that act as obstacles."""
# Create computer car object obstacles
for n in range(COMPUTER_PLAYER_COUNT):
# Randomly initialise the initial (x, y) co-ordinate for this computer
init_x = random.randrange(
0, SCREEN_SIZE[0] - COMPUTER_PLAYER_IMAGE_SIZE[0])
init_y = random.randrange(-125, -25)
# Randomise the rate of change in the y-plane, starting with slow speeds
init_delta_y = random.randint(2, 3)
# Create a new computer player
global computer_players
computer_player_car = Car(
pos_x=init_x,
pos_y=init_y,
delta_x=0,
delta_y=init_delta_y,
human=False)
# Transform and scale the size of the computer player car image
computer_player_car.load_transform_image()
# Add the new computer player to the list of computer players
computer_players.append(computer_player_car)
3.7. Overtaking
Next we define a function called computers_overtaken()
which checks to see whether a computer-controlled car object has moved out of the subset of our two-dimensional space that is visible to the user as defined by the dimensions of the display. If a computer-controlled car object has exceeded the visible limit relative to the positive direction along the y-axis, this means that the human-controlled car object has 'overtaken' this computer-controlled car object without colliding with it, in which case we increment the score counter.
If this happens, we 'reset' the computer-controlled car object by resetting its x and y co-ordinates using a similar logic found in the create_computer_players()
function so that its new position relative to the y-axis is a random number in the range -125 and -25. The main difference now however is the calculation behind the new value assigned to delta_y
for this resetted computer-controlled car object. This calculation takes into account the current total score that the human player has accumulated, where the value assigned to delta_y
increases in proportion with the total score. This has the effect of increasing the speed at which the resetted computer-controlled object moves in the positive direction along the y-axis, thereby increasing the difficulty of the game the longer the player goes without colliding with a computer-controlled car or the screen boundaries.
# Game Configuration
SCREEN_SIZE = (600, 800)
COMPUTER_PLAYER_COUNT = 3
OVERTAKE_COMPUTER_SCORE_INCREMENT = 10
# Keep score
score = 0
def computers_overtaken():
"""Check whether a computer car object has moved out of visibility on
the y-plane. If so, increment the player score and recycle the computer car
object, gradually increasing its delta-y speed along the y-plane in
proportion with the increasing player score."""
# Check whether computer players have moved out of the visible y-plane
global score, speed_increment
for n in range(COMPUTER_PLAYER_COUNT):
# Render the current computer player and increment its y-plane position
computer_players[n].render_image()
computer_players[n].pos_y += computer_players[n].delta_y
# Test whether the current computer player has moved out of visibility
if computer_players[n].pos_y > SCREEN_SIZE[1]:
# Increment the score
score += OVERTAKE_COMPUTER_SCORE_INCREMENT
# Reset the computer player
computer_players[n].pos_x = random.randrange(
0, SCREEN_SIZE[0] - COMPUTER_PLAYER_IMAGE_SIZE[0])
computer_players[n].pos_y = random.randrange(-125, -25)
# Incrementally increase the speed of the computer vertical movement
# Increase the speed every time the score increments by another 100
speed_increment = max(2, math.floor(score / 100) + 1)
computer_players[n].delta_y = random.randint(
speed_increment, speed_increment + 1)
3.8. Collisions
Next we define two functions called collision_with_screen_boundaries()
and collision_with_computer()
that check whether the human-controlled car object has 'collided' with either the screen boundaries or a computer-controlled car object. Since the human-controlled car can only move relative to the x-axis, collision_with_screen_boundaries()
checks to see whether the current position of the human-controlled car either exceeds the width of the screen minus the width of the human-controlled car image, or is less than zero. If any of these conditions are true, then the human-controlled car is deemed to have 'collided' with the screen boundaries, in which case a boolean value of True
is returned which is subsequently used to break out of the indefinite game loop and to call the game_over()
function.
# Game Configuration
SCREEN_SIZE = (600, 800)
HUMAN_PLAYER_IMAGE_SIZE = (72, 168)
def collision_with_screen_boundaries():
"""Check whether the human car object has exceeded the screen boundaries
along the x-plane."""
# Check whether the position of the player exceeds the screen boundaries
if human_player.pos_x > SCREEN_SIZE[0] - HUMAN_PLAYER_IMAGE_SIZE[0] or \
human_player.pos_x < 0:
return True
return False
Similarly, the collision_with_computer()
function checks to see whether the human-controlled car object has 'collided' with one of the computer-controlled car objects. It does this by testing whether the current position of the human-controlled car relative to the x-axis falls between the current position of a computer-controlled car relative to the x-axis plus the width of the computer controlled car image. Similarly it tests whether the current position of the human-controlled car relative to the y-axis falls between the current position of a computer-controlled car relative to the y-axis plus the height of the computer-controlled car image. If both of these conditions are true, then the human-controlled car is deemed to have 'collided' with the computer-controlled car, in which case a boolean value of True
is returned which is subsequently used to break out of the indefinite game loop and to call the game_over()
function.
# Game Configuration
HUMAN_PLAYER_IMAGE_SIZE = (72, 168)
COMPUTER_PLAYER_IMAGE_SIZE = (72, 168)
COMPUTER_PLAYER_COUNT = 3
def collision_with_computer():
"""Check whether the human car object has collided with one of the
computer car objects."""
# Check whether the player has collided with a computer player
for n in range(COMPUTER_PLAYER_COUNT):
# Get the current (x, y) position of the current computer player
computer_pos_x = computer_players[n].pos_x
computer_pos_y = computer_players[n].pos_y
if (human_player.pos_x + HUMAN_PLAYER_IMAGE_SIZE[0] > computer_pos_x) \
and (human_player.pos_x < computer_pos_x +
COMPUTER_PLAYER_IMAGE_SIZE[0]) \
and (human_player.pos_y < computer_pos_y +
COMPUTER_PLAYER_IMAGE_SIZE[1]) \
and (human_player.pos_y +
HUMAN_PLAYER_IMAGE_SIZE[1] > computer_pos_y):
return True
return False
3.9. Game Over
Next we define a function called game_over()
that is called if a collision event is detected. This function renders a 'game over' message onto the display and then pauses for two seconds before resetting the global game paramters (such as the score counter, the list of computer-controlled cars, and the collision detected boolean) and invoking a new indefinite game loop, as follows:
def game_over():
"""If a collision event has occurred, render a game over message, then reset
the game parameters before starting a new indefinite game loop after
a brief pause."""
# Display the game over message along with the score
global score, speed_increment
font = pygame.font.Font(MESSAGE_FONT, MESSAGE_FONT_SIZE)
text = font.render(MESSAGE_GAME_OVER + str(score), True, BLACK)
text_rectangle = text.get_rect()
text_rectangle.center = ((SCREEN_SIZE[0] / 2), (SCREEN_SIZE[1] / 2))
screen.blit(text, text_rectangle)
pygame.display.update()
# Pause the application before continuing with a new game loop
time.sleep(2)
# Reset the game parameters
global clock, human_player, computer_players, collision_event_detected
clock = pygame.time.Clock()
human_player = create_human_player()
computer_players = []
create_computer_players()
collision_event_detected = False
score = 0
speed_increment = 2
# Start a new game via the indefinite game loop
indefinite_game_loop()
3.10. Indefinite Game Loop
Finally, and in order to continuously listen for relevant events and invoke the relevant response functions as defined above, we define the logic and behaviour of our FormulaPy game in a function called indefinite_game_loop()
and then within a while
loop. This while
loop will continue indefinitely until either the user closes the window (pygame.QUIT
event), or a collision event is detected in which case the game_over()
function is invoked. While no collision event is detected, the logic defined in the while
loop will listen for left and right arrow key press events which have the effect of updating the delta_x
attribute of the human-controlled car object. It will then render the human-controlled car image at the current position in our two-dimensional space and invoke the turn_left_right()
method to update the position of the human-controlled car image relative to the x-axis. Finally it will then check for overtake and collision events. If overtake events are detected, then the score will increase and the speed at which the computer-controlled car objects move in the positive direction along the y-axis will increase in proportion with the score. If a collision event is detected, then the game_over()
function is invoked. If no collision event is detected, then the contents of the entire display is updated via pygame.display.update()
after which this end-to-end logic within the while
loop will repeat indefinitely at a framerate determined by the clock.tick()
method, as follows:
def indefinite_game_loop():
"""FormulaPy game events and subsequent display rendering actions."""
# ----- FORMULAPY GAME LOOP -----
while not request_window_close:
# ----- EVENT DETECTION LOOP -----
for event in pygame.event.get():
# Close Window Event
if event.type == pygame.QUIT:
pygame.quit()
quit()
# Keyboard Key Down Event
if event.type == pygame.KEYDOWN:
# Left Key
if event.key == pygame.K_LEFT:
human_player.delta_x = DELTA_X_LEFT_CONSTANT
# Right Key
if event.key == pygame.K_RIGHT:
human_player.delta_x = DELTA_X_RIGHT_CONSTANT
# Keyboard Key Up Event
if event.type == pygame.KEYUP:
# Left or Right Key
if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT:
human_player.delta_x = 0
# ----- UPDATE DISPLAY -----
# Fill the display with a white background
screen.fill(GREY)
# Whilst there is no collision event
global collision_event_detected
if not collision_event_detected:
# Render the road stripe markings
render_road_stripe_marks()
# Move the road stripe markings
move_road_stripe_marks()
# Render the player car object
human_player.render_image()
# Move the human player car object as a result of key down events
# See event detection loop - keyboard key down events
human_player.turn_left_right()
# Check whether computer players have been overtaken i.e. whether
# a computer has moved out of the visible y-plane. If so,
# increment the score and reset the computer player
computers_overtaken()
# Check for a collision event with the screen boundaries
if collision_with_screen_boundaries():
collision_event_detected = True
# Check for a collision event with a computer player
if collision_with_computer():
collision_event_detected = True
# Collision event detected
else:
# Display the game over message and wait before starting a new game
game_over()
# Update the contents of the entire display
pygame.display.update()
clock.tick(60)
3.11. Source Code
Our FormulaPy car racing game is now complete! For ease of reference, the final and full source code for our FormulaPy car racing game is provided below:
#!/usr/bin/env python3
"""Car racing game written in Python.
This module implements a simple car racing game in Python using object
oriented programming principles and the Pygame engine. The aim of the game is
to overtake other cars without colliding with them. If you do collide with
either the other cars or the screen boundary, then it is game over.
Attribution:
Splash Screen Image:
<a href='https://www.freepik.com/vectors/background'>
Background vector created by macrovector - www.freepik.com
</a>
Human Player Car Image:
<a href="https://www.freepik.com/vectors/car">
Car vector created by freepik - www.freepik.com
</a>
Computer Player Car Image:
https://pixabay.com/vectors/car-racing-speed-auto-green-312571/
"""
import math
import pygame
import random
import time
# Game Configuration
SCREEN_SIZE = (600, 800)
SCREEN_DISPLAY_CAPTION = 'FormulaPy'
SPLASH_SCREEN_TIME = 5
SPLASH_SCREEN_IMAGE_FILENAME = 'splash-screen.jpg'
HUMAN_PLAYER_IMAGE_FILENAME = 'human-player-racecar.png'
COMPUTER_PLAYER_IMAGE_FILENAME = 'computer-player-racecar.png'
HUMAN_PLAYER_IMAGE_SIZE = (72, 168)
COMPUTER_PLAYER_IMAGE_SIZE = (72, 168)
COMPUTER_PLAYER_COUNT = 3
ROAD_STRIPE_COUNT = 15
ROAD_STRIPE_WIDTH = 15
ROAD_STRIPE_HEIGHT = 75
ROAD_STRIPE_SPACING = 25
ROAD_STRIPE_Y_DELTA_CONSTANT = 5
MESSAGE_FONT = 'freesansbold.ttf'
MESSAGE_FONT_SIZE = 25
MESSAGE_GAME_OVER = 'You Crashed! Your score was: '
CLOCK_FPS = 60
DELTA_X_LEFT_CONSTANT = -5
DELTA_X_RIGHT_CONSTANT = 5
OVERTAKE_COMPUTER_SCORE_INCREMENT = 10
# Colors
BLACK = (0, 0, 0)
GREY = (211, 211, 211)
WHITE = (255, 255, 255)
class Car:
def __init__(self, pos_x=0, pos_y=0, delta_x=DELTA_X_RIGHT_CONSTANT,
delta_y=0, human=False):
"""Initialise a car object.
Args:
pos_x (int): The position of the car on the x-plane.
pos_y (int): The position of the car on the y-plane.
delta_x (int): The amount by which to move the car on the x-plane.
delta_y (int): The amount by which to move the car on the y-plane.
human (bool): Whether the car is a human player or not.
"""
self.pos_x = pos_x
self.pos_y = pos_y
self.delta_x = delta_x
self.delta_y = delta_y
self.human = human
self.image = None
def load_transform_image(self):
"""Load the car image from the filesystem."""
self.image = pygame.image.load(
HUMAN_PLAYER_IMAGE_FILENAME).convert() if self.human else \
pygame.image.load(COMPUTER_PLAYER_IMAGE_FILENAME).convert()
self.image.set_colorkey(BLACK)
self.image = pygame.transform.scale(
self.image, HUMAN_PLAYER_IMAGE_SIZE) if self.human else \
pygame.transform.scale(self.image, COMPUTER_PLAYER_IMAGE_SIZE)
def render_image(self):
"""Render the car image on the display."""
screen.blit(self.image, [self.pos_x, self.pos_y])
def turn_left_right(self):
"""Move the car on the x-plane by delta x."""
self.pos_x += self.delta_x
def move_up_down(self):
"""Move the car on the y-plane by delta y."""
self.pos_y += self.delta_y
def initialize_screen():
"""Return the display with an initial splash screen."""
# Set the display caption
pygame.display.set_caption(SCREEN_DISPLAY_CAPTION)
# Instantiate a new display with the given screen size
display = pygame.display.set_mode(SCREEN_SIZE)
# Load and transform the splash screen image
splash_screen_image = pygame.image.load(
SPLASH_SCREEN_IMAGE_FILENAME).convert()
splash_screen_image.set_colorkey(BLACK)
splash_screen_image = pygame.transform.scale(
splash_screen_image, SCREEN_SIZE)
# Display the splash screen for a defined period of time
display.blit(splash_screen_image, [0, 0])
pygame.display.flip()
time.sleep(SPLASH_SCREEN_TIME)
return display
def create_human_player():
"""Create a car object that is controlled by the human player."""
# Create the human player car object
human_player_car = Car(
pos_x=int(round((SCREEN_SIZE[0]/2) - (HUMAN_PLAYER_IMAGE_SIZE[0]/2))),
pos_y=SCREEN_SIZE[1] - HUMAN_PLAYER_IMAGE_SIZE[1] - 50,
delta_x=0,
delta_y=0,
human=True)
# Transform and scale the size of the player car image to fit the screen
human_player_car.load_transform_image()
return human_player_car
def create_computer_players():
"""Create a list of car objects that act as obstacles."""
# Create computer car object obstacles
for n in range(COMPUTER_PLAYER_COUNT):
# Randomly initialise the initial (x, y) co-ordinate for this computer
init_x = random.randrange(
0, SCREEN_SIZE[0] - COMPUTER_PLAYER_IMAGE_SIZE[0])
init_y = random.randrange(-125, -25)
# Randomise the rate of change in the y-plane, starting with slow speeds
init_delta_y = random.randint(2, 3)
# Create a new computer player
global computer_players
computer_player_car = Car(
pos_x=init_x,
pos_y=init_y,
delta_x=0,
delta_y=init_delta_y,
human=False)
# Transform and scale the size of the computer player car image
computer_player_car.load_transform_image()
# Add the new computer player to the list of computer players
computer_players.append(computer_player_car)
def create_road_stripe_marks():
"""Create a list of road stripe marking co-ordinates."""
# Initialise a list to store the road strip markings
road_stripe_marks = []
# Create the road stripe markings and add them to the list
road_stripe_pos_x = int(round((SCREEN_SIZE[0]/2) - (ROAD_STRIPE_WIDTH/2)))
road_stripe_pos_y = -10
for n in range(ROAD_STRIPE_COUNT):
road_stripe_marks.append([road_stripe_pos_x, road_stripe_pos_y])
road_stripe_pos_y += ROAD_STRIPE_HEIGHT + ROAD_STRIPE_SPACING
return road_stripe_marks
def render_road_stripe_marks():
"""Render the list of road stripe markings as rectangles."""
# Render the road stripe marks
global road_stripe_markings
for n in range(ROAD_STRIPE_COUNT):
pygame.draw.rect(screen, WHITE, [
road_stripe_markings[n][0], road_stripe_markings[n][1],
ROAD_STRIPE_WIDTH, ROAD_STRIPE_HEIGHT])
def move_road_stripe_marks():
"""Move the road stripe marking rectangles on the y-plane."""
# Move the road stripe marks
global road_stripe_markings
for n in range(ROAD_STRIPE_COUNT):
road_stripe_markings[n][1] += ROAD_STRIPE_Y_DELTA_CONSTANT
if road_stripe_markings[n][1] > SCREEN_SIZE[1]:
road_stripe_markings[n][1] = -30 - ROAD_STRIPE_HEIGHT
def computers_overtaken():
"""Check whether a computer car object has moved out of visibility on
the y-plane. If so, increment the player score and recycle the computer car
object, gradually increasing its delta-y speed along the y-plane in
proportion with the increasing player score."""
# Check whether computer players have moved out of the visible y-plane
global score, speed_increment
for n in range(COMPUTER_PLAYER_COUNT):
# Render the current computer player and increment its y-plane position
computer_players[n].render_image()
computer_players[n].pos_y += computer_players[n].delta_y
# Test whether the current computer player has moved out of visibility
if computer_players[n].pos_y > SCREEN_SIZE[1]:
# Increment the score
score += OVERTAKE_COMPUTER_SCORE_INCREMENT
# Reset the computer player
computer_players[n].pos_x = random.randrange(
0, SCREEN_SIZE[0] - COMPUTER_PLAYER_IMAGE_SIZE[0])
computer_players[n].pos_y = random.randrange(-125, -25)
# Incrementally increase the speed of the computer vertical movement
# Increase the speed every time the score increments by another 100
speed_increment = max(2, math.floor(score / 100) + 1)
computer_players[n].delta_y = random.randint(
speed_increment, speed_increment + 1)
def collision_with_screen_boundaries():
"""Check whether the human car object has exceeded the screen boundaries
along the x-plane."""
# Check whether the position of the player exceeds the screen boundaries
if human_player.pos_x > SCREEN_SIZE[0] - HUMAN_PLAYER_IMAGE_SIZE[0] or \
human_player.pos_x < 0:
return True
return False
def collision_with_computer():
"""Check whether the human car object has collided with one of the
computer car objects."""
# Check whether the player has collided with a computer player
for n in range(COMPUTER_PLAYER_COUNT):
# Get the current (x, y) position of the current computer player
computer_pos_x = computer_players[n].pos_x
computer_pos_y = computer_players[n].pos_y
if (human_player.pos_x + HUMAN_PLAYER_IMAGE_SIZE[0] > computer_pos_x) \
and (human_player.pos_x < computer_pos_x +
COMPUTER_PLAYER_IMAGE_SIZE[0]) \
and (human_player.pos_y < computer_pos_y +
COMPUTER_PLAYER_IMAGE_SIZE[1]) \
and (human_player.pos_y +
HUMAN_PLAYER_IMAGE_SIZE[1] > computer_pos_y):
return True
return False
def game_over():
"""If a collision event has occurred, render a game over message, then reset
the game parameters before starting a new indefinite game loop after
a brief pause."""
# Display the game over message along with the score
global score, speed_increment
font = pygame.font.Font(MESSAGE_FONT, MESSAGE_FONT_SIZE)
text = font.render(MESSAGE_GAME_OVER + str(score), True, BLACK)
text_rectangle = text.get_rect()
text_rectangle.center = ((SCREEN_SIZE[0] / 2), (SCREEN_SIZE[1] / 2))
screen.blit(text, text_rectangle)
pygame.display.update()
# Pause the application before continuing with a new game loop
time.sleep(2)
# Reset the game parameters
global clock, human_player, computer_players, collision_event_detected
clock = pygame.time.Clock()
human_player = create_human_player()
computer_players = []
create_computer_players()
collision_event_detected = False
score = 0
speed_increment = 2
# Start a new game via the indefinite game loop
indefinite_game_loop()
def indefinite_game_loop():
"""FormulaPy game events and subsequent display rendering actions."""
# ----- FORMULAPY GAME LOOP -----
while not request_window_close:
# ----- EVENT DETECTION LOOP -----
for event in pygame.event.get():
# Close Window Event
if event.type == pygame.QUIT:
pygame.quit()
quit()
# Keyboard Key Down Event
if event.type == pygame.KEYDOWN:
# Left Key
if event.key == pygame.K_LEFT:
human_player.delta_x = DELTA_X_LEFT_CONSTANT
# Right Key
if event.key == pygame.K_RIGHT:
human_player.delta_x = DELTA_X_RIGHT_CONSTANT
# Keyboard Key Up Event
if event.type == pygame.KEYUP:
# Left or Right Key
if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT:
human_player.delta_x = 0
# ----- UPDATE DISPLAY -----
# Fill the display with a white background
screen.fill(GREY)
# Whilst there is no collision event
global collision_event_detected
if not collision_event_detected:
# Render the road stripe markings
render_road_stripe_marks()
# Move the road stripe markings
move_road_stripe_marks()
# Render the player car object
human_player.render_image()
# Move the human player car object as a result of key down events
# See event detection loop - keyboard key down events
human_player.turn_left_right()
# Check whether computer players have been overtaken i.e. whether
# a computer has moved out of the visible y-plane. If so,
# increment the score and reset the computer player
computers_overtaken()
# Check for a collision event with the screen boundaries
if collision_with_screen_boundaries():
collision_event_detected = True
# Check for a collision event with a computer player
if collision_with_computer():
collision_event_detected = True
# Collision event detected
else:
# Display the game over message and wait before starting a new game
game_over()
# Update the contents of the entire display
pygame.display.update()
clock.tick(60)
# Initialise the imported PyGame modules
pygame.init()
# Start the screen
screen = initialize_screen()
# Maintain the screen until the user closes the window
request_window_close = False
# Initialise a clock to track time
clock = pygame.time.Clock()
# Create the human player
human_player = create_human_player()
# Initialise a list to store the computer players
computer_players = []
create_computer_players()
# Create road stripe marks
road_stripe_markings = create_road_stripe_marks()
# Maintain the game loop until a collision event
collision_event_detected = False
# Keep score
score = 0
# Maintain a game speed incrementer
speed_increment = 2
# Start the indefinite game loop
indefinite_game_loop()
Summary
In this module we have consolidated and applied our knowledge of object oriented programming (OOP) principles together with what we have learnt from the other modules throughout this course thus far to create a car racing game in Python. We also have a high-level understanding of the Pygame API with which we can create video games and multimedia programs in Python.
Homework
- Pygame - FormulaPy
Extend our FormulaPy car racing game such that the human-controlled car object has the ability to move along the y-axis via the up and down arrows on the keyboard respectively. Once the human-controlled car object meets the limit of the visible two-dimensional space as defined by the display i.e. y=0 or y=screen height, then it cannot move any further in that direction along the y-axis, but no collision event is generated.
What's Next
In the next module, we will explore error handling Python, including defining and raising our own exceptions, handling files and basic I/O operations.
Curriculum
- 1. Getting Started in Python
- 2. Control and Evaluations Part 1
- 3. Control and Evaluations Part 2
- 4. Data Aggregates Part 1
- 5. Data Aggregates Part 2
- 6. Functions and Modules Part 1
- 7. Functions and Modules Part 2
- 8. Classes and Objects Part 1
- 9. Classes and Objects Part 2
- 10. IO and Exceptions
- 11. PCAP Practice Exam