The final project

Day 30: Project Preparation (pygame basics)

Python Guru planting a flag atop of a mountain made of abstract cubes, representing his progress in learning to code. With a dark pink background to match the day 30 image.

Welcome to the first of two project posts for the final day of the 30 Days of Python series. In this post we're going to learning how to use pygame, which is what we're going to use to build our Snake game.

You can find the project brief and walkthrough here.

Installation

Installing pygame is pretty simple.

If you're using PyCharm, you can follow the usual process, as discussed in day 27.

If you're using a different editor, or you want to use pip for whatever reason, you can find detailed instructions on how to install pygamehere.

Creating a pygame window

Once we have pygame installed, we can use it to create an application window.

Here is a small example program:

app.py
import pygame

pygame.init()

screen = pygame.display.set_mode([640, 480])

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

main()

Right at the top we import pygame and then we call the init function located in the pygame package. This function does a lot of setup work for us, but we don't need to worry too much about what is going on behind the scenes here.

On the next line we tell pygame how big we want our application window to be. We do this using the set_mode function located in the display module.

Like our own programs, pygame is split up into several different files, and different parts of the library are located in these different files. Things to do with the main display window can be found in the display module. We'll be using it quite a lot.

The set_mode function is what actually creates the display window, and we can call it without any arguments if we want to. If we pass in no arguments, we get a window that fills our entire screen. If we want to limit the window to some specific size, we can instead pass in a two element sequence, like a list a list or a tuple. The values inside must be integers, where the first represents the width of the window in pixels, and the second represents the height of the window in pixels.

The rest of the application is concerned with keeping the window around, and ensuring that the user can close the program using the window controls. We'll talk about his more when we discuss events. Pygame window If we want to set a custom title for the window, we can do that using the display.set_caption function, like so:

app.py
import pygame

pygame.init()

pygame.display.set_caption("My game!")
screen = pygame.display.set_mode([640, 480])

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

main()

Now our window looks like this:

Pygame window with custom title

This is pretty cool, but it'll be a lot more impressive once we have something more than a plain black box, so let's learn how to draw things in this window.

Drawing basics

You may have been wondering before why we bothered to assign the result of calling set_mode to a variable. This because set_mode returns a Surface object.

If you try to print screen, you'll see something like this:

<Surface(640x480x32 SW)>

Surface objects are used by pygame to represent an image. They're also surfaces that we can draw onto, and we have several methods at our disposal for handling this kind of operation.

The fill method

The first of these that we're going to look at is the fill method. fill allows us to fill an area of a surface with some colour, specified using either RGB or RGBA as sequence of integers. These integers must be in the range 0 to 255 (inclusive).

RGB is a common means of defining colours using ratios of red, green, and blue. Each of these colour channels is given a value between 0 and 255.

RGBA has an additional value, which represents transparency. The "A" stands for alpha.

For example, let's say we want to make background of our window tealish rather than black. The RGB combination for the colour I want is (36, 188, 168), so we can pass these values as a list or a tuple to screen.fill.

app.py
import pygame

pygame.init()

pygame.display.set_caption("My game!")

screen = pygame.display.set_mode([640, 480])
screen.fill([36, 188, 168])

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

main()

However,  this didn't actually appear to do anything. If you run the code, the screen is still black.

This is because these drawing operations are not shown until we request an update to the display. This is to prevent the user seeing things like partially modified images. The changes are done behind the scenes, and then we request an update to the display when they're done.

There are a couple of ways to do this, but we're going to use the update function in the display module.

app.py
import pygame

pygame.init()

pygame.display.set_caption("My game!")

screen = pygame.display.set_mode([640, 480])
screen.fill([36, 188, 168])

pygame.display.update()

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

main()

Now our background is a lovely shade of "tealish". Pygame window with a tealish background Since colours are something we tend to reuse in many places in our application, it's a good idea to store colour values in constants. It might also be a good opportunity to use a namedtuple, since we can create instances of an RGB tuple using keyword arguments, which gives some context to the values.

