Code Your First Game in PyGame
Learn Game Development with Python
Have you ever wondered how to make video games? Modern game engines make it easier than ever for anybody to start making their own games. Today we’re going to be taking a look at the PyGame library for Python and creating a version on of the classic game Snake.
Once finished you should feel more comfortable with the PyGame library as well as more familiar with game programming concepts like drawing, game loops, game timers and collision management.
What We’re Building
This is what the final prouct will look like. We will be coding everything that is required for the functionality of the below gif.
We will not be covering more advanced PyGame features such as sprites, data persistence, or scene logic.
This article makes the following assumptions:
- You either have Python 2.7 or 3.3+ installed on your system
- You are familar with object oriented Python
- Familarity with game programming a bonus, but not required
The Source Code
The completed source code for this project can be found on Github. All code samples are written as Python 3 but are compatible with Python 2 as well.
Optional First Step — Virtualenv
I highly recommend utilizing a virtual environment for all Python projects, but this step is optional and won’t be specifically covered here. For more information on installing and utilizing the Virtualenv library, see my article here.
Let’s start by creating our directory structure and initializing a Python module for our game.
$ mkdir Anaconda && cd Anaconda
$ mkdir game
$ touch game/__init__.py
Now that we have our directories and our game Python module, let’s go ahead and setup our virtual environment and install PyGame.
$ virtualenv venv --python=python3.6 # Optional
$ source venv/bin/activate # Optional
$ pip install pygame
Next create a file in the root of our directory called
Anaconda.py with the following contents:
This simply imports the main game class that we’ll create momentarily, makes a new instance, and calls the
game.loop() function to kick off our game loop.
Next create a file in the
src directory called
Game.py with the following contents:
This defines a class with a single function
loop that outputs ‘game loop’ . We can now test our initial setup.
$ python Anaconda.py
If you get ‘game loop’ as your output. Then that means PyGame is being imported successfully and our module is setup correctly.
Initialization and the Game Loop
Now that we know everything is working successfully so far, let’s next add the code to bootstrap PyGame and start our game loop. Let’s first add a bit of code to give our window a size and a title. This will be plaed at the top of the
main function in our
This will create an 500 x 500 pixel game window with a title of “Anaconda”. Pay special attention to the fact that
pygame.display.set_mode() is accepting a tuple of
(width, height) rather than multiple parameters. We also pass a reference to
display into the
Game object, we will look at this closer later when we get to drawing.
The next step will be to finish the rest of our setup related code and initialize our game loop. There’s a bit going on here, so let’s modify our
Game.py file and and then we’ll step through the new code.
In the constructor, we just save a instance level property to store a reference to our display.
loop function, we start off by initializing the game clock provided to us by Pygame, this in combination with the
clock.tick() call on line 15 slows the loop iteration rate down to the rate of the game. It accepts a number of ticks per second, so we have set our game timer to 30 frames per second.
It always feels a little strange recommending an infinite loop, but this is exactly what we want in this case. While inside that loop, we iterate through the return value of
pygame.event.get() which gives us our user input events.
Pygame maps common inputs such as keyboard strokes or mouse movement; so we do a check if the
event.type received is
pygame.QUIT and exit the application if so because this means the user has pressed the X button.
When you run the
Anaconda.py script again, as you type or move the mouse you should see something similar to the following screenshot in your console.
Colors, Config, and Magic Numbers
In game programming, it can be very easy for our code to begin to lose clarity. There are often shapes or sprites being drawn at arbitrary X and Y coordinates, and those sprites often have movement, colors, and animations. Next thing you know, you’ve got magic numbers and repeated values spread out everywhere.
Let’s go ahead and take a brief moment to correct this before we move on. In our
src directory, create a file called
Config.py with the following contents:
This includes the settings we’ve already used as well as a few new ones that will utilize soon enough for our Snake and our Apple.
The biggest take away here is the way the colors are set, notice how each color is a set of RGB values stored as a tuple.
Let’s next replace the few values we have already used with their configuration values.
Unchanged code has been ommited while preserving proper indentation level. This trend will continue in future code examples (where applicable).
Building the Snake
Our next step is going to be building our Snake and the logic around it moving. Lets think about the bits of information and functionality this class will require.
- We need to track an X and Y coordinate of both the snake head as well as any elements of it’s body
- We need to store a max length dictated by the number of apples the snake has consumed and ensure our body does not increase past this value
- We need a function that will draw our snake to the screen
- We need a function that moves the snake by adjusting it’s X and Y position.
This is what that looks like in code. I have left out collision, movement, and the body, but we will get to that soon.
We begin with our
y_pos that will serve as the X ad Y position of the head of our snake at the middle of the screen.
draw function, we finally make something visible appear in our game. This function is going to be called for each frame from our
loop function inside the
game class. Take notice of
pygame.draw.rect accepting three parameters, with X position, Y position, and rectangle height/width being contained in a list as the third parameter.
Special Note About Drawing & Coordinates
When specifying coordinates for where something will be drawn, these coordinates always start from the top left hand corner.
move function accepts a change value and adds it to the X and Y position of the snake’s head. This will allow us to pass positive numbers for up and right and negative numbers for left and down.
draw function should be called after our event loop but before the screen updates. We only want to re-draw once per frame or misplace elements we want to draw, so it’s important we ensure that update is called once and all drawing occurs before.
Now’s a good time to run the game and make sure everything is working up to this point. Your game should now look like this.
Progress! Next up we’ll switch over to our
Game class and start getting our snake moving.
We will once again utilize PyGame’s event handlers, this time to respond to the arrow keys to guide our snake. Inside the
Game.py file, add the following before our game loop inside the
This instantiates our snake with a reference to the display and sets
y_change will will hold our velocity for both axes.
Don’t forget to import the Snake class as well.
from src.Snake import Snake
Directly under and at the same indention level to where we check
pygame.QUIT, add the following:
When a key is pressed, we simply check if the key is one of any of the four potential keys we are looking for and set the appropriate change variable. For up and left we use negative speed because our
move() function called on line 18 is designed to add its input to the snake’s position.
Prior to moving then drawing the snake, we are filling the entire screen with the color black. Otherwise you would see the Snake grow infinitely immediately due to previous draws never being wiped from the screen.
If you run
Anaconda.py now, you should now be able to move the snake around using the arrow keys. Currently the snake is able to disappear into off-screen purgatory but we will address this soon.
Before we start coding apples or collision rules, we first must define what our game area is going to be. Currently our window is 500 x 500 pixels, has a solid black background,and the entire field is playable.
So we’re going to go ahead and add the score and a text-header to our game so that we can calculate the available dimensions for apple spawning and collision.
The first step is creating a setting for our border size.
The goal is to have a consistent border all the way around of 30 pixels. Rather than drawing four rectangles, we are going to change the background color of our display to green and then draw a black rectangle over it.
Where we were setting the fill to black before, we’ll want to place the following code. Keep in mind that drawing is top to bottom, so you must draw them in the order you want them to overlap.
Your game should now look like this:
Download Open Source Font for Game
This game uses the font “Now” made freely available under the OFL license by Alfredo Marco Pradil. You can locate information for and download this font here.
Create a folder in your root directory called
assets and move the file
Now-Regular.otf to this directory. If you use a different font, make sure to change the upcoming code sample to match the correct file name.
Title and Score Text
From building menu items to showing player dialogue, you will end up using a lot of text in your games. Fortunately with PyGame this is no more difficult than anything else we have seen so far.
Since we already know we will need to updatae the score frequently as the snake consumes more apples, let’s go ahead and create an instance variable in the initialization function of the
Below where we call our
Snake.draw() funtion in
Game.py file, add the following code.
Let’s step through what is happening here.
We initialize the PyGame font module and we load in the font file from our assets directory. Keep in mind that in a more formal project where you would likely create your own helper functions for handling text, that this bit only needs to happen once.
We call the
render method on the
font object which accepts the string we want to output, a boolean value of True or False for enabling anti-aliasing, and a tuple with three values representing an RGB color which we have called from our settings.
We are then able to call
get_rect on the subsequent object which gives us the bounding box size around our text, we can pass the
center argument in order to help with centering with the offset of the items size.
If everything is still going according to plan, your game window should look something like this:
Colliding with the Wall
Now that we have a clear distinction of where our apples are going to spawn and where we wish our snake to be able to tread, let’s start the collision detection with triggering a restart when the snake collides with a wall.
To keep things simple here, rather with dealing with separate loops in multiple scenes, we are going to take advantage of the Snake being stationary until the first arrow key is pressed and simply restart our main function.
Let’s think about this. So our original window, assuming you have kept the same settings, is 500 x 500, but there is now a 35 pixel border on all four edges, effectively reducing our play area to 440 x 440. So these will be the dimensions we need to check for.
To re-iterate from earlier in this article, drawing coordinates begin at the top left hand corner, so when checking for collisions will need to account for this by adding the length and width of our snake cells in our calculations for the right and bottom walls.
Beneath the call to
snake.draw() in the
Game.py file, add the following code:
We calculate where the bumpers are at and store them to their own variables to keep the code a little shorter, these represent the right hand and bottom borders.
We then simply check if the snakes current position is less than the left or top or greater than the right and bottom borders, adding the snake’s size to the X and Y positions to account for the coordinates being in the top left hand side. If any checks are true, we call the game loop again to restart the game.
Now when you run your game, the snake’s position should be reset to the middle of the screen.
Building an Apple
Our snake can’t grow without having a steady supply of apples to eat. Let’s breakdown the requirements for our apple.
- We will need an X and Y position at which to draw the apple
- A randomize function that will generate random coordinates on the playable game area as needed
- A draw function to display the apple
Create a new file in your
src folder with the file name of
Apple.py with the following contents:
__init__, we initialize our
y_pos properties and store the reference to the display. Then we call the randomize function for the Apple’s final placement.
randomize function generates a random integer between the minimum and maximum of both directions.
draw funtion works identically to our Snake draw function and places the apple at the randomly generated X and Y coordinates.
To actually start drawing the apple on game load, add the following right above our calls to
snake.draw() inside the
Game.py file. The initialization statement needs to be added at the top under where the
Snake object is initialized.
If you run the game now, you should see our apple appearing on the game surface. You can trigger a restart by hitting a wall a few times to ensure the randomize functionality is working and our apple moves as desired.
Eating an Apple
We will be utilizing the build in collision detection for PyGame rectangles to determine when an apple has been eaten. We need to revise a few pieces of code to make this happen.
First, the draw methods in both the
Apple class and
Snake class need to be modified to return the
rect method they are already calling.
The call sites of these two functions needs to be modifed to store this returend rectangle object.
Now that we have a reference to both rectangles, we can add the following collison code to our
Game.py file directly beneath the wall collision logic.
Now when you test your game, you should be able to pass over an apple and the apple re-generate itswhere and you should see the score increase now that we are incrementing the score property of our
Game object each time an apple is eaten.
Increasing Size of Snake
The system we are going to use to keep track of the various body segments of the snake is going to work by storing the last position of the head in an array, pushing to the top, each time the
move metod of
Snake is called.
Snake.py file, add the following instance properties to the
body is where we will contain a list of tuples, each with an X and Y value, for our body while max size dictates how big the Snake can get and is increased with each eaten apple.
Next we will need to add new functions for growing the snake, drawing the body, and lastly make modifications to the
move method to store history and regulate the size of the list.
The previous position is writen to body each time it is changed, but old entries are removed as when the list is longer than it’s supposed to be.
Drawing the body works the same was as our head cell, but we are iterating over each item in the body and drawing a cube for each record instead.
Resetting the Score
In order to reset the score on each reset, add the following line directly above
while True: in the loop function of the
self.score = 0
Snake Colliding With Itsself
To check if we have collided with ourselves, we do a cut and dry check where we iterate over each body element and see if it has the same X and Y coordinate positions of the head.
If so, this causes an eror and restarts the game. This also works for trying to move the snake backwards.
Add this logic directly beneath the existing collision detection logic in the
The game should successfully end on collision between the snake head and snake body now.
Our basic snake game is now playable and I hope you feel a lot more comfortable with PyGame and game programming then you did before you read this article.
Here’s a few ideas for improvements you can make to dive deeper:
- Menu Screen
- High Score with database to persist through closes
- Pause / game over screens
Thanks for taking the time to read this and if you have any questions or comments, please let me know.