Homework 11

Due: Tuesday, May 13 at 11:59 pm

Submission: On Courseworks

Instructions

Please read the instructions carefully. An incorrectly formatted submission may not be counted.

This assignment starts from skeleton code - available in this zip.

Please include comments in your code explaining your chosen features and detailing your thought process where appropriate.

The code extracts to a folder called uni-hw11. Rename this to use your uni (so for me this would be tkp2108-hw10). Then compress this folder into a .zip file. For most operating systems, you should be able to right click and have a “compress” option in the context menu. This should create a file called tkp2108-hw11.zip (with your uni). Submit this on courseworks.

PyGame

In this final project, we will build a video game using the Pygame framework.

What is Pygame?

Pygame is an open source game development framework. It makes it easy to render shapes and images, interact with windows and screens, manage timing and user events, and more.

It isn’t as full-featured as a large scale professional game engines like Unity or Unreal Engine, but this is a good thing for us because it means the framework is easily approachable for students in an introductory class like ours.

Like all of the libraries we’ve been using, Pygame has an extensive website including a large number of tutorials and examples which you are welcome to read and reference in your submission.

Installation

Unlike the libraries used in other homeworks, Pygame does not come with Anaconda by default. However, Anaconda makes it easy to install additional libraries.

First, open Anaconda Navigator and navigate to “Environments”.

Next, choose “all” available libraries.

Search Pygame in the search box on the top right.

Check the box next to Pygame and hit “apply”.

After this resolved, click “apply” to apply the changes.

Background

In this homework, we will make our own version of the Google Dinosaur Game.

Before we look at the dino.py file in the skeleton code, let’s start from some simpler code.

import pygame

def main():
    # pygame setup
    pygame.init()
    screen = pygame.display.set_mode((768, 432))
    clock = pygame.time.Clock()
    running = True

    # MAIN GAME LOOP
    while running:
        # poll for events
        # pygame.QUIT event means the user clicked X to close your window
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        # fill the screen with a color to wipe away anything from last frame
        screen.fill("purple")

        # flip() the display to put your work on screen
        pygame.display.flip()

        # limits Frames Per second displayed to 60
        clock.tick(60)

    # Close pygame and clean up resources
    pygame.quit()


if __name__ == "__main__":
    main()

If you run this code, your program should display a purple screen. Lets step through every line

We start off by importing the library and running pygame.init() to tell Pygame that we’re starting. We need a screen to display our game on, and set_mode lets us pick the size of the window. The clock will be used to make sure that the game behaves the same regardless of what computer we run the program on (some computers display faster or slower than other computers). We will later use the clock to ensure that those speeds do not effect our physics. Lastly running will be a boolean thats true or false depending upon if our game is running our not!

import pygame

# pygame setup
pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
running = True

These next lines of code are the “game loop”. Like previous homeworks where we repeatedly asked the user “do you want to play again”, this loop will continue until the game is over. Any user input events, rendering of images, updating of object state, etc, will occur within this main loop.

The first thing we do inside our loop is check for user events. User events include things like closing the window, typing on the keyboard, and clicking with the mouse. When the user closes the window, Pygame will send a pygame.QUIT event, in which case we want to exit the loop.

If a user didnt close the window, we set the color of the window to purple, and call flip to push all of our changes to the screen. Note that we don’t immediately update the screen, as this might too costly if we want to update a bunch of things simulataneously. Like our N-Body problem, we want to deal with all updates first, then when everything is ready to go, tell the screen to update with pygame.display.flip().

We call clock.tick(60) every time in the game loop to insure that every second we pass through this loop 60 times, or we update the screen 60 Frames per second. The clock also returns the time since the last update to the screen which we will use later on.

while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # fill the screen with a color to wipe away anything from last frame
    screen.fill("purple")

    # flip() the display to put your work on screen
    pygame.display.flip()

    clock.tick(60)  # limits FPS to 60

The last line outside the game loop just closes the program and cleans up all the resources used by Pygame.

pygame.quit()

Now using our Python knowledge and some other library functions we can change the background to something cooler.

Surfaces & Changing up the Background

Let’s take a second to talk about how Pygame manages Surfaces, which are objects displayed on the screen. When you want to display something on the screen, you will load it using the pygame.image.load() function which will return a surface for you to display. The screen itself is also a type of surface, so to put your surface on the screen, you need to use the blit function which is short for Block Transfer.