app.py
import pygame
from collections import namedtuple

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)

pygame.init()

pygame.display.set_caption("My game!")

screen = pygame.display.set_mode([640, 480])
screen.fill(BACKGROUND_COLOUR)

pygame.display.update()

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

main()

The draw module

The draw module contains several functions for drawing simple shapes onto a surface.

Let's start by drawing a rectangle using the rect function. rect takes a few different arguments.

First, it takes a surface to draw on, which can be any Surface object.

The second argument is color, which expects a colour in the same format as we discussed for fill.

The final required parameter is rect, which expects either a Rect object, or a sequence of values containing the information that a Rect object would usually hold.

Rect objects are really just containers for four important values: the x and y position of the rectangle's top left corner, expressed as coordinates; the width of the rectangle; and the height of the rectangle. This means we can replace them with a four element list or tuple that contains the same information.

With that in mind, let's try to draw a white circle on the screen surface, positioned in the top left corner.

Note

One thing you have to keep in mind is that the coordinate (0, 0) is in the top left corner, and as the x and y values increase, we move down and right across the screen.

app.py
import pygame
from collections import namedtuple

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)
RECTANGLE_COLOUR = Colour(red=255, green=255, blue=255)

pygame.init()

pygame.display.set_caption("My game!")

screen = pygame.display.set_mode([640, 480])
screen.fill(BACKGROUND_COLOUR)

pygame.draw.rect(screen, RECTANGLE_COLOUR, [0, 0, 100, 50])

pygame.display.update()

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

main()

The RGB code for white is (255, 255, 255), and I've made another constant to store this colour.

The window now looks like this, with our white rectangle in the top left corner, as excepted: Pygame window with a tealish background and a white rectangle in the top left corner Now let's try to draw a circle in the middle of the screen. We can do this with the draw.circle function, which has a slightly different signature to rect.

circle requires four things from us: a surface to draw on, a colour for the circle, the centre point of the circle, and the circle's radius.

Let's draw a yellow circle in the centre of the screen with a radius of 40px.

app.py
import pygame
from collections import namedtuple

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)
CIRCLE_COLOUR = Colour(red=255, green=253, blue=65)
RECTANGLE_COLOUR = Colour(red=255, green=255, blue=255)

pygame.init()

pygame.display.set_caption("My game!")

screen = pygame.display.set_mode([640, 480])
screen.fill(BACKGROUND_COLOUR)

pygame.draw.rect(screen, RECTANGLE_COLOUR, [0, 0, 100, 50])
pygame.draw.circle(screen, CIRCLE_COLOUR, [320, 240], 40)

pygame.display.update()

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

main()

Now our window looks like this: Pygame window with a tealish background, a white rectangle, and a yellow circle

Note

If we wanted our code to be able to account for a varying window size, so that the circle always ended up in the middle, we could find the size of the screen using the screen.get_height() and screen.get_width() methods.

Drawing text

Drawing text onto a surface is a little more complicated. We first have to create a Font object from the pygame.font module like so:

font = pygame.font.Font(None, 28)

The first argument here a file name , but we can just write None here. The second argument is a font size.

Once we have a Font object, we can call the render method to give us something we can actually draw onto a surface.

The render method takes a string, which will form the text content, a Boolean, and a colour. The Boolean represents whether or not you want to use anti-aliasing.

font = pygame.font.Font(None, 28)
text = font.render("Woo! This is some text!", True, (0, 0, 0))

Finally, we can draw the text onto a surface by calling the blit method on the surface we want to draw onto.

font = pygame.font.Font(None, 28)
text = font.render("Woo! This is some text!", True, (0, 0, 0))
screen.blit(text, (50, 50))

blit really just means draw, but it's a funny word you're going to have to get used to.

The blit method takes in the thing you want to blit, and then a position.

Moving items

Moving items on the screen is really just a matter of repeatedly drawing items to the screen in different positions, and covering up the old content.

