Welcome to an introduction to Pygame. Familiarity in Python will be required, but not game development. The course covers the basic setup of a Pygame file, drawing images and getting input, all the way to the use of built-in Sprites and Rects for game functionality.
Pygame is a great tool for developers. It isn’t set up to be easily compatible with mobile devices, and due to the nature of Python it isn’t great for commercial products in general; however, its strengths include its ease of use and accessibility. The flow of the Pygame workflow is an amazing way for beginners to learn game programming at a deeper level because it is straightforward while also having a fair amount of depth. It prepares you thoroughly to begin using much bigger and better libraries that are used commercially like libGDX with Java, and even the LÖVE framework in Lua.
I originally learned to make games by using Pygame, but despite moving onto newer frameworks with more appropriate languages, I still use Pygame often, especially for small programs like prototypes that I want to quickly complete. Regardless of experience, Pygame is an excellent addition to your tool belt.
(Sorry, my programmer art is showing)
Now with logistics out of the way, lets get to the fun stuff.
Take a look at this file, it is about the most default Pygame file that will do something. Go ahead, look at it, run it if you’d like. You should see a window similar to the picture above, but it should be switching between black and white. Be content with understanding none of it, be pleased if you see anything you recognize, but get excited because we’re going to learn all of it.
# Needed to use any and all python resources. import pygame import sys # Defines common colors background_one = (0, 0, 0) # black background_two = (255, 255, 255) # white # red: (255, 0, 0) # purple: (255, 0, 255) # light salmon: (255, 160, 122) # Initializes all pygame functionality. pygame.init() # Set the size of the window, variables can be used for positioning # within program. window_width = 700 window_height = 400 # Creates the window and puts a Surface into "screen". screen = pygame.display.set_mode((window_width, window_height)) # Sets title of window, not needed for the game to run but # unless you want to try telling everyone your game is called # "Game Title", make sure to set the caption :) pygame.display.set_caption("Game Title Here") # Used for timing within the program. clock = pygame.time.Clock() # Used for timed events. milli_timer = 0 white_flash_time = 1000 # Milliseconds between the screen being filled white black_flash_time = 500 # Milliseconds between the screen being filled black # Main loop of the program. while True: # Event processing here, stuff the users does. for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() # When user presses a key. elif event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: print("UP") elif event.key == pygame.K_DOWN: print("DOWN") # Add the amount of milliseconds passed # from the last frame. milli_timer += clock.get_time() # Every 1000 milliseconds, fill it with white if milli_timer > white_flash_time: screen.fill(background_two) milli_timer = 0 # Reset timer # Every 500 milliseconds, fill it with black. elif milli_timer > black_flash_time: screen.fill(background_one) # Display all images drawn. # This removes flickering images and makes it easier for the processor. pygame.display.flip() # Defines the frame rate. The number is number of frames per second. clock.tick(20)
But first, lets get an overview of how a game works. A game runs with a loop, each run through the loop is one frame. For every single frame, the computer takes in input from the user, does calculations on the game logic, and colors certain pixels on the screen certain colors.
Table of contents
Source code files
You can download the tutorial source code files here.
Become a Game Developer by building 15 games
If you want to become a professional game programmer check out Zenva‘s online course The Complete Mobile Game Development Course – Build 15 Games.
When we write a game, all our logic will be dependent on the current frame. Consider each frame to be contained within itself. All that matters to us is making sure each frame does its thing properly, and from there all frames work together cohesively. That isn’t to say every frame is completely isolated, they still can impact the future. Check out this example:
In the top right the ball is moving towards the wall, totally content with the world. In the second frame, it has reached the wall and is overlapping with it, which is a no no. During this frame, shown by the right picture, the ball is moved back outside of the wall, and the velocity has been changed. Now in the third frame it is moving with the changed velocity, though unaware of the collision in frame 2.
Back to the Python file.
First we want to look at line 36 where we define an endless loop. This is the main game loop that we described above. Everything that happens each frame goes on in here. The loop breaks when the user presses the X button in the corner and two functions are called. We will discuss them later on.
import pygame import sys
Lines 2-3: Simply classic import statements. ‘import pygame’ allows you to use all of its functionality, it is the only import statement you will need at the start and all access to Pygame functions start with ‘pygame.’. We import sys for one reason, closing down the process with sys.exit() on line 38 which we’ll discuss shortly.
# Defines common colors background_one = (0, 0, 0) # black background_two = (255, 255, 255) # white # red: (255, 0, 0) # purple: (255, 0, 255) # light salmon: (255, 160, 122)
Lines 6-7: We define two colors here. Simple color storage in Pygame holds them in tuples or arrays of the form (red, green, blue) with red, green, and blue being an integer from 0-255, inclusive. The higher the number, the more of the color. This is why (0, 0, 0) is black, because there is no color, and why (255, 255, 255) is white, because there is all color. Look at the following commented lines for a few more examples. (There is also a Color class that you can feel free to use, but it is not necessary).
Note: Notice the variable names. I do not call them ‘black’ or ‘white’. What if instead of black and white as the colors I wanted to change them to red and blue? I would have two options: change the variable names to ‘red’ and ‘blue’ and thus have to change every occurrence of them in the program, or just change the value and now have variables named ‘black’ or ‘white’ which store completely different colors. Instead I name them after their function, so that changing the background color is as simple as changing the value of the tuple. This is a good general programming concept to keep in mind.
Line 13: This is Pygame’s way of telling the computer “hey, help me out man, set me up”. More curious readers may try to run this program without the line and notice that it still works. That is because this program uses only simple modules; however, once you begin using other modules like pygame.mixer it becomes necessary to initialize them to work properly. It is good practice to include it by default to avoid forgetting it in the future and end up spending several hours troubleshooting the Pygame install before realizing you are simply missing one line. (I am definitely not speaking from personal experience…)
window_width = 700 window_height = 400 screen = pygame.display.set_mode((window_width, window_height)) pygame.display.set_caption("Game Title Here")
Lines 17-18, 21, and 25: Here we actually create the window. This set_mode function not only sets the type of window, but also creates it. We send in a tuple with dimensions (width, height) in pixels for the screen size. There are other advanced parameters as well, like full screen. We set the ‘screen’ variable equal to what it returns, which is a special Surface. Surfaces are Pygame’s way of working with images, whether those are drawn at runtime or loaded in. This special Surface acts just like any other, except for the fact that it is the Surface officially drawn onto the monitor.
clock = pygame.time.Clock()
Line 28: Here we creatively name a Clock variable ‘clock’. This Pygame module enables us to control the frame rate of our game (line 64), and enables us to create timers that work independently from the frame rate.
Lines 31-33, 49, 52-57: These lines are a simple timer in action, but first look at this example, this BAD example:
import pygame import sys background_one = (0, 0, 0) background_two = (255, 255, 255) pygame.init() window_width = 700 window_height = 400 screen = pygame.display.set_mode((window_width, window_height)) pygame.display.set_caption("Bad Example") clock = pygame.time.Clock() # BAD COUNTER >:( frame_counter = 0 # Main loop of the program. while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() #DON'T DO THIS >:( frame_counter += 1 if frame_counter > 20: screen.fill(background_two) frame_counter = 0 elif frame_counter > 10: screen.fill(background_one) pygame.display.flip() clock.tick(20)
A naive way of making a timer, shown above, would be to simply add 1 to a counter every frame, but we must take into consideration different systems. What if someone is playing on a $3000 rig that runs our game at 10000 FPS, compared to someone who has hacked a toaster and is playing at .01 FPS by burning each frame onto a piece of bread? The speed of the game would be wildly different between the two, and wildly different from your system, which completely changes the intended experience.
milli_timer = 0 # Millisecond counter white_flash_time = 1000 # Milliseconds between the screen being filled white black_flash_time = 500 ... milli_timer += clock.get_time() # Every 1000 milliseconds, fill it with white if milli_timer > white_flash_time: screen.fill(background_two) milli_timer = 0 # Reset timer # Every 500 milliseconds, fill it with black. elif milli_timer > black_flash_time: screen.fill(background_one)
Now the better way. Check out comments on lines 31-33, and look at 49. ‘Clock.get_time()’ returns the amount of milliseconds passed since the last frame. By using the time between frames, the timing works independently of how quickly the game is running. 10,000FPS with .002 milliseconds between frames: 10,000 * .002 = 20 counted each second. 1 FPS with 20 milliseconds between frames: 1 * 20 = 20 counted each second.
However, there is a flaw with this program’s way of switching between the white and black background states at especially low frame rates, can you figure it out? Comment below the problem and a possible solution.
while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: print("UP") elif event.key == pygame.K_DOWN: print("DOWN")
Lines 36-47: Here is that main loop, it is endless because the only reason for it to end is when the game itself closes. You could also use a boolean variable if you enjoy being quirky.
Line 38 (line 2 in the above chunk) is a biggie, it is what allows the user to interact with the game. pygame.event.get() returns a list of all keys pressed down or lifted up, exit buttons selected, and others on the given frame. To get the event type needed, you run through all of the events and check to see if it’s the type of event you need. Lines 39 (3) and 43 (6) do this. Here is a list of all the other event types. The pygame.QUIT event is activated when the user presses the X button on the window. pygame.quit() shut downs the game, and because we are kind to our computers, we call sys.exit() to help out the cleanup process.
Line 43 is another type of event, pygame.KEYDOWN is an event for pressing a key. Inside this statement is another if structure which works to find the actual key pressed, which is stored inside event.key.
Note: Remember I said on the given frame, so it only activates for a single frame when a key is pressed, and then forgets about it.
screen.fill(background_two) ... screen.fill(background_one)
Lines 53 and 57: This function is fairly self explanatory, it fills the Surface object with the color that you pass to it. In this case we are filling ‘screen’, and remember that screen’s contents are drawn onto the screen, which leads to…
Line 61: The line that turns the program from being a window opening simulator into game. This is the method that updates the display with ‘screen’, revealing the drawings we have done. Imagine it’s one of those secret turning walls that spin into another room. On the side open to the public is the frame that is currently displayed, while the back side points to our secret operation of code and numbers and magic where we paint the next frame onto. When pygame.display.flip() is called, we are turning that wall, updating the display and letting the new scene be viewed while we begin the process over on the new back.
Line 64: This function is simple, the integer you pass to it is how many frames per second you want the game to target. Note that the FPS will never go over the number, but it can go under if the computer is too slow.
Now that we have a nice understanding of every line in this file, lets make this second-rate seizure inducer actually do something.
Making Things Move
class Entity(pygame.sprite.Sprite): """Inherited by any object in the game.""" def __init__(self, x, y, width, height): pygame.sprite.Sprite.__init__(self) self.x = x self.y = y self.width = width self.height = height # This makes a rectangle around the entity, used for anything # from collision to moving around. self.rect = pygame.Rect(self.x, self.y, self.width, self.height) class Paddle(Entity): """ Player controlled or AI controlled, main interaction with the game """ def __init__(self, x, y, width, height): super(Paddle, self).__init__(x, y, width, height) self.image = pygame.Surface([self.width, self.height]) self.image.fill(entity_color)
First look at these classes, Entity will simply be the most default class for anything that appears in the game. Notice it inherits the pygame.sprite.Sprite class, using this Sprite class enables the usage of sprite Groups that are very important to handling entities in our game, we will get to them later. Also, notice the first line of __init__(), this is just the initializing function for the Sprite parent. Otherwise, Entity is simply a rectangle defined by coordinates and size. This pygame.Rect class comes with lots of useful tools, from moving the Entity to detecting collisions with others.
Paddle expands on Entity by giving the object an image. The image is what gets drawn to the screen, via our old variable ‘screen’. We just make a Surface by passing a width and height and then filling it with white, and the image will be drawn at the coordinates of the object’s Rect. Check out other things Surfaces can do here.
Paddle doesn’t do anything on its own, so we’ll give it some functionality:
class Player(Paddle): """The player controlled Paddle""" def __init__(self, x, y, width, height): super(Player, self).__init__(x, y, width, height) # How many pixels the Player Paddle should move on a given frame. self.y_change = 0 # How many pixels the paddle should move each frame a key is pressed. self.y_dist = 5 def MoveKeyDown(self, key): """Responds to a key-down event and moves accordingly""" if (key == pygame.K_UP): self.y_change += -self.y_dist elif (key == pygame.K_DOWN): self.y_change += self.y_dist def MoveKeyUp(self, key): """Responds to a key-up event and stops movement accordingly""" if (key == pygame.K_UP): self.y_change += self.y_dist elif (key == pygame.K_DOWN): self.y_change += -self.y_dist def update(self): """ Moves the paddle while ensuring it stays in bounds """ # Moves it relative to its current location. self.rect.move_ip(0, self.y_change) # If the paddle moves off the screen, put it back on. if self.rect.y < 0: self.rect.y = 0 elif self.rect.y > window_height - self.height: self.rect.y = window_height - self.height ... for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif event.type == pygame.KEYDOWN: player.MoveKeyDown(event.key) elif event.type == pygame.KEYUP: player.MoveKeyUp(event.key)
You’ll notice first the addition of some fancy functions. Look down at the event structure (which will really be in the while True loop), and you’ll see that when a key is pressed or unpressed, that key is passed to the Player’s MoveKeyDown and MoveKeyUp functions. When the up arrow is pressed, the y_change is added a…negative y_dist? That seems odd, especially if you have no prior experience with game development. The coordinate system on the window works kind of like a cartesian plane flipped over the x-axis. X values still increase from left to right, but Y values increase from bottom to top, so the origin (0, 0) is in the top left. Therefore, in order to go up, you actually move in a negative direction.
To counter act this, in MoveKeyUp, when the up arrow is released y_change is added y_dist again to go back to 0.
Down in the update function, which gets called each and every frame, we use rect.move_ip(x, y) to move the Rect x pixels in the x direction and y pixels in the y direction. The next lines just make sure the paddle stays on screen.
Here is what the Player Paddle looks like.
Note: Rects only store coordinates with integers, so for precise movement any decimal values are thrown out, this requires other means of coordinate handling.
Now having a lone paddle on screen isn’t the most exciting game, so let’s add the ball.
class Ball(Entity): """ The ball! Moves around the screen. """ def __init__(self, x, y, width, height): super(Ball, self).__init__(x, y, width, height) self.image = pygame.Surface([width, height]) self.image.fill(entity_color) self.x_direction = 1 # Positive = down, negative = up self.y_direction = 1 # Current speed. self.speed = 3 def update(self): # Move the ball! self.rect.move_ip(self.speed * self.x_direction, self.speed * self.y_direction) # Keep the ball in bounds, and make it bounce off the sides. if self.rect.y < 0: self.y_direction *= -1 elif self.rect.y > window_height - 20: self.y_direction *= -1 if self.rect.x < 0: self.x_direction *= -1 elif self.rect.x > window_width - 20: self.x_direction *= -1
In this class x_direction and y_direction are used to hold if the ball is moving right or left and up or down respectively. They will always be 1 or -1. Multiplying this by the speed in move_ip() in the update function makes it move speed pixels per frame in the correct direction. The if structures are similar to in the Player class, where it keeps the ball on screen, but in this case by negating a direction depending on which side it hits. Basically, the ball is continuously moving around the screen, bouncing off the sides.
Here we have the ball as well.
Any great Pong player needs a great opponent, so lets make that too.
class Enemy(Paddle): """ AI controlled paddle, simply moves towards the ball and nothing else. """ def __init__(self, x, y, width, height): super(Enemy, self).__init__(x, y, width, height) self.y_change = 4 def update(self): """ Moves the Paddle while ensuring it stays in bounds """ # Moves the Paddle up if the ball is above, # and down if below. if ball.rect.y < self.rect.y: self.rect.y -= self.y_change elif ball.rect.y > self.rect.y: self.rect.y += self.y_change # The paddle can never go above the window since it follows # the ball, but this keeps it from going under. if self.rect.y + self.height > window_height: self.rect.y = window_height - self.height
This class is the enemy paddle and will be controlled by an extremely complex and in depth algorithm: move up if the ball is above it and move down if it is below. The comments in the code should explain this class pretty well, there isn’t anything crazy going on.
ball = Ball(window_width / 2, window_height / 2, 20, 20) player = Player(20, window_height / 2, 20, 50) enemy = Enemy(window_width - 40, window_height / 2, 20, 50) all_sprites_list = pygame.sprite.Group() all_sprites_list.add(ball) all_sprites_list.add(player) all_sprites_list.add(enemy)
Moving down below the classes in the source file you’ll see a setup that should be very familiar. Additions include actually making the ball, player, and enemy instances by using the window_width and window_height variables to position them properly. Then below that we create all_sprites_list which is a sprite.Group(). This Group class is why Entity inherits the Sprite class as it enables us to store all our entities in this sprite Group.
Note: Groups use add() instead of append() to add elements to the list.
for ent in all_sprites_list: ent.update() screen.fill(background) all_sprites_list.draw(screen)
On line 167 and especially 172, there are some initial examples of the Group’s usefulness. 167 shows that the list can be iterated through just like any other array in Python, and each Entity in the list has its update function called. On 172, a sprite.Group actually has a draw() function. Calling this function iterates through each of the Sprites in it, takes the object’s image value, and draws it at the object’s rect coordinates. It requires each Sprite to have an image and a rect, but with this correct set up it handles the drawing of all images to the screen Surface.
And with that, we have an interactive, moving program. Check out the full source code below. It will take a bit more to turn it into something that we could call a game, but this is all the foundation that will keep our building from collapsing into rubble, and plenty to make something on your own! Make sure to let me know if you’d like this tutorial to continue! But if you would like to investigate further yourself, here are some recommendations:
- First things first, get the ball to actually bounce off the paddle, check out documentation for sprite Groups or Rects to find an answer.
- Next, if you noticed that if the enemy moves at a faster speed then the ball, then the enemy will never miss. How could you make the enemy miss occasionally?
- Third, how can you calculate and display the score of the game, or even add a main menu?
- Lastly, try to expand on Pong’s simple concept and add extra features like powerups.