Lab 6: Building Games with Pygame

Learning Goals

In this lab, you will build your own game. It is going to look something like shown in this video but you can add a lot more cool bells and whistles if you want.

You will learn to:

  • Create a Pygame window where your game will be played

  • Draw a rectangle that will represent your player

  • Implement code allowing your player to move

  • Create a state machine to implement gravity in your game

  • Add collision to your game objects that the player can interact with

  • Import images for your Pygame objects and work with sprites


Setting up your repl and your git repo

For this lab, you will follow the same process as in Lab02 (if you forgot how the process works, you can check there). There is no starter code to import for this assignment. This means you can first create a shared repl and then push that to github. However, when creating a repl, you must use "Pygame" as the template instead of "Python".

  1. Have one person create a new repl (again, make sure you use "Pygame" instead of "Python") for this lab named spis22-lab06-Name1-Name2 (with your names instead of Name1 and Name2) and then click on the share icon on the top right corner. Type in your partner’s email address and send the invite. Your partner should now be able to access the repl via the email sent to them.

  2. As in previous assignments, you will create only one private repository between the two of you. Go ahead and create this repo now by clicking on the Version Control tab in repl.it and clicking on Connect to Github. Following our naming convention, your repo should be called spis22-lab06-Name1-Name2. Again, make sure the repo is private.


Workflow

All the programming exercises in this lab will be completed as a pair. Make sure to commit your code frequently. We recommend pushing only working versions of your code to GitHub, or otherwise label them as “WIP” (work in progress).

Developing a game takes a lot of tweaking so do not expect your code to work on your very first try. Work through your code to identify where the bugs are and frequently test your new additions to your game. Also, seek help if you feel you are stuck!

Building the Game

1) Setting Up the Screen and Background

Let’s begin with our very first warm-up exercise, which consists of importing pygame and setting up our game window. Before you start coding, download the assets folder by clicking on this link (either download the entire folder or download the individual files and place them in a folder named “assets”). Once the folder is done downloading, click on the File Tab on repl.it, then click on ‘Add Folder’, and upload the assets folder you just downloaded.

In main.py, write the following lines of code:

import pygame

import os

# These are the dimensions of the background image for our game

screen_length = 600

screen_height = 427

dim_field = (screen_length, screen_height)


screen = pygame.display.set_mode(dim_field)

background = pygame.image.load(os.path.join("assets","background.jpg"))

background = pygame.transform.scale(background, dim_field)


# Location 1


screen.blit(background, (0,0))

pygame.display.update()

Note: If you get the following error

Traceback (most recent call last):

File "main.py", line 9, in <module>

screen = pygame.display.set_mode(dim_field)

pygame.error: No available video device

this means that you did not select "Pygame" as the template when you created your repl. To resolve this issue, you need to start from scratch: create a new repl and make sure you select "Pygame" instead of "Python".


This code will define the dimensions of the game screen, load in a background image, and set the background of the game screen. There are a lot of functions that are probably new to you. Most of them are part of the pygame library, which we use to program game mechanics, and we’ll explain them below.

  • pygame.image.load() loads an image file from its file path (in this case, our assets folder). Game developers usually group game resource files (images, sounds, videos) together in folders to keep their working directory organized. Because we need to access a file inside of a folder, we use os.path.join() and give it the folder name “assets” and the file name “background.jpg” to create the file path argument for load().

  • pygame.transform.scale() takes in two arguments: an image to transform and the dimensions we want to transform it into. In this code, we take the background image and transform it so that it fits the screen size.

  • screen.blit() takes in two arguments: an image to display on the screen and a point on the screen. The image will be drawn on the screen such that the top left corner of the image will match the point given in the second argument. Here, we draw the background onto the screen, and set the top left corner to match the top left corner of the screen. Recall from the previous image lab that in the coordinates system, (0,0) is the top left corner of the screen, with the y-axis increasing downward and the x-axis increasing to the right.

  • pygame.display.update() updates the screen so that we can see everything that gets drawn there. If you omit this line, any changes you make (e.g., adding a background with screen.blit above) will not show up.

Once you run this code, you should see the background image span the length of the screen. Make sure you expand the display area in Repl.it so that you can see the entire pygame window.

Now that we have a screen (and a pretty background), we have somewhere to draw the player. However, before you move on, make sure both of you understand what every line of code is doing in your program thus far ...

2) Drawing the Player on the Screen

Next, we are going to draw our player in the middle of the screen. Let’s start by using a simple rectangle as our player. A rectangle needs an x and y coordinate as well as width and height. Create variables to store these four parameters, which will make it easier if you want to change the numbers later on. For x and y, give them the value 200. For the player’s width, we will use 24, and the height we will use 26. All of these numbers are in number of pixels, by the way.

