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.

Prerequisites

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.

Getting Started

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 Anaconda.py file.

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.

In the 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.

Game.py

Anaconda.py

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 x_pos and y_pos that will serve as the X ad Y position of the head of our snake at the middle of the screen.

With our 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.

The 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.

The 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.

Movement

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 loop() function.

This instantiates our snake with a reference to the display and sets x_change and 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 event.type for 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.

Game Area

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 Game object.

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:

In __init__, we initialize our x_pos and y_pos properties and store the reference to the display. Then we call the randomize function for the Apple’s final placement.

The randomize function generates a random integer between the minimum and maximum of both directions.

The 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.move() and 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.

Inside the Snake.py file, add the following instance properties to the __init__ function.

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 Game class.

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 Game.py file.

The game should successfully end on collision between the snake head and snake body now.

Conclusion

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
  • Background

Thanks for taking the time to read this and if you have any questions or comments, please let me know.