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
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.
Have one person create a new repl for this lab named spis20-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.
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 spis20-lab06-Name1-Name2. Again, make sure the repo is private.
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!
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
screenLength = 800
screenHeight = 427
dim_field = (screenLength, screenHeight)
def main():
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()
main()
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. When you downloaded the files above, we packaged them in a folder called “assets”. 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. Now that we have a screen (and a pretty background), we have somewhere to draw the player.
Before you move on, make sure both of you understand what every line of code is doing in your program thus far ...
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. It is easiest to create variables to store these first, 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, 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)
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 :-). Verify that you now are able to see the rectangle on top of the background image.
Every game has a loop (also called the “game loop”) that will execute at a frame rate you define.
You will need to replace the main function in your current code with the version below. We will explain what it does below.
def main():
screen = pygame.display.set_mode(dim_field)
clock = pygame.time.Clock()
background = pygame.image.load(os.path.join("assets","background.jpg"))
background = pygame.transform.scale(background, dim_field)
rect_name = pygame.Rect(x, y, width, height)
# Game loop
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()
Note that it 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 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 below 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).
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.
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 stepsize 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 stepsize to the left in the x-direction when the user presses the left arrow key (pygame.K_LEFT). Similarly, move the player rectangle by stepsize to the right when the user presses the right arrow key (pygame.K_RIGHT). To write your code, based yourself 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.
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, like this:
keys = pygame.key.get_pressed()
All the code in this section should go where we have placed the comment # Location 3.
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 stepsize 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.
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.
Awesome. 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 = screenLength
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
Now that our player is bound to our visible play space, let’s add some more game mechanics to spice up the playing field! Jumping seems like a great place to start.
We will use Finite State Machines to efficiently implement our player behavior. Remember Finite State Machines from Picobot?
We will go step-by-step here. Consider a simple situation where a player can only be in two states: jumping or not-jumping. We can easily represent this with a isJumping variable. Let’s set it’s default value as False since our player will begin the game on the floor.
Now add code that changes the isJump variable to True the moment the space key is pressed. Remember how to do this? You can check back to where we used ‘q’ to quit the game (and we also provided a link there to the names of all the keys on the keyboard).
What should happen when you are in the jumping state? We will want the player’s rectangle to move up vertically for 10 consecutive frames (i.e., 10 iterations of your while loop). You can do this by using a counter. After the 10 frames, the player should go back to the not-jumping state.
When you get this work, you will notice this will be a bit weird: the player jumps up, but then will not fall down and just hover (at which point they can “jump” again). This doesn’t look quite right. The reason is that we haven’t included gravity yet and so the player is never falling down again. Don’t worry, we’ll do that soon.
However, before we deal with gravity and falling, let’s create some platforms in our game. They will be represented by Rect objects, just like our player.
Write code to create 1 or 2 platforms as different Rect variables. You can position them wherever you want and make them however big you want. We recommend that you make the platforms relatively thin so that they look like platforms; 10 pixels is a good height if you can’t decide.
Also create a platform 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. The platform does not necessarily have to be within the borders of the screen, but the player should at least appear to be standing on 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. If your ground platform is outside the borders of the screen, you won’t see it (but that’s ok).
Love it or hate it, gravity is an essential part of any platformer. We will expand our Finite State Machine to keep track of what should be happening to the player in a given state.
Below is an example of a state machine that uses three states to keep track of jumping/gravity for our player. You don’t have to follow this exactly. It is given as a starting point.
Add onto the code you wrote so far to simulate gravity as well. The state machine diagram above shows that falling should only occur when the jump animation finishes or when the player is no longer standing on a platform.
Finally, we also need to implement collision detection with the platforms to stop the player from falling through them.
In order to easily check for collisions, we want to first gather all the platform objects into a list called platformList. Just create a list that contains all the platform objects (do you remember how to create a list?).
We can then use the player.collidelist() method to check if a rectangle collides with any other rectangles in a list. The method returns the location of the colliding rectangle 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(platformList)
Here’s some pseudo code to help you implement this mechanic:
The player falls according to fallStep (i.e., how many pixel to go down each frame; you can decide what a good value is)
Check if the player collides with a platform
If the player does collide with a platform
The player is not in the air anymore (i.e., we have a change in state)
The player should be standing on the platform they collide with (the bottom of the player should touch the top of the platform). Remember, the collidelist() method returns the index in the platform list, which helps you figure out which platform was involved in the collision.
If the player does not collide with a platform
The player is still falling (and still in the air)
If your player can jump, fall, and land on a platform, you’ve implemented this functionality! Try it out and play around with it until the mechanics work as you hope they would. There is not one way of doing this. Feel free to experiment and make your game do what you feel it should do to make it interesting.
With all our 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 “playerSprite.png” from the assets folder using the code below.
playerSprite = pygame.image.load(os.path.join("assets", "playerSprite.png")).convert()
playerSprite.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 but use the sprite instead. We do this by replacing the line of code:
pygame.draw.rect(screen, (255,0,0), rect_player)
with
screen.blit(playerSprite, 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 original draw.rect() function would draw this rectangle on the screen in a particular color. Now, we replaced it with 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.
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 “flagSprite.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.
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! If you want to keep on improving your game and adding on to it, you can of course. You can check out the challenge problems below for some ideas or just come up with your own.
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 ...
Now that you know how to load in sprites and keep track of states, use the file playerSpriteJump.png from the assets folder to change the player’s sprite from his idle stance to the jump stance when the player is in the air.
Create some platforms that move left to right (or up and down) that the player can interact with.
The gravity system we implemented in this lab 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.