Heres a picture to help you visualize how this works!

The following code will load a local png image the surface called my_local_surface and then copy the contents of that surface to the screen using blit at the location provided in the tuple (0,0).

my_local_surface = pygame.image.load("my_local_image.png")
screen.blit(my_local_surface, (0,0))

Coordinates

In Pygame the coordinate system for drawing to the screen starts in the top left corner of the display and increases in X and Y value as we move down and to the right.

This will also be important for our physics!.

Full Background

Using all of this information, we can now create a fancy background and floor for our game. In the assets folder of the skeleton code, you will find a bunch of images that we will be using to create the game. You are welcome to modify these for your submission, but you might need to change the size of your window to fit your backgrounds.

For our game, there are 5 background images that will produce a sense of depth. In the following code, we setup some helper functions to load our images and scale them 2x to fit our window. These images are loaded and stored in a list of surfaces before our main loop. We will also load up an image to represent the floor in the same way. During the game loop, after we fill the screen with purple, we will draw the backgrounds and then the floors.

Pygame gives us a lot of helpful functionality like screen.get_height(), which we can use to return the height of the screen and the correct coordinates to draw the floor. The only other detail is when we call convert_alpha on the surface, which ensures that any transparent parts of the original image remain transparent.

# load an image with a given path and scale it the given amount and return it
def load_image(path, scale):
    image = pygame.image.load(path).convert_alpha()
    image = pygame.transform.scale(image, (image.get_width() * scale, image.get_height() * scale))
    return image

# setup the background images and return an array with all the background images
# note: the order the backgrounds are loaded in is the order they are loaded in, backgrounds with the most depth first!!!
def setup_backgrounds():
    background_1 = load_image('assets/background/bg1.png', 2)
    background_2 = load_image('assets/background/bg2.png', 2)
    background_3 = load_image('assets/background/bg3.png', 2)
    background_4 = load_image('assets/background/bg4.png', 2)
    background_5 = load_image('assets/background/bg5.png', 2)

    return [background_1, background_2, background_3, background_4, background_5]

# setup the floor
def setup_floor():
    floor = load_image('assets/background/floor.png', 2)
    return floor

# draw the backgrounds in the given array to the screen in a loop
def draw_backgrounds(screen, backgrounds):
    for background in backgrounds:
        screen.blit(background, (0,0))

# draw the floor to the screen at the bottom of the screen
def draw_floor(screen, floor):
    screen.blit(floor, (0,screen.get_height()-floor.get_height()))

After adding this code, we call setup_backgrounds() and setup_floor() before the game loop. Inside the game loop, we call draw_backgrounds() and draw_floor(). When we run the code, we will now see the much prettier background.

Setting up the Dinosaur

Now that we have a fancy backgrounds, lets add our character into our game. The character sprite we are going to be using is in the same assets folder as the backgrounds, but now in the dino subfolder.

Because the dinosaur will have some state and functionality associated it, we will leverage the concept of encapsulation discussed in class and used in Homework 9 and build a class around it. We will include functionality like drawing it, updating the position and velocity, and changing the sprite for when we are running or jumping. Here is the skeleton code for this.

class Dino:
    # intialize the surfaces for each of the states - run 1 and run 2 are used to make it look like the dinosaur is running
    def setup_dino(self):
        self.dino_idle = load_image('assets/dino/dino_idle.png', 4)
        self.dino_jump = load_image('assets/dino/dino_jump.png', 4)
        self.dino_run1 = load_image('assets/dino/dino_run1.png', 4)
        self.dino_run2 = load_image('assets/dino/dino_run2.png', 4)

    # return the current surface of the dinosaur depending upon the state
    def current_sprite(self):
        if self.state == "idle":
            return self.dino_idle
        elif self.state == "jump":
            return self.dino_jump
        elif self.state == "run": # we have 2 sprites that make it look the the dinosaur is running with his right and left leg that change every frame!
            if self.running_frame == 1:
                self.running_frame = 2
                return self.dino_run1
            else:
                self.running_frame = 1
                return self.dino_run2

    # get the height of the dinosaur surface
    def get_height(self):
        return self.dino_idle.get_height()

    # get the width of the dinosaur surface
    def get_width(self):
        return self.dino_idle.get_width()

    # draw the dinosaur to the screen
    def draw(self, screen):
        screen.blit(self.current_sprite(), (self.x, self.y))

     # update the position of the dinosaur
    def set_position(self, x, y, is_floor=False):
        self.x = x
        self.y = y

        # if the y we are passing in is the same as the floor, we can set this bool to true and update value
        if is_floor:
            self.floor_y = y

    # call this if jump button was pressed - we can only jump if we are not falling
      def jumped(self):
        if not self.falling:
            self.jump = True
            self.state = 'jump'

    # call this function every frame to update the position and velocity of the dinosaur based on inputs
    def physics(self):
        pass

    # initialize all of the instance variables of the dinosaur
    def __init__(self):
        self.setup_dino()
        self.x = 0
        self.y = 0
        self.floor_y = 0
        self.state = "run"
        self.running_frame = 1
        self.jump = False
        self.falling = False
        self.vel_y = 0

