Intro to Pygame

This lesson assumes you have some prior Python experience and knowledge of Python syntax.

We will use a library called Pygame in many lessons. Pygame provides an API for drawing on the screen, so you will be able to visualize the effects of your code. Programming is a very abstract discipline, so it can be helpful to make the result as visible as possible to develop understanding. If you make a mistake, you will see the impact of that mistake very clearly on the screen.

What is a Library

Since we will be using a library, we should define what a library is. A library is a group of routines (think functions) intended to be used by another program. Each library has an API (application programmer interface) which is the collection of functions and classes that are provided for the developer who uses it to support his program. The library may contain other functions that are not part of the API, and these are for the library itself to use. A good library should always have a document you can read that carefully describes its API, so that you know what to expect. At first, the effort to learn and set up a library may not seem worth it. Sometimes, this is the case. But consider that you already use libraries - there are some that you get for free when you install Python! One example is the math library, which you can bring into your program with import math. Once the library has been imported, you can call functions in its API: x = math.sqrt(2). The sqrt function is part of the math library's API, and you can find its documentation on the official Python website. This is left as an exercise to the reader.

Semantic Versioning

A discussion on libraries bears a short introduction to versioning. Have you been in the process of writing a program before and been dissatisfied with the wording of a print statement, or discovered a bug? How many different versions of your program did it take until you were satisfied with it? As a student, you will not usually be expected to think a lot about this concept of versions of a program, but if you ever use Python professionally, you will need to understand it. Sharing a program with someone else and then deciding to change it again is a common scenario, and this is where the versioning problems start. How does the other person know whether his copy of your program has your new change or not? How does he know whether your new change might break his own program if you send it to him?

Many library authors solve these problems with semantic versioning. To follow semantic versioning, a library author attaches a number to each version of his program in the format major.minor.patch. The version describes the library's APII. Its functions that are not part of the API do not need a version number; the library user does not know or care about them so his program should not break if they change. The major number increments whenever the API changes in a way that might break the user's program, the minor number increments whenever the API gets new features that won't break the user's program, and the patch number increments for bugfixes. For example, we will use version 2.0.0 or higher of Pygame in this tutorial, but not version 3.0.0 or higher. Everything in the tutorial should work with major version 2, but major version 3 could break it. At the time of writing, Pygame does not have a version 3.0.0, and likely won't for quite a while, because most library authors are careful to make changes that break their user's programs as rarely as possible. But if they do and they use semantic versioning, you will not be taken by surprise, and you will be able to reason about which versions of their libraries will work for you.

You can read more about semantic versioning here.

So what about Pygame?

Installing Pygame

Pygame is what we call a "third-party" library, because it is not written by the party that maintains Python and its built-in libraries like math. Because of that, it won't come installed with Python by default. Since there are different ways to install libraries for Python depending on what operating system you have, I won't go into detail here about how to install it. I encourage you to do a little research and reach out to me if you need help, but for a Mac, I think the easiest way is brew install python3-pygame.

Overview

I said earlier that every good libraries should have documentation. Pygame does, and you can read it here. They also provide some tutorials - I especially recommend the one about sprites. However, I'll provide an overview of the basic parts of the API anyway. You do not need to remember every function in the API and what parameters it has - even I don't remember that, and I tend to remember a lot of oddly specific details. I find the Pygame API frustratingly difficult to remember, so I recommend using their documentation as a quick cheat sheet. To do that effectively, you need to know what you're looking for, so without further ado, let's summarize the structure of the Pygame API.

Initialization

The nature of computer graphics requires that you set some things up to be able to draw to the screen before you can actually draw. Pygame makes this very easy (which is a big understatement) but you need to remember to do it. Thankfully, it's the easiest part of the API to remember. After importing pygame, call pygame.init() to initialize everything. See? Easy.

Surfaces

There is a class in Pygame called Surface that represents something you can draw on. One of these will be your main surface and represents an actual window where your drawings will appear. You can have other surfaces, too, but you likely won't need any except your main surface for the basic things we'll be doing. To create the special main surface, call `pygame.set_mode((width_pixels, height_pixels))`. For example, if you call `pygame.display.set_mode((400, 300))`, you'll get a window 400 pixels wide and 300 pixels high. If you pass in zeroes, you'll get a fullscreen window. It returns a Surface object you can use to do things with your window - don't forget to save that! By the way, this could be considered part of initialization. You should only do it once, and this surface is special.