Now, we need to create a rectangle object. You can do this with the function pygame.Rect(). It takes in four arguments: x, y, width, and height (which we defined above), and returns a link to the rectangle. This is also very similar to what we did when creating a new turtle object (remember, to create a turtle object names bob, we used bob = turtle.Turtle()).

rect_player = pygame.Rect(x, y, width, height)

You can add this line right after the screen is created.

Now that our player’s rectangle has been defined, now draw it on the screen using the function pygame.draw.rect() which takes in three arguments: the screen to draw on, the color to use for the rectangle, and the rectangle object (i.e., the link to the rectangle you created earlier):

pygame.draw.rect(screen, (255,0,0), rect_player)

Place this where have the comment # Location 1., i.e., right above the statement to blit the background. BTW, in the example code above, what color are we using for our rectangle?

Run your code. Does it work? Before moving forward, discuss with your partner why you think this happened and try to fix the code.

Hint: Objects drawn later will overlap objects drawn before


Solution: If you draw the rectangle and then the background afterward, the background will be drawn over your rectangle. Make you fix this by rearranging your code so that everything is actually showing up correctly before continuing (yes, we told you the wrong spot where to insert your code earlier :-). Also make sure that pygame.display.update() is executed AFTER you have drawn everything, to actually show it on the screen. Verify that you now are able to see the rectangle on top of the background image.

3) Setting Up the Main Game Loop and the Clock

Every game has a loop (also called the “game loop”) that will execute at a frame rate you define.

Replace your last three lines of code (where you blit the background, draw the rectangle and then update the display) with the version below that adds a game loop. Note that those last three lines you had are still the last three lines, but are now inside the while loop.

# Game loop

clock = pygame.time.Clock()

running = True

while running:

clock.tick(FPS)

# Location 2


# Location 3

screen.blit(background, (0,0))

pygame.draw.rect(screen, (255,0,0), rect_player)

pygame.display.update()

This new piece of code creates a “clock” that helps the program keep track of time. This is done using the function pygame.time.Clock() that returns a clock object. It is not part of your game loop (the while loop) because we want create the clock only once.

The function clock.tick(FPS) is used to lock the game at the specified frame rate so that there is a consistent amount of times that the game is updated every second. This is inside the game loop itself. For our game, we will want a frame rate (stored in the variable FPS) equal to 60, i.e., 60 frames per second.

The game loop is essentially a while loop where each iteration corresponds to a new frame in your game. We use the running variable, which is a simple boolean (True or False) to indicate when the game should be running or not. It’s important that the code used for drawing objects is inside the game loop so that we can update them every frame and be guaranteed that they will be redrawn with the proper values.

Note that the code above is missing one thing. You can try to run the code and see what goes wrong. You should have all the information you need to fix it. Once you fix this issue, you will notice your code will run but it will not do much and essentially be stuck in an infinite loop. Make sure you understand why. We will fix this issue next by asking for user input (to move the player or end the game, etc). For now, you can just kill the program (by pressing stop).

4) Getting Some User Input

To avoid the issue earlier with the game being stuck in an infinite loop without the ability to end it, we will add in the functionality to quit the game when the user presses the letter ‘q’ on the keyboard.

We can do this by inserting the code below inside the game loop where we have the comment # Location 2.

The function pygame.event.get() will return all the events that occurred this frame which allows us to iterate through each and process them. In our case, we are checking if any key was pressed, and then checking if the key that caused this “pressed” event was ‘q’. If so, we quit the game.

# Processing events

for event in pygame.event.get():

# Quit game

if event.type == pygame.KEYDOWN:

if event.key == pygame.K_q:

running = False

We use constants defined by pygame to identify different keys (pygame.K_q represents the 'q' key), which are listed in the pygame documentation. Please take a look at the list of keys and their corresponding constants on this webpage, because we will use some other keys to implement more control mechanics later in the lab.

Important: When you run the code in Repl.it, you need to click on the game screen to make it active and have it register your inputs!!!

5) Horizontal Movement - Version 1

Now let’s implement some game mechanics. Player movement is a good place to start (since we don’t have that yet), and in this section we allow the player to move left and right with keyboard controls.

Because we want to constantly be able to respond to key input, the code in this section belongs inside the main game loop.

We also want to define how much the player can move in each frame. Define a variable called step_size and set it to 12; this step size gives the number of pixels the player moves each time.

We move the player by using the move_ip() method applied to a rectangle object. This method takes in two arguments: the first is how far the rectangle should move on the x-axis and the second is how far it should move on the y-axis. The ip in the function name stands for “in place”, which means that calling the function will change the rectangle that called the function. (Note that there is another similar method move(), which creates a new rectangle and does not move the original one. We will not use it here). For the line of code below, note that you still need to fill in the correct arguments (how for to move in the x and y directions).