Theres a lot of code here so lets break it down. In our __init__ function, we setup our dinosaur by loading the surfaces for the different states our dinosaur will be in (running, jumping, idle). Then we setup our x, y, and floor_y variables, set our intitial state to running and the animation frame to 1, our jump and falling booleans to false, and our y_velocity to 0.

We also have a bunch of helper functions:

There are also a few global configuration variables that are used to control the dinosaurs initial velocity and gravity (just like our projectile examples from class) and the dinosaur’s offset from the left side of the screen.

DINO_JUMP_VEL = -40 # the dinosaurs jumping height
GRAVITY = 4
DINO_X_OFFSET = 50 # offset from the left edge of the screen

Now that the Dinosaur class is setup, we can create an instance of the dinosaur object before the main game loop and set its position to the correct height and left offset like so:

dino = Dino()
dino.set_position(DINO_X_OFFSET, screen.get_height() - floor.get_height() - dino.get_height(), True)

If we draw out this math using the coordinate system above, we can see that it makes sense. The second argument is True because we start in the resting position of the dinosaur (e.g. not somewhere in the middle of the jump).

Last thing we need to do to get the dinosaur setup is to connect pressing of the space key to the dinosaur’s jumping physics, and draw the dinosaur on the screen.

# MAIN GAME LOOP
while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
            dino.jumped()

    # fill the screen with a color to wipe away anything from last frame
    screen.fill("purple")

    # draw background and floor
    draw_backgrounds(screen, backgrounds)
    draw_floor(screen, floor)

    # Draw the dino to the screen
    dino.physics()
    dino.draw(screen)

    # flip() the display to put your work on screen
    pygame.display.flip()

    # limits Frames Per second displayed to 60
    clock.tick(60)

Now when we run all this code (available in the skeleton code), we should see the dinosaur running on the screen. When you press the space bar, nothing happens. Thats because we need to implement the dinosaur’s event physics.

If we took a naive approach, we might end up with some code like this:

if self.jump is True:
    self.vel_y += DINO_JUMP_VEL
    self.falling = True
    self.jump = False

if self.falling:
    self.vel_y += GRAVITY

new_y_position = self.y + self.vel_y
self.set_position(self.x, new_y_position, False)

However, you’ll see something wrong - we never check if we hit the ground. Luckily, we setup some functionality beforehand to allow us to make a check for this.

if new_y_position >= self.floor_y:
    new_y_position = self.floor_y
    self.falling = False
    self.vel_y = 0
    self.state = 'run'

We can simply check if our new position is going to be greater than the Y position of the floor - if it is, we know that we are about to hit the floor and we can just set our Y position to the floor, set our falling variable to False and our velocity to 0. We can also change our state back to run so that we start the running animation again.

Try messing around with global configuration variables and set your own jump velocity and gravity to make it feel right.

Obstacles

The last feature we are going to add is obstacles and collision. This is going to be the most complicated and important feature so take your time going through this.

Rects and Collision

In Pygame, we can check for collisions by using Rect objects. Rects are exactly what you think they are. They are just boxes with a x and y position of the top left corner and a width and a height. Rects can be used to check for collision using the rect.colliderect() function. If you pass in another Rect, it will determine if there is any overlap.

We can add a rectangle to the Dinosaur that has the same position as the dinosaur itself, and the same width and height as the surface. This will be our Dinosaur’s collision box. Then we can add Rectangles to our obstacles that we can check if are colliding with the player Dinosaur. If you are confused by how this works, this video by Coding with Russ explains it well.

Updating the Dinosaur