In the example above, that's going to mean filling the screen with the background colour, drawing new shapes, and using the update function to display the new content.

We're also going to need one more thing, which is a Clock object. The Clock is going to let us limit how often the window gets painted, by setting a maximum frame rate. We do this by providing a frames per second value to the tick method from inside our loop.

With all this in mind. let's remove the white rectangle and make it so that the circle moves to the right in increments of 5px.

app.py
import pygame
from collections import namedtuple

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)
CIRCLE_COLOUR = Colour(red=255, green=253, blue=65)

pygame.init()

pygame.display.set_caption("My game!")

clock = pygame.time.Clock()
screen = pygame.display.set_mode([640, 480])

def main():
    circle_position = [320, 240]

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

        screen.fill(BACKGROUND_COLOUR)
        pygame.draw.circle(screen, CIRCLE_COLOUR, circle_position, 40)
        pygame.display.update()

        circle_position[0] += 5

        clock.tick(60)

main()

The circle quickly moves off the screen, but we did at least get it to move. We'll return to collision detection in a little bit so that we can keep the circle inside the window.

First though, let's talk about events.

Events

Events are how pygame communicates that something has happened in the application. This might be something like the user moving their mouse, or pressing a key, or clicking the little cross to close the application.

Since the very first example, we've had some code in our main function which is checking for a pygame.QUIT event. You can see it here:

app.py
for event in pygame.event.get():
    if event.type == pygame.QUIT:
        return

Here we're checking through some set of events that pygame has logged, and we're looking to see if any of those events are pygame.QUIT events. This event is what gets triggered when a user presses the button to close the application.

Clicking this button doesn't actually close the window: we have to do something to cause that to happen. In our case, we return from the main function, which leaves Python with no more code to run. This causes the application to terminate.

pygame has a lot of different events types, and we can check for those events in the same way that we're checking for pygame.QUIT. For example, we can check for mouse movement using pygame.MOUSEMOTION.

Let's create a new application where we just print out the position of the mouse to the console. We can find the position of the mouse from a pygame.MOUSEMOTION event by accessing an attribute on the event called __dict__.

This gives us a dictionary with a key called "pos" which contains coordinates for the mouse position when that event was triggered.

app.py
import pygame

pygame.init()

pygame.display.set_caption("Mousetracker")
screen = pygame.display.set_mode([640, 480])

def main():
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return
            elif event.type == pygame.MOUSEMOTION:
                position = event.__dict__["pos"]
                print(position)

main()

That gives us a lot of console output, but we can see that as we move our mouse around the window, it logs the current position.

Let's upgrade this little app so that a ball follows the cursor around the screen.

This is fairly simple, we just have to keep replacing the circle's centre value with whatever the current mouse position is.

app.py
import pygame
from collections import namedtuple

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)
CIRCLE_COLOUR = Colour(red=255, green=253, blue=65)

pygame.init()

pygame.display.set_caption("Mousetracker")

clock = pygame.time.Clock()
screen = pygame.display.set_mode([640, 480])

def main():
    circle_position = (screen.get_width() // 2), (screen.get_height() // 2)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return
            elif event.type == pygame.MOUSEMOTION:
                circle_position = event.__dict__["pos"]

        screen.fill(BACKGROUND_COLOUR)
        pygame.draw.circle(screen, CIRCLE_COLOUR, circle_position, 20)
        pygame.display.update()

        clock.tick(60)

main()

A bouncing ball

Let's look at one more example.

I want to return to the ball which moves on its own, but now I'd like it to bounce off the sides, rather than flying out of view.

In order to do this, we're going to keep track of the ball's velocity, which is going to describe how far the ball will move in a given direction for each tick of our clock. To keep things interesting, we can randomise the starting values for this velocity.

app.py
import pygame
from collections import namedtuple
from random import randint

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)
BALL_COLOUR = Colour(red=255, green=253, blue=65)

BALL_RADIUS = 20

