Course Module • Jillur Quddus

9. Classes and Objects Part 2

Introduction to Python

Classes and Objects Part 2

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.

FormulaPy splash screen
FormulaPy splash screen
FormulaPy in-game screenshot
FormulaPy in-game screenshot

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:

Pygame two dimensional space
Pygame two dimensional space

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 attributes pos_x and pos_y respectively.
  • turn_left_right() - increase (or decrease) the value assigned to pos_x by the current value assigned to delta_x. If delta_x is positive, then this has the effect of moving the object to the right along the x-axis. If delta_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 to pos_y by the current value assigned to delta_y. If delta_y is positive, then this has the effect of moving the object down along the y-axis. If delta_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

  1. 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.