To add a Rect to our dinosaur, we can change 2 functions:

# intialize the surfaces for each of the states - run 1 and run 2 are used to make it look like the dinosaur is running
def setup_dino(self):
    self.dino_idle = load_image('dino-pygame/assets/dino/dino_idle.png', 4)
    self.dino_jump = load_image('dino-pygame/assets/dino/dino_jump.png', 4)
    self.dino_run1 = load_image('dino-pygame/assets/dino/dino_run1.png', 4)
    self.dino_run2 = load_image('dino-pygame/assets/dino/dino_run2.png', 4)

    # instantiate the collider rect with the default position and the width and height
    self.collider_rect = pygame.Rect( 0, 0, self.get_width(), self.get_height())

# return the dinos collider rect
def get_collider_rect(self):
    return self.collider_rect

# update the position of the dinosaur
def set_position(self, x, y, is_floor=False):
    self.x = x
    self.y = y

    # update the collider rect with the new position
    self.collider_rect.topleft = (x, y)

    # if the y we are passing in is the same as the floor, we can set this bool to true and update value
    if is_floor:
        self.floor_y = y

First we just create a collider_rect that is the same position as the default position (0,0) and has the same width and height as our dinosaur. Then we add a function to return that collider rect so that it can be accessed outside of the dinosaur. Lastly we need to make sure we update the position of the collider rect every time we move the dinosaur. If we don’t update the collider when the dinosaur moves, the hitbox is not going to align with the visual representation of the dinosaur.

Obstacle Class and Inheritance

Just like Homework 9, we are going to use object oriented programming to setup our Obstacle class and any other classes that want to inherit from it. In the future, if we want to add new obstacles it will make it easier to get started.

Let’s create our base class:

# base obstacle class
class Obstacle:
    def __init__(self):
        self.x = 0
        self.y = 0
        self.x_vel = 0

        # collider rect is pretty much a box around the object that represents where it can be hit
        self.collider_rect = pygame.Rect(0,0,1,1)

        # if a obstacle is active it is on the screen, otherwise its inactive and can be despawned
        self.active = True

    # update the positon of the obstacle and collider
    def set_position(self, x, y):
        self.x = x
        self.y = y
        self.collider_rect.topleft = (self.x, self.y)

    # return the collider rect
    def get_collider_rect(self):
        return self.collider_rect

    def draw(self, screen):
        # Should be overridden by subclasses
        pass

    def physics(self):
        # Should be overridden by subclasses
        pass

For our __init__ function we instantiate the variables we need like position, x-velocity (all obstacles will move from right to left), the obstacle’s collision rectangle, and whether or not the object is active. This last variable will be used to remove our objects when they move off the left side of the screen.

We also have some helper functions that look similar to the ones we saw in the Dinosaur.

We are now going to create a class that inherits from this Obstacle class called Stump, that will draw Stump objects to the screen using the asset in the assets folder from before.

# stump class - inherits from obstacle and uses the stump asset
class Stump(Obstacle):
    def setup_stump(self):
        self.stump_sprite = load_image('assets/obstacles/stump.png', 2)
        # setup the collider rect with the width and height of the stump
        self.collider_rect = pygame.Rect( 0, 0, self.get_width(), self.get_height())

    # get the height of the stump surface
    def get_height(self):
        return self.stump_sprite.get_height()

    # get the width of the stump surface
    def get_width(self):
        return self.stump_sprite.get_width()

    # draw to the screen
    def draw(self, screen):
        screen.blit(self.stump_sprite, (self.x, self.y))

    # set position and collider position
    def set_position(self, x, y):
       self.x = x
       self.y = y
       self.collider_rect.topleft = (x, y)

    # stump just moves left - also check if its off screen for setting inactive
    def physics(self):
        new_x_pos = self.x + STUMP_VEL
        self.set_position(new_x_pos, self.y)

        if new_x_pos < 0 - self.get_width():
            self.active = False

    # return the collider rect
    def get_collider_rect(self):
        return self.collider_rect

    # call obstacle init and setup the stump
    def __init__(self):
        super().__init__()
        self.setup_stump()

This looks pretty similar to what we have seen before, with the small change that we deactive the object when it passes offscreen inside the physics method.

We will also configure the stumps' velocities with a global variable.

# velocity the stump moves to the left
STUMP_VEL = -15

The Obstacle Handler