Let's see a full example: main_window = pygame.display.set_mode((0, 0)). We're setting up our main window in fullscreen mode. Look at that again. This is where your troubles begin. The person or people who write Pygame chose how it would work, and when you use their library you need some forbearance. There are two issues here: there are 4 parenthesis in total, not 2, because it takes 1 argument, a tuple of 2 elements, and you're going to forget which number is width and which is height. The documentation for the function will answer both questions when you forget, and for the first issue I have no better advice. For the second issue, take note that most if not all graphics developers have an unspoken rule that width comes before height. If you don't do a lot of computer graphics and don't know the order, it'll be hard to remember, but it's the right order by convention.

Updating the Window

Before we talk about how to draw, we will talk about the workflow of drawing. This part applies to computer graphics in general, not just to the Pygame library, although we'll look at how the Pygame library does it specifically. We'll also have a little fun and talk about hardware. Graphics programs are designed to run on GPUs (graphical processing units), which are similar to CPUs (central processing units), but designed to be extremely fast at certain mathematical operations useful for graphical tasks. When you draw on the screen, the things you draw are represented as primitive geometrical shapes, and the GPU does a sequence of operations (we call this pipelined) on them to convert them into pixels with the right location and color; this can be as advanced as automatically computing shading from a light source or making an object appear textured. The GPU is designed to do these calculations in batches and in parallel, at a speed that's hard to comprehend. Games typically run at 60 frames per second. That means that 60 times a second, the GPU converts all the geometry (possibly up millions of triangles at a time) into pixels, figures out how to shade them all, and their correct colors. To draw, your program must set up the geometry, and tell the GPU to do its thing when ready.

Enough about the details, why do we care? If nothing else, you will appreciate how easy Python makes this. But also, the way GPUs are designed has the consequence that doing things in batches is very efficient. Thus to be fast and avoid wastefulness, we will typically draw a whole frame before updating the screen. Another consequence is that you will not see any change on the screen when you draw - you have to update the display with what you drew to make it visiblee. his results in a common structure graphical applications share: forever do { process events; draw; update; } end. We'll address the "process events" part later. Let's look at how we can update the screen first.

There are two ways to update the screen: the easy, slow way, and the hard, fast way. Yep, tradeoffs. The easy, slow way is pygame.display.flip(). It redraws the entire screen, hence it's slow. The hard, fast way is pygame.display.update(rectangles). This one takes a list of rectangles describing which parts of the screen to redraw. It won't have to redraw everything, but you also have to figure out which parts to redraw. It's fine to use the easy way for now. The second one becomes easier once you start using Sprite objects which can pass for rectangles.

OK But Why Update The Screen if I Can't Draw Stuff!!?

Right, this is all about the drawing, we're finally there. You can draw shapes on the screen using functions like pygame.draw.circle and pygame.draw.rect (for a rectangle). The first argument to these functions is always the Surface you want to draw it on. For simple programs, this will usually be the one you got from pygame.display.set_mode, which is the one that gets drawn to the screen when you update the display. The other arguments tell it where to draw it, what color it should be, how wide the border should be, and so on. I don't remember what order they go in. Check the documentation for that.

By default, you'll get a black background, but sometimes you want to change it to another color. I will often run main_window.fill("white") at the beginning of every frame if I'm using pygame.display.flip, or once right after initialization if I'm using pygame.display.update.

You can also draw text and images. This is where it starts to get a little trickier, so we will review it if we use images later. Just for completeness, I'll summarize it briefly here. There are functions to load images or draw text in a particular font, and these create new Surface objects that have the text or image drawn on them. But that's a bummer, because to get them on the display, you need them on your special main surface. To solve that problem, you "blit" the new surfaces onto your main surface. The Surface class has a method called blit to do this. You're welcome to read the docs for it and try it out if you're curious.

Event Processing

We won't need to interact with the user a lot, but we would at least like the quit button to work (most people do, except the people who make ads). To make that work, we need to know when the user clicks the button. There are different ways to get user events in Pygame, but a good way to do it is for event in pygame.event.get(). This will iterate over all the new events that happened. Based on the event type, you can choose what to do. For example, you can do if event.type == pygame.QUIT to test whether the user clicked the quit button.

Conclusion

That's all we need to get started. There are things we haven't covered, such as how to run at a particular FPS (so you don't waste CPU power), sprites, and collision detection. To conclude, take a look at the examples in the Quick start at the Pygame docs. They also show how to use pygame.time.Clock to limit your FPS, which is good practice. If you don't do this, your loop will run as fast as it can, which is a lot faster than you need it to, eating up all your computer's resources and possibly lagging it for no good reason. Finally, they also call pygame.quit() to shut down cleanly. This is the opposite of the initialization at the beginning, and you should also do it to make sure your program finishes in a clean state with no errors.

Exercises

Try out the examples in the Quick start. Then try writing a program that draws a smiley face (or a frowny face, but that would be sad). The more detail, the better!