rect_player.move_ip( , )


Now use this method to move the player rectangle by step_size to the left in the x-direction when the user presses the left arrow key (pygame.K_LEFT). Similarly, move the player rectangle by step_size to the right when the user presses the right arrow key (pygame.K_RIGHT). To write your code, base it on the code from the previous section, where we quit the game when ‘q’ was pressed. In fact, you just need to add to that code.

Make sure you are able to move the player rectangle left and right before moving on. Note how you need to keep tapping the keys for repeated movement.

6) Horizontal Movement - Version 2

Maybe you feel that the way to move left and right, having to press the arrow key repeatedly, is rather clunky and awkward for movement controls. Instead, most games these days allow the player to move continuously in a certain direction if you keep holding down the movement control keys. Here’s how to implement this kind of movement control system:


The function pygame.key.get_pressed() returns a list of booleans telling us whether a key was being pressed down or not. We want to use this multiple times later, so we'll save it into a variable keys. All the code in this section should go where we have placed the comment # Location 3.

keys = pygame.key.get_pressed()

if keys[pygame.K_LEFT]:

# move the rect_player to the left by step_size

If we access an element of the keys list, we can tell whether or not the corresponding key is currently pushed down. For example, keys[pygame.K_LEFT] will be True if the left arrow key is currently held down, and False otherwise. This is different from the event-based key detection we used in the previous section, because the value will still be True while you hold down the key, not just for the moment when you press the key down. We want to use this kind of key detection for player movement, so that the user can hold down the corresponding key to keep moving left or right.

We want to move the player to the left if the left arrow key is held down, and move the player to the right if the right arrow key is held down. Using the keys list, as explained above, write the code that moves the rectangle by step_size in the correct direction if the left or right arrow key is pressed down for the current frame. You also need to comment out the code you wrote for the previous part as we don’t want to use that kind of motion control anymore. However, you must still keep the part where you check for presses of the 'q' key to quit the game; so do not delete or comment out the code from part 4 (otherwise your new code will not work either).

If you run the code now, you should be able to move the player using your left and right arrow keys more naturally: as long as the left or right arrow key is pressed, the player keeps on moving left or right.

7) Boundaries

We can now move our player horizontally! But… they can go off the screen. Let’s fix that.

You have to add additional logic to your code to make sure that the player will not leave either edge of the screen based on the player’s x coordinate.

Some hints:

  • The left side of the screen is x = 0, and the right side of the screen is x = screen_length (remember, we defined the variable screen_length in step 1)

  • The player left, right, top, and bottom position can be accessed and updated with the following instance variables: rect_player.left, rect_player.right, rect_player.top, rect_player.bottom

8) Gravity

Right now, we have our player just hovering in the air. Let's add in the effect of gravity to make it more realistic. To do this, for each frame, move the player’s rectangle down by a value step_size_fall. For this new variable, you pick the same value as step_size, but feel free to play around with different numbers to find something that looks right.

The result is that our player now quickly falls off our screen. This is more realistic, but not really that interesting. We probably want to build some solid structures for our player to stand on instead. This is where platforms come in.

9) Platforms

To provide the player with something to stand on, we will add some platforms to our game. They will be represented by Rect objects, just like our player. You can look at the code that was used in step 2 to create a Rect object.

We recommend that you make the platforms relatively thin so that they look like platforms; 10 pixels is a good height. For the first platform, make it 200 pixels wide and place it somewhere just below our player.

Also create a platform (i.e., another Rect object) to represent the “ground”. This means that the platform should cover the entire length of the screen and be positioned at the bottom of the screen.

In order to display the platforms, you need to use the pygame.draw.rect() function from before to draw these platforms onto the screen. While all of this does add platforms to our game environment, the player just falls through them at the moment (try it). We will fix that in the next section.

10) Collisions

To avoid the player falling through the platforms, we need to implement collision detection between the player and platform objects. In order to easily check for collisions, we want to first gather all the platform objects into a list called platform_list. Add the line of code below right after where you defined the rectangles for the platform and the floor, and replace rect_platform1 and rect_platform2 by the names you used for these rectangles.


platform_list = [ rect_platform1, rect_platform2 ]


We can then use the player.collidelist() method to check if another rectangle (in this case the player rectangle) collides with any of the rectangles in the list. The method returns the location of the colliding rectangle in the list (i.e., the index in the list). In our example, we can use it to check if our player rectangle collides with any of the platform rectangles in the list of platforms.

index = rect_player.collidelist(platform_list)