Since we will have the Dinosaur and some number of objects, it makes sense for us to make a manager class to deal with these objects and their interactions. This is a common pattern in game development, where there can often be many simultaneously interacting objects.

As before, we will use a new class to encapsulate all of this complexity.

# Obstacle handler class that spawns, despawns, and checks for collisions with obstacles
class ObstacleHandler():
    def __init__(self):
        self.obstacle_classes = [] # this is a list of the classes of obstacles it can spawn!
        self.obstacles = [] # list of the current existing obstacles
        self.time_to_next_obstacle = pygame.time.get_ticks() + random.randint(OBSTACLE_SPAWN_RATE[0], OBSTACLE_SPAWN_RATE[1]) # time until the next obstacle spawns

    # add an obstacle class to the list of possible obstacles
    def add_obstacle_class(self, obstacle_class):
        self.obstacle_classes.append(obstacle_class)

    # spawn an obstacle - called every frame, checks clock to see if we have passed time for next obstacle
    def spawn_obstacle(self, clock):
        if pygame.time.get_ticks() >= self.time_to_next_obstacle:
            # update the next obstacle timer
            self.time_to_next_obstacle = pygame.time.get_ticks() + random.randint(OBSTACLE_SPAWN_RATE[0], OBSTACLE_SPAWN_RATE[1])
            if len(self.obstacle_classes) > 0:
                new_obstacle_class = random.choice(self.obstacle_classes) # chose a new random obstacle
                new_obstacle = new_obstacle_class()

                # set its position to be on the floor - the + 5 is just to make sure it lines up nicely
                new_obstacle.set_position(SCREEN_WIDTH + new_obstacle.get_width(), FLOOR_HEIGHT - new_obstacle.get_height() + 5)
                self.obstacles.append(new_obstacle)

    # draw all the obstacles
    def draw_obstacles(self, screen):
        for obstacle in self.obstacles:
            obstacle.draw(screen)

    # update the physcis for all the obstacles
    def obstacles_physics(self):
        for obstacle in self.obstacles:
            obstacle.physics()

        # if a obstacle is off screen we set it to inactive - we remove all the ianctive obstacles from the list and they despawn
        self.obstacles = [o for o in self.obstacles if o.active]

    # check if an objects collider rect has collided with any of the obstacles collider rects
    def check_collisions(self, object_collider_rect):
        for obstacle in self.obstacles:
            # collide_rect returns true if a collision occurs between 2 rects
            if object_collider_rect.colliderect(obstacle.get_collider_rect()):
                return True

        return False

Let’s go through this step by step again.

The obstacle handler is going to have a list of all the classes that it can spawn. That list is the obstacle_classes list. We will add the Stump class to that list so that our obstacle handler can create Stump instances. Our obstacle handler will track these instances while they’re on the screen. We will also have a variable that decides when the next obstacle should be generated, using random.radint with a range of values representing milliseconds.

# the interval at which obstacles can spawn in ms
OBSTACLE_SPAWN_RATE = [500, 1500]

pygame.time.get_ticks() returns the current time in milliseconds and we can add some random amount of time to that to signify when we should next spawn an obstacle. For example, if we create the obstacle handler at time t = 0 and want to create obstacles every 500ms, we can add 500ms to 0 and wait until pygame.time.get_ticks() returns 500 to know that we have reached the time to spawn an obstacle.

The spawn_obstacle function shows how this is used. This function should be called every frame in the game loop and checks if we are ready to spawn a new obstacle. If so, it sets the next obstacle spawn time, checks if there are types of obstacles for us to pick from, uses random.choice (which we have also seen before) to pick an obstacle from the list, creates an object of that object class and then sets the position of the object given the screen width and floor height.

# DIMENSIONS OF THE SCREEN
SCREEN_WIDTH = 768
SCREEN_HEIGHT = 432

# the height of the floor on the screen
FLOOR_HEIGHT = 0

Since we don’t know the exact screen height, we use the global keyword in our main function to indicate our desire to edit the global variable FLOOR_HEIGHT. Without it, we’d be creating a new local variable with the same name instead of editing the existing one.

# global lets us edit globally created variables - we are editing the FLOOR_HEIGHT at the top of the file to be used when spawning objects
global FLOOR_HEIGHT
FLOOR_HEIGHT = screen.get_height()-floor.get_height()