pygame.init()

pygame.display.set_caption("Bouncing Ball")

clock = pygame.time.Clock()
screen = pygame.display.set_mode([640, 480])

def main():
    ball_position = [(screen.get_width() // 2), (screen.get_height() // 2)]
    ball_velocity = [randint(-5, 5), randint(-5, 5)]

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

        screen.fill(BACKGROUND_COLOUR)
        pygame.draw.circle(screen, BALL_COLOUR, ball_position, BALL_RADIUS)
        pygame.display.update()

        clock.tick(60)

main()

Here we have our application window with a circle in the middle. At the moment nothing much is happening, but we do have the initial ball position and velocity defined.

The next step is checking that the ball hasn't exceeded any of the "walls" of our window. If it has, we need to redirect the ball, preventing it from continuing on its current trajectory. In order to do this, we're going to modify the ball's velocity.

app.py
import pygame
from collections import namedtuple
from random import randint

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)
BALL_COLOUR = Colour(red=255, green=253, blue=65)

BALL_RADIUS = 20

pygame.init()

pygame.display.set_caption("Bouncing Ball")

clock = pygame.time.Clock()
screen = pygame.display.set_mode([640, 480])

def main():
    ball_position = [(screen.get_width() // 2), (screen.get_height() // 2)]
    ball_velocity = [randint(-5, 5), randint(-5, 5)]

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

        screen.fill(BACKGROUND_COLOUR)
        pygame.draw.circle(screen, BALL_COLOUR, ball_position, BALL_RADIUS)
        pygame.display.update()

        # Check for left and right collisions
        if ball_position[0] - BALL_RADIUS < 0:
            ball_velocity[0] = -ball_velocity[0]
        elif ball_position[0] + BALL_RADIUS > screen.get_width():
            ball_velocity[0] = -ball_velocity[0]

        # Check for top and bottom collisions
        if ball_position[1] - BALL_RADIUS < 0:
            ball_velocity[1] = -ball_velocity[1]
        elif ball_position[1] + BALL_RADIUS > screen.get_height():
            ball_velocity[1] = -ball_velocity[1]

        clock.tick(60)

main()

Finally, we just need to move the ball for each tick, adding the current velocity to the ball's position.

app.py
import pygame
from collections import namedtuple
from random import randint

Colour = namedtuple("Colour", ["red", "green", "blue"])

BACKGROUND_COLOUR = Colour(red=36, green=188, blue=168)
BALL_COLOUR = Colour(red=255, green=253, blue=65)

BALL_RADIUS = 20

pygame.init()

pygame.display.set_caption("Bouncing Ball")

clock = pygame.time.Clock()
screen = pygame.display.set_mode([640, 480])

def main():
    ball_position = [(screen.get_width() // 2), (screen.get_height() // 2)]
    ball_velocity = [randint(-5, 5), randint(-5, 5)]

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

        screen.fill(BACKGROUND_COLOUR)
        pygame.draw.circle(screen, BALL_COLOUR, ball_position, BALL_RADIUS)
        pygame.display.update()

        # Check for left and right collisions
        if ball_position[0] - BALL_RADIUS < 0:
            ball_velocity[0] = -ball_velocity[0]
        elif ball_position[0] + BALL_RADIUS > screen.get_width():
            ball_velocity[0] = -ball_velocity[0]

        # Check for top and bottom collisions
        if ball_position[1] - BALL_RADIUS < 0:
            ball_velocity[1] = -ball_velocity[1]
        elif ball_position[1] + BALL_RADIUS > screen.get_height():
            ball_velocity[1] = -ball_velocity[1]

        ball_position[0] += ball_velocity[0]
        ball_position[1] += ball_velocity[1]

        clock.tick(60)

main()

We now have everything we need to tackle the project! If you want to look into pygame a bit further and learn about its other features, you can check out the pygame website here.

There's also a tonne of examples of things other people have built with pygame, so it might serve as a source of inspiration for your next project!