If the player rectangle collides with one of the rectangles in the list, index contains the index in that list (indexes start at 0). If it does not collide, index will get the value -1 instead. You can use this information to detect when a collision occurs, which means the player stops falling.

Add the appropriate code to your program. It may be that in your solution, the player still falls halfway through a platform. Try to figure out how you can fix that.

11) Sprites

With all basic game mechanics down, let’s make our player a little less rectangular. We do this by adding "sprites" to our game objects (sprites are basically small images).

Similar to how you imported the background image in step 1, import the file “player.png” from the assets folder using the code below.

player = pygame.image.load(os.path.join("assets", "player.png")).convert()

player.set_colorkey((101, 141, 209))

Note that we added .convert(). What this does is that it changes the file format so that we can make the background of the image transparent (as opposed to the current light blue). The actual changing of the background to transparent is done with the set_colorkey() method. It takes one argument: an RGB value as a tuple. The function tells pygame to make any pixel in the sprite with said RGB value transparent. For our sprite, the background RGB value (i.e., that light blue color) is (101, 141, 209). It is common for sprite sheets to not have transparent backgrounds. So this method of creating a transparent background can be useful.


Now that we have a sprite ready to display, we will no longer draw the player as a simple rectangle. We do this by adding the following line of code

screen.blit(player, rect_player)

right AFTER this line (where you draw the player rectangle, see step 2).

pygame.draw.rect(screen, (255,0,0), rect_player)

Let’s quickly explain what this does. The rect_player object is a rectangle object, which contains all the relevant information about this rectangle: its size and its coordinates. The draw.rect() function draws this rectangle on the screen in a particular color. Now, we added the screen.blit(). This function draws a sprite on the screen at the coordinates given by the rectangle object which is passed as the second argument of this function.

Note that we used blit() before to draw the background on the screen. There we specified the coordinates directly as a tuple (0,0). It turns out blit() is flexible like that: the first argument is the image you want to draw and the second argument is the position where you want to draw it. You can specify this position as a tuple with coordinates or a rectangle (with the top-left corner of the Rect used as the coordinates). This is an example of method overloading, which happens when two methods with the same name take in different types or amounts of arguments, and possibly do different things.

At the moment, we still have the rectangle drawn together with the sprite. You can comment out the line:

# pygame.draw.rect(screen, (255,0,0), rect_player)

The player sprite still gets shown. The rectangle is not shown directly, but used to represent the location of the player sprite.

You may wonder why we have this roundabout way to position the player sprite: keep track of a rectangle and then use its coordinates, rather than enter the coordinates directly (as we did with the background). The reason is that the player rectangle is not only used to determine the position of the player sprite, but also to detect collisions! There are other ways to detect collisions, but using rectangles is an easy way to implement this functionality.

12) Jumping

You can have the player not just fall, but also give them the ability to jump. For example, when you press the spacebar, you can have them to up for 10 consecutive frames, after which gravity takes over (see the example video).

One way to think about how to structure your code to implement this functionality is to use Finite State Machines. Remember Finite State Machines from Picobot? For example, you could use three states representing "on the platform", "falling", and "jumping". The state transitions could be caused by a user input, a condition (such as colliding with a platform or no longer colliding with a platform) or by having been in a state for a number of consecutive frames (in the case of jumping). It is useful to sketch your state diagram first before starting to code. Refer to our lecture on Picobot for examples of such diagrams.

Also, if you want the player to look different when they jump, there is the file player_jump.png in the assets folder.

13) Win Trigger

Now we just need to figure out what it means to “win” in our game. In our case, we win when we reach the flag.

You can import the sprite “flag.png” from the assets folder The image already has a transparent background so you don’t need to do anything special. Place it anywhere in your field (reachable preferably if you want your player to have a chance of winning). The flag has size 50 by 50. You may need to scale the size of it (see part 1 on how to do this).

When the player reaches the flag (i.e., collides with it), we want to end the game. You can do this by setting running to False, which will exit our while loop.

Once you have this functionality working, congratulations, you've build a complete pygame!

We encourage you to keep on improving your game and adding on to it. You can check out the challenge problems below for some ideas or just come up with your own.

Challenge Problems

Now that you build the basics of your game, you can add a lot more features if you want. Below are some suggestions but feel free to be creative ...


Option 1. Moving Platforms

You could have your platforms move as well, rather than keeping them static.


Option 2. Realistic Falling Physics

The gravity system we implemented looks pretty good, but more advanced game engines are able to simulate really complex physics interactions, including accurate simulations of gravity acceleration. Using what you might know from physics class about gravity acceleration, try to change up the gravity code to make the jumping and falling animation look more realistic! Or you can add horizontal speed into the jump as well.