Moving back to our obstacle handler, add_obstacle_class appends a class of an obstacle to the list of potential obstacle classes to spawn. The draw_obstacles function iterates through all the obstacles in our list and draws them on the screen given using the obstacle’s own draw function. The obstacles_physics function iterates through all the obstacles and calls their physics functions like draw_obstacles. Additionally, it checks if any of the objects in the list have been deactived and if so, it removes them from the list.

Our last function in our obstacle handler is check_collisions. This function takes in the collider Rect of another object (in our case, the dinosaur) and iterates through all the obstacles to check for collisions. If there are any collisions, this function will return true.

Back to the beginning

Lets go back to our main function add in the obstacle handler and stump:

# create the obstacle handler and add the stump as one of the classes that cna be instantiated
obstacle_handler = ObstacleHandler()
obstacle_handler.add_obstacle_class(Stump)

Now, we just have to integrate the obstacle handler’s functions. You can see how this is done in the skeleton code.

After the obstacles are being drawn, we can add the collision detection code:

if(obstacle_handler.check_collisions(dino.get_collider_rect())):
    print("Hit!")

In the skeleton code, we change this to end the main loop, thus terminating the game:

if(obstacle_handler.check_collisions(dino.get_collider_rect())):
    running = False

Now everytime you hit a stump, the game will stop.

Congrats!

That is the end of the background information! In addition to the Pygame website and tutorials, there are lots of other resources available. Here is a super helpful YouTube video which was partially used as inspiration.

The skeleton code here was written by our wonderful TA Jake Torres, and he has agreed to do a short tutorial video which will be posted in the coming days. You can keep up with his work on GitHub.

The Task

This final homework has a lot of background, but the code itself is only around 300 lines. We want to leverage the skills we’ve built in an unfamiliar setting, which goes to the heart of what it is to be an engineer.

For the final project, you have a few options ranging from easier to harder (in our opinion). You must choose at least two tasks, but you are welcome and encouraged to do more (and you can receive extra credit for doing so).

Please use comments at the top of the dino.py file to indicate which you’ve chosen.

Option 1: More obstacles

Add at least 2 more obstacles to the game. Ensure these obstacles are different sizes from the stump, and spawn at different frequencies.

A flying obstacle would be cool…

Hint: Make sure the game is not impossibly difficult!

Option 2: Track Score

Add a way to track score. You can use a function of time, number of cleared obstacles, etc. Overlay this score on the running game. When the player hits an obstacle, pause before exiting to allow the user to see their final score.

Hint: Use the Pygame.font object to display text on the screen: https://www.pygame.org/docs/ref/font.html https://www.geeksforgeeks.org/python-display-text-to-pygame-window/

Option 3: Menus

Most games have nice user menus for starting a new game, restarting after a previous game, and quitting. Add these to the game:

Option 4: Progressive difficulty

Increase the speed of the game as you get a higher score - Think about what makes the gamer faster and what functionality we setup in the tutorial that you can change to reflect this.

Hint: you have some options here:

Choose options that progressively increase the difficulty slowly as the user gets further along

Option 5: New Moves

Give the dinosaur new moves. Maybe a double-jump that charges over time. Maybe you’ve added some flying obstacles and now want to give the dinosaur a crouch or “slide” move.

Hint: Make sure any move that changes the dinosaur’s shape also changes its hit box!

Option 6: Leaderboard

Our game doesn’t maintain history, but it would be cool if it did. Borrowing some code from our personal finances app in Homework 4, and from Option 2 above, we could track score over multiple game runs and display it.

Hint: You can use an input box object to get input from the user after the main loop is done: https://stackoverflow.com/questions/46390231/how-can-i-create-a-text-input-box-with-pygame.

Option 7: Parallax Scrolling

Right now, the movement of our dinosaur is indicated by the obstacles. This is fine to start off, but pretty boring.

In real games, we tend to have parallax movement:

Notice how the elements in the foreground move faster than the elements in the background.

In our game we created 5 different layers for the background. Parallax requires you to move the backgrounds at different speeds, with the closer backgrounds moving faster than the ones further away. To implement this, you have to create a copy of every background and then move them every frame. Once the original is off screen, the copy should move onto the screen and the original should teleport behind the flipped version like a conveyor belt.

In this task, add parallax movement.

Hint: This is hard. Start with the foreground grass, if you can get this moving then the backgrounds won’t be that much harder.

Skeleton Code

Skeleton code and all assets